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:
+69
-2
@@ -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
@@ -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"],
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+241
-9
@@ -137,6 +137,133 @@ const ensureCarlaForPlayback = (synthEngine) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ── Môi trường chạy: docker vs standalone ─────────────────────────────────
|
||||||
|
// environment: "docker" → âm instrument qua FluidSynthWASM (client); "standalone"
|
||||||
|
// → xử lí trực tiếp trên OS (backend native pyfluidsynth / Carla). Quy tắc:
|
||||||
|
// KHÔNG phải docker = standalone (Windows/Linux/macOS chạy trực 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 phải 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 bằng backend native FluidSynth (pyfluidsynth) → play WAV.
|
||||||
|
// Mỗi key (thường = track.id) một Audio element — note mới stop note cũ;
|
||||||
|
// token chống stale (response cũ không đè response mới).
|
||||||
|
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 bằng native FluidSynth → decode AudioBuffer →
|
||||||
|
// schedule nguồn audio đúng vị trí item (giống audio clip). opts:
|
||||||
|
// baseOffsetSec: offset thêm (section: secStart) | limitSec: chặn tại secEnd
|
||||||
|
// isActive(): guard stop giữa chừng | sources: mảng nguồn để 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,9 +8518,13 @@ 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);
|
||||||
|
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);
|
window.SonicSF.playNote(notes[clickedNoteIdx].pitch, 100, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Shift+click: toggle selection (multi-select) — user 09:30 (trước đây là
|
// Shift+click: toggle selection (multi-select) — user 09:30 (trước đây là
|
||||||
// Ctrl+click — Ctrl giờ dành cho COPY-drag)
|
// Ctrl+click — Ctrl giờ dành cho COPY-drag)
|
||||||
@@ -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) {
|
|
||||||
const ctx = getAudioContext();
|
|
||||||
var dwTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
var dwTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
||||||
var dwCh = dwTrk ? assignTrackMidiChannel(dwTrk, activeTracks) : 0;
|
var dwCh = dwTrk ? assignTrackMidiChannel(dwTrk, activeTracks) : 0;
|
||||||
var dwPitch = snapToScaleRef.current ? snapPitchToScale(pitch, selectedScaleRef.current) : pitch;
|
var dwPitch = snapToScaleRef.current ? snapPitchToScale(pitch, selectedScaleRef.current) : pitch;
|
||||||
var dwDurMs = Math.max(100, Math.round(initialDur * (60 / bpm) * 1000));
|
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();
|
||||||
// playNote (FluidSynth — nhạc cụ THẬT của track). _playNoteFallback chỉ
|
// playNote (FluidSynth — nhạc cụ THẬT của track). _playNoteFallback chỉ
|
||||||
// là oscillator beep (sai âm với percussion/soundfont — user: note vẽ
|
// là oscillator beep (sai âm với percussion/soundfont — user: note vẽ
|
||||||
// mới nghe nhạc cụ track trước).
|
// mới nghe nhạc 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 của main out khi chain bật)
|
// mastering FX của main out khi chain bật)
|
||||||
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {}
|
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {}
|
||||||
if (window.SonicSF && window.SonicSF.playNote) {
|
|
||||||
var pvCtx = getAudioContext();
|
|
||||||
var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
||||||
var pvCtxInst = resolveTrackInstrumentCtx(pvTrk, activeTracks);
|
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 pvVel = Math.round(brushVelocityRef.current * 127);
|
var pvVel = Math.round(brushVelocityRef.current * 127);
|
||||||
// playNote (FluidSynth — nhạc cụ THẬT). _playNoteFallback = oscillator
|
// playNote (FluidSynth — nhạc cụ THẬT). _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 thời gian bấm phím — durationMs lớn (5s)
|
// ⚠️ FIX: giữ note theo thời gian bấm phím — durationMs lớn (5s)
|
||||||
// chỉ là auto-off phòng hờ; mouseup/mouseleave gọi stopNote dừng
|
// chỉ là auto-off phòng hờ; mouseup/mouseleave gọi stopNote dừng
|
||||||
// NGAY (trước đây 500ms → note tự tắt giữa chừng khi giữ phím).
|
// NGAY (trước đây 500ms → note tự tắt giữa chừng 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) — dừng bằng mouseup/leave
|
// giữ note khi kéo qua phím (mouse enter) — dừng bằng 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;
|
||||||
// Dừng note khi thả phím — tránh kẹt âm (loop liên tục) với soundfont
|
// Dừng note khi thả phím — tránh kẹt âm (loop liên tục) với 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 chuột ra khỏi phím → dừng note của phím đó
|
// Kéo chuột ra khỏi phím → dừng note của 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 bằng native
|
||||||
|
// FluidSynth (backend) → play WAV (loop theo flag/lựa chọn), bỏ per-note
|
||||||
|
// WASM (WASM không phải luồng âm của 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) { }
|
||||||
|
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);
|
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 của sub-tab phát VSTi realtime giống keybed.
|
// track VSTi của sub-tab phát VSTi realtime giống 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) { }
|
||||||
|
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);
|
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 — giống hệt keybed ảo. Trước
|
// local): phát VSTi realtime — giống hệt keybed ảo. Trước
|
||||||
// đây chỉ keybed gọi SonicCarlaMidi; keyboard hardware bị
|
// đây chỉ keybed gọi 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
|
||||||
// kẹt âm VSTi (giống keybed ảo).
|
// kẹt âm VSTi (giống 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);
|
||||||
@@ -21389,6 +21598,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) {
|
||||||
// Play audio normally (the buffer has the audio data)
|
// Play audio normally (the buffer has the audio data)
|
||||||
@@ -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 vẫn thấy heldCnt > 0 → giữ peak → VU dính mãi (user bug 08:08).
|
// tick vẫn thấy 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); }
|
||||||
// Dừng triệt để: noteoff từng note + hủy scheduled note-on (hết âm stuck)
|
// Dừng triệt để: noteoff từng note + hủy 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
@@ -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) }),
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user