FIX: (1) MIDI item route CHỈ qua Carla bridge khi VSTi loaded (bỏ FluidSynth GM sai âm) ở mọi đường playback + ghosts; (2) updateSfRouting sync route gains nhánh gainNode + ensureMasteringRouting force-sync mọi node — preview MIDI keyboard qua mastering không cần bật/tắt power; (3) stopNote/panic dừng fallback oscillator theo channel:pitch — hết âm kêu liên tục sau khi thả phím
This commit is contained in:
+135
-69
@@ -20805,6 +20805,16 @@ const App = () => {
|
||||
}
|
||||
if (_prNode.gainNode) {
|
||||
window.SonicSF.setOutputDestination(_prNode.gainNode);
|
||||
// ⚠️ FIX: sync route gains NGAY tại thời điểm routing — trước đây
|
||||
// bỏ qua → node giữ bypass cũ (tạo khi mastering OFF / audioBypass
|
||||
// true) → preview âm soundfont KHÔNG qua mastering cho tới khi bật/
|
||||
// tắt power (effect re-sync). MIDI preview route theo ♪ (midiBypass).
|
||||
if (_prNode.route && _prNode.route.routeGain && _prNode.route.dryGain) {
|
||||
const _prTrk2 = list.find(t => t.id === _activeSub.trackId);
|
||||
const _b2 = effMidiBypass(_prTrk2 || { id: _activeSub.trackId });
|
||||
_prNode.route.routeGain.gain.value = _b2 ? 0 : 1;
|
||||
_prNode.route.dryGain.gain.value = _b2 ? 1 : 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -20845,6 +20855,13 @@ const App = () => {
|
||||
if (node && node.gainNode && window.SonicSF && window.SonicSF.setOutputDestination) {
|
||||
console.log('[SFRoute] dest=gainNode node', t.id, '(no sfEntry) — route.routeGain=' + (node.route ? node.route.routeGain.gain.value : 'MISSING'), 'route.dryGain=' + (node.route ? node.route.dryGain.gain.value : 'MISSING'));
|
||||
window.SonicSF.setOutputDestination(node.gainNode);
|
||||
// ⚠️ FIX: sync route gains — node giữ bypass cũ → preview không qua
|
||||
// mastering cho tới khi bật/tắt power. MIDI preview theo ♪.
|
||||
if (node.route && node.route.routeGain && node.route.dryGain) {
|
||||
const _b3 = effMidiBypass(t);
|
||||
node.route.routeGain.gain.value = _b3 ? 0 : 1;
|
||||
node.route.dryGain.gain.value = _b3 ? 1 : 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -20875,6 +20892,27 @@ const App = () => {
|
||||
toggleMasteringOnMaster(!!_s.masterConnected, !!_s.isBypassed);
|
||||
applyMasteringSettings(_s);
|
||||
updateSfRouting();
|
||||
// Force-sync MỌI node đang tồn tại (route + sf route) theo trạng thái
|
||||
// hiện tại — phòng node tạo từ trước với bypass cũ (mastering OFF lúc
|
||||
// tạo node / audioBypass default true của section) → preview LUÔN qua
|
||||
// mastering khi chain bật, không cần bật/tắt power.
|
||||
try {
|
||||
const _list2 = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : tracks;
|
||||
_list2.forEach(_t => {
|
||||
const _n = activeTrackNodesRef.current[_t.id];
|
||||
if (!_n) return;
|
||||
if (_n.sfRouteGain && _n.sfDryGain) {
|
||||
const _mb = effMidiBypass(_t);
|
||||
_n.sfRouteGain.gain.value = _mb ? 0 : 1;
|
||||
_n.sfDryGain.gain.value = _mb ? 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;
|
||||
}
|
||||
});
|
||||
} catch (e2) {}
|
||||
} catch (e) {
|
||||
console.warn('ensureMasteringRouting error:', e);
|
||||
}
|
||||
@@ -21115,22 +21153,28 @@ const App = () => {
|
||||
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;
|
||||
window.SonicSF.playNote(
|
||||
note.pitch || 60,
|
||||
note.velocity || 0.8,
|
||||
durationMs,
|
||||
startTime,
|
||||
program,
|
||||
gainNode,
|
||||
trkCh,
|
||||
track.synth_engine
|
||||
);
|
||||
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.
|
||||
scheduleCarlaNote(track.synth_engine, trkCh, note.pitch || 60, note.velocity || 0.8, startTime, durationMs);
|
||||
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
|
||||
@@ -21144,18 +21188,20 @@ const App = () => {
|
||||
} else {
|
||||
const playOffset = offsetTime - noteStartSec;
|
||||
const remainingDurMs = (noteEndSec - offsetTime) * 1000;
|
||||
window.SonicSF.playNote(
|
||||
note.pitch || 60,
|
||||
note.velocity || 0.8,
|
||||
remainingDurMs,
|
||||
context.currentTime,
|
||||
program,
|
||||
gainNode,
|
||||
trkCh,
|
||||
track.synth_engine
|
||||
);
|
||||
if (!routeToCarla && window.SonicSF) {
|
||||
window.SonicSF.playNote(
|
||||
note.pitch || 60,
|
||||
note.velocity || 0.8,
|
||||
remainingDurMs,
|
||||
context.currentTime,
|
||||
program,
|
||||
gainNode,
|
||||
trkCh,
|
||||
track.synth_engine
|
||||
);
|
||||
}
|
||||
// MIDI items → Carla bridge (track VSTi + Carla local)
|
||||
scheduleCarlaNote(track.synth_engine, trkCh, note.pitch || 60, note.velocity || 0.8, context.currentTime, remainingDurMs);
|
||||
if (routeToCarla) scheduleCarlaNote(track.synth_engine, trkCh, note.pitch || 60, note.velocity || 0.8, context.currentTime, remainingDurMs);
|
||||
// Trigger VU meter flash instantly
|
||||
if (window.triggerMidiVuActivity) {
|
||||
window.triggerMidiVuActivity((activeTab && activeTab.startsWith('session_') ? '_sess_' : '') + track.id, note.velocity || 0.8);
|
||||
@@ -21255,21 +21301,27 @@ const App = () => {
|
||||
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;
|
||||
window.SonicSF.playNote(
|
||||
note.pitch || 60,
|
||||
note.velocity || 0.8,
|
||||
playDurMs,
|
||||
startTime,
|
||||
program,
|
||||
subNode,
|
||||
subCh,
|
||||
subTrack.synth_engine
|
||||
);
|
||||
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) →
|
||||
@@ -21287,16 +21339,19 @@ const App = () => {
|
||||
}, delay * 1000);
|
||||
} else {
|
||||
const remainingDurMs = (notePlayEndMain - offsetTime) * 1000;
|
||||
window.SonicSF.playNote(
|
||||
note.pitch || 60,
|
||||
note.velocity || 0.8,
|
||||
remainingDurMs,
|
||||
context.currentTime,
|
||||
program,
|
||||
subNode,
|
||||
subCh,
|
||||
subTrack.synth_engine
|
||||
);
|
||||
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);
|
||||
@@ -21369,35 +21424,42 @@ const App = () => {
|
||||
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;
|
||||
window.SonicSF.playNote(
|
||||
note.pitch || 60,
|
||||
note.velocity || 0.8,
|
||||
durationMs,
|
||||
startTime,
|
||||
program,
|
||||
gainNode,
|
||||
lcCh,
|
||||
track.synth_engine
|
||||
);
|
||||
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)
|
||||
scheduleCarlaNote(track.synth_engine, lcCh, note.pitch || 60, note.velocity || 0.8, startTime, durationMs);
|
||||
if (routeToCarla) scheduleCarlaNote(track.synth_engine, lcCh, note.pitch || 60, note.velocity || 0.8, startTime, durationMs);
|
||||
} else {
|
||||
const remainingDurMs = (noteEndSec - offsetTime) * 1000;
|
||||
window.SonicSF.playNote(
|
||||
note.pitch || 60,
|
||||
note.velocity || 0.8,
|
||||
remainingDurMs,
|
||||
context.currentTime,
|
||||
program,
|
||||
gainNode,
|
||||
lcCh,
|
||||
track.synth_engine
|
||||
);
|
||||
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)
|
||||
scheduleCarlaNote(track.synth_engine, lcCh, note.pitch || 60, note.velocity || 0.8, context.currentTime, remainingDurMs);
|
||||
if (routeToCarla) scheduleCarlaNote(track.synth_engine, lcCh, note.pitch || 60, note.velocity || 0.8, context.currentTime, remainingDurMs);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -21440,12 +21502,15 @@ const App = () => {
|
||||
const effectiveDur = noteDurSec - Math.max(0, offsetSeconds - noteStartSec);
|
||||
const scheduledTime = startWallTime + effectiveStart;
|
||||
const durMs = effectiveDur * 1000;
|
||||
if (window.SonicSF) {
|
||||
// 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
|
||||
// (schedule theo audio clock bằng setTimeout — preview, timing gần đúng).
|
||||
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(synthEngine)) {
|
||||
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;
|
||||
@@ -21474,11 +21539,12 @@ const App = () => {
|
||||
var effDur = durSec - Math.max(0, offsetSeconds - startSec);
|
||||
var schedTime = startWallTime + effStart;
|
||||
var durMs = effDur * 1000;
|
||||
if (window.SonicSF) {
|
||||
// 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);
|
||||
}
|
||||
// Ghost notes → Carla bridge (ghost track VSTi + Carla local)
|
||||
scheduleCarlaNote(ghostSynth, ghostCh, note.pitch || 60, note.velocity || 0.8, schedTime, durMs);
|
||||
if (gRouteCarla) scheduleCarlaNote(ghostSynth, ghostCh, note.pitch || 60, note.velocity || 0.8, schedTime, durMs);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1269,7 +1269,11 @@ if(_activeSub&&_activeSub.type==='PIANO_ROLL'&&!isPlayingRef.current){// PIANO R
|
||||
// không có sub-node → null (masterBus.input — qua mastering).
|
||||
let _prNode=activeTrackNodesRef.current[_activeSub.trackId];if(_activeSub.parent_tab_id&&_activeSub.parent_tab_id.startsWith('session_')){const _prSubKeys=Object.keys(activeTrackNodesRef.current).filter(k=>k.endsWith('_sub_'+_activeSub.trackId));_prNode=_prSubKeys.length?activeTrackNodesRef.current[_prSubKeys[0]]:null;}if(_prNode&&window.SonicSF&&window.SonicSF.setOutputDestination){if(_prNode.sfEntry){window.SonicSF.setOutputDestination(_prNode.sfEntry);// Ép route qua mastering chain NGAY tại thời điểm routing (node có
|
||||
// thể tạo khi mastering OFF → route dry — sửa ngay nếu chain ON).
|
||||
if(_prNode.sfRouteGain&&_prNode.sfDryGain){const _prTrk=list.find(t=>t.id===_activeSub.trackId);const _b=effMidiBypass(_prTrk||{id:_activeSub.trackId});_prNode.sfRouteGain.gain.value=_b?0:1;_prNode.sfDryGain.gain.value=_b?1:0;}return;}if(_prNode.gainNode){window.SonicSF.setOutputDestination(_prNode.gainNode);return;}}}const midiAudible=list.filter(t=>t.midiItems&&t.midiItems.length>0&&computeTrackAudibleGain(list,t)>0);console.log('[SFRoute] activeTab='+_activeTabId,'midiAudible='+midiAudible.map(t=>t.id+'(midi'+(t.midiItems||[]).length+')').join(',')||'none');if(midiAudible.length===1){const t=midiAudible[0];// SECTION-TAB: track id của section clone TRÙNG main track id → node
|
||||
if(_prNode.sfRouteGain&&_prNode.sfDryGain){const _prTrk=list.find(t=>t.id===_activeSub.trackId);const _b=effMidiBypass(_prTrk||{id:_activeSub.trackId});_prNode.sfRouteGain.gain.value=_b?0:1;_prNode.sfDryGain.gain.value=_b?1:0;}return;}if(_prNode.gainNode){window.SonicSF.setOutputDestination(_prNode.gainNode);// ⚠️ FIX: sync route gains NGAY tại thời điểm routing — trước đây
|
||||
// bỏ qua → node giữ bypass cũ (tạo khi mastering OFF / audioBypass
|
||||
// true) → preview âm soundfont KHÔNG qua mastering cho tới khi bật/
|
||||
// tắt power (effect re-sync). MIDI preview route theo ♪ (midiBypass).
|
||||
if(_prNode.route&&_prNode.route.routeGain&&_prNode.route.dryGain){const _prTrk2=list.find(t=>t.id===_activeSub.trackId);const _b2=effMidiBypass(_prTrk2||{id:_activeSub.trackId});_prNode.route.routeGain.gain.value=_b2?0:1;_prNode.route.dryGain.gain.value=_b2?1:0;}return;}}}const midiAudible=list.filter(t=>t.midiItems&&t.midiItems.length>0&&computeTrackAudibleGain(list,t)>0);console.log('[SFRoute] activeTab='+_activeTabId,'midiAudible='+midiAudible.map(t=>t.id+'(midi'+(t.midiItems||[]).length+')').join(',')||'none');if(midiAudible.length===1){const t=midiAudible[0];// SECTION-TAB: track id của section clone TRÙNG main track id → node
|
||||
// chính (key = id) là node MAIN (route theo ♪ main — SAI context, MIDI
|
||||
// section bị kéo vào dry). ƯU TIÊN SUB-NODE (key <parent>_sub_<id> —
|
||||
// route theo ♪ section + thẳng masterBus.input) — không có sub-node →
|
||||
@@ -1282,7 +1286,9 @@ if(node.sfRouteGain&&node.sfDryGain){// ⚠️ FIX: truyền track OBJECT (t)
|
||||
// → effMidiBypass đọc track.id=undefined → map lookup sai → _b luôn
|
||||
// false → sfRouteGain bị ép 1 (không bypass) — nút ♪ vô hiệu qua
|
||||
// đường này.
|
||||
const _b=effMidiBypass(t);node.sfRouteGain.gain.value=_b?0:1;node.sfDryGain.gain.value=_b?1:0;console.log('[Bypass] sf route sync track',t.id,'midiBypass='+_b,_b?'DRY':'MASTERING');}return;}if(node&&node.gainNode&&window.SonicSF&&window.SonicSF.setOutputDestination){console.log('[SFRoute] dest=gainNode node',t.id,'(no sfEntry) — route.routeGain='+(node.route?node.route.routeGain.gain.value:'MISSING'),'route.dryGain='+(node.route?node.route.dryGain.gain.value:'MISSING'));window.SonicSF.setOutputDestination(node.gainNode);return;}}if(window.SonicSF&&window.SonicSF.setOutputDestination){window.SonicSF.setOutputDestination(null);}}catch(e){console.warn('updateSfRouting error:',e);}};const updateSfRoutingRef=useRef(null);// ── Đồng bộ mastering + routing NGAY TRƯỚC KHI PREVIEW (keybed/draw/MIDI file) ──
|
||||
const _b=effMidiBypass(t);node.sfRouteGain.gain.value=_b?0:1;node.sfDryGain.gain.value=_b?1:0;console.log('[Bypass] sf route sync track',t.id,'midiBypass='+_b,_b?'DRY':'MASTERING');}return;}if(node&&node.gainNode&&window.SonicSF&&window.SonicSF.setOutputDestination){console.log('[SFRoute] dest=gainNode node',t.id,'(no sfEntry) — route.routeGain='+(node.route?node.route.routeGain.gain.value:'MISSING'),'route.dryGain='+(node.route?node.route.dryGain.gain.value:'MISSING'));window.SonicSF.setOutputDestination(node.gainNode);// ⚠️ FIX: sync route gains — node giữ bypass cũ → preview không qua
|
||||
// mastering cho tới khi bật/tắt power. MIDI preview theo ♪.
|
||||
if(node.route&&node.route.routeGain&&node.route.dryGain){const _b3=effMidiBypass(t);node.route.routeGain.gain.value=_b3?0:1;node.route.dryGain.gain.value=_b3?1:0;}return;}}if(window.SonicSF&&window.SonicSF.setOutputDestination){window.SonicSF.setOutputDestination(null);}}catch(e){console.warn('updateSfRouting error:',e);}};const updateSfRoutingRef=useRef(null);// ── Đồng bộ mastering + routing NGAY TRƯỚC KHI PREVIEW (keybed/draw/MIDI file) ──
|
||||
// Bug: preview instrument soundfont với MIDI keyboard KHÔNG tự động đi qua
|
||||
// mastering FX của main out — phải bật/tắt nút power mới có tác dụng. Nguyên
|
||||
// nhân: routing SF (setOutputDestination + sfRouteGain/sfDryGain) và graph
|
||||
@@ -1292,7 +1298,11 @@ const _b=effMidiBypass(t);node.sfRouteGain.gain.value=_b?0:1;node.sfDryGain.gain
|
||||
// gains) — gọi tại mọi điểm preview → âm preview LUÔN qua mastering khi bật.
|
||||
const ensureMasteringRouting=()=>{try{const _s=window.currentMasteringSettings;if(!_s)return;// Đảm bảo audioCtx + masterBus đã tồn tại trước khi sync (lần preview
|
||||
// đầu tiên audio chưa init → getAudioContext tạo ngay)
|
||||
getAudioContext();if(!masterBus||!audioCtx)return;toggleMasteringOnMaster(!!_s.masterConnected,!!_s.isBypassed);applyMasteringSettings(_s);updateSfRouting();}catch(e){console.warn('ensureMasteringRouting error:',e);}};const ensureMasteringRoutingRef=useRef(null);ensureMasteringRoutingRef.current=ensureMasteringRouting;window.__ensureMasteringRouting=ensureMasteringRouting;// ── MIDI preview cache (bounce nhanh offline) ──
|
||||
getAudioContext();if(!masterBus||!audioCtx)return;toggleMasteringOnMaster(!!_s.masterConnected,!!_s.isBypassed);applyMasteringSettings(_s);updateSfRouting();// Force-sync MỌI node đang tồn tại (route + sf route) theo trạng thái
|
||||
// hiện tại — phòng node tạo từ trước với bypass cũ (mastering OFF lúc
|
||||
// tạo node / audioBypass default true của section) → preview LUÔN qua
|
||||
// mastering khi chain bật, không cần bật/tắt power.
|
||||
try{const _list2=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracks;_list2.forEach(_t=>{const _n=activeTrackNodesRef.current[_t.id];if(!_n)return;if(_n.sfRouteGain&&_n.sfDryGain){const _mb=effMidiBypass(_t);_n.sfRouteGain.gain.value=_mb?0:1;_n.sfDryGain.gain.value=_mb?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;}});}catch(e2){}}catch(e){console.warn('ensureMasteringRouting error:',e);}};const ensureMasteringRoutingRef=useRef(null);ensureMasteringRoutingRef.current=ensureMasteringRouting;window.__ensureMasteringRouting=ensureMasteringRouting;// ── MIDI preview cache (bounce nhanh offline) ──
|
||||
// Khi preview: capture tiếng đàn (SF output — pre track-FX) vào cache theo
|
||||
// track. Khi export: nếu cache đủ độ dài → render OFFLINE NHANH bằng cache
|
||||
// (qua FX Rack + Mastering như playback) — không cần chờ bounce realtime.
|
||||
@@ -1330,14 +1340,17 @@ const midiItems=track.midiItems||[];if((track.type==='MIDI'||midiItems.length>0)
|
||||
ensureMidiCapture(track,activeTrackNodesRef.current[track.id]);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;if(offsetTime<noteStartSec){const delay=noteStartSec-offsetTime;const startTime=context.currentTime+delay;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 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
|
||||
// realtime — yêu cầu: MIDI item PHẢI play qua Carla khi VSTi loaded.
|
||||
scheduleCarlaNote(track.synth_engine,trkCh,note.pitch||60,note.velocity||0.8,startTime,durationMs);// Trigger VU meter flash when the note starts playing
|
||||
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
|
||||
// sau khi âm đã dừng — user bug 05:45).
|
||||
if(!isPlayingRef.current)return;if(window.triggerMidiVuActivity){// VU key theo CONTEXT (tách MAIN vs SECTION — user 07:20)
|
||||
window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+track.id,note.velocity||0.8);}},delay*1000);}else{const playOffset=offsetTime-noteStartSec;const remainingDurMs=(noteEndSec-offsetTime)*1000;window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,gainNode,trkCh,track.synth_engine);// MIDI items → Carla bridge (track VSTi + Carla local)
|
||||
scheduleCarlaNote(track.synth_engine,trkCh,note.pitch||60,note.velocity||0.8,context.currentTime,remainingDurMs);// Trigger VU meter flash instantly
|
||||
window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+track.id,note.velocity||0.8);}},delay*1000);}else{const playOffset=offsetTime-noteStartSec;const remainingDurMs=(noteEndSec-offsetTime)*1000;if(!routeToCarla&&window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,gainNode,trkCh,track.synth_engine);}// MIDI items → Carla bridge (track VSTi + Carla local)
|
||||
if(routeToCarla)scheduleCarlaNote(track.synth_engine,trkCh,note.pitch||60,note.velocity||0.8,context.currentTime,remainingDurMs);// Trigger VU meter flash instantly
|
||||
if(window.triggerMidiVuActivity){window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+track.id,note.velocity||0.8);}}}});});}// If track has instrumentId set but no MIDI items, create scheduled oscillators
|
||||
if(track.instrumentId&&midiItems.length===0&&track.buffer){// Play audio normally (the buffer has the audio data)
|
||||
// This block is intentionally empty - audio clips already play above
|
||||
@@ -1349,7 +1362,9 @@ const subNodeObj=activeTrackNodesRef.current[track.id+'_sub_'+subTrack.id];if(su
|
||||
const subClips=subTrack.clips||[];subClips.forEach(clip=>{if(!clip.buffer)return;const clipStartLocal=clip.startTime||0;const clipDurLocal=clip.buffer.duration/(clip.speed||1.0);const clipStartMain=secStart+clipStartLocal;const clipEndMain=clipStartMain+clipDurLocal;// Only play within the bounds of [secStart, secEnd]
|
||||
const activeStart=Math.max(clipStartMain,secStart);const activeEnd=Math.min(clipEndMain,secEnd);if(activeStart<activeEnd&&offsetTime<activeEnd){const source=context.createBufferSource();source.buffer=clip.buffer;source.playbackRate.value=clip.speed||1.0;source.connect(subNode);if(offsetTime<activeStart){const delay=activeStart-offsetTime;const playOffset=activeStart-clipStartMain;const playDuration=activeEnd-activeStart;source.start(context.currentTime+delay,playOffset*(clip.speed||1.0),playDuration);activeSourcesRef.current.push(source);}else{const playOffset=offsetTime-clipStartMain;const playDuration=activeEnd-offsetTime;source.start(context.currentTime,playOffset*(clip.speed||1.0),playDuration);activeSourcesRef.current.push(source);}}});// 2. Play MIDI items in subTrack
|
||||
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&¬eStartMain<secEnd&&offsetTime<noteEndMain){const notePlayEndMain=Math.min(noteEndMain,secEnd);const program=subTrack.instrumentProgram;var subCh=assignTrackMidiChannel(subTrack,secTracks);if(offsetTime<noteStartMain){const delay=noteStartMain-offsetTime;const startTime=context.currentTime+delay;const playDurMs=(notePlayEndMain-noteStartMain)*1000;window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,playDurMs,startTime,program,subNode,subCh,subTrack.synth_engine);// Trigger VU meter flash đúng nhịp (cả subTrack — VU
|
||||
if(noteStartMain>=secStart&¬eStartMain<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
|
||||
// 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
|
||||
@@ -1357,7 +1372,7 @@ if(noteStartMain>=secStart&¬eStartMain<secEnd&&offsetTime<noteEndMain){const
|
||||
setTimeout(()=>{// ⚠️ Guard: stop → setTimeout sót không được fire (VU
|
||||
// nhảy sau khi âm đã dừng — user bug 05:45).
|
||||
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;window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,subNode,subCh,subTrack.synth_engine);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 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
|
||||
@@ -1365,9 +1380,11 @@ const gainNode=getOrCreateTrackNode(track,context);const pannerNode=activeTrackN
|
||||
// chorus/reverb → pannerNode. A direct tap would BYPASS the FX Rack
|
||||
// chain for audio clips (double path: with + without FX).
|
||||
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;if(offsetTime<noteStartSec){const delay=noteStartSec-offsetTime;const startTime=context.currentTime+delay;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)
|
||||
scheduleCarlaNote(track.synth_engine,lcCh,note.pitch||60,note.velocity||0.8,startTime,durationMs);}else{const remainingDurMs=(noteEndSec-offsetTime)*1000;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)
|
||||
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 "đã
|
||||
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)
|
||||
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 "đã
|
||||
// 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
|
||||
@@ -1376,11 +1393,13 @@ const instCtx=resolveTrackInstrumentCtx(track,activeTracksRef.current||[]);const
|
||||
// items placed later in the project play `item.startTime` seconds in the
|
||||
// future (silence when pressing play). Ghost notes are already relative to
|
||||
// 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;if(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
|
||||
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
|
||||
// (schedule theo audio clock bằng setTimeout — preview, timing gần đúng).
|
||||
if(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(synthEngine)){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;if(window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durMs,schedTime,ghostProg,ghostDest,ghostCh,ghostSynth);}// Ghost notes → Carla bridge (ghost track VSTi + Carla local)
|
||||
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
|
||||
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
|
||||
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).
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
let _pendingNoteTimers = [];
|
||||
let _loadedFonts = {};
|
||||
let _activeOscillators = {};
|
||||
// Fallback oscillator theo 'channel:pitch' — để stopNote/stopAll dừng được
|
||||
// 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 = {};
|
||||
let _gainNode = null;
|
||||
let _pendingOutputDestination = null;
|
||||
let _outputDestination = null; // cache đích route — dedupe swap dư giữa stream
|
||||
@@ -479,6 +483,23 @@
|
||||
// Hủy note đang CHỜ LOAD soundfont (chưa noteon) — trước đây note
|
||||
// này vẫn bắn TRỄ sau khi thả phím → âm loop không dừng được.
|
||||
if (_pendingNoteOns[key]) delete _pendingNoteOns[key];
|
||||
// ⚠️ FIX: dừng fallback oscillator theo 'channel:pitch' — trước đây
|
||||
// stopNote chỉ noteoff FluidSynth, KHÔNG dừng oscillator (khi WASM
|
||||
// chưa sẵn sàng / font load fail → _playNoteFallback) → âm kêu liên
|
||||
// tục mặc dù đã thả phím (không xử lí noteoff).
|
||||
var _oscIds = _activeOscillatorsByKey[key];
|
||||
if (_oscIds && _oscIds.length) {
|
||||
var _nowT = getCtx().currentTime;
|
||||
_oscIds.slice().forEach(function (oid) {
|
||||
var entry = _activeOscillators[oid];
|
||||
if (entry) {
|
||||
try { entry.gain.gain.cancelScheduledValues(_nowT); entry.gain.gain.setValueAtTime(0, _nowT); } catch (e) {}
|
||||
try { entry.osc.stop(_nowT); } catch (e) {}
|
||||
delete _activeOscillators[oid];
|
||||
}
|
||||
});
|
||||
delete _activeOscillatorsByKey[key];
|
||||
}
|
||||
if (_initialized && _fluidModule) {
|
||||
var mappedChs = _activeNotes[key];
|
||||
if (mappedChs === undefined) mappedChs = [channel];
|
||||
@@ -711,6 +732,17 @@
|
||||
for (var c = 0; c < 16; c++) _fluidModule._fluid_synth_all_notes_off(_synthPtr, c);
|
||||
} catch (e) {}
|
||||
}
|
||||
// Dừng fallback oscillator (FluidSynth chưa sẵn sàng / font fail)
|
||||
try {
|
||||
var _pctx = getCtx();
|
||||
var _pnow = _pctx.currentTime;
|
||||
Object.values(_activeOscillators).forEach(function (entry) {
|
||||
try { if (entry.gain) { entry.gain.gain.cancelScheduledValues(_pnow); entry.gain.gain.setValueAtTime(0, _pnow); } } catch (e) {}
|
||||
try { if (entry.osc) entry.osc.stop(_pnow); } catch (e) {}
|
||||
});
|
||||
Object.keys(_activeOscillators).forEach(function (k) { delete _activeOscillators[k]; });
|
||||
_activeOscillatorsByKey = {};
|
||||
} catch (e) {}
|
||||
_activeNotes = {};
|
||||
},
|
||||
|
||||
@@ -768,7 +800,20 @@
|
||||
osc.stop(stopAt);
|
||||
var oscId = note + '_' + Date.now() + '_' + Math.random();
|
||||
_activeOscillators[oscId] = { osc: osc, gain: noteGain };
|
||||
setTimeout(function () { delete _activeOscillators[oscId]; }, (stopAt - ctx.currentTime) * 1000 + 100);
|
||||
// Đăng ký theo 'channel:pitch' để stopNote dừng được khi thả phím
|
||||
var oscKey = (channel !== undefined && channel >= 0 && channel < 16) ? (channel + ':' + note) : null;
|
||||
if (oscKey) {
|
||||
if (!_activeOscillatorsByKey[oscKey]) _activeOscillatorsByKey[oscKey] = [];
|
||||
_activeOscillatorsByKey[oscKey].push(oscId);
|
||||
}
|
||||
setTimeout(function () {
|
||||
delete _activeOscillators[oscId];
|
||||
if (oscKey && _activeOscillatorsByKey[oscKey]) {
|
||||
var _oi = _activeOscillatorsByKey[oscKey].indexOf(oscId);
|
||||
if (_oi >= 0) _activeOscillatorsByKey[oscKey].splice(_oi, 1);
|
||||
if (_activeOscillatorsByKey[oscKey].length === 0) delete _activeOscillatorsByKey[oscKey];
|
||||
}
|
||||
}, (stopAt - ctx.currentTime) * 1000 + 100);
|
||||
return osc;
|
||||
},
|
||||
|
||||
@@ -805,6 +850,7 @@
|
||||
} catch (e) {}
|
||||
});
|
||||
Object.keys(_activeOscillators).forEach(function (k) { delete _activeOscillators[k]; });
|
||||
_activeOscillatorsByKey = {};
|
||||
_activeNotes = {};
|
||||
},
|
||||
|
||||
|
||||
@@ -31,12 +31,12 @@
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
|
||||
<script src="/static/js/services/fluidsynthLoader.js?v=202607271245"></script>
|
||||
<script src="/static/js/services/runtime.js?v=202608101500"></script>
|
||||
<script src="/static/js/services/api.js?v=202608101500"></script>
|
||||
<script src="/static/js/services/runtime.js?v=202608101600"></script>
|
||||
<script src="/static/js/services/api.js?v=202608101600"></script>
|
||||
<script src="/static/js/services/audioEngine.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/storage.js?v=202608038200"></script>
|
||||
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/soundfontPlayer.js?v=202608101500"></script>
|
||||
<script src="/static/js/services/soundfontPlayer.js?v=202608101600"></script>
|
||||
<script src="/static/js/services/aiGateway.js?v=202608037200"></script>
|
||||
<script src="/static/js/services/dawCommandDispatcher.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/pianoRollTabService.js?v=202607272044"></script>
|
||||
@@ -44,7 +44,7 @@
|
||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608101500" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608101600" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
Reference in New Issue
Block a user