chore: bo target/ root (artifact lan) khoi index + gitignore

This commit is contained in:
2026-08-29 17:50:45 +07:00
parent 788a371f72
commit a7630f0e6d
4 changed files with 3 additions and 3463 deletions
+3
View File
@@ -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>