chore: bo target/ root (artifact lan) khoi index + gitignore
This commit is contained in:
@@ -62,3 +62,6 @@ native_bridge/debug/shm_selfcheck.exe
|
||||
install/daw_vst_bridge
|
||||
install/libsfizz.so.1
|
||||
build_juce
|
||||
|
||||
# build artifact (root, khong phai deploy dir)
|
||||
target/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,360 +0,0 @@
|
||||
// sf-fx-realtime.js — AudioWorkletProcessor nối WebAudio → SHM FX engine.
|
||||
// process() gom 128-sample quantum thành block 256 stereo float32 (khớp
|
||||
// FXRT_BLOCK trong FxRealtimeIPC.h) → postMessage cho main thread gửi WS;
|
||||
// nhận block đã xử lý từ main thread → queue → phát ra output. Queue rỗng →
|
||||
// zero fill (WS chưa kịp round-trip).
|
||||
//
|
||||
// BURST (fix lag/giật/crackle): gom 4 block (1024 mẫu) vào 1 frame rồi post.
|
||||
// Bridge VST3 (fx_vst_bridge) chạy chậm hơn realtime khi take=1 (256 mẫu:
|
||||
// ~5.5ms/call > budget 5.33ms → 179/s < 187.5/s → engine drop input →
|
||||
// crackle). Gửi burst 4 block → bridge take=4 → 1 call process 1024 mẫu
|
||||
// (amortize overhead: 14-19ms/4 block ≈ 3.5-4.75ms/block) → đủ 187.5/s.
|
||||
// Latency thêm ~4 block (~21ms) — worklet outQueue 32 block hấp thụ.
|
||||
//
|
||||
// FILL (hết crackle do jitter): không phát ngay khi có block đầu — chờ queue
|
||||
// ≥ FILL (8 block ~43ms @48k) rồi mới phát. Engine gửi ASAP (không pace
|
||||
// realtime) → client tích lũy buffer an toàn; mọi jitter (bridge process VST
|
||||
// biến thiên, GC, WS, main thread rAF) bị hấp thụ bởi FILL thay vì queue ≈1
|
||||
// → cạn → ngắt quãng.
|
||||
// UNDERRUN (queue cạn khi đang phát): V23 — KHÔNG lặp block cuối (lặp tạo
|
||||
// jump pha ở điểm vào hold + mỗi wrap → click), KHÔNG zero-fill: fade-out
|
||||
// tuyến tính từ mẫu output cuối (HOLD_FADE block ~21ms) tới im lặng; resume
|
||||
// NGAY khi có block mới (không đợi đủ FILL): đã im lặng → fade-in từ 0, đang
|
||||
// giữa fade → crossfade 64 mẫu từ mẫu hiện tại (hết jump). Cap-drop (queue
|
||||
// tràn sau stall) crossfade 64 mẫu (hết click) — giống bridgeAudioWorklet.
|
||||
// Cap 64 block (~341ms) chặn overflow khi bridge nhanh hơn realtime.
|
||||
// ponytail: thay fade-out bằng adaptive stretch khi cần chất lượng cao hơn.
|
||||
class SfFxRealtimeProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.BLOCK = 256; // FXRT_BLOCK
|
||||
this.BURST = 4; // số block gom 1 frame (bridge batch)
|
||||
this.FILL = 24; // V44: 24 block ~128ms @48k: buffer khởi đầu — hấp thụ jitter main-thread + stall deschedule bridge 68-138ms. PDC 7936 = (7+24)*256 (fxRtApplyPdc). V43 16 -> cushion ~9 (q 3-9) chua du: stall 68-82ms van gay underrun. V44 len 24 -> cushion ~17 (~90ms).
|
||||
this.RESUME_FILL = 24; // V26: re-buffer khi resume SAU STOP (flush_for_stop) — 16 block ~85ms (V43: = FILL) → play kế tiếp bắt đầu nhanh; underrun-stall vẫn dùng FILL (crackle/wobble).
|
||||
this.resumeFill = this.FILL; // mức re-buffer khi resume sau flush — stop: RESUME_FILL, underrun-stall: FILL
|
||||
this.CAP = 32; // V40: cap queue chặn overflow (~170ms)
|
||||
this.HOLD_FADE = 4; // 4 block ~21ms: fade 1→0 sau HOLD hết (hết click cắt cứng)
|
||||
this.holdFadeLeft = 0; // mẫu còn lại của fade-out khi underrun
|
||||
this.silenced = false; // đã fade về 0 → im lặng; resume phải fade-in, không hold lại
|
||||
this.bufLen = this.BLOCK * this.BURST;
|
||||
this.inL = new Float32Array(this.bufLen);
|
||||
this.inR = new Float32Array(this.bufLen);
|
||||
this.inCount = 0;
|
||||
this.outQueue = []; // [{l: Float32Array, r: Float32Array}]
|
||||
this.outPos = 0;
|
||||
this.playing = false; // false → im lặng (đang chờ đủ FILL)
|
||||
this.hasStarted = false; // đã phát block đầu chưa — FILL chỉ chờ ở lần khởi đầu
|
||||
this.flushPending = false; // V24: đang chờ engine flush (resume sau underrun) — drop block stale
|
||||
this.resumeFadeIn = false; // V24: interruption đã xử lí (clear+flush) — resume sẽ fade-in từ 0
|
||||
this.FADE = 512; // fade-in 512 mẫu (~11ms @48k) khi bắt đầu phát/resume — hết click
|
||||
this.outGain = 0; // gain phát hiện tại (0→1 trong fade)
|
||||
this.fadeLeft = 0; // số mẫu còn lại của fade
|
||||
this.skipFade = 0; // >0: crossfade đang chạy sau jump (cap-drop/resume/wrap) — hết click
|
||||
this.skipFadeTotal = 64; // V42: độ dài crossfade hiện tại (64 mẫu thường; WRAP_FADE 2048 tại seam wrap)
|
||||
this.skipFromL = 0.0; // mẫu output tại điểm jump — điểm bắt đầu crossfade
|
||||
this.skipFromR = 0.0;
|
||||
this.lastOutL = 0.0; // mẫu output cuối — nguồn crossfade + điểm bắt đầu fade-out
|
||||
this.lastOutR = 0.0;
|
||||
this.wrapFadeBlocks = -1; // V42: số block tail pass cũ còn lại sau clear_queue('loop') — đếm ngược để crossfade đúng seam tail→head
|
||||
this.latencySamples = 0; // G3: latency chain thật (engine qua WS) — align wrap fade theo processed tail
|
||||
this.roundtripSamples = 0; // V45: roundtrip engine→bridge→worklet (main báo qua 'latency') — căn wrap crossfade đúng seam
|
||||
this.WRAP_FADE = 2048; // V42a: crossfade 2048 mẫu (~42.7ms @48k) tại seam wrap — 256 quá ngắn cho jump nội dung end→start (nghe giật 1 cái); 2048 che mượt seam loop
|
||||
this.fadeFromL = 0.0; // mẫu cố định khi fade-out underrun (fade tuyến tính tới 0)
|
||||
this.fadeFromR = 0.0;
|
||||
this.port.onmessage = (e) => {
|
||||
const d = e.data;
|
||||
if (d && d.type === 'set_param') { this.port.postMessage(d); return; }
|
||||
if (d && d.type === 'flush_done') { this.flushPending = false; return; }
|
||||
if (d && d.type === 'latency') { this.latencySamples = (d.samples || 0); if (typeof d.roundtrip === 'number') this.roundtripSamples = d.roundtrip; return; }
|
||||
if (d && d.type === 'flush_for_stop') {
|
||||
// V26: user stop — main thread báo xóa audio tail trong queue (audio đã
|
||||
// xử lí còn phát ~0.3-0.5s sau stop = tiếng "ding"). Fade-out 21ms từ
|
||||
// mẫu cuối (hết click) → im lặng; báo main flush engine ring (audio
|
||||
// stale trong WS/SHM không chơi tiếp). Resume lần sau: RESUME_FILL
|
||||
// (8 block ~43ms, V40: = FILL) — play kế tiếp không chờ trễ.
|
||||
this.outQueue.length = 0;
|
||||
this.outPos = 0;
|
||||
this.playing = false;
|
||||
this.wrapFadeBlocks = -1; // V42: stop — hủy wrap crossfade chờ
|
||||
this.flushPending = true;
|
||||
this.resumeFadeIn = true;
|
||||
this.resumeFill = this.RESUME_FILL;
|
||||
this.silenced = false;
|
||||
// V30: fade-out tu mau output CUOI (lastOut) — truoc day dung fadeFrom
|
||||
// cu (mac dinh 0) -> jump mau -> click/crackle ngay khi stop.
|
||||
this.fadeFromL = this.lastOutL;
|
||||
this.fadeFromR = this.lastOutR;
|
||||
this.holdFadeLeft = this.HOLD_FADE * this.BLOCK;
|
||||
this.port.postMessage({ type: 'flush' });
|
||||
return;
|
||||
}
|
||||
if (d && d.type === 'clear_queue') {
|
||||
// V34: backlog guard o DAU handler (V33 dat sau mutation -> outQueue.length=0
|
||||
// chay truoc -> guard luon return, clear van drop queue + fade + re-buffer moi
|
||||
// lan = V31). Queue nho (< FILL 8 block) -> bo qua hoan toan (khong mutation,
|
||||
// khong re-buffer) -> het ARM crackle (queue ARM chi 0-8 block, engine ~realtime).
|
||||
// Queue day (idle lau, engine chay truoc ~32 block) -> clear dung (fix lag 170ms).
|
||||
// V35: mode:'play' (start/section/pianoroll) LUON clear - queue < FILL luc
|
||||
// play-start con stale block idle/ARM (~16) -> bo qua => phat audio cu roi
|
||||
// can queue ~500ms (cold start). mode:'live' (ARM) giu guard het crackle.
|
||||
// V37 ARM probe: reset dbg flags BEFORE guard so live notes (small queue) also log fx_hop
|
||||
this.dbgClearAt = currentTime * 1000;
|
||||
this.dbgInFirst = true;
|
||||
this.dbgOutFirst = true;
|
||||
this.dbgPlayFirst = true;
|
||||
// V39: loop wrap — KHONG wipe: giu queue chay lien tuc (tail pass cu
|
||||
// pre-roll pass moi qua VST lookahead) -> het wobble dau bai. Chi reset
|
||||
// dbg flags. mode 'play' van wipe (bao ve audio cu khi seek/cold start).
|
||||
// V42: dem so block tail pass cu con lai trong queue -> wrap crossfade
|
||||
// trigger dung luc block dau pass moi len front (seam tail->head).
|
||||
if (d.mode === 'loop') {
|
||||
// V45: crossfade căn giữa seam — seam = block đầu pass mới lên front ≈
|
||||
// roundtrip engine→bridge→worklet (FX_RT_ROUNDTRIP_SAMPLES 7936 = 31
|
||||
// block). Fire sớm hơn seam WRAP_FADE/2 → fade [fire, fire+WRAP_FADE]
|
||||
// che đúng chỗ jump end→start. Trước: fire = queue+latency dao động
|
||||
// 113-166ms (4/5 kết thúc TRƯỚC seam) → jump raw → giật.
|
||||
// Fallback công thức cũ khi roundtrip chưa về (main chưa báo).
|
||||
if (this.roundtripSamples > 0) {
|
||||
this.wrapFadeBlocks = Math.max(1, Math.round(this.roundtripSamples / this.BLOCK) - Math.round(this.WRAP_FADE / this.BLOCK / 2));
|
||||
} else {
|
||||
this.wrapFadeBlocks = this.outQueue.length + Math.ceil((this.latencySamples || 0) / this.BLOCK);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (d.mode !== 'play' && this.outQueue.length < this.FILL) return;
|
||||
// V31: play/ARM live note — main thread bao xoa backlog block idle
|
||||
// (engine render lien tuc khi STOP -> outQueue day ~32 block ~170ms
|
||||
// audio cu; play moi phai cho phia sau -> lag 0.3-0.5s). KHONG post
|
||||
// 'flush' (khong chaym engine WS — note-on play vua day xuong se bi
|
||||
// drop). Resume sau 8 block (~43ms) + fade-out/fade-in tu mau cuoi.
|
||||
this.outQueue.length = 0;
|
||||
this.outPos = 0;
|
||||
this.playing = false;
|
||||
this.wrapFadeBlocks = -1; // V42: wipe — hủy wrap crossfade chờ (không có tail cũ)
|
||||
this.resumeFadeIn = true;
|
||||
// V34: mode phan biet — play re-buffer FILL 8 (~43ms, cushion du hap thu
|
||||
// engine ramp/jitter dau play); ARM live note giu RESUME_FILL 8 (~43ms) — preview
|
||||
// nhanh, khong cho FILL 8.
|
||||
this.resumeFill = (d.mode === 'play') ? this.FILL : this.RESUME_FILL;
|
||||
this.silenced = false;
|
||||
this.fadeFromL = this.lastOutL;
|
||||
this.fadeFromR = this.lastOutR;
|
||||
this.holdFadeLeft = this.HOLD_FADE * this.BLOCK;
|
||||
return;
|
||||
}
|
||||
if (!d || d.type !== 'out' || !d.l || !d.r) return;
|
||||
// V24: đang chờ flush (resume sau underrun/stall) → drop block stale.
|
||||
// Backlog engine gửi khi worklet im là audio CŨ (chơi lại → lag cộng dồn
|
||||
// ~1s) — chỉ nhận block MỚI từ sau flush_done.
|
||||
if (this.flushPending) return;
|
||||
this.outQueue.push({ l: d.l, r: d.r });
|
||||
if (this.dbgOutFirst) {
|
||||
this.dbgOutFirst = false;
|
||||
this.port.postMessage({ type: 'dbg', tag: 'fx_out_first', t: Math.round(currentTime * 1000), q: this.outQueue.length });
|
||||
}
|
||||
while (this.outQueue.length > this.CAP) {
|
||||
this.outQueue.shift();
|
||||
this.statCapDrop++;
|
||||
// V23: drop cứng = jump mẫu → click. Crossfade 64 mẫu từ mẫu output
|
||||
// cuối sang block mới; chỉ đặt 1 lần cho đợt drop liên tiếp. outPos=0:
|
||||
// block mới (front) phải phát từ đầu, không tiếp tục vị trí block cũ.
|
||||
if (this.skipFade <= 0) {
|
||||
this.skipFade = 64;
|
||||
this.skipFadeTotal = 64;
|
||||
this.skipFromL = this.lastOutL;
|
||||
this.skipFromR = this.lastOutR;
|
||||
}
|
||||
this.outPos = 0;
|
||||
}
|
||||
};
|
||||
// Telemetry (chẩn đoán crackle trên session thật): đếm sự kiện + báo main
|
||||
// thread mỗi ~2s. Nếu user bật master FX nghe crackle mà stats cho thấy
|
||||
// underruns>0 → delivery bị stall (main-thread busy) → nguồn crackle thật.
|
||||
this.statUnderrun = 0; this.statHold = 0; this.statFade = 0;
|
||||
this.statSilence = 0; this.statCapDrop = 0; this.statFadeIn = 0;
|
||||
this.statCalls = 0;
|
||||
// V32: do latency master FX path — danh dau clear_queue, bao main thread hop dau
|
||||
// sau clear (in_first / out_first / play_first) → dbg.log delta vs play_start.
|
||||
this.dbgClearAt = 0;
|
||||
this.dbgInFirst = false;
|
||||
this.dbgOutFirst = false;
|
||||
this.dbgPlayFirst = false;
|
||||
}
|
||||
process(inputs, outputs) {
|
||||
const input = inputs[0];
|
||||
const output = outputs[0];
|
||||
const n = output[0] ? output[0].length : 128;
|
||||
const outL = output[0];
|
||||
const outR = output[1];
|
||||
|
||||
// Góp input thành burst 4 block (1024 mẫu) → gửi main thread
|
||||
// (transferable buffer). Main thread tách 4 WS frame → bridge batch 4.
|
||||
if (input && input[0]) {
|
||||
const inL = input[0];
|
||||
const inR = input[1] || input[0];
|
||||
for (let i = 0; i < n; i++) {
|
||||
if (this.inCount < this.bufLen) {
|
||||
this.inL[this.inCount] = inL[i];
|
||||
this.inR[this.inCount] = inR[i];
|
||||
}
|
||||
this.inCount++;
|
||||
if (this.inCount >= this.bufLen) {
|
||||
const inter = new Float32Array(this.bufLen * 2);
|
||||
for (let j = 0; j < this.bufLen; j++) {
|
||||
inter[j * 2] = this.inL[j];
|
||||
inter[j * 2 + 1] = this.inR[j];
|
||||
}
|
||||
this.port.postMessage({ type: 'in', data: inter.buffer }, [inter.buffer]);
|
||||
this.inCount = 0;
|
||||
if (this.dbgInFirst) {
|
||||
this.dbgInFirst = false;
|
||||
this.port.postMessage({ type: 'dbg', tag: 'fx_in_first', t: Math.round(currentTime * 1000) });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phát output: chờ queue ≥ FILL trước khi phát lần đầu (hấp thụ jitter
|
||||
// engine/bridge/WS); underrun GIỮA stream → fade-out 21ms từ mẫu cuối rồi
|
||||
// im lặng (V23); resume NGAY khi có block mới (không đợi re-buffer FILL),
|
||||
// đã im lặng → fade-in, giữa fade → crossfade 64 mẫu (hết jump).
|
||||
for (let i = 0; i < n; i++) {
|
||||
this.statCalls++;
|
||||
if (this.statCalls >= 1500) { // ~2s @48k/128
|
||||
this.statCalls = 0;
|
||||
this.port.postMessage({ type: 'stats',
|
||||
underruns: this.statUnderrun, holds: this.statHold, fades: this.statFade,
|
||||
silences: this.statSilence, capDrops: this.statCapDrop, fadeIns: this.statFadeIn,
|
||||
queueLen: this.outQueue.length });
|
||||
}
|
||||
if (this.playing) {
|
||||
const blk = this.outQueue[0];
|
||||
if (blk) {
|
||||
if (this.fadeLeft > 0) {
|
||||
this.outGain = Math.min(1, this.outGain + 1 / this.FADE);
|
||||
this.fadeLeft--;
|
||||
}
|
||||
if (this.skipFade > 0) {
|
||||
const t = 1 - this.skipFade / this.skipFadeTotal;
|
||||
outL[i] = (this.skipFromL * (1 - t) + blk.l[this.outPos] * t) * this.outGain;
|
||||
if (outR) outR[i] = (this.skipFromR * (1 - t) + blk.r[this.outPos] * t) * this.outGain;
|
||||
this.skipFade--;
|
||||
} else {
|
||||
outL[i] = blk.l[this.outPos] * this.outGain;
|
||||
if (outR) outR[i] = blk.r[this.outPos] * this.outGain;
|
||||
}
|
||||
this.lastOutL = outL[i];
|
||||
this.lastOutR = outR ? outR[i] : outL[i];
|
||||
this.outPos++;
|
||||
if (this.outPos >= this.BLOCK) {
|
||||
this.outQueue.shift();
|
||||
this.outPos = 0;
|
||||
// V42: seam tail→head — sau khi shift hết N block tail pass cũ, block
|
||||
// đầu pass mới lên front → crossfade WRAP_FADE từ mẫu cuối (che click
|
||||
// jump nội dung end→start, FX path). Trigger 1 lần mỗi wrap.
|
||||
if (this.wrapFadeBlocks > 0) {
|
||||
this.wrapFadeBlocks--;
|
||||
if (this.wrapFadeBlocks === 0) {
|
||||
this.skipFade = this.WRAP_FADE;
|
||||
this.skipFadeTotal = this.WRAP_FADE;
|
||||
this.skipFromL = this.lastOutL;
|
||||
this.skipFromR = this.lastOutR;
|
||||
this.port.postMessage({ type: 'dbg', tag: 'fx_wrap_xfade', t: Math.round(currentTime * 1000), q: this.outQueue.length });
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Underrun giữa stream — V23: fade-out NGAY (không lặp block cuối:
|
||||
// lặp tạo jump pha ở điểm vào + mỗi wrap → click). Fade tuyến tính từ
|
||||
// mẫu output cuối tới 0 (21ms) — vào liền mạch (fadeFrom = lastOut),
|
||||
// hết click cắt cứng. Resume → crossfade/fade-in (xem dưới).
|
||||
this.playing = false;
|
||||
this.wrapFadeBlocks = -1; // V42: underrun — hủy wrap crossfade chờ (resume qua fade-in/crossfade thường)
|
||||
this.statUnderrun++;
|
||||
if (!this.silenced) { // đã im lặng (fade xong) → giữ im lặng
|
||||
this.fadeFromL = this.lastOutL;
|
||||
this.fadeFromR = this.lastOutR;
|
||||
this.holdFadeLeft = this.HOLD_FADE * this.BLOCK;
|
||||
this.statHold++;
|
||||
}
|
||||
}
|
||||
// Không phát: resume khi đủ block (lần đầu chờ FILL; sau đó chỉ cần 1
|
||||
// block — hold che khoảng chờ). V25: sau flush (resumeFadeIn=true) chờ
|
||||
// re-buffer đủ FILL mới resume — chặn loop underrun→flush→resume-1-block
|
||||
// →underrun (wobble): 1 block phát hết ~5ms → cạn → flush lại.
|
||||
if (this.outQueue.length >= (this.hasStarted ? (this.resumeFadeIn ? this.resumeFill : 1) : this.FILL)) {
|
||||
// V24: interruption (underrun sau stall) — backlog trong queue là audio
|
||||
// LẠC HẬU (chơi lại → lag cộng dồn ~1s). KHÔNG chơi backlog: clear
|
||||
// queue + báo main thread flush (engine xóa input/output ring, main
|
||||
// drop output stale in-flight) → chờ flush_done → resume chỉ với block
|
||||
// MỚI. flushPending → onmessage drop block stale trong lúc chờ (hết
|
||||
// race ack). Fade-out đang chạy (nếu có) tiếp tục tới im lặng — hết
|
||||
// click. resumeFadeIn guard chống re-trigger flush khi ack tới mà fade
|
||||
// chưa xong. resume → fade-in từ 0 (đã im) hoặc crossfade từ mẫu hiện
|
||||
// tại (giữa fade) — hết jump.
|
||||
const wasInterrupted = !this.resumeFadeIn && (this.holdFadeLeft > 0 || this.silenced) && this.hasStarted;
|
||||
if (wasInterrupted) {
|
||||
this.outQueue.length = 0;
|
||||
this.outPos = 0;
|
||||
this.flushPending = true;
|
||||
this.resumeFadeIn = true;
|
||||
this.resumeFill = this.FILL; // underrun-stall: giữ cushion FILL đầy đủ (V25 wobble)
|
||||
this.port.postMessage({ type: 'flush' });
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
this.playing = true;
|
||||
this.outPos = 0;
|
||||
this.holdFadeLeft = 0;
|
||||
if (this.dbgPlayFirst) {
|
||||
this.dbgPlayFirst = false;
|
||||
this.port.postMessage({ type: 'dbg', tag: 'fx_play_first', t: Math.round(currentTime * 1000), q: this.outQueue.length });
|
||||
}
|
||||
if (this.resumeFadeIn) {
|
||||
this.resumeFadeIn = false;
|
||||
if (this.silenced) {
|
||||
// Đã im lặng (fade xong / sau flush) → fade-in từ 0 (hết jump ~0→1).
|
||||
this.outGain = 0;
|
||||
this.fadeLeft = this.FADE;
|
||||
this.statFadeIn++;
|
||||
} else {
|
||||
// Giữa fade-out → crossfade ngắn từ mẫu hiện tại (không jump về 0).
|
||||
this.skipFade = 64;
|
||||
this.skipFadeTotal = 64;
|
||||
this.skipFromL = this.lastOutL;
|
||||
this.skipFromR = this.lastOutR;
|
||||
}
|
||||
}
|
||||
this.silenced = false;
|
||||
if (!this.hasStarted) {
|
||||
this.hasStarted = true;
|
||||
this.outGain = 0;
|
||||
this.fadeLeft = this.FADE;
|
||||
// Báo main thread 1 lần để crossfade dry→fx đúng thời điểm.
|
||||
this.port.postMessage({ type: 'started' });
|
||||
}
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
if (this.holdFadeLeft > 0) {
|
||||
// Fade-out 1→0 (underrun): fade tuyến tính từ mẫu cuối, im lặng êm.
|
||||
const g = this.outGain * (this.holdFadeLeft / (this.HOLD_FADE * this.BLOCK));
|
||||
outL[i] = this.fadeFromL * g;
|
||||
if (outR) outR[i] = this.fadeFromR * g;
|
||||
this.holdFadeLeft--;
|
||||
if (this.holdFadeLeft <= 0) { this.silenced = true; this.statFade++; this.statSilence++; }
|
||||
this.lastOutL = outL[i];
|
||||
this.lastOutR = outR ? outR[i] : outL[i];
|
||||
continue;
|
||||
}
|
||||
outL[i] = 0;
|
||||
if (outR) outR[i] = 0;
|
||||
this.lastOutL = 0;
|
||||
this.lastOutR = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('sf-fx-realtime', SfFxRealtimeProcessor);
|
||||
@@ -1,479 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>ScoreFX Studio - Professional DAW Editor</title>
|
||||
<script>
|
||||
// ── Chặn BROWSER ZOOM toàn trang ──
|
||||
// UI dùng px cố định cho item/button/label — browser zoom (Ctrl+wheel,
|
||||
// Ctrl+plus/minus/0, pinch) scale TOÀN BỘ làm control phóng to theo.
|
||||
// Chặn ở capture phase bằng preventDefault() (KHÔNG stopPropagation —
|
||||
// các vùng zoom chuyên dụng: timeline, piano roll, canvas, EQ vẫn nhận
|
||||
// event và tự xử lý zoom nội dung của chúng).
|
||||
document.addEventListener('wheel', function (e) {
|
||||
if (e.ctrlKey || e.metaKey) e.preventDefault();
|
||||
}, { capture: true, passive: false });
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if ((e.ctrlKey || e.metaKey) && ['+', '-', '=', '_', '0'].indexOf(e.key) !== -1) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}, { capture: true });
|
||||
// Pinch zoom (Safari/WebKit gesture events)
|
||||
document.addEventListener('gesturestart', function (e) { e.preventDefault(); }, { passive: false });
|
||||
document.addEventListener('gesturechange', function (e) { e.preventDefault(); }, { passive: false });
|
||||
</script>
|
||||
<link rel="icon" type="image/png" href="/static/img/scorefx_favicon.png">
|
||||
<!-- Bug #11: vendor local — trước đây load CDN (unpkg/cdn.tailwindcss/cdnjs) →
|
||||
desktop Tauri offline trắng màn hình. Các file này nằm /static/vendor/. -->
|
||||
<script src="/static/vendor/tailwindcdn.js"></script>
|
||||
<script src="/static/vendor/lucide.min.js"></script>
|
||||
<link rel="stylesheet" href="/static/vendor/fontawesome.min.css">
|
||||
<script src="/static/vendor/react.production.min.js"></script>
|
||||
<script src="/static/vendor/react-dom.production.min.js"></script>
|
||||
<script src="/static/js/services/fluidsynthLoader.js?v=202608261629201545"></script>
|
||||
<script src="/static/js/services/runtime.js?v=202608101800"></script>
|
||||
<script src="/static/js/services/api.js?v=202608212400"></script>
|
||||
<script src="/static/js/services/audioEngine.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/storage.js?v=202608038200"></script>
|
||||
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/soundfontPlayer.js?v=202608101800"></script>
|
||||
<script src="/static/js/services/nativeBridgeService.js?v=202608250700"></script>
|
||||
<script src="/static/js/services/bridgeAudioNode.js?v=202608250700"></script>
|
||||
<script src="/static/js/services/unifiedMidiRouter.js?v=202608271630"></script>
|
||||
<script src="/static/js/services/audioRoutingEngine.js?v=202608250700"></script>
|
||||
<script src="/static/js/services/aiGateway.js?v=202608037200"></script>
|
||||
<script src="/static/js/services/dawCommandDispatcher.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/pianoRollTabService.js?v=202607272044"></script>
|
||||
<script src="/static/js/services/ghostNoteExtractor.js?v=202607271727"></script>
|
||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||
<script src="/static/js/services/chordTheory.js?v=202608211300"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608291125" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
--right-sidebar-width: 320px;
|
||||
--bottom-strip-height: 220px;
|
||||
--top-bar-height: 80px;
|
||||
--status-bar-height: 25px;
|
||||
--panel-border-color: #2a2a2a;
|
||||
/* Theme (Preferences) — có thể override bằng data-theme trên <html> */
|
||||
--sf-bg: #1e1e1e;
|
||||
--sf-panel: #262626;
|
||||
--sf-header: #2e2e2e;
|
||||
--sf-border: #181818;
|
||||
--sf-accent: #00ffcc;
|
||||
}
|
||||
|
||||
/* ── THEME presets (Tools → Preferences) ── */
|
||||
html[data-theme="dark"] {
|
||||
--sf-bg: #1e1e1e; --sf-panel: #262626; --sf-header: #2e2e2e; --sf-border: #181818; --sf-accent: #00ffcc;
|
||||
}
|
||||
html[data-theme="midnight"] {
|
||||
--sf-bg: #0f172a; --sf-panel: #1e293b; --sf-header: #1e293b; --sf-border: #0f172a; --sf-accent: #38bdf8;
|
||||
}
|
||||
html[data-theme="forest"] {
|
||||
--sf-bg: #111c15; --sf-panel: #1b2a1e; --sf-header: #1e2d21; --sf-border: #0c1510; --sf-accent: #34d399;
|
||||
}
|
||||
html[data-theme="violet"] {
|
||||
--sf-bg: #170f26; --sf-panel: #221537; --sf-header: #251640; --sf-border: #110a1c; --sf-accent: #a78bfa;
|
||||
}
|
||||
html[data-theme="graphite"] {
|
||||
--sf-bg: #18181b; --sf-panel: #232327; --sf-header: #27272a; --sf-border: #101012; --sf-accent: #e4e4e7;
|
||||
}
|
||||
|
||||
/* Áp theme lên các vùng chính của app shell */
|
||||
.daw-app-shell { background-color: var(--sf-bg) !important; }
|
||||
.daw-panel { background-color: var(--sf-panel) !important; }
|
||||
.daw-header { background-color: var(--sf-header) !important; }
|
||||
body { background-color: var(--sf-bg) !important; }
|
||||
::-webkit-scrollbar-thumb { background: var(--sf-accent); }
|
||||
|
||||
/* ── BUTTON FONT SIZE (Tools → Preferences) ── */
|
||||
html[data-btnfont="sm"] button { font-size: 10px !important; }
|
||||
html[data-btnfont="md"] button { font-size: 12px !important; }
|
||||
html[data-btnfont="lg"] button { font-size: 14px !important; }
|
||||
|
||||
body {
|
||||
background-color: #1a1a1a;
|
||||
color: #c0c0c0;
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.daw-bg {
|
||||
background-color: #1e1e1e;
|
||||
}
|
||||
|
||||
.daw-panel {
|
||||
background-color: #262626;
|
||||
}
|
||||
|
||||
.daw-header {
|
||||
background-color: #2e2e2e;
|
||||
}
|
||||
|
||||
.daw-border {
|
||||
border-color: #181818;
|
||||
}
|
||||
|
||||
.daw-track-active {
|
||||
background-color: #333333;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #141414;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #3a3a3a;
|
||||
border: 2px solid #141414;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #4a4a4a;
|
||||
}
|
||||
|
||||
.knob-container {
|
||||
position: relative;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.knob-dial {
|
||||
transform-origin: center;
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.selection-interactive-box {
|
||||
min-width: 4px;
|
||||
}
|
||||
|
||||
.no-scrollbar {
|
||||
scrollbar-width: none;
|
||||
/* Firefox */
|
||||
-ms-overflow-style: none;
|
||||
/* IE 10+ */
|
||||
}
|
||||
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
/* Safari and Chrome */
|
||||
}
|
||||
|
||||
/* Fullscreen Fixed App Shell */
|
||||
.daw-app-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background-color: #121212;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
/* Middle Section holding Main Workspace and Right Sidebar */
|
||||
.daw-body-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: calc(100vh - var(--top-bar-height) - var(--bottom-strip-height) - var(--status-bar-height));
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Auto-expanding Main Workspace */
|
||||
.daw-main-workspace {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Right Sidebar with Width controlled via CSS Variable */
|
||||
.daw-right-sidebar {
|
||||
width: var(--right-sidebar-width);
|
||||
min-width: 200px;
|
||||
max-width: 600px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #1a1a1a;
|
||||
border-left: 1px solid var(--panel-border-color);
|
||||
}
|
||||
|
||||
/* Vertically stacked child Panels in Right Sidebar */
|
||||
.sidebar-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: #1e1e1e;
|
||||
border-bottom: 1px solid var(--panel-border-color);
|
||||
}
|
||||
|
||||
#panel-media-explorer {
|
||||
height: 50%;
|
||||
/* Default 50/50 split */
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
#panel-ai {
|
||||
flex: 1;
|
||||
/* Fills remaining height */
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
/* BOTTOM ROW: Enables Horizontal Scrolling */
|
||||
.daw-bottom-strip {
|
||||
height: var(--bottom-strip-height);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px;
|
||||
overflow-x: auto;
|
||||
/* Enables horizontal scroll when panels overflow */
|
||||
overflow-y: hidden;
|
||||
background-color: #161616;
|
||||
border-top: 1px solid var(--panel-border-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Optimized custom horizontal scrollbar for DAW styling */
|
||||
.daw-bottom-strip::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.daw-bottom-strip::-webkit-scrollbar-thumb {
|
||||
background: #3a3a3a;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.daw-bottom-strip::-webkit-scrollbar-thumb:hover {
|
||||
background: #00ffcc;
|
||||
}
|
||||
|
||||
/* Sub-panels inside the bottom strip */
|
||||
.bottom-panel {
|
||||
flex: 0 0 auto;
|
||||
/* Prevents shrinking, locks content dimensions */
|
||||
width: 320px;
|
||||
height: 100%;
|
||||
background-color: #222;
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* RESIZER HANDLES */
|
||||
.resizer-col-handle {
|
||||
width: 5px;
|
||||
cursor: ew-resize;
|
||||
/* Horizontal resize cursor */
|
||||
background: transparent;
|
||||
transition: background 0.2s;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.resizer-col-handle:hover,
|
||||
.resizer-col-handle:active {
|
||||
background: #00ffcc;
|
||||
}
|
||||
|
||||
.resizer-row-handle {
|
||||
height: 5px;
|
||||
cursor: ns-resize;
|
||||
/* Vertical resize cursor */
|
||||
background: transparent;
|
||||
transition: background 0.2s;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.resizer-row-handle:hover,
|
||||
.resizer-row-handle:active {
|
||||
background: #00ffcc;
|
||||
}
|
||||
|
||||
/* Disable mouse pointer events on SVG icons to prevent SVGAnimatedString className type errors in event listeners */
|
||||
svg, i[data-lucide] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Ozone Mastering Module Styles */
|
||||
.oz-panel {
|
||||
background: linear-gradient(180deg, #131822 0%, #0d1118 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||
}
|
||||
.oz-card {
|
||||
background: #18202c;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.oz-card-active {
|
||||
background: #1e2a3a;
|
||||
border-color: #38bdf8;
|
||||
box-shadow: 0 0 12px rgba(56, 189, 248, 0.2);
|
||||
}
|
||||
.knob-container { touch-action: none; user-select: none; }
|
||||
.knob-dial {
|
||||
transition: transform 0.05s ease-out;
|
||||
box-shadow: inset 0 1px 3px rgba(255,255,255,0.15), 0 3px 8px rgba(0,0,0,0.8);
|
||||
}
|
||||
.oz-scrollbar::-webkit-scrollbar { width: 4px; height: 4px; }
|
||||
.oz-scrollbar::-webkit-scrollbar-track { background: #080b12; }
|
||||
.oz-scrollbar::-webkit-scrollbar-thumb { background: #1e293b; border-radius: 2px; }
|
||||
.oz-scrollbar::-webkit-scrollbar-thumb:hover { background: #334155; }
|
||||
.oz-font-mono { font-family: 'JetBrains Mono', monospace; }
|
||||
|
||||
/* Master & Track Strip Console Styles (from md/47, md/48) */
|
||||
.strip-bg {
|
||||
background: linear-gradient(180deg, #2f2f2f 0%, #222222 100%);
|
||||
border: 1px solid #141414;
|
||||
box-shadow: inset 1px 1px 0 rgba(255,255,255,0.08), 0 8px 24px rgba(0,0,0,0.8);
|
||||
}
|
||||
.strip-bg-track {
|
||||
background: linear-gradient(180deg, #2c2c2c 0%, #1e1e1e 100%);
|
||||
border: 1px solid #111111;
|
||||
box-shadow: inset 1px 1px 0 rgba(255,255,255,0.08), 0 10px 30px rgba(0,0,0,0.85);
|
||||
}
|
||||
|
||||
.fader-track {
|
||||
background: #121212;
|
||||
box-shadow: inset 1px 1px 3px rgba(0,0,0,0.9), 1px 1px 0 rgba(255,255,255,0.05);
|
||||
}
|
||||
.fader-track-bg {
|
||||
background: #101010;
|
||||
box-shadow: inset 1px 1px 3px rgba(0,0,0,0.9), 1px 1px 0 rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
/* DAW Metallic Button */
|
||||
.btn-daw {
|
||||
background: linear-gradient(180deg, #444 0%, #2a2a2a 100%);
|
||||
border: 1px solid #1a1a1a;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.15), 0 2px 4px rgba(0,0,0,0.5);
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
.btn-daw:hover {
|
||||
background: linear-gradient(180deg, #555 0%, #333 100%);
|
||||
}
|
||||
.btn-daw:active {
|
||||
box-shadow: inset 0 2px 4px rgba(0,0,0,0.8);
|
||||
transform: translateY(0.5px);
|
||||
}
|
||||
|
||||
/* Teal Active State (FX & Power Button) */
|
||||
.btn-teal-active {
|
||||
background: linear-gradient(180deg, #10b981 0%, #047857 100%) !important;
|
||||
border-color: #34d399 !important;
|
||||
color: #ffffff !important;
|
||||
box-shadow: 0 0 10px rgba(16, 185, 129, 0.6) !important;
|
||||
}
|
||||
|
||||
/* Mute Active State */
|
||||
.btn-mute-active {
|
||||
background: linear-gradient(180deg, #ef4444 0%, #991b1b 100%) !important;
|
||||
border-color: #f87171 !important;
|
||||
color: #ffffff !important;
|
||||
box-shadow: 0 0 8px rgba(239, 68, 68, 0.6) !important;
|
||||
}
|
||||
|
||||
/* Solo Active State */
|
||||
.btn-solo-active {
|
||||
background: linear-gradient(180deg, #eab308 0%, #854d0e 100%) !important;
|
||||
border-color: #fde047 !important;
|
||||
color: #000000 !important;
|
||||
box-shadow: 0 0 8px rgba(234, 179, 8, 0.6) !important;
|
||||
}
|
||||
|
||||
/* Mono Active State */
|
||||
.btn-mono-active {
|
||||
background: linear-gradient(180deg, #3b82f6 0%, #1e40af 100%) !important;
|
||||
border-color: #60a5fa !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Record Arm Active State */
|
||||
.btn-arm-active {
|
||||
background: radial-gradient(circle, #ef4444 0%, #7f1d1d 100%) !important;
|
||||
border-color: #fca5a5 !important;
|
||||
box-shadow: 0 0 12px rgba(239, 68, 68, 0.9), inset 0 1px 2px rgba(255,255,255,0.8) !important;
|
||||
animation: pulse-red 1.2s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes pulse-red {
|
||||
0% { box-shadow: 0 0 6px rgba(239, 68, 68, 0.6); }
|
||||
100% { box-shadow: 0 0 16px rgba(239, 68, 68, 1); }
|
||||
}
|
||||
|
||||
/* Custom Vertical Range Fader Slider */
|
||||
input[type=range].fader-slider {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
writing-mode: vertical-lr;
|
||||
direction: rtl;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
min-height: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
input[type=range].fader-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
height: 24px;
|
||||
width: 22px;
|
||||
background: linear-gradient(180deg, #e2e8f0 0%, #64748b 45%, #1e293b 50%, #94a3b8 100%);
|
||||
border: 1px solid #000000;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.9), inset 0 1px 1px rgba(255,255,255,0.9);
|
||||
}
|
||||
input[type=range].fader-slider::-moz-range-thumb {
|
||||
height: 24px;
|
||||
width: 22px;
|
||||
background: linear-gradient(180deg, #e2e8f0 0%, #64748b 45%, #1e293b 50%, #94a3b8 100%);
|
||||
border: 1px solid #000000;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.9), inset 0 1px 1px rgba(255,255,255,0.9);
|
||||
}
|
||||
|
||||
/* Media Explorer: horizontal volume slider + selected file row */
|
||||
input[type=range].me-fader-slider {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: #111;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #555;
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type=range].me-fader-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
height: 16px;
|
||||
width: 12px;
|
||||
background: linear-gradient(180deg, #e2e8f0 0%, #64748b 50%, #1e293b 100%);
|
||||
border: 1px solid #000;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.5);
|
||||
cursor: pointer;
|
||||
}
|
||||
.file-row-selected {
|
||||
background-color: #3399ff !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.file-row-selected .file-icon { color: #ffffff !important; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="h-screen w-screen flex flex-col">
|
||||
<div id="root" class="h-full w-full flex flex-col"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user