FIX: (1) masteringConnected default true + ensureMasteringRouting/refreshCarlaStatus ở mọi đường play — MIDI item/preview qua mastering ngay khi chạy app; (2) _instrumentEpoch chống cache stale + retry bank 0 — hết chọn Synth String nghe Piano; (3) /carla-status + auto-open Carla + route exclusive chỉ khi Carla sống (fallback FluidSynth tránh câm); (4) keybed giữ note theo thời gian bấm (auto-off 5s phòng hờ, mouseup dừng ngay)

This commit is contained in:
2026-08-10 09:51:58 +00:00
parent f492104869
commit 75eba27dcf
7 changed files with 135 additions and 26 deletions
+54 -8
View File
@@ -101,6 +101,38 @@ const scheduleCarlaNote = (synthEngine, channel, pitch, velocity, startWallTime,
} catch (e) { /* routing thất bại im lặng — soundfont vẫn chơi qua FluidSynth */ }
};
// Carla bridge alive tracking + auto-open
// window.__carlaRunning: undefined = chưa biết | true = đang chy | false = đã chết.
// Quyết đnh route MIDI item EXCLUSIVE qua Carla hay fallback FluidSynth
// tránh "câm toàn phn" khi Carla b đóng (route ch-Carla mà Carla chết = im lng).
const refreshCarlaStatus = () => {
try {
if (!window.SonicAPI || !window.SonicAPI.carlaStatus) return;
window.SonicAPI.carlaStatus().then(st => {
window.__carlaRunning = !!(st && st.running);
}).catch(() => { window.__carlaRunning = false; });
} catch (e) {}
};
// Carla chưa chy T ĐNG m vi VSTi ca track (fire-and-forget) đ MIDI
// item play qua Carla bridge đúng yêu cu.
const ensureCarlaForPlayback = (synthEngine) => {
try {
if (!window.SonicCarlaMidi || !window.SonicCarlaMidi.shouldRoutePlayback(synthEngine)) return;
if (!window.SonicAPI || !window.SonicAPI.carlaStatus) return;
window.SonicAPI.carlaStatus().then(st => {
if (st && st.running) { window.__carlaRunning = true; return; }
window.__carlaRunning = false;
const pid = synthEngine && synthEngine.plugin_id;
if (pid) {
window.SonicAPI.openInCarla(pid).then(r => {
if (r && r.success) { window.__carlaRunning = true; }
else { window.__carlaRunning = false; }
}).catch(() => { window.__carlaRunning = false; });
}
}).catch(() => { window.__carlaRunning = false; });
} catch (e) {}
};
// Handle ?sfs=<encoded> from double-clicking a .sfs file (opens domain -> loads project)
(function handleSfsDeepLink() {
@@ -9101,7 +9133,10 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100);
}
if (window.SonicSF) {
window.SonicSF.playNote(pitch, 100, 500, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
// FIX: gi note theo thi gian bm phím durationMs ln (5s)
// 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).
window.SonicSF.playNote(pitch, 100, 5000, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
}
// MIDI Carla (track VSTi + ARM + Carla local): preview realtime
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(kbCtx.synthEngine, st.isArmed)) {
@@ -9120,7 +9155,8 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100);
}
if (window.SonicSF) {
window.SonicSF.playNote(pitch, 100, 200, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
// 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);
}
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(kbCtx.synthEngine, st.isArmed)) {
window.SonicCarlaMidi.noteOn(kbCtx.ch, pitch, 100);
@@ -16392,7 +16428,7 @@ const App = () => {
const [fxRackTarget, setFxRackTarget] = useState(null);
window.__openFxRack = (trackId, trackName) => setFxRackTarget({ trackId, trackName });
const [masteringSettings, setMasteringSettings] = useState({
masterConnected: false,
masterConnected: true, // FIX: bt ON mc đnh MIDI item/preview PHI đưc x lí qua mastering FX ngay khi chy app (không cn bt/tt power)
activeModule: 'eq',
eqActive: true,
imagerActive: true,
@@ -21078,6 +21114,10 @@ const App = () => {
const startTrackPlayback = offsetTime => {
const context = getAudioContext();
// FIX: đng b mastering + Carla status NGAY khi play MIDI item phi
// qua mastering FX (khi bt) và qua Carla bridge (khi VSTi loaded).
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {}
refreshCarlaStatus();
const allPlayTracks = activeTracksRef.current && activeTracksRef.current.length ? activeTracksRef.current : activeTracks;
// Capture items signature at schedule time updatePlayhead so sánh vi
// signature này đ phát hin items đi v trí gia lúc play (re-schedule).
@@ -21134,6 +21174,7 @@ const App = () => {
if ((track.type === 'MIDI' || midiItems.length > 0) && window.SonicSF) {
// Preview cache: capture the soundfont (pre track-FX) for fast offline export
ensureMidiCapture(track, activeTrackNodesRef.current[track.id]);
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine)) ensureCarlaForPlayback(track.synth_engine);
var trkCh = assignTrackMidiChannel(track, allPlayTracks);
// Ensure instrument is loaded in FluidSynth
if (track.synth_engine && track.synth_engine.type === 'soundfont' && track.synth_engine.soundfont_id) {
@@ -21156,7 +21197,7 @@ const App = () => {
// Track VSTi + Carla local MIDI item PHI play qua Carla bridge
// (ch route Carla KHÔNG play qua FluidSynth GM na, tránh âm
// sai instrument chng lên). Không có Carla FluidSynth như cũ.
const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine));
const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine)) && window.__carlaRunning !== false;
if (offsetTime < noteStartSec) {
const delay = noteStartSec - offsetTime;
const startTime = context.currentTime + delay;
@@ -21303,7 +21344,8 @@ const App = () => {
var subCh = assignTrackMidiChannel(subTrack, secTracks);
// MIDI item trong SECTION Carla bridge khi VSTi loaded
// (ch route Carla không play FluidSynth GM sai âm)
const subRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine));
const subRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine)) && window.__carlaRunning !== false;
if (subRouteCarla || window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine)) ensureCarlaForPlayback(subTrack.synth_engine);
if (offsetTime < noteStartMain) {
const delay = noteStartMain - offsetTime;
@@ -21372,6 +21414,8 @@ const App = () => {
// Solo playback for Local Selection Loop (LOOP_MAKER.md §2.2)
const startLocalTrackPlayback = (trackId, offsetTime) => {
const context = getAudioContext();
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {}
refreshCarlaStatus();
const track = tracks.find(t => t.id === trackId);
if (!track) return;
const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{
@@ -21426,7 +21470,7 @@ const App = () => {
const program = track.instrumentProgram !== undefined ? track.instrumentProgram : 0;
// MIDI item Carla bridge khi VSTi loaded (ch route Carla không
// play FluidSynth GM sai âm chng lên)
const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine));
const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine)) && window.__carlaRunning !== false;
if (offsetTime < noteStartSec) {
const delay = noteStartSec - offsetTime;
const startTime = context.currentTime + delay;
@@ -21469,6 +21513,8 @@ const App = () => {
const schedulePianoRollMidi = (st, offsetSeconds, notesOverride) => {
if (st.type !== 'PIANO_ROLL') return;
const context = getAudioContext();
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {}
refreshCarlaStatus();
const midiNotes = notesOverride || st.notes || [];
const bpmVal = parseInt(bpm) || 120;
const secondsPerBeat = 60.0 / bpmVal;
@@ -21504,7 +21550,7 @@ const App = () => {
const durMs = effectiveDur * 1000;
// Track VSTi + Carla local note PHI play qua Carla bridge (ch route
// Carla không play FluidSynth GM sai âm chng lên)
const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(synthEngine));
const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(synthEngine)) && window.__carlaRunning !== false;
if (!routeToCarla && window.SonicSF) {
window.SonicSF.playNote(note.pitch || 60, note.velocity || 0.8, durMs, scheduledTime, instrumentProgram, destNode, mainCh, synthEngine);
}
@@ -21540,7 +21586,7 @@ const App = () => {
var schedTime = startWallTime + effStart;
var durMs = effDur * 1000;
// Ghost note Carla bridge khi ghost track VSTi (ch route Carla)
var gRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(ghostSynth));
var gRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(ghostSynth)) && window.__carlaRunning !== false;
if (!gRouteCarla && window.SonicSF) {
window.SonicSF.playNote(note.pitch || 60, note.velocity || 0.8, durMs, schedTime, ghostProg, ghostDest, ghostCh, ghostSynth);
}
+26 -13
View File
@@ -20,7 +20,13 @@ const ensureSonicInstrument=ctx=>{try{if(!window.SonicSF||!window.SonicSF.select
// track dùng VSTi + có Carla local. Dùng chung cho mọi đường playback
// (main timeline, local loop, piano roll, ghost notes) để MIDI item PHẢI play
// qua Carla bridge khi VSTi được loaded.
const scheduleCarlaNote=(synthEngine,channel,pitch,velocity,startWallTime,durMs)=>{try{if(!window.SonicCarlaMidi||!window.SonicCarlaMidi.shouldRoutePlayback(synthEngine))return;const ctx=getAudioContext();const delay=Math.max(0,(startWallTime-ctx.currentTime)*1000);const carlaVel=Math.round((velocity||0.8)*127);const carlaCh=synthEngine&&synthEngine.midi_channel!==undefined?synthEngine.midi_channel:channel||0;setTimeout(function(){window.SonicCarlaMidi.noteOn(carlaCh,pitch,carlaVel);},delay);setTimeout(function(){window.SonicCarlaMidi.noteOff(carlaCh,pitch);},delay+(durMs||300)+30);}catch(e){/* routing thất bại im lặng — soundfont vẫn chơi qua FluidSynth */}};// Handle ?sfs=<encoded> from double-clicking a .sfs file (opens domain -> loads project)
const scheduleCarlaNote=(synthEngine,channel,pitch,velocity,startWallTime,durMs)=>{try{if(!window.SonicCarlaMidi||!window.SonicCarlaMidi.shouldRoutePlayback(synthEngine))return;const ctx=getAudioContext();const delay=Math.max(0,(startWallTime-ctx.currentTime)*1000);const carlaVel=Math.round((velocity||0.8)*127);const carlaCh=synthEngine&&synthEngine.midi_channel!==undefined?synthEngine.midi_channel:channel||0;setTimeout(function(){window.SonicCarlaMidi.noteOn(carlaCh,pitch,carlaVel);},delay);setTimeout(function(){window.SonicCarlaMidi.noteOff(carlaCh,pitch);},delay+(durMs||300)+30);}catch(e){/* routing thất bại im lặng — soundfont vẫn chơi qua FluidSynth */}};// ── Carla bridge alive tracking + auto-open ────────────────────────────────
// window.__carlaRunning: undefined = chưa biết | true = đang chạy | false = đã chết.
// Quyết định route MIDI item EXCLUSIVE qua Carla hay fallback FluidSynth —
// tránh "câm toàn phần" khi Carla bị đóng (route chỉ-Carla mà Carla chết = im lặng).
const refreshCarlaStatus=()=>{try{if(!window.SonicAPI||!window.SonicAPI.carlaStatus)return;window.SonicAPI.carlaStatus().then(st=>{window.__carlaRunning=!!(st&&st.running);}).catch(()=>{window.__carlaRunning=false;});}catch(e){}};// Carla chưa chạy → TỰ ĐỘNG mở với VSTi của track (fire-and-forget) để MIDI
// item play qua Carla bridge đúng yêu cầu.
const ensureCarlaForPlayback=synthEngine=>{try{if(!window.SonicCarlaMidi||!window.SonicCarlaMidi.shouldRoutePlayback(synthEngine))return;if(!window.SonicAPI||!window.SonicAPI.carlaStatus)return;window.SonicAPI.carlaStatus().then(st=>{if(st&&st.running){window.__carlaRunning=true;return;}window.__carlaRunning=false;const pid=synthEngine&&synthEngine.plugin_id;if(pid){window.SonicAPI.openInCarla(pid).then(r=>{if(r&&r.success){window.__carlaRunning=true;}else{window.__carlaRunning=false;}}).catch(()=>{window.__carlaRunning=false;});}}).catch(()=>{window.__carlaRunning=false;});}catch(e){}};// Handle ?sfs=<encoded> from double-clicking a .sfs file (opens domain -> loads project)
(function handleSfsDeepLink(){try{const params=new URLSearchParams(window.location.search);const sfsParam=params.get('sfs');if(!sfsParam)return;const decoded=JSON.parse(decodeURIComponent(sfsParam));window.__pendingSfsProject=decoded;// consumed after auth in App
if(window.history.replaceState){window.history.replaceState({},document.title,window.location.pathname);}}catch(e){window.__pendingSfsProject=null;}})();// Storage for server-side file IDs mapped to track IDs
let serverFileIdMap={};let audioCtx;let masterBus=null;// { input, compressor, analyser, output, masteringActive, dryInput, dryOutput }
@@ -521,8 +527,12 @@ const idxs=findCCNoteIndicesAtBeat(beat);ccDragRef.current={active:true,lastBeat
const candidateIdxs=findCCNoteIndicesAtBeat(beat);const unpainted=candidateIdxs.filter(ci=>!painted.includes(ci));if(unpainted.length>0){const updatedNotes=notes.map((n,i)=>{if(!unpainted.includes(i))return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};});setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);drag.lastPainted=[...painted,...unpainted];}};const renderKeybed=()=>{var kbTrk=activeTracks.find(function(t){return t.id===st.trackId;});var kbCtx=resolveTrackInstrumentCtx(kbTrk,activeTracks);ensureSonicInstrument(kbCtx);const keys=[];for(let pitch=127;pitch>=PITCH_START;pitch--){const isBlack=[1,3,6,8,10].includes(pitch%12);const notesArray=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];const octave=Math.floor(pitch/12)-1;const label=`${notesArray[pitch%12]}${octave}`;const showLabel=pitch%12===0;keys.push(/*#__PURE__*/React.createElement("div",{key:pitch,style:{height:`${NoteHeight}px`},className:`w-[60px] shrink-0 border-b border-zinc-800 text-[9px] font-bold flex items-center justify-end pr-2 transition cursor-pointer ${activeMidiPitches&&activeMidiPitches.has(pitch)?'bg-emerald-500 text-white border-emerald-400':isBlack?'bg-zinc-950 text-slate-500 border-zinc-900 hover:bg-zinc-800':'bg-white text-zinc-800 border-r border-zinc-400 hover:bg-amber-100'}`,onMouseDown:e=>{e.stopPropagation();keybedMouseDownRef.current=true;try{// Đồng bộ mastering + routing SF NGAY trước khi preview — âm
// keybed PHẢI qua mastering FX của main out khi chain đang bật
// (trước đây phải bật/tắt power mới có tác dụng).
try{if(window.__ensureMasteringRouting)window.__ensureMasteringRouting();}catch(er){}if(window.triggerMidiVuActivity){window.triggerMidiVuActivity((st.parent_tab_id&&st.parent_tab_id.startsWith('session_')?'_sess_':'')+st.trackId,100);}if(window.SonicSF){window.SonicSF.playNote(pitch,100,500,undefined,kbCtx.program,null,kbCtx.ch,kbCtx.synthEngine);}// MIDI → Carla (track VSTi + ARM + Carla local): preview realtime
if(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoute(kbCtx.synthEngine,st.isArmed)){window.SonicCarlaMidi.noteOn(kbCtx.ch,pitch,100);if(window.__carlaKeybedTimer)clearTimeout(window.__carlaKeybedTimer);window.__carlaKeybedTimer=setTimeout(function(){window.SonicCarlaMidi.noteOff(kbCtx.ch,pitch);},650);}}catch(err){console.error('playNote error:',err);}},onMouseEnter:e=>{if(keybedMouseDownRef.current){try{if(window.triggerMidiVuActivity){window.triggerMidiVuActivity((st.parent_tab_id&&st.parent_tab_id.startsWith('session_')?'_sess_':'')+st.trackId,100);}if(window.SonicSF){window.SonicSF.playNote(pitch,100,200,undefined,kbCtx.program,null,kbCtx.ch,kbCtx.synthEngine);}if(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoute(kbCtx.synthEngine,st.isArmed)){window.SonicCarlaMidi.noteOn(kbCtx.ch,pitch,100);if(window.__carlaKeybedTimer)clearTimeout(window.__carlaKeybedTimer);window.__carlaKeybedTimer=setTimeout(function(){window.SonicCarlaMidi.noteOff(kbCtx.ch,pitch);},650);}}catch(err){console.error('playNote error:',err);}}},onMouseUp:()=>{keybedMouseDownRef.current=false;// Dừng note khi thả phím — tránh kẹt âm (loop liên tục) với soundfont
try{if(window.__ensureMasteringRouting)window.__ensureMasteringRouting();}catch(er){}if(window.triggerMidiVuActivity){window.triggerMidiVuActivity((st.parent_tab_id&&st.parent_tab_id.startsWith('session_')?'_sess_':'')+st.trackId,100);}if(window.SonicSF){// ⚠️ 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
// NGAY (trước đây 500ms → note tự tắt giữa chừng khi giữ phím).
window.SonicSF.playNote(pitch,100,5000,undefined,kbCtx.program,null,kbCtx.ch,kbCtx.synthEngine);}// MIDI → Carla (track VSTi + ARM + Carla local): preview realtime
if(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoute(kbCtx.synthEngine,st.isArmed)){window.SonicCarlaMidi.noteOn(kbCtx.ch,pitch,100);if(window.__carlaKeybedTimer)clearTimeout(window.__carlaKeybedTimer);window.__carlaKeybedTimer=setTimeout(function(){window.SonicCarlaMidi.noteOff(kbCtx.ch,pitch);},650);}}catch(err){console.error('playNote error:',err);}},onMouseEnter:e=>{if(keybedMouseDownRef.current){try{if(window.triggerMidiVuActivity){window.triggerMidiVuActivity((st.parent_tab_id&&st.parent_tab_id.startsWith('session_')?'_sess_':'')+st.trackId,100);}if(window.SonicSF){// 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);}if(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoute(kbCtx.synthEngine,st.isArmed)){window.SonicCarlaMidi.noteOn(kbCtx.ch,pitch,100);if(window.__carlaKeybedTimer)clearTimeout(window.__carlaKeybedTimer);window.__carlaKeybedTimer=setTimeout(function(){window.SonicCarlaMidi.noteOff(kbCtx.ch,pitch);},650);}}catch(err){console.error('playNote error:',err);}}},onMouseUp:()=>{keybedMouseDownRef.current=false;// Dừng note khi thả phím — tránh kẹt âm (loop liên tục) với soundfont
try{if(window.SonicSF&&window.SonicSF.stopNote)window.SonicSF.stopNote(kbCtx.ch,pitch);}catch(e){}if(window.__carlaKeybedTimer){clearTimeout(window.__carlaKeybedTimer);window.__carlaKeybedTimer=null;}if(window.SonicCarlaMidi){try{window.SonicCarlaMidi.noteOff(kbCtx.ch,pitch);}catch(e){}}},onMouseLeave:()=>{if(!keybedMouseDownRef.current)return;// Kéo chuột ra khỏi phím → dừng note của phím đó
try{if(window.SonicSF&&window.SonicSF.stopNote)window.SonicSF.stopNote(kbCtx.ch,pitch);}catch(e){}if(window.SonicCarlaMidi){try{window.SonicCarlaMidi.noteOff(kbCtx.ch,pitch);}catch(e){}}}},showLabel&&label));}return keys;};const handleScroll=e=>{if(keybedRef.current){keybedRef.current.scrollTop=e.currentTarget.scrollTop;}if(rulerScrollRef.current){rulerScrollRef.current.scrollLeft=e.currentTarget.scrollLeft;}if(ccWrapperRef.current){ccWrapperRef.current.scrollLeft=e.currentTarget.scrollLeft;}const el=e.currentTarget;const THRESHOLD=200;if(el.scrollLeft+el.clientWidth>=el.scrollWidth-THRESHOLD){const newBeats=rollBeatsRef.current+16;setRollBeats(newBeats);}};const handleKeybedScroll=e=>{if(gridScrollRef.current){gridScrollRef.current.scrollTop=e.currentTarget.scrollTop;}};const SCALES={"None":null,"Diatonic":{"Major":[0,2,4,5,7,9,11],"Minor":[0,2,3,5,7,8,10],"Dorian":[0,2,3,5,7,9,10],"Phrygian":[0,1,3,5,7,8,10],"Lydian":[0,2,4,6,7,9,11],"Mixolydian":[0,2,4,5,7,9,10],"Locrian":[0,1,3,5,6,8,10]},"Pentatonic":{"Major":[0,2,4,7,9],"Minor":[0,3,5,7,10],"Blues":[0,3,5,6,7,10],"Chinese":[0,2,4,7,9],"Vietnam":[0,2,6,7,10],"India":[0,2,5,7,9],"Japan":[0,2,5,7,9],"Africa":[0,3,5,7,10]},"Church":{"Dorian":[0,2,3,5,7,9,10],"Phrygian":[0,1,3,5,7,8,10],"Lydian":[0,2,4,6,7,9,11],"Mixolydian":[0,2,4,5,7,9,10]},"Jazz":{"Blues":[0,3,5,6,7,10],"Bebop":[0,2,4,5,7,9,10,11],"Diminished":[0,2,3,5,6,8,9,11]},"Asian Traditional":{"Chinese":[0,2,4,7,9],"Vietnam":[0,2,6,7,10],"Japan":[0,2,5,7,9],"India":[0,2,5,7,9],"Gamelan":[0,2,4,7,9],"Korea":[0,2,4,5,7,9]},"Middle Eastern":{"Hijaz":[0,1,4,5,7,8,11],"Nikriz":[0,1,4,5,7,8,10],"Rast":[0,2,4,5,7,8,10],"Saba":[0,1,3,4,7,8,10],"Bayati":[0,2,3,4,7,8,10]}};const[selectedScale,setSelectedScale]=React.useState(null);const selectedScaleRef=React.useRef(null);selectedScaleRef.current=selectedScale;// Root note (0-11 — C..B) — transpose scale highlight + snap (spec 20:12)
const[scaleRoot,setScaleRoot]=React.useState(0);const scaleRootRef=React.useRef(0);scaleRootRef.current=scaleRoot;// Modal states: Arpeggiator / Strummer / Humanize (spec 20:12)
@@ -891,7 +901,8 @@ const[aboutModalOpen,setAboutModalOpen]=useState(false);const[helpModalOpen,setH
useEffect(()=>{try{document.documentElement.setAttribute('data-theme',prefs.theme||'dark');document.documentElement.setAttribute('data-btnfont',prefs.buttonFontSize||'md');localStorage.setItem('sf_prefs',JSON.stringify(prefs));}catch(e){}},[prefs]);// Load preferences từ server (nếu có tài khoản) khi khởi động
useEffect(()=>{let cancelled=false;if(window.SonicAPI&&window.SonicAPI.getPreferences){window.SonicAPI.getPreferences().then(res=>{if(cancelled||!res||!res.success||!res.preferences)return;const p=res.preferences;if(p.theme||p.language||p.buttonFontSize){setPrefs(prev=>({theme:p.theme||prev.theme,language:p.language||prev.language,buttonFontSize:p.buttonFontSize||prev.buttonFontSize}));}}).catch(()=>{});}return()=>{cancelled=true;};},[]);// Lưu preferences lên server khi đổi
const handlePrefsChange=next=>{setPrefs(next);try{localStorage.setItem('sf_prefs',JSON.stringify(next));}catch(e){}if(window.SonicAPI&&window.SonicAPI.savePreferences){window.SonicAPI.savePreferences(next).catch(()=>{});}};// Unified FX Rack target context (unified_fx_rack_panel.md): { trackId, trackName } | null
const[fxRackTarget,setFxRackTarget]=useState(null);window.__openFxRack=(trackId,trackName)=>setFxRackTarget({trackId,trackName});const[masteringSettings,setMasteringSettings]=useState({masterConnected:false,activeModule:'eq',eqActive:true,imagerActive:true,maximizerActive:true,eqLowGain:1.5,eqMid1Gain:-1.0,eqMid2Gain:2.0,eqHighGain:1.8,w1:0,w2:115,w3:135,w4:150,imagerScale:'v2',maxGain:5.4,maxUpward:2.0,maxSoftClip:15,maxTransient:25,ceiling:-0.1,isBypassed:false,chain:DEFAULT_MASTER_CHAIN.map(m=>({...m})),compActive:false,compThreshold:-16,compRatio:3,compMakeup:0,limActive:false,limThreshold:-1.0,excActive:false,excDrive:40,rebalActive:false,rebalMid:0,rebalSide:0});useEffect(()=>{window.currentMasteringSettings=masteringSettings;if(audioCtx&&masterBus){toggleMasteringOnMaster(masteringSettings.masterConnected,masteringSettings.isBypassed);applyMasteringSettings(masteringSettings);// Re-sync live track routes: mastering ON → mọi track qua chain (♪ bị
const[fxRackTarget,setFxRackTarget]=useState(null);window.__openFxRack=(trackId,trackName)=>setFxRackTarget({trackId,trackName});const[masteringSettings,setMasteringSettings]=useState({masterConnected:true,// ⚠️ FIX: bật ON mặc định — MIDI item/preview PHẢI được xử lí qua mastering FX ngay khi chạy app (không cần bật/tắt power)
activeModule:'eq',eqActive:true,imagerActive:true,maximizerActive:true,eqLowGain:1.5,eqMid1Gain:-1.0,eqMid2Gain:2.0,eqHighGain:1.8,w1:0,w2:115,w3:135,w4:150,imagerScale:'v2',maxGain:5.4,maxUpward:2.0,maxSoftClip:15,maxTransient:25,ceiling:-0.1,isBypassed:false,chain:DEFAULT_MASTER_CHAIN.map(m=>({...m})),compActive:false,compThreshold:-16,compRatio:3,compMakeup:0,limActive:false,limThreshold:-1.0,excActive:false,excDrive:40,rebalActive:false,rebalMid:0,rebalSide:0});useEffect(()=>{window.currentMasteringSettings=masteringSettings;if(audioCtx&&masterBus){toggleMasteringOnMaster(masteringSettings.masterConnected,masteringSettings.isBypassed);applyMasteringSettings(masteringSettings);// Re-sync live track routes: mastering ON → mọi track qua chain (♪ bị
// override bởi effMidiBypass/effAudioBypass) — nút PWR bật/tắt phải áp
// ngay lên node đang phát (không chờ tracks effect).
try{const _list=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracks;_list.forEach(_t=>{const _n=activeTrackNodesRef.current[_t.id];if(_n){if(_n.sfRouteGain&&_n.sfDryGain){const _b=effMidiBypass(_t);_n.sfRouteGain.gain.value=_b?0:1;_n.sfDryGain.gain.value=_b?1:0;}if(_n.route&&_n.route.routeGain&&_n.route.dryGain){const _ab=routeBypassFor(_t);_n.route.routeGain.gain.value=_ab?0:1;_n.route.dryGain.gain.value=_ab?1:0;}}});updateSfRouting();}catch(e){}}},[masteringSettings]);const[pluginManagerModalOpen,setPluginManagerModalOpen]=useState(false);const[pluginsData,setPluginsData]=useState(null);const loadAudioBuffersForTracks=async tracksList=>{let hasLoadedAny=false;const loadBuffer=async url=>{const res=await fetch(url);if(!res.ok)return null;const blob=await res.blob();return await window.SonicAudio.decodeAudioFile(blob);};const tryLoad=async fileId=>{if(!fileId)return null;try{const result=await loadBuffer('/static/audio/uploads/'+fileId);if(result)return result;}catch(_){}try{const result=await loadBuffer(`${API_AUDIO}/download/${fileId}`);if(result)return result;}catch(_){}return null;};const updatedTracks=await Promise.all(tracksList.map(async t=>{let trackBuffer=t.buffer;let trackChannelInfo=t.channelInfo;if(t.serverFileId&&!trackBuffer){const result=await tryLoad(t.serverFileId);if(result){trackBuffer=result.audioBuffer;trackChannelInfo=result.channelInfo;hasLoadedAny=true;}}const updatedClips=await Promise.all((t.clips||[]).map(async c=>{let clipBuffer=c.buffer;const targetFileId=c.serverFileId||t.serverFileId;if(targetFileId&&!clipBuffer){const result=await tryLoad(targetFileId);if(result){clipBuffer=result.audioBuffer;hasLoadedAny=true;}}return{...c,buffer:clipBuffer};}));// Section items: nạp CẢ audioclip bên trong section (trước đây bỏ sót →
@@ -1329,7 +1340,9 @@ const subAnalyser=context.createAnalyser();subAnalyser.fftSize=256;subAnalyser.s
// parent có route riêng với bypass riêng — parent audio_bypass=true →
// nối qua parent làm MIDI/audio section bị kéo vào dry bus = bypass
// mastering dù sub-track ♪/A đang tắt).
if(window.masterBus&&window.masterBus.input){routeGain.connect(window.masterBus.input);}else{routeGain.connect(parentNode);}if(window.masterBus&&window.masterBus.dryInput){dryGain.connect(window.masterBus.dryInput);}else{dryGain.connect(parentNode);}node={gainNode,pannerNode:null,analyserNode:subAnalyser,route:{routeGain:routeGain,dryGain:dryGain,_trackId:subTrack.id}};activeTrackNodesRef.current[subKey]=node;}return node.gainNode;};const playMidiPreviewNote=(pitch,velocity=0.8,durationMs=500)=>{if(!window.SonicSF)return;const context=getAudioContext();const tab=subTabs.find(s=>s.id===activeTabRef.current);const trackId=tab?tab.trackId:selectedTrackId;const track=activeTracks.find(t=>t.id===trackId);const destNode=getOrCreateTrackNode(track,context);const program=track?track.instrumentProgram:undefined;var prevCh=track?assignTrackMidiChannel(track,activeTracks):0;window.SonicSF.playNote(pitch,velocity,durationMs,context.currentTime,program,destNode,prevCh,track?track.synth_engine:undefined);};const startTrackPlayback=offsetTime=>{const context=getAudioContext();const allPlayTracks=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:activeTracks;// Capture items signature at schedule time — updatePlayhead so sánh với
if(window.masterBus&&window.masterBus.input){routeGain.connect(window.masterBus.input);}else{routeGain.connect(parentNode);}if(window.masterBus&&window.masterBus.dryInput){dryGain.connect(window.masterBus.dryInput);}else{dryGain.connect(parentNode);}node={gainNode,pannerNode:null,analyserNode:subAnalyser,route:{routeGain:routeGain,dryGain:dryGain,_trackId:subTrack.id}};activeTrackNodesRef.current[subKey]=node;}return node.gainNode;};const playMidiPreviewNote=(pitch,velocity=0.8,durationMs=500)=>{if(!window.SonicSF)return;const context=getAudioContext();const tab=subTabs.find(s=>s.id===activeTabRef.current);const trackId=tab?tab.trackId:selectedTrackId;const track=activeTracks.find(t=>t.id===trackId);const destNode=getOrCreateTrackNode(track,context);const program=track?track.instrumentProgram:undefined;var prevCh=track?assignTrackMidiChannel(track,activeTracks):0;window.SonicSF.playNote(pitch,velocity,durationMs,context.currentTime,program,destNode,prevCh,track?track.synth_engine:undefined);};const startTrackPlayback=offsetTime=>{const context=getAudioContext();// ⚠️ FIX: đồng bộ mastering + Carla status NGAY khi play — MIDI item phải
// qua mastering FX (khi bật) và qua Carla bridge (khi VSTi loaded).
try{if(window.__ensureMasteringRouting)window.__ensureMasteringRouting();}catch(e){}refreshCarlaStatus();const allPlayTracks=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:activeTracks;// Capture items signature at schedule time — updatePlayhead so sánh với
// signature này để phát hiện items đổi vị trí giữa lúc play (re-schedule).
scheduledItemsSigRef.current=buildItemsSignature(allPlayTracks,sessionTabs);console.log('[Play] offset='+offsetTime+' tracks='+allPlayTracks.length+' masterBus='+!!masterBus+' dest='+(context.destination?'ok':'MISSING')+' projectEnd='+(projectEndRef.current||0).toFixed(2));const hasSolo=allPlayTracks.some(t=>t.solo);allPlayTracks.forEach(track=>{const isPlayable=hasSolo?track.solo:!track.muted;if(!isPlayable)return;const gainNode=getOrCreateTrackNode(track,context);const pannerNode=activeTrackNodesRef.current[track.id].pannerNode;// Re-apply route gains theo content-type NGAY tại thời điểm play — node
// có thể STALE (tạo từ MAIN play với track cùng id — route theo data cũ)
@@ -1337,13 +1350,13 @@ scheduledItemsSigRef.current=buildItemsSignature(allPlayTracks,sessionTabs);cons
// routeBypassFor(track) mỗi lần schedule sẽ hết chu kỳ "stop+play mới đúng".
const playNode=activeTrackNodesRef.current[track.id];if(playNode&&playNode.route&&playNode.route.routeGain){const rb=routeBypassFor(track);playNode.route.routeGain.gain.value=rb?0:1;playNode.route.dryGain.gain.value=rb?1:0;console.log('[Bypass] reapply track',track.id,'contentType='+(routeBypassFor._lastType?'midi':'audio'),'bypass='+rb,'→ routeGain='+playNode.route.routeGain.gain.value+'/dryGain='+playNode.route.dryGain.gain.value);}console.log('[Play] track',track.id,track.name,'node ok:',!!(gainNode&&pannerNode),'muted:',track.muted);const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default',buffer:track.buffer,startTime:track.startTime||0,name:track.name,speed:track.speed||1.0}]:[];clips.forEach(clip=>{if(!clip.buffer)return;const source=context.createBufferSource();source.buffer=clip.buffer;source.playbackRate.value=clip.speed||1.0;source.connect(gainNode);const clipStart=clip.startTime||0;const clipDuration=clip.buffer.duration/(clip.speed||1.0);const clipEnd=clipStart+clipDuration;if(offsetTime<clipStart){const delay=clipStart-offsetTime;source.start(context.currentTime+delay,0);activeSourcesRef.current.push(source);}else if(offsetTime<clipEnd){const playOffset=offsetTime-clipStart;source.start(context.currentTime,playOffset*(clip.speed||1.0));activeSourcesRef.current.push(source);}});// MIDI items playback
const midiItems=track.midiItems||[];if((track.type==='MIDI'||midiItems.length>0)&&window.SonicSF){// Preview cache: capture the soundfont (pre track-FX) for fast offline export
ensureMidiCapture(track,activeTrackNodesRef.current[track.id]);var trkCh=assignTrackMidiChannel(track,allPlayTracks);// Ensure instrument is loaded in FluidSynth
ensureMidiCapture(track,activeTrackNodesRef.current[track.id]);if(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine))ensureCarlaForPlayback(track.synth_engine);var trkCh=assignTrackMidiChannel(track,allPlayTracks);// Ensure instrument is loaded in FluidSynth
if(track.synth_engine&&track.synth_engine.type==='soundfont'&&track.synth_engine.soundfont_id){window.SonicSF.selectInstrument(trkCh,track.synth_engine.soundfont_bank||0,track.synth_engine.soundfont_program||0,track.synth_engine.soundfont_id);}const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;midiItems.forEach(item=>{const itemEndSec=item.startTime+(item.duration||4);const notes=item.notes||[];notes.forEach(note=>{// note start/duration is in beats (for MIDI items)
const noteStartSec=item.startTime+(note.start_beat||0)*secondsPerBeat;if(noteStartSec>=itemEndSec)return;// skip notes outside item's visual duration
const noteDurSec=(note.duration_beats||1)*secondsPerBeat;const noteEndSec=noteStartSec+noteDurSec;if(offsetTime<noteEndSec){const durationMs=noteDurSec*1000;const program=track._isSectionClone?track.instrumentProgram:track.instrumentProgram!==undefined?track.instrumentProgram:0;// Track VSTi + Carla local → MIDI item PHẢI play qua Carla bridge
// (chỉ route Carla — KHÔNG play qua FluidSynth GM nữa, tránh âm
// sai instrument chồng lên). Không có Carla → FluidSynth như cũ.
const routeToCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine));if(offsetTime<noteStartSec){const delay=noteStartSec-offsetTime;const startTime=context.currentTime+delay;if(!routeToCarla&&window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durationMs,startTime,program,gainNode,trkCh,track.synth_engine);}// MIDI items → Carla bridge (track VSTi + Carla local): phát VSTi
const routeToCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine))&&window.__carlaRunning!==false;if(offsetTime<noteStartSec){const delay=noteStartSec-offsetTime;const startTime=context.currentTime+delay;if(!routeToCarla&&window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durationMs,startTime,program,gainNode,trkCh,track.synth_engine);}// MIDI items → Carla bridge (track VSTi + Carla local): phát VSTi
// realtime — yêu cầu: MIDI item PHẢI play qua Carla khi VSTi loaded.
if(routeToCarla)scheduleCarlaNote(track.synth_engine,trkCh,note.pitch||60,note.velocity||0.8,startTime,durationMs);// Trigger VU meter flash when the note starts playing
setTimeout(()=>{// ⚠️ Guard: stop → setTimeout sót không được fire (VU nhảy
@@ -1364,7 +1377,7 @@ const activeStart=Math.max(clipStartMain,secStart);const activeEnd=Math.min(clip
const subMidiItems=subTrack.midiItems||[];if(window.SonicSF&&subMidiItems.length>0){subMidiItems.forEach(item=>{const notes=item.notes||[];notes.forEach(note=>{const noteStartSecLocal=item.startTime+(note.start_beat||0)*secondsPerBeat;const noteDurSec=(note.duration_beats||1)*secondsPerBeat;const noteStartMain=secStart+noteStartSecLocal;const noteEndMain=noteStartMain+noteDurSec;// Only play if the note starts inside the Section item bounds and hasn't finished yet
if(noteStartMain>=secStart&&noteStartMain<secEnd&&offsetTime<noteEndMain){const notePlayEndMain=Math.min(noteEndMain,secEnd);const program=subTrack.instrumentProgram;var subCh=assignTrackMidiChannel(subTrack,secTracks);// MIDI item trong SECTION → Carla bridge khi VSTi loaded
// (chỉ route Carla — không play FluidSynth GM sai âm)
const subRouteCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine));if(offsetTime<noteStartMain){const delay=noteStartMain-offsetTime;const startTime=context.currentTime+delay;const playDurMs=(notePlayEndMain-noteStartMain)*1000;if(!subRouteCarla&&window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,playDurMs,startTime,program,subNode,subCh,subTrack.synth_engine);}if(subRouteCarla)scheduleCarlaNote(subTrack.synth_engine,subCh,note.pitch||60,note.velocity||0.8,startTime,playDurMs);// Trigger VU meter flash đúng nhịp (cả subTrack — VU
const subRouteCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine))&&window.__carlaRunning!==false;if(subRouteCarla||window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine))ensureCarlaForPlayback(subTrack.synth_engine);if(offsetTime<noteStartMain){const delay=noteStartMain-offsetTime;const startTime=context.currentTime+delay;const playDurMs=(notePlayEndMain-noteStartMain)*1000;if(!subRouteCarla&&window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,playDurMs,startTime,program,subNode,subCh,subTrack.synth_engine);}if(subRouteCarla)scheduleCarlaNote(subTrack.synth_engine,subCh,note.pitch||60,note.velocity||0.8,startTime,playDurMs);// Trigger VU meter flash đúng nhịp (cả subTrack — VU
// section tab + parent track — VU MAIN): MIDI section đi
// SF → masterBus.input (không qua sub-node analyser) →
// VU track chỉ nhảy qua midiVuActivity. (Bị mất khi
@@ -1374,7 +1387,7 @@ setTimeout(()=>{// ⚠️ Guard: stop → setTimeout sót không được fire (
if(!isPlayingRef.current)return;if(window.triggerMidiVuActivity){// VU key theo CONTEXT (tách MAIN vs SECTION — 07:20)
window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+subTrack.id,note.velocity||0.8);window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+track.id,note.velocity||0.8);}},delay*1000);}else{const remainingDurMs=(notePlayEndMain-offsetTime)*1000;if(!subRouteCarla&&window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,subNode,subCh,subTrack.synth_engine);}if(subRouteCarla)scheduleCarlaNote(subTrack.synth_engine,subCh,note.pitch||60,note.velocity||0.8,context.currentTime,remainingDurMs);if(window.triggerMidiVuActivity){// VU key theo CONTEXT (tách MAIN vs SECTION — 07:20)
window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+subTrack.id,note.velocity||0.8);window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+track.id,note.velocity||0.8);}}}});});}});});}});updateSfRouting();};// Solo playback for Local Selection Loop (LOOP_MAKER.md §2.2)
const startLocalTrackPlayback=(trackId,offsetTime)=>{const context=getAudioContext();const track=tracks.find(t=>t.id===trackId);if(!track)return;const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default',buffer:track.buffer,startTime:track.startTime||0,name:track.name,speed:track.speed||1.0}]:[];// Get or create persistent gain & panner for real-time control
const startLocalTrackPlayback=(trackId,offsetTime)=>{const context=getAudioContext();try{if(window.__ensureMasteringRouting)window.__ensureMasteringRouting();}catch(e){}refreshCarlaStatus();const track=tracks.find(t=>t.id===trackId);if(!track)return;const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default',buffer:track.buffer,startTime:track.startTime||0,name:track.name,speed:track.speed||1.0}]:[];// Get or create persistent gain & panner for real-time control
const gainNode=getOrCreateTrackNode(track,context);const pannerNode=activeTrackNodesRef.current[track.id].pannerNode;clips.forEach(clip=>{if(!clip.buffer)return;const source=context.createBufferSource();source.buffer=clip.buffer;source.playbackRate.value=clip.speed||1.0;source.connect(gainNode);// NOTE: do NOT connect gainNode → pannerNode directly here — the track
// node already wires gainNode → fxEntry → [FX modules] → fxLegacyIn →
// chorus/reverb → pannerNode. A direct tap would BYPASS the FX Rack
@@ -1382,9 +1395,9 @@ const gainNode=getOrCreateTrackNode(track,context);const pannerNode=activeTrackN
const clipStart=clip.startTime||0;const clipDuration=clip.buffer.duration/(clip.speed||1.0);const clipEnd=clipStart+clipDuration;if(offsetTime<clipStart){const delay=clipStart-offsetTime;source.start(context.currentTime+delay,0);activeSourcesRef.current.push(source);}else if(offsetTime<clipEnd){const playOffset=offsetTime-clipStart;source.start(context.currentTime,playOffset*(clip.speed||1.0));activeSourcesRef.current.push(source);}});// MIDI items playback
const midiItems=track.midiItems||[];if((track.type==='MIDI'||midiItems.length>0)&&window.SonicSF){var lcCh=assignTrackMidiChannel(track,tracks);const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;midiItems.forEach(item=>{const notes=item.notes||[];notes.forEach(note=>{const noteStartSec=item.startTime+(note.start_beat||0)*secondsPerBeat;const noteDurSec=(note.duration_beats||1)*secondsPerBeat;const noteEndSec=noteStartSec+noteDurSec;if(offsetTime<noteEndSec){const durationMs=noteDurSec*1000;const program=track.instrumentProgram!==undefined?track.instrumentProgram:0;// MIDI item → Carla bridge khi VSTi loaded (chỉ route Carla — không
// play FluidSynth GM sai âm chồng lên)
const routeToCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine));if(offsetTime<noteStartSec){const delay=noteStartSec-offsetTime;const startTime=context.currentTime+delay;if(!routeToCarla&&window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durationMs,startTime,program,gainNode,lcCh,track.synth_engine);}// MIDI items → Carla bridge (track VSTi + Carla local)
const routeToCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine))&&window.__carlaRunning!==false;if(offsetTime<noteStartSec){const delay=noteStartSec-offsetTime;const startTime=context.currentTime+delay;if(!routeToCarla&&window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durationMs,startTime,program,gainNode,lcCh,track.synth_engine);}// MIDI items → Carla bridge (track VSTi + Carla local)
if(routeToCarla)scheduleCarlaNote(track.synth_engine,lcCh,note.pitch||60,note.velocity||0.8,startTime,durationMs);}else{const remainingDurMs=(noteEndSec-offsetTime)*1000;if(!routeToCarla&&window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,gainNode,lcCh,track.synth_engine);}// MIDI items → Carla bridge (track VSTi + Carla local)
if(routeToCarla)scheduleCarlaNote(track.synth_engine,lcCh,note.pitch||60,note.velocity||0.8,context.currentTime,remainingDurMs);}}});});}};const schedulePianoRollMidi=(st,offsetSeconds,notesOverride)=>{if(st.type!=='PIANO_ROLL')return;const context=getAudioContext();const midiNotes=notesOverride||st.notes||[];const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;const startWallTime=context.currentTime;const track=activeTracksRef.current?activeTracksRef.current.find(t=>t.id===st.trackId):null;if(!track){console.warn('[Play] Piano Roll track not found:',st.trackId);return;}const destNode=getOrCreateTrackNode(track,context);// Instrument context resolve từ TRACK live (nguồn duy nhất) — track "đã
if(routeToCarla)scheduleCarlaNote(track.synth_engine,lcCh,note.pitch||60,note.velocity||0.8,context.currentTime,remainingDurMs);}}});});}};const schedulePianoRollMidi=(st,offsetSeconds,notesOverride)=>{if(st.type!=='PIANO_ROLL')return;const context=getAudioContext();try{if(window.__ensureMasteringRouting)window.__ensureMasteringRouting();}catch(e){}refreshCarlaStatus();const midiNotes=notesOverride||st.notes||[];const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;const startWallTime=context.currentTime;const track=activeTracksRef.current?activeTracksRef.current.find(t=>t.id===st.trackId):null;if(!track){console.warn('[Play] Piano Roll track not found:',st.trackId);return;}const destNode=getOrCreateTrackNode(track,context);// Instrument context resolve từ TRACK live (nguồn duy nhất) — track "đã
// loaded instrument" (instrumentProgram GM hoặc synth_engine soundfont)
// phải chơi ĐÚNG instrument đó. ensureSonicInstrument select channel đúng
// trước khi notes bắn (fire-and-forget — playNote tự load+retry nếu SF
@@ -1395,11 +1408,11 @@ const instCtx=resolveTrackInstrumentCtx(track,activeTracksRef.current||[]);const
// the item window, so no absolute-session offset is applied anywhere here.
midiNotes.forEach(note=>{const noteOnBeat=note.start_beat||0;const noteDurBeat=note.duration_beats||1;const noteStartSec=noteOnBeat*secondsPerBeat;const noteDurSec=noteDurBeat*secondsPerBeat;if(noteStartSec+noteDurSec>offsetSeconds){const effectiveStart=Math.max(0,noteStartSec-offsetSeconds);const effectiveDur=noteDurSec-Math.max(0,offsetSeconds-noteStartSec);const scheduledTime=startWallTime+effectiveStart;const durMs=effectiveDur*1000;// Track VSTi + Carla local → note PHẢI play qua Carla bridge (chỉ route
// Carla — không play FluidSynth GM sai âm chồng lên)
const routeToCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(synthEngine));if(!routeToCarla&&window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durMs,scheduledTime,instrumentProgram,destNode,mainCh,synthEngine);}// MIDI items → Carla (track VSTi + Carla local): phát VSTi realtime
const routeToCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(synthEngine))&&window.__carlaRunning!==false;if(!routeToCarla&&window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durMs,scheduledTime,instrumentProgram,destNode,mainCh,synthEngine);}// MIDI items → Carla (track VSTi + Carla local): phát VSTi realtime
// (schedule theo audio clock bằng setTimeout — preview, timing gần đúng).
if(routeToCarla){const carlaDelay=Math.max(0,(scheduledTime-context.currentTime)*1000);const carlaVel=Math.round((note.velocity||0.8)*127);const carlaCh=synthEngine&&synthEngine.midi_channel!==undefined?synthEngine.midi_channel:mainCh;const carlaPitch=note.pitch||60;setTimeout(function(){window.SonicCarlaMidi.noteOn(carlaCh,carlaPitch,carlaVel);},carlaDelay);setTimeout(function(){window.SonicCarlaMidi.noteOff(carlaCh,carlaPitch);},carlaDelay+durMs+30);}}});// Play ghost notes from active tracks
var ghostLayers=st.ghostPlayLayers||[];ghostLayers.forEach(function(layer){var ghostTrack=allTracks.find(function(t){return t.id===layer.trackId;});var ghostProg=layer.instrumentProgram!==undefined?layer.instrumentProgram:ghostTrack?ghostTrack.instrumentProgram:undefined;var ghostSynth=layer.synthEngine||(ghostTrack?ghostTrack.synth_engine:undefined);if(ghostProg===undefined&&!ghostSynth)return;var ghostDest=getOrCreateTrackNode(ghostTrack,context);var ghostCh=ghostTrack?assignTrackMidiChannel(ghostTrack,allTracks):0;layer.notes.forEach(function(note){var beat=note.start_beat||0;var dur=note.duration_beats||1;var startSec=beat*secondsPerBeat;var durSec=dur*secondsPerBeat;if(startSec+durSec>offsetSeconds){var effStart=Math.max(0,startSec-offsetSeconds);var effDur=durSec-Math.max(0,offsetSeconds-startSec);var schedTime=startWallTime+effStart;var durMs=effDur*1000;// Ghost note → Carla bridge khi ghost track VSTi (chỉ route Carla)
var gRouteCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(ghostSynth));if(!gRouteCarla&&window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durMs,schedTime,ghostProg,ghostDest,ghostCh,ghostSynth);}if(gRouteCarla)scheduleCarlaNote(ghostSynth,ghostCh,note.pitch||60,note.velocity||0.8,schedTime,durMs);}});});};const handlePlayPause=()=>{console.log('[Play] click activeTab=',activeTab,'isPlaying=',isPlaying,'subPlaying=',subTabs.filter(s=>s.isPlaying).length);if(activeTab!=='main'&&!activeTab.startsWith('session_')){// Sub-tab playback transport
var gRouteCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(ghostSynth))&&window.__carlaRunning!==false;if(!gRouteCarla&&window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durMs,schedTime,ghostProg,ghostDest,ghostCh,ghostSynth);}if(gRouteCarla)scheduleCarlaNote(ghostSynth,ghostCh,note.pitch||60,note.velocity||0.8,schedTime,durMs);}});});};const handlePlayPause=()=>{console.log('[Play] click activeTab=',activeTab,'isPlaying=',isPlaying,'subPlaying=',subTabs.filter(s=>s.isPlaying).length);if(activeTab!=='main'&&!activeTab.startsWith('session_')){// Sub-tab playback transport
const st=subTabs.find(s=>s.id===activeTab);if(!st||!st.buffer&&st.type!=='PIANO_ROLL')return;// MAIN/SECTION đang play (ngầm — MIDI section/main còn kêu sau khi mở
// tab): Space phải STOP trước — không được play sub-tab (user: trong
// PIANO ROLL không phát hiện MIDI đang play → space 2 lần mới stop).
+3
View File
@@ -79,6 +79,9 @@ window.API_BASE_URL = window.API_BASE_URL || window.location.origin;
// Unload Carla bridge: tắt mọi note đang ngân + terminate tiến trình Carla
// do app spawn (gọi khi track chuyển từ VSTi sang instrument soundfont)
stopCarla: () => apiRequest('/api/v1/plugins/carla-stop', { method: 'POST', body: JSON.stringify({}) }),
// Kiểm tra Carla còn sống không (app-spawn) + cổng OSC — quyết định
// route MIDI item exclusive qua Carla hay fallback FluidSynth
carlaStatus: () => apiRequest('/api/v1/plugins/carla-status', { method: 'GET' }),
// Gửi MIDI note (track ARM → Carla OSC) để preview VSTi realtime
carlaMidi: (payload) => apiRequest('/api/v1/plugins/carla-midi', { method: 'POST', body: JSON.stringify(payload) }),
// Quick-render preview VSTi (âm thật = âm export, cùng code path)
+24 -1
View File
@@ -25,6 +25,11 @@
// note khi thả phím (trước đây stopNote không dừng oscillator → âm kêu
// liên tục dù đã thả phím khi FluidSynth chưa sẵn sàng / font load fail).
let _activeOscillatorsByKey = {};
// Epoch chống cache channel STALE: tăng mỗi khi selectInstrument/load
// soundfont — channel cache chỉ được tin khi khớp epoch hiện tại, nếu
// không → note KẾ TIẾP luôn program_select lại (hết "chọn Synth String
// nghe Piano" do cache cũ skip program_select).
let _instrumentEpoch = 0;
let _gainNode = null;
let _pendingOutputDestination = null;
let _outputDestination = null; // cache đích route — dedupe swap dư giữa stream
@@ -332,6 +337,7 @@
_sfHandleMap.set(sfId, cachedOk);
_currentSfId = sfId;
_loadedFonts[sfId] = true;
_instrumentEpoch++;
console.log("[SonicSF] SoundFont loaded from cache:", sfId, "handle:", cachedOk);
return true;
}
@@ -367,6 +373,7 @@
_sfHandleMap.set(sfId, sfHandle);
_currentSfId = sfId;
_loadedFonts[sfId] = true;
_instrumentEpoch++;
console.log("[SonicSF] SoundFont loaded:", sfId, "handle:", sfHandle);
return true;
} catch (e) {
@@ -396,9 +403,17 @@
_engineChMap[engKey] = channel;
}
var sfHandle = _sfHandleMap.get(sfId);
// ⚠️ FIX âm sai instrument: tăng epoch → channel cache cũ thành
// stale → note kế tiếp LUÔN program_select lại (hết "chọn Synth
// String nghe Piano" do cache cũ skip).
_instrumentEpoch++;
if (sfHandle !== undefined) {
try {
_fluidModule._fluid_synth_program_select(_synthPtr, channel, sfHandle, bank, program);
var _sr = _fluidModule._fluid_synth_program_select(_synthPtr, channel, sfHandle, bank, program);
if (_sr !== 0) {
// Preset không tồn tại ở bank/program này → thử bank 0
try { _fluidModule._fluid_synth_program_select(_synthPtr, channel, sfHandle, 0, program); } catch (e2) {}
}
} catch (e) {}
} else {
try { _fluidModule._fluid_synth_bank_select(_synthPtr, channel, bank); } catch (e) {}
@@ -408,6 +423,7 @@
_channels[channel].program = program;
_channels[channel].isPercussion = (bank === 128);
_channels[channel].sfId = sfId;
_channels[channel]._epoch = _instrumentEpoch;
},
controllerChange: function (channel, controller, value) {
@@ -632,6 +648,7 @@
// ⚠️ Chỉ skip khi handle SF vẫn CÒN HỢP LỆ trong map — nếu
// không → vẫn program_select lại (tránh dùng handle đã unload).
var progAlreadySet = cachedCh && cachedCh.program === finalProg && cachedCh.bank === finalBank && cachedCh.sfId === finalSfId
&& cachedCh._epoch === _instrumentEpoch
&& (finalSfId ? _sfHandleMap.has(finalSfId) : true);
if ((synthEngine || program !== undefined) && !progAlreadySet) {
var sfHandle = finalSfId ? _sfHandleMap.get(finalSfId) : undefined;
@@ -666,6 +683,11 @@
}
}
var _selRet = _fluidModule._fluid_synth_program_select(_synthPtr, ch, sfHandle, finalBank, finalProg);
if (_selRet !== 0 && finalBank !== 0) {
// Preset không tồn tại ở bank này → thử bank 0
// (hết "noteon preset rỗng = âm sai/không đúng")
try { _fluidModule._fluid_synth_program_select(_synthPtr, ch, sfHandle, 0, finalProg); } catch (e2) {}
}
} catch (e) {}
} else {
try { _fluidModule._fluid_synth_bank_select(_synthPtr, ch, finalBank); } catch (e) {}
@@ -675,6 +697,7 @@
_channels[ch].bank = finalBank;
_channels[ch].program = finalProg;
_channels[ch].sfId = finalSfId;
_channels[ch]._epoch = _instrumentEpoch;
}
console.log('[SonicSF] noteon channel:', ch, 'pitch:', midiPitch, 'vel:', midiVel, 'sfId:', finalSfId);
_fluidModule._fluid_synth_noteon(_synthPtr, ch, midiPitch, midiVel);