Merge main (V45.1-32 fixes: ReferenceError guard, crackle/solo/seek fixes) vao standalone-shm-bridge (giu FILL 96)

This commit is contained in:
2026-09-01 16:03:28 +07:00
25 changed files with 1291 additions and 287 deletions
+7 -3
View File
@@ -1,13 +1,10 @@
import os, sys, uuid, json, tempfile, subprocess, time as _time, threading, asyncio, logging
import numpy as np
import soundfile as sf
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, BackgroundTasks, Query, WebSocket, WebSocketDisconnect
from fastapi.responses import FileResponse
from pydantic import BaseModel
from typing import Optional, Any
from app.config import settings
from app.core.vst_engine import PluginManager, HAS_PYFLUIDSYNTH, render_soundfont_midi_to_audio, RENDER_LOCK, _render_locked
from app.core.render_engine import PythonRenderEngine, _find_sf2_path, _find_default_sf2
from app.core.soundfont_inspector import SoundFontInspector
from app.core.soundfont_converter import SoundFontConverter
from app.core.soundfont_scanner import SoundFontAutoScanner
@@ -593,6 +590,7 @@ async def delete_soundfont(sf_id: str, current_user: dict = Depends(get_current_
@router.get("/soundfonts/download/{sf_id}")
async def download_soundfont_asset(sf_id: str):
from app.core.render_engine import _find_sf2_path, _find_default_sf2
clean_id = sf_id.replace("sf_", "") if sf_id.startswith("sf_") else sf_id
# Cũng tìm trong static/soundfonts (font bundled theo deployment) — trước
# đây chỉ UPLOAD + SYSTEM → font bundled 404 → incognito (IndexedDB rỗng)
@@ -986,6 +984,7 @@ class FxRenderRequest(BaseModel):
@router.post("/fx-render")
async def fx_render(req: FxRenderRequest, current_user: dict = Depends(get_current_user)):
"""Áp fx_chain lên WAV đầu vào → WAV mới trong PROCESSED_DIR."""
import soundfile as sf
enforce_password_changed(current_user)
src = (req.input_path or "").strip()
if not src:
@@ -1265,6 +1264,8 @@ async def soundfont_render(req: SoundfontRenderRequest, current_user: dict = Dep
Khác FluidSynthWASM (docker): âm render server-side bằng pyfluidsynth, trả
WAV để client play. Dùng cho: preview/keyboard, play MIDI item khi chạy
transport ở môi trường standalone."""
from app.core.render_engine import _find_sf2_path, _find_default_sf2
import soundfile as sf
enforce_password_changed(current_user)
if not req.notes:
raise HTTPException(status_code=400, detail="Chưa có nốt nhạc để render")
@@ -1320,6 +1321,9 @@ def render_project(
current_user: dict = Depends(get_current_user)
):
enforce_password_changed(current_user)
from app.core.render_engine import PythonRenderEngine
import soundfile as sf
import numpy as np
engine = PythonRenderEngine()
# Prevent path traversal: strip directory components; force .wav/.ogg.
safe_name = os.path.basename((req.output_filename or "render_output.wav").replace("\\", "/"))
+4 -3
View File
@@ -23,10 +23,7 @@ import threading
import time
import uuid
import numpy as np
from app.config import settings
from app.core.native_render import find_fx_bridge_exe, find_juce_fx_bridge_exe
FXRT_MAGIC = 0x46585254
FXRT_BLOCK = 256
@@ -91,6 +88,7 @@ class FxRealtimeSession:
"""Một session realtime: SHM + subprocess fx_vst_bridge + I/O block."""
def __init__(self, fx_chain, sample_rate, block_size=FXRT_BLOCK):
import numpy as np
from multiprocessing import shared_memory
self.fx_chain = fx_chain or []
self.sample_rate = int(sample_rate)
@@ -163,6 +161,7 @@ class FxRealtimeSession:
return out
def _spawn(self):
from app.core.native_render import find_fx_bridge_exe, find_juce_fx_bridge_exe
juce_exe = os.getenv("SF_JUCE_FX_BRIDGE_PATH", "").strip()
# G3: mặc định dùng JUCE bridge (latency graph thật + processed samples);
# fallback bridge cũ khi không có exe JUCE (regression an toàn).
@@ -265,6 +264,7 @@ class FxRealtimeSession:
"""Gom FXRT_IN_SLOTS frame vào pending, đủ 4 → publish atomic vào ring
(ghi data toàn bộ slot trước, sau đó in_write += n 1 lần). Ring đầy khi
publish → drop batch cũ nhất (giữ latency thấp)."""
import numpy as np
arr = np.frombuffer(interleaved, dtype=np.float32)
if arr.size != self.block_size * 2:
return
@@ -333,6 +333,7 @@ class FxRealtimeSession:
self.stats['flush'] += 1
def read_output(self):
import numpy as np
"""Drain output ring → list bytes (mỗi block 2048 bytes interleaved)."""
out = []
with self._lock:
+3 -1
View File
@@ -1,9 +1,9 @@
# SonicForge Studio VST / VSTi Engine Service
from __future__ import annotations
import os
import json
import re
import time as _time
import numpy as np
import functools
import threading
from ctypes import c_char_p
@@ -52,6 +52,7 @@ def render_soundfont_midi_to_audio(midi_events: list, sf_path: str, bank: int =
Xử lí đúng note CHỒNG nhau + đuôi release (event-stream noteon/noteoff).
Trả stereo float32 (2, N). Raise RuntimeError nếu pyfluidsynth thiếu / SF
không load được."""
import numpy as np
import fluidsynth as _fs
settings = _fs.new_fluid_settings()
_fs.fluid_settings_setnum(settings, b'synth.sample-rate', float(sr))
@@ -112,6 +113,7 @@ def render_soundfont_midi_to_audio(midi_events: list, sf_path: str, bank: int =
@_render_locked
def render_midi_events_to_audio(midi_events: list, sr: int = 44100, bpm: float = 120.0, instrument: str = 'synth') -> np.ndarray:
import numpy as np
beat_duration_sec = 60.0 / max(30.0, bpm)
max_duration_sec = 2.0
for event in midi_events:
+525 -138
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+7 -2
View File
@@ -16,6 +16,7 @@
var _ctx = null;
var _initialized = false;
var _useWorklet = false;
var _workletStats = null; // stats tu worklet (overrun/underrun) — getDiag
var _dest = null; // destination gain node da noi (chong connect trung)
function _getCtx() {
@@ -31,7 +32,7 @@
console.warn('[BridgeAudioNode] AudioWorklet unavailable — ScriptProcessor fallback');
return false;
}
var url = '/static/js/services/bridgeAudioWorklet.js?v=202608272301';
var url = '/static/js/services/bridgeAudioWorklet.js?v=202608311858';
_ctx.audioWorklet.addModule(url).then(function () {
if (!_initialized) return; // disconnect() happened while loading
if (_useWorklet) return; // already set up (init retry)
@@ -42,6 +43,9 @@
outputChannelCount: [2]
});
_workletNode.connect(_gainNode);
_workletNode.port.onmessage = function (ev) {
if (ev.data && ev.data.type === 'stats') _workletStats = ev.data;
};
_useWorklet = true;
// drain pre-buffer into worklet (order preserved)
while (_preBuffer.length) {
@@ -168,7 +172,8 @@
hasWorkletNode: !!_workletNode,
preBufferLen: _preBuffer.length,
chunksLen: _chunks.length,
gain: _gainNode ? _gainNode.gain.value : -1
gain: _gainNode ? _gainNode.gain.value : -1,
ws: _workletStats
};
}
};
+39 -8
View File
@@ -1,22 +1,30 @@
// app/static/js/services/bridgeAudioWorklet.js
// VSTi crackle fix: audio sink chay tren audio thread (khong jank main thread).
// - Ring block 256 mau, toi da 8 block (~42ms @48k) — hap thu jitter pump
// nhung gioi han backlog: 64 block (~371ms) tao tre ~0.3-0.4s khi play/
// keyboard preview (audio moi phai cho ring cu chay het).
// - Ring block 256 mau, toi da 32 block (~170ms @48k) — hap thu jitter pump
// (V45.27: 16->32 de dung crackle VST2 loop/tremolo — render loop C++ bi
// stall khi instrument xu li loop/tremolo nang CPU; 8->16 truoc do da dung
// tremolo crackle, 16 van chua du cho stall >85ms). Flush o play start
// (app.jsx) xoa ring nen do tre khong tang: ring thuong can o preview.
// - Underrun: fade ve 0 thay vi click (hard silence).
// - Resume sau silence: fade vao tu 0.
// - Overrun: drop block cu nhat + crossfade 64 mau tai diem drop (het click).
// - Stats: dem overrun/underrun, gui ve main thread moi ~0.5s — kiem tra
// con crackle nao la do ring can (underrun) hay ring tran (overrun).
class BridgeAudioSinkProcessor extends AudioWorkletProcessor {
constructor() {
super();
this._ring = [];
this._MAX = 8;
this._MAX = 32;
this._gain = 1.0;
this._lastOutL = 0.0;
this._lastOutR = 0.0;
this._skipFade = 0; // >0: crossfade dang chay sau overrun drop
this._skipFade = 0; // >0: con lai cua crossfade dang chay sau overrun drop
this._fadeLen = 0; // tong chieu dai crossfade hien tai (cap 1024)
this._skipFromL = 0.0;
this._skipFromR = 0.0;
this._wasUnderrun = false;
this._statAccum = 0;
this._stats = { overruns: 0, underruns: 0 };
this.port.onmessage = (e) => {
const m = e.data;
if (!m) return;
@@ -24,13 +32,19 @@ class BridgeAudioSinkProcessor extends AudioWorkletProcessor {
this._ring.push({ l: m.l, r: m.r });
while (this._ring.length > this._MAX) {
// overrun: bo block cu nhat; lan consume toi se crossfade ranh gioi
// V45.32: crossfade tich luy — moi block drop cong 256 mau fade, cap
// 1024 (~21ms) thay vi co dinh 64 mau (1.3ms qua ngan -> blip/wow khi
// drop loat block o chord nhieu voice = crackle VST2 tremolo/loop)
this._ring.shift();
this._skipFade = 64;
this._skipFade = Math.min(this._skipFade + 256, 1024);
this._fadeLen = this._skipFade;
this._stats.overruns++;
}
} else if (m.type === 'flush') {
this._ring.length = 0;
this._gain = 1.0;
this._skipFade = 0;
this._fadeLen = 0;
}
};
}
@@ -43,12 +57,14 @@ class BridgeAudioSinkProcessor extends AudioWorkletProcessor {
let w = 0;
while (w < n) {
if (this._ring.length) {
if (this._wasUnderrun) this._wasUnderrun = false;
const blk = this._ring[0];
const avail = blk.l.length;
const take = Math.min(avail, n - w);
if (this._skipFade > 0) {
const done = this._fadeLen - this._skipFade;
for (let i = 0; i < take; i++) {
const t = i / 64;
const t = (done + i) / this._fadeLen;
L[w + i] = (this._lastOutL * (1 - t) + blk.l[i] * t) * this._gain;
R[w + i] = (this._lastOutR * (1 - t) + blk.r[i] * t) * this._gain;
}
@@ -70,7 +86,8 @@ class BridgeAudioSinkProcessor extends AudioWorkletProcessor {
blk.r = blk.r.subarray(take);
}
} else {
// underrun: fade ve 0 (khong click)
// underrun: fade ve 0 (khong click) — dem 1 lan moi lan roi vao trang thai trong
if (!this._wasUnderrun) { this._wasUnderrun = true; this._stats.underruns++; }
for (let i = w; i < n; i++) {
this._gain = Math.max(0, this._gain - 1.0 / 256.0);
L[i] = this._lastOutL * this._gain;
@@ -81,6 +98,20 @@ class BridgeAudioSinkProcessor extends AudioWorkletProcessor {
w = n;
}
}
// Stats: gui ve main thread moi ~0.5s (24000 mau @48k) — log overrun/underrun
this._statAccum += n;
if (this._statAccum >= 24000) {
this._statAccum = 0;
try {
this.port.postMessage({
type: 'stats',
overruns: this._stats.overruns,
underruns: this._stats.underruns,
ring: this._ring.length,
max: this._MAX
});
} catch (e) {}
}
return true;
}
}
@@ -17,8 +17,13 @@
/** D10: UI subscribes to bridge connection changes. cb({connected}) or null to clear. */
onStatusChange: function (cb) { this._statusCb = cb; },
_notifyStatus: function (connected) {
this.isBridgeConnected = !!connected;
if (this._statusCb) this._statusCb({ connected: !!connected });
connected = !!connected;
// Loop guard (V45.7): poller queryStatus moi 400ms tung goi cb du connected
// khong doi -> App reset map + reload toan bo track -> mark loading lai ->
// poller lai thay loading -> LOAD skip vo han. Chi fire khi TRANG THAI DOI.
if (this.isBridgeConnected === connected) return;
this.isBridgeConnected = connected;
if (this._statusCb) this._statusCb({ connected: connected });
},
/** Fix C: re-establish bridge routing once audio frames arrive again
+23 -2
View File
@@ -6,6 +6,10 @@
var MELODIC_CHANNELS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14]; // skip 9 (percussion); 15 reserved for Media Explorer preview
var _lastTimelineDbgTs = 0;
// V45.26: dem note live dang giu + lan live event gan nhat — quyet dinh co
// clear master FX outQueue khong (chi idle->active; KHONG khi dang phat lien tuc).
var _liveHeldCount = 0;
var _lastLiveEventAt = 0;
var router = {
bridgeConnected: false,
@@ -59,7 +63,23 @@
// V31: live note (ARM keyboard preview) — clear master FX outQueue backlog
// (idle block ~341ms) truoc khi note moi render; timeline note (live=false)
// KHONG clear — tranh drop audio dang phat.
if (opts.live && opts.cmd === 'NOTE_ON') { try { if (window.__clearMasterFxQueue) window.__clearMasterFxQueue(); } catch (e) {} }
// V45.26: chi clear khi THAT SU idle -> active (khong con note dang giu
// VA im lang >500ms). Truoc day clear MOI note-on live -> rapid 1/8 lien
// tuc: moi note wipe outQueue (dang chua tail note truoc + resume fade-in)
// -> wobble/crackle (bug user). Note dang ngan / chuoi note nhanh -> giu
// queue -> tail note truoc khong bi cat.
if (opts.live) {
try {
var _nowEv = (window.performance && performance.now) ? performance.now() : Date.now();
if (opts.cmd === 'NOTE_ON') {
if (_liveHeldCount === 0 && (_nowEv - _lastLiveEventAt) > 500 && window.__clearMasterFxQueue) window.__clearMasterFxQueue();
_liveHeldCount++;
} else if (opts.cmd === 'NOTE_OFF') {
if (_liveHeldCount > 0) _liveHeldCount--;
}
_lastLiveEventAt = _nowEv;
} catch (e) {}
}
var cmd = opts.cmd, ch = opts.channel, pitch = opts.pitch;
var vel = (opts.velocity === undefined ? 1.0 : opts.velocity);
var sampleOffset = opts.sampleOffset || 0;
@@ -73,7 +93,7 @@
var _nowMs = (window.performance && performance.now) ? performance.now() : Date.now();
if (window.__dbgLog && (opts.live || _nowMs - _lastTimelineDbgTs > 200)) {
if (!opts.live) _lastTimelineDbgTs = _nowMs;
window.__dbgLog('midi_push', 'cmd=' + cmd + ' ch=' + ch + ' live=' + (opts.live ? 1 : 0) + ' vel=' + Math.round((vel || 0) * 100) + (window.__dbgBridgeGain ? ' ' + window.__dbgBridgeGain() : '') + ' q=' + (window.__fxRtQueueLen ? window.__fxRtQueueLen() : 'n/a'));
window.__dbgLog('midi_push', 'cmd=' + cmd + ' ch=' + ch + ' p=' + (pitch === undefined ? 'n/a' : pitch) + ' live=' + (opts.live ? 1 : 0) + ' vel=' + Math.round((vel || 0) * 100) + (window.__dbgBridgeGain ? ' ' + window.__dbgBridgeGain() : '') + ' q=' + (window.__fxRtQueueLen ? window.__fxRtQueueLen() : 'n/a'));
}
} catch (e) {}
if (this.isBridgeActive()) {
@@ -85,6 +105,7 @@
/** Stop/panic -> bridge transport panic (flush all notes) + fallback local stopAll. */
panic: function () {
_liveHeldCount = 0;
if (this.isBridgeActive() && window.NativeBridgeService.transport) {
window.NativeBridgeService.transport('panic');
}
+60 -4
View File
@@ -57,6 +57,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
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.wrapMode = false; // V45.3: dang trong wrap (clear_queue loop) — resume underrun bo flush + re-buffer nho
this.WRAP_FADE = 256; // V42: crossfade 256 mẫu (~5.3ms @48k) tại seam wrap — che click jump nội dung end→start (FX path)
this.fadeFromL = 0.0; // mẫu cố định khi fade-out underrun (fade tuyến tính tới 0)
this.fadeFromR = 0.0;
@@ -79,6 +80,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
this.resumeFadeIn = true;
this.resumeFill = this.RESUME_FILL;
this.silenced = false;
this.wrapMode = 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;
@@ -88,6 +90,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
return;
}
if (d && d.type === 'clear_queue') {
this.clearMode = d.mode; // V45.19: nho mode de tag fx_seek_first tai dbgPlayFirst
// 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,
@@ -110,9 +113,51 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
// G3: align processed-tail — tail pass cũ còn in-flight trong bridge
// (FX latency thật từ engine) chưa về queue; đếm đủ block tới seam
// (block đầu pass mới lên front) → crossfade đúng chỗ jump end→start.
// V45.2 (option B): cắt bớt tail cũ giữ KEEP block thay vì giữ nguyên
// FILL 24 (~128ms) → downbeat pass mới không trễ ~110ms. skipFade
// crossfade từ lastOut che jump cắt front; wrapFadeBlocks đếm lại từ
// KEEP để seam crossfade vẫn đúng lúc.
// V45.3: KEEP 4 -> 16 (~85ms): KEEP 4 (~21ms) engine (stall bridge
// 68-138ms sau transport play) khong refill kip -> queue can -> underrun
// -> flush + re-buffer FILL 24 = gap ~110-235ms moi wrap (WORSE hon
// truoc V45.2: downbeat tre 106-128ms nhung khong gap). KEEP 16 che
// stall dien hinh 68-82ms lien tuc (khong gap, downbeat ~85ms); stall
// dai hon roi vao nhanh resume wrap (bo flush + resumeFill 8).
const KEEP = 16;
this.wrapMode = true;
if (this.outQueue.length > KEEP) {
const head = this.outQueue.length - KEEP;
this.outQueue.splice(0, head);
this.outPos = 0;
if (this.playing) {
this.skipFade = this.WRAP_FADE;
this.skipFadeTotal = this.WRAP_FADE;
this.skipFromL = this.lastOutL;
this.skipFromR = this.lastOutR;
}
}
this.wrapFadeBlocks = this.outQueue.length + Math.ceil((this.latencySamples || 0) / this.BLOCK);
return;
}
if (d.mode === 'seek') {
// V45.18: seek khi play master FX active — wipe queue cu (audio cu
// choi tiep 0-50ms sau click) + resume nhanh RESUME_FILL 8 (~43ms)
// thay vi FILL 24 (~128ms) — het "am tre 1 beat" (hit dau roi vao
// vung cam 128ms). Giong resume wrap V45.4 (proven khong gap).
this.outQueue.length = 0;
this.outPos = 0;
this.playing = false;
this.wrapFadeBlocks = -1;
this.wrapMode = false;
this.resumeFadeIn = true;
this.resumeFill = 8;
this.silenced = false;
this.fadeFromL = this.lastOutL;
this.fadeFromR = this.lastOutR;
this.holdFadeLeft = this.HOLD_FADE * this.BLOCK;
this.port.postMessage({ type: 'dbg', tag: 'fx_seek_clear', t: Math.round(currentTime * 1000), q: 0 });
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
@@ -123,6 +168,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
this.outPos = 0;
this.playing = false;
this.wrapFadeBlocks = -1; // V42: wipe — hủy wrap crossfade chờ (không có tail cũ)
this.wrapMode = false; // V45.3: play/seek — het context wrap
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
@@ -300,21 +346,31 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
// tại (giữa fade) — hết jump.
const wasInterrupted = !this.resumeFadeIn && (this.holdFadeLeft > 0 || this.silenced) && this.hasStarted;
if (wasInterrupted) {
// V45.3: wrap (loop restart) — engine da transport('play') tu main
// (D17): block moi = audio pass moi, KHONG phai backlog stale (boi
// worklet flush gui 'flush' xuong engine ring = lam mat tac dung D17
// skip-flush o stopAllPlayback -> gap giua seam). Bo flush (round-trip)
// + re-buffer nho hon FILL 24 (~128ms) -> resume sau stall som hon
// (~43ms) -> gap wrap giam tu ~110-235ms xuong con vai chuc ms.
const _wrap = this.wrapMode;
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' });
this.resumeFill = _wrap ? 8 : this.FILL;
this.flushPending = _wrap ? false : true;
if (!_wrap) this.port.postMessage({ type: 'flush' });
i--;
continue;
}
this.playing = true;
this.outPos = 0;
this.holdFadeLeft = 0;
this.wrapMode = false; // V45.3: resume xong — ve che do thuong (underrun sau nay flush nhu cu)
if (this.dbgPlayFirst) {
this.dbgPlayFirst = false;
this.port.postMessage({ type: 'dbg', tag: 'fx_play_first', t: Math.round(currentTime * 1000), q: this.outQueue.length });
// V45.19 Bug 1b: seek -> tag fx_seek_first (main thread snap khong can
// guard 50ms — seek resume nhanh ~43ms < 50ms). Wrap/play -> fx_play_first.
this.port.postMessage({ type: 'dbg', tag: (this.clearMode === 'seek' ? 'fx_seek_first' : 'fx_play_first'), t: Math.round(currentTime * 1000), q: this.outQueue.length, m: this.clearMode || '' });
}
if (this.resumeFadeIn) {
this.resumeFadeIn = false;
+21 -21
View File
@@ -32,27 +32,27 @@
<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=202608281631" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<script src="/static/js/services/fluidsynthLoader.js?v=202608290945201545"></script>
<script src="/static/js/services/runtime.js?v=202608290945"></script>
<script src="/static/js/services/api.js?v=202608290945"></script>
<script src="/static/js/services/audioEngine.js?v=202608290945"></script>
<script src="/static/js/services/storage.js?v=202608290945"></script>
<script src="/static/js/services/soundfontStorage.js?v=202608290945"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202608290945"></script>
<script src="/static/js/services/nativeBridgeService.js?v=202608291510"></script>
<script src="/static/js/services/bridgeAudioNode.js?v=202608311858"></script>
<script src="/static/js/services/unifiedMidiRouter.js?v=202608311756"></script>
<script src="/static/js/services/audioRoutingEngine.js?v=202608290945"></script>
<script src="/static/js/services/aiGateway.js?v=202608290945"></script>
<script src="/static/js/services/dawCommandDispatcher.js?v=202608290945"></script>
<script src="/static/js/services/pianoRollTabService.js?v=202608290945"></script>
<script src="/static/js/services/ghostNoteExtractor.js?v=202608290945"></script>
<script src="/static/js/services/midiExtractor.js?v=202608290945"></script>
<script src="/static/js/services/promptTemplateManager.js?v=202608290945"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202608290945"></script>
<script src="/static/js/services/chordTheory.js?v=202608290945"></script>
<script src="/static/js/app.precompiled.js?v=202608311858" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202608290945">
<style>
:root {
--right-sidebar-width: 320px;