feat: tách luồng âm instrument theo môi trường — docker dùng FluidSynthWASM, standalone dùng native pyfluidsynth (+Carla VSTi)

- runtime: detect()/capabilities() trả environment (docker|standalone)
- vst_engine: render_soundfont_midi_to_audio (pyfluidsynth native, event-stream, release tail)
- plugins: POST /soundfont-render — render MIDI notes -> WAV (auth, fallback default SF)
- frontend: SonicRuntime.environment + dataset.environment; app.jsx route preview/transport:
  standalone+SF -> soundfont-render WAV (playNativeSfNote/scheduleNativeSfItem),
  docker giữ WASM, VSTi standalone giữ Carla bridge
This commit is contained in:
2026-08-10 20:38:54 +07:00
parent e89a594b7c
commit a0d8725541
8 changed files with 447 additions and 48 deletions
+69 -2
View File
@@ -6,8 +6,8 @@ from fastapi.responses import FileResponse
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional, Any from typing import Optional, Any
from app.config import settings from app.config import settings
from app.core.vst_engine import PluginManager, HAS_PEDALBOARD, HAS_PYFLUIDSYNTH, apply_preset_to_plugin from app.core.vst_engine import PluginManager, HAS_PEDALBOARD, HAS_PYFLUIDSYNTH, apply_preset_to_plugin, render_soundfont_midi_to_audio
from app.core.render_engine import PythonRenderEngine from app.core.render_engine import PythonRenderEngine, _find_sf2_path, _find_default_sf2
from app.core.soundfont_inspector import SoundFontInspector from app.core.soundfont_inspector import SoundFontInspector
from app.core.soundfont_converter import SoundFontConverter from app.core.soundfont_converter import SoundFontConverter
from app.core.soundfont_scanner import SoundFontAutoScanner from app.core.soundfont_scanner import SoundFontAutoScanner
@@ -992,6 +992,73 @@ def _render_midi_notes_pedalboard(instrument_id: str, notes: list, bpm: float,
return out_path, buf.shape[1] / float(sample_rate) return out_path, buf.shape[1] / float(sample_rate)
class SoundfontRenderRequest(BaseModel):
"""Render MIDI notes → WAV bằng soundfont THẬT qua native FluidSynth
(pyfluidsynth) — luồng âm instrument cho môi trường STANDALONE (xử lí
trực tiếp trên OS; docker dùng FluidSynthWASM ở client)."""
soundfont_id: Optional[str] = None
soundfont_path: Optional[str] = None
bank: int = 0
program: int = 0
notes: list = []
bpm: float = 120.0
sample_rate: int = 44100
@router.post("/soundfont-render")
async def soundfont_render(req: SoundfontRenderRequest, current_user: dict = Depends(get_current_user)):
"""Preview/play MIDI notes bằng âm soundfont thật — native FluidSynth trên OS.
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."""
enforce_password_changed(current_user)
if not req.notes:
raise HTTPException(status_code=400, detail="Chưa có nốt nhạc để render")
if not HAS_PYFLUIDSYNTH:
raise HTTPException(status_code=501, detail="pyfluidsynth không khả dụng — không render được soundfont native")
# Resolve path: soundfont_path → soundfont_id (upload/user dirs) → default
sf_path = (req.soundfont_path or "").strip()
if not sf_path and req.soundfont_id:
sf_path = _find_sf2_path(req.soundfont_id)
if not sf_path or not os.path.exists(sf_path):
sf_path = _find_default_sf2()
if not sf_path or not os.path.exists(sf_path):
raise HTTPException(status_code=404, detail="SoundFont không tìm thấy — hãy thêm soundfont vào Plugin Manager")
midi_events = [{
"note": int(n.get("pitch", 60)),
"start_beat": float(n.get("start_beat", 0)),
"duration_beats": float(n.get("duration_beats", 1)),
"velocity": int(float(n.get("velocity", 0.8)) * 127),
} for n in req.notes]
out_path = os.path.join(settings.PROCESSED_DIR, f"sf_{uuid.uuid4().hex[:10]}.wav")
try:
audio = render_soundfont_midi_to_audio(
midi_events, sf_path,
bank=req.bank, program=req.program,
sr=req.sample_rate, bpm=req.bpm,
)
sf.write(out_path, audio.T, req.sample_rate)
return {
"success": True,
"file_id": os.path.basename(out_path),
"url": f"/static/audio/processed/{os.path.basename(out_path)}",
"path": out_path,
"duration_sec": round(audio.shape[1] / float(req.sample_rate), 3),
"render_mode": "native-fluidsynth",
"soundfont": os.path.basename(sf_path),
}
except HTTPException:
raise
except Exception as e:
try:
if os.path.exists(out_path):
os.remove(out_path)
except Exception:
pass
raise HTTPException(status_code=500, detail=f"Render soundfont thất bại: {e}")
class CarlaPlayNotesRequest(BaseModel): class CarlaPlayNotesRequest(BaseModel):
"""Phát dãy MIDI notes qua Carla bridge (OSC, realtime) — preview khi """Phát dãy MIDI notes qua Carla bridge (OSC, realtime) — preview khi
pedalboard không render được plugin (VD VST2). Cần Carla đang chạy với pedalboard không render được plugin (VD VST2). Cần Carla đang chạy với
+4 -1
View File
@@ -332,10 +332,12 @@ def detect() -> dict:
platform = _detect_platform() platform = _detect_platform()
runtime = _detect_runtime() runtime = _detect_runtime()
carla = find_carla() if runtime == "desktop" else "" carla = find_carla() if runtime == "desktop" else ""
docker = _in_docker()
return { return {
"platform": platform, "platform": platform,
"runtime": runtime, # "desktop" | "headless" "runtime": runtime, # "desktop" | "headless"
"docker": _in_docker(), "docker": docker,
"environment": "docker" if docker else "standalone", # luồng âm instrument: docker→FluidSynthWASM, standalone→native OS
"has_display": _has_display(), "has_display": _has_display(),
"tauri_bridge": _tauri_bridge_ready(), "tauri_bridge": _tauri_bridge_ready(),
"carla_path": carla, "carla_path": carla,
@@ -375,6 +377,7 @@ def capabilities() -> dict:
"runtime": d["runtime"], "runtime": d["runtime"],
"platform": d["platform"], "platform": d["platform"],
"docker": d["docker"], "docker": d["docker"],
"environment": d["environment"],
"features": features, "features": features,
"default_dirs": { "default_dirs": {
"vst": d["default_vst_dirs"], "vst": d["default_vst_dirs"],
+68
View File
@@ -8,6 +8,74 @@ from ctypes import c_char_p
def midi_note_to_freq(note_number: int) -> float: def midi_note_to_freq(note_number: int) -> float:
return 440.0 * (2.0 ** ((note_number - 69) / 12.0)) return 440.0 * (2.0 ** ((note_number - 69) / 12.0))
def render_soundfont_midi_to_audio(midi_events: list, sf_path: str, bank: int = 0,
program: int = 0, sr: int = 44100, bpm: float = 120.0) -> np.ndarray:
"""Native FluidSynth (pyfluidsynth) — render MIDI events bằng soundfont THẬT.
Dùng cho môi trường standalone: âm instrument xử lí trực tiếp trên OS
(backend pyfluidsynth), không phải FluidSynthWASM trong browser.
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 fluidsynth as _fs
settings = _fs.new_fluid_settings()
_fs.fluid_settings_setnum(settings, b'synth.sample-rate', float(sr))
synth = _fs.new_fluid_synth(settings)
try:
fid = _fs.fluid_synth_sfload(synth, os.fspath(sf_path).encode('utf-8'), 1)
if fid == -1:
raise RuntimeError(f"SoundFont load failed: {sf_path}")
_fs.fluid_synth_program_select(synth, 0, fid, int(bank), int(program))
beat_sec = 60.0 / max(30.0, bpm)
events = []
total_sec = 0.0
for ev in midi_events:
start_s = int(float(ev.get('start_beat', 0)) * beat_sec * sr)
dur_s = int(float(ev.get('duration_beats', 1)) * beat_sec * sr)
note = int(ev.get('note', 60))
vel = min(127, max(1, int(float(ev.get('velocity', 100)))))
events.append((start_s, 'on', note, vel))
events.append((start_s + dur_s, 'off', note, 0))
end_sec = (start_s + dur_s) / float(sr)
if end_sec > total_sec:
total_sec = end_sec
events.sort(key=lambda e: e[0])
total_sec = max(total_sec, 0.25) + 0.5 # đuôi reverb/release
n = int(total_sec * sr)
out = np.zeros((2, n), dtype=np.float32)
pos = 0
def _render_until(target: int):
"""Render từ pos tới target, CỘNG vào out (giữ âm đang ngân)."""
nonlocal pos
target = min(target, n)
while pos < target:
chunk = min(target - pos, 44100)
block = _fs.fluid_synth_write_s16_stereo(synth, chunk)
b = block.astype(np.float32).reshape(-1, 2).T / 32768.0
take = min(chunk, b.shape[1], target - pos)
if take > 0:
out[:, pos:pos + take] += b[:, :take]
pos += take
for t, kind, note, vel in events:
if t > pos:
_render_until(t)
if pos >= n:
break
if kind == 'on':
_fs.fluid_synth_noteon(synth, 0, note, vel)
else:
_fs.fluid_synth_noteoff(synth, 0, note)
_render_until(n) # đuôi release của notes cuối
return out
finally:
try:
_fs.delete_fluid_synth(synth)
except Exception:
pass
def render_midi_events_to_audio(midi_events: list, sr: int = 44100, bpm: float = 120.0, instrument: str = 'synth') -> np.ndarray: def render_midi_events_to_audio(midi_events: list, sr: int = 44100, bpm: float = 120.0, instrument: str = 'synth') -> np.ndarray:
beat_duration_sec = 60.0 / max(30.0, bpm) beat_duration_sec = 60.0 / max(30.0, bpm)
max_duration_sec = 2.0 max_duration_sec = 2.0
+248 -16
View File
@@ -137,6 +137,133 @@ const ensureCarlaForPlayback = (synthEngine) => {
}; };
// Môi trưng chy: docker vs standalone
// environment: "docker" âm instrument qua FluidSynthWASM (client); "standalone"
// x lí trc tiếp trên OS (backend native pyfluidsynth / Carla). Quy tc:
// KHÔNG phi docker = standalone (Windows/Linux/macOS chy trc tiếp).
const sfEnv = () => {
try {
const r = window.SonicRuntime;
if (r && r.environment) return r.environment;
const c = r && r.capabilities;
return (c && c.docker) ? 'docker' : 'standalone';
} catch (e) { return 'docker'; }
};
const isDockerSf = () => sfEnv() === 'docker';
const isStandaloneSf = () => sfEnv() === 'standalone';
// Track dùng âm soundfont (không phi VSTi) route native khi standalone.
const isSfTrackEngine = (se) => {
if (!se) return false;
return !!(se.soundfont_id || String(se.type || '').indexOf('soundfont') !== -1 || String(se.type || '').indexOf('sf3') !== -1);
};
const isVstTrackEngine = (se) => !!se && String(se.type || '').indexOf('vst') !== -1;
// Track dùng VSTi + Carla local route Carla (native GUI, realtime).
const shouldRouteCarla = (se) => !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(se));
// Native soundfont preview (standalone)
// Render 1 note bng backend native FluidSynth (pyfluidsynth) play WAV.
// Mi key (thưng = track.id) mt Audio element note mi stop note cũ;
// token chng stale (response cũ không đè response mi).
const _nativeSfPreviews = {};
const playNativeSfNote = (track, pitch, velocity, durationMs, startTime, key) => {
try {
const eng = track && track.synth_engine;
const sfId = (eng && eng.soundfont_id) || (track && track.instrumentId && String(track.instrumentId).startsWith('sf_') ? track.instrumentId : null);
if (!sfId) return;
const bank = (eng && eng.soundfont_bank) || 0;
const program = (eng && eng.soundfont_program) || (track && track.instrumentProgram !== undefined ? track.instrumentProgram : 0);
const k = key || (track ? track.id : 'global');
const prev = _nativeSfPreviews[k];
const token = (prev ? prev.token : 0) + 1;
const durationSec = Math.max(0.2, (durationMs || 500) / 1000);
window.SonicAPI.soundfontRender({
soundfont_id: sfId,
bank: bank,
program: program,
bpm: 120,
notes: [{ pitch: pitch, start_beat: 0, duration_beats: durationSec * 2, velocity: velocity != null ? velocity : 0.8 }],
}).then(function (res) {
if (!res || !res.success || !res.url) return;
if (_nativeSfPreviews[k] && _nativeSfPreviews[k].token !== token) return; // stale
const audio = new Audio(API_BASE_URL + res.url);
_nativeSfPreviews[k] = { audio: audio, token: token };
const ctx = getAudioContext();
const delay = startTime ? Math.max(0, (startTime - ctx.currentTime) * 1000) : 0;
setTimeout(function () {
if (!_nativeSfPreviews[k] || _nativeSfPreviews[k].audio !== audio) return;
audio.play().catch(function () {});
audio._sfStopTimer && clearTimeout(audio._sfStopTimer);
audio._sfStopTimer = setTimeout(function () { try { audio.pause(); } catch (e) {} }, durationSec * 1000 + 400);
}, delay);
}).catch(function () {});
} catch (e) { console.warn('[NativeSF] playNativeSfNote error:', e); }
};
const stopNativeSfNote = (key) => {
try {
const k = key || 'global';
const prev = _nativeSfPreviews[k];
if (!prev) return;
prev.token++;
try { if (prev.audio) { prev.audio.pause(); prev.audio.currentTime = 0; } } catch (e) {}
} catch (e) {}
};
const stopAllNativeSfNotes = () => {
try { Object.keys(_nativeSfPreviews).forEach(function (k) { stopNativeSfNote(k); }); } catch (e) {}
};
// Native soundfont item render (standalone, transport)
// Render TOÀN B MIDI item bng native FluidSynth decode AudioBuffer
// schedule ngun audio đúng v trí item (ging audio clip). opts:
// baseOffsetSec: offset thêm (section: secStart) | limitSec: chn ti secEnd
// isActive(): guard stop gia chng | sources: mng ngun đ stop.
const scheduleNativeSfItem = (track, item, offsetTime, context, destNode, bpm, opts) => {
try {
const eng = track && track.synth_engine;
const sfId = eng && eng.soundfont_id;
if (!sfId) return;
const notes = (item && item.notes) || [];
if (!notes.length) return;
const secPerBeat = 60.0 / (parseInt(bpm) || 120);
const baseOffsetSec = (opts && opts.baseOffsetSec) || 0;
const itemStartAbs = baseOffsetSec + (item.startTime || 0);
let itemEndAbs = itemStartAbs + 0.05;
notes.forEach(function (n) {
const end = itemStartAbs + ((n.start_beat || 0) + (n.duration_beats || 1)) * secPerBeat;
if (end > itemEndAbs) itemEndAbs = end;
});
window.SonicAPI.soundfontRender({
soundfont_id: sfId,
bank: (eng && eng.soundfont_bank) || 0,
program: (eng && eng.soundfont_program) || 0,
bpm: parseFloat(bpm) || 120,
notes: notes.map(function (n) { return { pitch: n.pitch || 60, start_beat: n.start_beat || 0, duration_beats: n.duration_beats || 1, velocity: n.velocity != null ? n.velocity : 0.8 }; }),
}).then(function (res) {
if (!res || !res.success || !res.url) return;
fetch(API_BASE_URL + res.url).then(function (r) { return r.arrayBuffer(); }).then(function (buf) {
context.decodeAudioData(buf, function (audioBuf) {
try {
if (opts && typeof opts.isActive === 'function' && !opts.isActive()) return;
const src = context.createBufferSource();
src.buffer = audioBuf;
src.connect(destNode);
if (offsetTime < itemStartAbs) {
src.start(context.currentTime + (itemStartAbs - offsetTime), 0);
} else if (offsetTime < itemEndAbs) {
src.start(context.currentTime, Math.min(offsetTime - itemStartAbs, Math.max(0, audioBuf.duration - 0.05)));
} else {
return;
}
if (opts && opts.limitSec && opts.limitSec < itemStartAbs + audioBuf.duration) {
src.stop(context.currentTime + Math.max(0.02, opts.limitSec - Math.max(offsetTime, itemStartAbs)));
}
if (opts && opts.sources && opts.sources.push) opts.sources.push(src);
} catch (e) { console.warn('[NativeSF] schedule decode error:', e); }
}, function () {});
}).catch(function () {});
}).catch(function () {});
} catch (e) { console.warn('[NativeSF] scheduleNativeSfItem error:', e); }
};
// Handle ?sfs=<encoded> from double-clicking a .sfs file (opens domain -> loads project) // Handle ?sfs=<encoded> from double-clicking a .sfs file (opens domain -> loads project)
(function handleSfsDeepLink() { (function handleSfsDeepLink() {
try { try {
@@ -8021,6 +8148,10 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
var pvCtx = resolveTrackInstrumentCtx(pvTrk, activeTracks); var pvCtx = resolveTrackInstrumentCtx(pvTrk, activeTracks);
ensureSonicInstrument(pvCtx); ensureSonicInstrument(pvCtx);
playing.forEach(n => { playing.forEach(n => {
if (isStandaloneSf() && isSfTrackEngine(pvCtx.synthEngine) && !shouldRouteCarla(pvCtx.synthEngine)) {
playNativeSfNote(pvTrk, n.pitch, n.velocity || 0.8, 200, undefined, 'pv_' + st.trackId);
return;
}
window.SonicSF.playNote(n.pitch, (n.velocity || 0.8) * 127, 200, ctx.currentTime, pvCtx.program, null, pvCtx.ch, pvCtx.synthEngine); window.SonicSF.playNote(n.pitch, (n.velocity || 0.8) * 127, 200, ctx.currentTime, pvCtx.program, null, pvCtx.ch, pvCtx.synthEngine);
}); });
} }
@@ -8387,7 +8518,11 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
var clTrk = activeTracks.find(function(t) { return t.id === st.trackId; }); var clTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
var clCtx = resolveTrackInstrumentCtx(clTrk, activeTracks); var clCtx = resolveTrackInstrumentCtx(clTrk, activeTracks);
ensureSonicInstrument(clCtx); ensureSonicInstrument(clCtx);
window.SonicSF.playNote(notes[clickedNoteIdx].pitch, 100, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine); if (isStandaloneSf() && isSfTrackEngine(clCtx.synthEngine) && !shouldRouteCarla(clCtx.synthEngine)) {
playNativeSfNote(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, undefined, 'pv_' + st.trackId);
} else {
window.SonicSF.playNote(notes[clickedNoteIdx].pitch, 100, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine);
}
} }
} }
@@ -8643,12 +8778,14 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
try { previewNodesRef.current.gain.disconnect(); } catch(e) {} try { previewNodesRef.current.gain.disconnect(); } catch(e) {}
previewNodesRef.current = null; previewNodesRef.current = null;
} }
if (window.SonicSF && window.SonicSF.playNote) { var dwTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
var dwCh = dwTrk ? assignTrackMidiChannel(dwTrk, activeTracks) : 0;
var dwPitch = snapToScaleRef.current ? snapPitchToScale(pitch, selectedScaleRef.current) : pitch;
var dwDurMs = Math.max(100, Math.round(initialDur * (60 / bpm) * 1000));
if (isStandaloneSf() && isSfTrackEngine(dwTrk && dwTrk.synth_engine) && !shouldRouteCarla(dwTrk && dwTrk.synth_engine)) {
playNativeSfNote(dwTrk, dwPitch, brushVelocityRef.current || 0.8, dwDurMs, undefined, 'pvdraw_' + st.trackId);
} else if (window.SonicSF && window.SonicSF.playNote) {
const ctx = getAudioContext(); const ctx = getAudioContext();
var dwTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
var dwCh = dwTrk ? assignTrackMidiChannel(dwTrk, activeTracks) : 0;
var dwPitch = snapToScaleRef.current ? snapPitchToScale(pitch, selectedScaleRef.current) : pitch;
var dwDurMs = Math.max(100, Math.round(initialDur * (60 / bpm) * 1000));
// playNote (FluidSynth nhc c THT ca track). _playNoteFallback ch // playNote (FluidSynth nhc c THT ca track). _playNoteFallback ch
// là oscillator beep (sai âm vi percussion/soundfont user: note v // là oscillator beep (sai âm vi percussion/soundfont user: note v
// mi nghe nhc c track trưc). // mi nghe nhc c track trưc).
@@ -8744,10 +8881,12 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
// Đng b mastering + routing SF trưc khi preview draw (âm qua // Đng b mastering + routing SF trưc khi preview draw (âm qua
// mastering FX ca main out khi chain bt) // mastering FX ca main out khi chain bt)
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {} try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {}
if (window.SonicSF && window.SonicSF.playNote) { var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
var pvCtxInst = resolveTrackInstrumentCtx(pvTrk, activeTracks);
if (isStandaloneSf() && isSfTrackEngine(pvCtxInst.synthEngine) && !shouldRouteCarla(pvCtxInst.synthEngine)) {
playNativeSfNote(pvTrk, p, brushVelocityRef.current || 0.8, durMs, undefined, 'pvdraw_' + st.trackId);
} else if (window.SonicSF && window.SonicSF.playNote) {
var pvCtx = getAudioContext(); var pvCtx = getAudioContext();
var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
var pvCtxInst = resolveTrackInstrumentCtx(pvTrk, activeTracks);
var pvVel = Math.round(brushVelocityRef.current * 127); var pvVel = Math.round(brushVelocityRef.current * 127);
// playNote (FluidSynth nhc c THT). _playNoteFallback = oscillator // playNote (FluidSynth nhc c THT). _playNoteFallback = oscillator
// beep sai âm (percussion/soundfont). // beep sai âm (percussion/soundfont).
@@ -9137,7 +9276,11 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (window.triggerMidiVuActivity) { if (window.triggerMidiVuActivity) {
window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100); window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100);
} }
if (window.SonicSF) { const kbNative = isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine);
if (kbNative) {
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
}
if (window.SonicSF && !kbNative) {
// FIX: gi note theo thi gian bm phím durationMs ln (5s) // FIX: gi note theo thi gian bm phím durationMs ln (5s)
// ch là auto-off phòng h; mouseup/mouseleave gi stopNote dng // ch là auto-off phòng h; mouseup/mouseleave gi stopNote dng
// NGAY (trưc đây 500ms note t tt gia chng khi gi phím). // NGAY (trưc đây 500ms note t tt gia chng khi gi phím).
@@ -9159,7 +9302,10 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (window.triggerMidiVuActivity) { if (window.triggerMidiVuActivity) {
window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100); window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100);
} }
if (window.SonicSF) { if (isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine)) {
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
}
if (window.SonicSF && !(isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine))) {
// gi note khi kéo qua phím (mouse enter) dng bng mouseup/leave // gi note khi kéo qua phím (mouse enter) dng bng mouseup/leave
window.SonicSF.playNote(pitch, 100, 5000, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine); window.SonicSF.playNote(pitch, 100, 5000, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
} }
@@ -9175,6 +9321,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
keybedMouseDownRef.current = false; keybedMouseDownRef.current = false;
// Dng note khi th phím tránh kt âm (loop liên tc) vi soundfont // Dng note khi th phím tránh kt âm (loop liên tc) vi soundfont
try { try {
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch); if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
} catch (e) {} } catch (e) {}
if (window.__carlaKeybedTimer) { clearTimeout(window.__carlaKeybedTimer); window.__carlaKeybedTimer = null; } if (window.__carlaKeybedTimer) { clearTimeout(window.__carlaKeybedTimer); window.__carlaKeybedTimer = null; }
@@ -9184,6 +9331,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (!keybedMouseDownRef.current) return; if (!keybedMouseDownRef.current) return;
// Kéo chut ra khi phím dng note ca phím đó // Kéo chut ra khi phím dng note ca phím đó
try { try {
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch); if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
} catch (e) {} } catch (e) {}
if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} } if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} }
@@ -13873,6 +14021,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
selectTokenRef.current++; selectTokenRef.current++;
const token = selectTokenRef.current; const token = selectTokenRef.current;
try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {} try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
stopAllNativeSfNotes();
playMidiPreview(cur, token); playMidiPreview(cur, token);
} }
}; };
@@ -13948,6 +14097,43 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
allNotes.push(Object.assign({}, note, { trackOffset: track.startTime || 0 })); allNotes.push(Object.assign({}, note, { trackOffset: track.startTime || 0 }));
}); });
}); });
// Standalone + instrument soundfont render C FILE bng native
// FluidSynth (backend) play WAV (loop theo flag/la chn), b per-note
// WASM (WASM không phi lung âm ca standalone).
if (isStandaloneSf() && sfId) {
const nativeNotes = allNotes
.filter(note => !(hasSelection && ((note.start_beat || 0) < loopStartBeats || (note.start_beat || 0) >= loopEndBeats)))
.map(note => {
const shiftedStartBeat = hasSelection ? ((note.start_beat || 0) - loopStartBeats) : (note.start_beat || 0);
return {
pitch: note.pitch || 60,
start_beat: shiftedStartBeat + ((note.trackOffset || 0) / secondsPerBeat),
duration_beats: note.duration_beats || 1,
velocity: note.velocity != null ? note.velocity : 0.8
};
});
if (nativeNotes.length) {
window.SonicAPI.soundfontRender({ soundfont_id: sfId, bank: bank, program: prog !== undefined ? prog : 0, bpm: bpmVal, notes: nativeNotes }).then(function (res) {
if (!res || !res.success || !res.url) return;
if (selectTokenRef.current !== (token || selectTokenRef.current)) return;
const audio = new Audio(API_BASE_URL + res.url);
audio.loop = !!isLoopingRef.current;
_nativeSfPreviews['midifile'] = { audio: audio, token: selectTokenRef.current };
audio.play().catch(function () {});
});
}
const startedAtTime = startWallTime - loopStartSec;
playStateRef.current = { source: null, ctx, startedAt: startedAtTime, fakeStart: startedAtTime, midiTotal: totalSec };
setMidiNotes(allNotes);
setMidiTotal(totalSec);
setMidiBars(midiResult[0].bars || 1);
setMidiTotalBeats(midiResult[0].totalBeats || 16);
setMidiFileBpm(midiResult[0].bpm || 120);
setIsPlaying(true);
setIsPaused(false);
startCanvasClock();
return;
}
const schedulePass = (passStartTime) => { const schedulePass = (passStartTime) => {
allNotes.forEach(note => { allNotes.forEach(note => {
const noteStartBeat = note.start_beat || 0; const noteStartBeat = note.start_beat || 0;
@@ -13996,6 +14182,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
if (rafRef.current) cancelAnimationFrame(rafRef.current); if (rafRef.current) cancelAnimationFrame(rafRef.current);
rafRef.current = null; rafRef.current = null;
if (loopTimerRef.current) { clearInterval(loopTimerRef.current); loopTimerRef.current = null; } if (loopTimerRef.current) { clearInterval(loopTimerRef.current); loopTimerRef.current = null; }
stopNativeSfNote('midifile');
if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') {
try { window.SonicSF.stopAll(); } catch (e) {} try { window.SonicSF.stopAll(); } catch (e) {}
} }
@@ -14310,6 +14497,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
selectTokenRef.current++; selectTokenRef.current++;
const token = selectTokenRef.current; const token = selectTokenRef.current;
try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {} try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
stopAllNativeSfNotes();
playMidiPreview(cur, token); playMidiPreview(cur, token);
} }
}; };
@@ -14480,6 +14668,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
selectTokenRef.current++; selectTokenRef.current++;
const token = selectTokenRef.current; const token = selectTokenRef.current;
try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {} try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
stopAllNativeSfNotes();
playMidiPreview(cur, token); playMidiPreview(cur, token);
} }
} }
@@ -15107,6 +15296,7 @@ const App = () => {
if (_wasVst && !_nowVst && window.SonicCarlaMidi && window.SonicCarlaMidi.stopBridge) { if (_wasVst && !_nowVst && window.SonicCarlaMidi && window.SonicCarlaMidi.stopBridge) {
window.SonicCarlaMidi.stopBridge(); window.SonicCarlaMidi.stopBridge();
try { if (window.SonicSF && window.SonicSF.stopAll) window.SonicSF.stopAll(); } catch (e) {} try { if (window.SonicSF && window.SonicSF.stopAll) window.SonicSF.stopAll(); } catch (e) {}
stopAllNativeSfNotes();
console.log('[Instrument] Carla bridge unloaded — track', trackId, 'switched from VSTi to non-VST'); console.log('[Instrument] Carla bridge unloaded — track', trackId, 'switched from VSTi to non-VST');
} }
} catch (e) { console.warn('[Instrument] carla-stop on instrument switch error:', e); } } catch (e) { console.warn('[Instrument] carla-stop on instrument switch error:', e); }
@@ -15364,7 +15554,11 @@ const App = () => {
try { try {
heldMidiNotesRef.current[as.trackId] = (heldMidiNotesRef.current[as.trackId] || 0) + 1; heldMidiNotesRef.current[as.trackId] = (heldMidiNotesRef.current[as.trackId] || 0) + 1;
} catch (err) { } } catch (err) { }
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, asProg, null, asCh, asSe); if (isStandaloneSf() && isSfTrackEngine(asSe) && !shouldRouteCarla(asSe)) {
playNativeSfNote(asTrk, pitch, scaledVel / 127, 60000, undefined, 'midi_' + as.trackId + '_' + pitch);
} else {
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, asProg, null, asCh, asSe);
}
// MIDI Keyboard Carla bridge (piano roll sub-tab ARM): // MIDI Keyboard Carla bridge (piano roll sub-tab ARM):
// track VSTi ca sub-tab phát VSTi realtime ging keybed. // track VSTi ca sub-tab phát VSTi realtime ging keybed.
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(asSe, true)) { if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(asSe, true)) {
@@ -15385,7 +15579,11 @@ const App = () => {
try { try {
heldMidiNotesRef.current[at.id] = (heldMidiNotesRef.current[at.id] || 0) + 1; heldMidiNotesRef.current[at.id] = (heldMidiNotesRef.current[at.id] || 0) + 1;
} catch (err) { } } catch (err) { }
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, atProg, atDest, atCh, atSe); if (isStandaloneSf() && isSfTrackEngine(atSe) && !shouldRouteCarla(atSe)) {
playNativeSfNote(at, pitch, scaledVel / 127, 60000, undefined, 'midi_' + at.id + '_' + pitch);
} else {
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, atProg, atDest, atCh, atSe);
}
// MIDI Keyboard Carla bridge (track VSTi + ARM + Carla // MIDI Keyboard Carla bridge (track VSTi + ARM + Carla
// local): phát VSTi realtime ging ht keybed o. Trưc // local): phát VSTi realtime ging ht keybed o. Trưc
// đây ch keybed gi SonicCarlaMidi; keyboard hardware b // đây ch keybed gi SonicCarlaMidi; keyboard hardware b
@@ -15414,6 +15612,7 @@ const App = () => {
// channel and kill its sound. // channel and kill its sound.
if (!st.synth_engine && st.midiChannel === undefined) return; if (!st.synth_engine && st.midiChannel === undefined) return;
var stCh = assignTrackMidiChannel(st, stopTracks); var stCh = assignTrackMidiChannel(st, stopTracks);
if (isStandaloneSf()) stopNativeSfNote('midi_' + st.id + '_' + pitch);
window.SonicSF.stopNote(stCh, pitch); window.SonicSF.stopNote(stCh, pitch);
// MIDI Keyboard Carla bridge: note-off khi th phím tránh // MIDI Keyboard Carla bridge: note-off khi th phím tránh
// kt âm VSTi (ging keybed o). // kt âm VSTi (ging keybed o).
@@ -21235,6 +21434,11 @@ const App = () => {
const destNode = getOrCreateTrackNode(track, context); const destNode = getOrCreateTrackNode(track, context);
const program = track ? track.instrumentProgram : undefined; const program = track ? track.instrumentProgram : undefined;
var prevCh = track ? assignTrackMidiChannel(track, activeTracks) : 0; var prevCh = track ? assignTrackMidiChannel(track, activeTracks) : 0;
const routeCarla = shouldRouteCarla(track && track.synth_engine);
if (isStandaloneSf() && !routeCarla && isSfTrackEngine(track && track.synth_engine)) {
playNativeSfNote(track, pitch, velocity, durationMs, null, track ? track.id : 'global');
return;
}
window.SonicSF.playNote( window.SonicSF.playNote(
pitch, pitch,
velocity, velocity,
@@ -21306,7 +21510,12 @@ const App = () => {
// MIDI items playback // MIDI items playback
const midiItems = track.midiItems || []; const midiItems = track.midiItems || [];
if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine)))) { const routeCarla = shouldRouteCarla(track.synth_engine);
const nativeSf = isStandaloneSf() && !routeCarla && isSfTrackEngine(track.synth_engine);
if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || routeCarla || nativeSf)) {
if (nativeSf) {
midiItems.forEach(item => scheduleNativeSfItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
} else {
// Preview cache: capture the soundfont (pre track-FX) for fast offline export // Preview cache: capture the soundfont (pre track-FX) for fast offline export
ensureMidiCapture(track, activeTrackNodesRef.current[track.id]); ensureMidiCapture(track, activeTrackNodesRef.current[track.id]);
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine)) ensureCarlaForPlayback(track.synth_engine); if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine)) ensureCarlaForPlayback(track.synth_engine);
@@ -21388,6 +21597,7 @@ const App = () => {
} }
}); });
}); });
}
} }
// If track has instrumentId set but no MIDI items, create scheduled oscillators // If track has instrumentId set but no MIDI items, create scheduled oscillators
if (track.instrumentId && midiItems.length === 0 && track.buffer) { if (track.instrumentId && midiItems.length === 0 && track.buffer) {
@@ -21464,7 +21674,13 @@ const App = () => {
// 2. Play MIDI items in subTrack // 2. Play MIDI items in subTrack
const subMidiItems = subTrack.midiItems || []; const subMidiItems = subTrack.midiItems || [];
if (subMidiItems.length > 0 && (window.SonicSF || (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine)))) { const subRouteCarla = shouldRouteCarla(subTrack.synth_engine);
const subNativeSf = isStandaloneSf() && !subRouteCarla && isSfTrackEngine(subTrack.synth_engine);
if (subMidiItems.length > 0 && (window.SonicSF || subRouteCarla || subNativeSf)) {
if (subNativeSf) {
subMidiItems.forEach(item => scheduleNativeSfItem(subTrack, item, offsetTime, context, subNode, bpm, { baseOffsetSec: secStart, limitSec: secEnd, sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
return; // native render audio buffer (không qua WASM/Carla per-note)
}
subMidiItems.forEach(item => { subMidiItems.forEach(item => {
const notes = item.notes || []; const notes = item.notes || [];
notes.forEach(note => { notes.forEach(note => {
@@ -21592,7 +21808,13 @@ const App = () => {
// MIDI items playback // MIDI items playback
const midiItems = track.midiItems || []; const midiItems = track.midiItems || [];
if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine)))) { const routeCarla = shouldRouteCarla(track.synth_engine);
const nativeSf = isStandaloneSf() && !routeCarla && isSfTrackEngine(track.synth_engine);
if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || routeCarla || nativeSf)) {
if (nativeSf) {
midiItems.forEach(item => scheduleNativeSfItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
return; // native render audio buffer (không qua WASM/Carla per-note)
}
var lcCh = assignTrackMidiChannel(track, tracks); var lcCh = assignTrackMidiChannel(track, tracks);
const bpmVal = parseInt(bpm) || 120; const bpmVal = parseInt(bpm) || 120;
const secondsPerBeat = 60.0 / bpmVal; const secondsPerBeat = 60.0 / bpmVal;
@@ -21675,6 +21897,11 @@ const App = () => {
// items placed later in the project play `item.startTime` seconds in the // items placed later in the project play `item.startTime` seconds in the
// future (silence when pressing play). Ghost notes are already relative to // future (silence when pressing play). Ghost notes are already relative to
// the item window, so no absolute-session offset is applied anywhere here. // the item window, so no absolute-session offset is applied anywhere here.
const routeCarla = shouldRouteCarla(synthEngine);
if (isStandaloneSf() && !routeCarla && isSfTrackEngine(synthEngine)) {
scheduleNativeSfItem(track, { startTime: 0, notes: midiNotes }, offsetSeconds, context, destNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
return; // native render c tab audio buffer (không per-note WASM/Carla)
}
midiNotes.forEach(note => { midiNotes.forEach(note => {
const noteOnBeat = note.start_beat || 0; const noteOnBeat = note.start_beat || 0;
const noteDurBeat = note.duration_beats || 1; const noteDurBeat = note.duration_beats || 1;
@@ -21815,6 +22042,7 @@ const App = () => {
} }
setTimeout(() => { setTimeout(() => {
window.SonicSF.stopAll(); window.SonicSF.stopAll();
stopAllNativeSfNotes();
if (prTNode && prTNode.gainNode) { if (prTNode && prTNode.gainNode) {
const prTrackData = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === prSt.trackId) : null; const prTrackData = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === prSt.trackId) : null;
const prVolDb = prTrackData ? (prTrackData.volumeDb ?? 0) : 0; const prVolDb = prTrackData ? (prTrackData.volumeDb ?? 0) : 0;
@@ -21854,6 +22082,7 @@ const App = () => {
// tick vn thy heldCnt > 0 gi peak VU dính mãi (user bug 08:08). // tick vn thy heldCnt > 0 gi peak VU dính mãi (user bug 08:08).
try { heldMidiNotesRef.current = {}; } catch (e) { } try { heldMidiNotesRef.current = {}; } catch (e) { }
stopMidiCapture(); stopMidiCapture();
stopAllNativeSfNotes();
if (window.SonicSF) { if (window.SonicSF) {
try { window.SonicSF.stopAll(); } catch (e) { console.warn('[Stop] stopAll error:', e); } try { window.SonicSF.stopAll(); } catch (e) { console.warn('[Stop] stopAll error:', e); }
// Dng trit đ: noteoff tng note + hy scheduled note-on (hết âm stuck) // Dng trit đ: noteoff tng note + hy scheduled note-on (hết âm stuck)
@@ -21877,6 +22106,7 @@ const App = () => {
if (st.isPlaying) { if (st.isPlaying) {
stopAllPlayback(); stopAllPlayback();
window.SonicSF.stopAll(); window.SonicSF.stopAll();
stopAllNativeSfNotes();
setSubTabs(prev => prev.map(s => s.id === activeTab ? { setSubTabs(prev => prev.map(s => s.id === activeTab ? {
...s, ...s,
currentTime: time, currentTime: time,
@@ -29439,6 +29669,7 @@ STRICT CONSTRAINTS:
} }
setTimeout(() => { setTimeout(() => {
window.SonicSF.stopAll(); window.SonicSF.stopAll();
stopAllNativeSfNotes();
if (tNode && tNode.gainNode) { if (tNode && tNode.gainNode) {
const trackData = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === playingSub.trackId) : null; const trackData = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === playingSub.trackId) : null;
const volDb = trackData ? (trackData.volumeDb ?? 0) : 0; const volDb = trackData ? (trackData.volumeDb ?? 0) : 0;
@@ -29459,6 +29690,7 @@ STRICT CONSTRAINTS:
if (seekSt.isPlaying) { if (seekSt.isPlaying) {
stopAllPlayback(); stopAllPlayback();
window.SonicSF.stopAll(); window.SonicSF.stopAll();
stopAllNativeSfNotes();
setSubTabs(prev => prev.map(s => s.id === seekSt.id ? { ...s, currentTime: clickTime, isPlaying: true } : s)); setSubTabs(prev => prev.map(s => s.id === seekSt.id ? { ...s, currentTime: clickTime, isPlaying: true } : s));
const ctx = getAudioContext(); const ctx = getAudioContext();
startOffsetTimeRef.current = clickTime; startOffsetTimeRef.current = clickTime;
File diff suppressed because one or more lines are too long
+1
View File
@@ -89,6 +89,7 @@ window.API_BASE_URL = window.API_BASE_URL || window.location.origin;
// Render MIDI notes → WAV bằng VSTi (pedalboard, âm thật) — trả file_id // Render MIDI notes → WAV bằng VSTi (pedalboard, âm thật) — trả file_id
// để UI preview / gán clip vào track / download // để UI preview / gán clip vào track / download
midiRender: (payload) => apiRequest('/api/v1/plugins/midi-render', { method: 'POST', body: JSON.stringify(payload) }), midiRender: (payload) => apiRequest('/api/v1/plugins/midi-render', { method: 'POST', body: JSON.stringify(payload) }),
soundfontRender: (payload) => apiRequest('/api/v1/plugins/soundfont-render', { method: 'POST', body: JSON.stringify(payload) }),
// Phát dãy MIDI notes realtime qua Carla bridge (OSC) — preview khi // Phát dãy MIDI notes realtime qua Carla bridge (OSC) — preview khi
// pedalboard không render được plugin (VD VST2) // pedalboard không render được plugin (VD VST2)
carlaPlayNotes: (payload) => apiRequest('/api/v1/plugins/carla-play-notes', { method: 'POST', body: JSON.stringify(payload) }), carlaPlayNotes: (payload) => apiRequest('/api/v1/plugins/carla-play-notes', { method: 'POST', body: JSON.stringify(payload) }),
+6
View File
@@ -20,9 +20,15 @@ window.SonicRuntime = window.SonicRuntime || { loaded: false, capabilities: null
var c = data && data.success ? data : { features: {} }; var c = data && data.success ? data : { features: {} };
window.SonicRuntime.capabilities = c; window.SonicRuntime.capabilities = c;
window.SonicRuntime.loaded = true; window.SonicRuntime.loaded = true;
// environment: "docker" | "standalone" — quyết định luồng âm
// instrument: docker → FluidSynthWASM (client), standalone →
// native (backend pyfluidsynth / Carla). Không phải docker =
// standalone (Windows/Linux/macOS chạy trực tiếp trên OS).
window.SonicRuntime.environment = c.environment || (c.docker ? 'docker' : 'standalone');
var html = document.documentElement; var html = document.documentElement;
html.dataset.runtime = c.runtime || 'unknown'; html.dataset.runtime = c.runtime || 'unknown';
html.dataset.platform = c.platform || ''; html.dataset.platform = c.platform || '';
html.dataset.environment = window.SonicRuntime.environment;
html.dataset.carla = (c.features && c.features.carla_local) ? '1' : '0'; html.dataset.carla = (c.features && c.features.carla_local) ? '1' : '0';
if (c.features && c.features.carla_local === false) { if (c.features && c.features.carla_local === false) {
document.querySelectorAll('[data-carla-only]').forEach(function (el) { document.querySelectorAll('[data-carla-only]').forEach(function (el) {
+1 -1
View File
@@ -46,7 +46,7 @@
<script src="/static/js/services/midiExtractor.js?v=202607281052"></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/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script> <script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=202608101904" defer></script> <script src="/static/js/app.precompiled.js?v=202608101914" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016"> <link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style> <style>
:root { :root {