V45.24: dao y/c - solo/mute CHI cuc bo MAIN/SECTION, PIANO ROLL TAB KHONG bi cam. REVERT V45.23 (4 gate computeTrackAudibleGain o preview) + lam PR doc lap: (1) scheduleMidiNoteDispatch them param prNote=true cho 6 nhanh PR (play, ghost, click, draw, brush, wheel) -> bo qua solo_guard + goi ensureTrackPrAudible (CC7=vol theo volumeDb bo qua mute/solo + setTrackNodeGain) tai thoi diem fire; (2) preview keybed vkbNoteOn + hardware attachMidiHandler (bridge + SonicSF fallback) khi _isPrTabNow goi ensureTrackPrAudible de channel khong con bi CC7=0 tu toggle MAIN; (3) applyAllTrackMuteSolo + sync useEffect bo qua gain=0/CC7=0/kill note khi track co sub-tab PR dang PLAY. Timeline MAIN giu solo_guard. v=202608311725
This commit is contained in:
+59
-28
@@ -785,6 +785,20 @@ function sendMidiVolumeCC7(t, list, audible) {
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// V45.24: PR tab audio DOC LAP voi mute/solo cua MAIN/SECTION — khoi phuc
|
||||
// volume track (theo volumeDb, bo qua mute/solo) truoc khi note PR phat.
|
||||
// sendMidiVolumeCC7(t, list, true) gui CC7=vol (channel khong con bi mute/solo
|
||||
// tu toggle MAIN); setTrackNodeGain phuc hoi gainNode (SonicSF fallback dest).
|
||||
function ensureTrackPrAudible(t, list, node) {
|
||||
if (!t) return;
|
||||
try {
|
||||
const volDb = Number(t.volumeDb);
|
||||
const volLin = (isNaN(volDb) || !isFinite(volDb)) ? 1.0 : (volDb <= -50 ? 0 : Math.pow(10, volDb / 20));
|
||||
if (node) setTrackNodeGain(node, volLin);
|
||||
sendMidiVolumeCC7(t, list, true);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// ── CC11 expression (realtime) ──────────────────────────────────────────
|
||||
// Mỗi note có expression_curve → chuỗi CC11 theo grid 16ms. Timer đăng ký
|
||||
// vào _cc11Timers để stopAllPlayback hủy sạch; epoch guard (_nativePlayEpoch)
|
||||
@@ -9666,7 +9680,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
playNativeVstNote(pvTrk, n.pitch, n.velocity || 0.8, 200, undefined, 'pv_' + st.trackId);
|
||||
return;
|
||||
}
|
||||
scheduleMidiNoteDispatch(pvTrk, n.pitch, n.velocity || 0.8, 200, ctx.currentTime, pvCtx.program, null, pvCtx.ch, pvCtx.synthEngine);
|
||||
scheduleMidiNoteDispatch(pvTrk, n.pitch, n.velocity || 0.8, 200, ctx.currentTime, pvCtx.program, null, pvCtx.ch, pvCtx.synthEngine, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -10181,7 +10195,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
} else if (isStandaloneSf() && isVstTrackEngine(clCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
|
||||
playNativeVstNote(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, undefined, 'pv_' + st.trackId);
|
||||
} else {
|
||||
scheduleMidiNoteDispatch(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine);
|
||||
scheduleMidiNoteDispatch(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10451,7 +10465,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
// playNote (FluidSynth — nhạc cụ THẬT của track). _playNoteFallback chỉ
|
||||
// là oscillator beep (sai âm với percussion/soundfont — user: note vẽ
|
||||
// mới nghe nhạc cụ track trước).
|
||||
scheduleMidiNoteDispatch(dwTrk, dwPitch, brushVelocityRef.current || 0.8, dwDurMs, ctx.currentTime, dwTrk ? dwTrk.instrumentProgram : undefined, null, dwCh, dwTrk ? dwTrk.synth_engine : undefined);
|
||||
scheduleMidiNoteDispatch(dwTrk, dwPitch, brushVelocityRef.current || 0.8, dwDurMs, ctx.currentTime, dwTrk ? dwTrk.instrumentProgram : undefined, null, dwCh, dwTrk ? dwTrk.synth_engine : undefined, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -10553,7 +10567,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
var pvCtx = getAudioContext();
|
||||
// playNote (FluidSynth — nhạc cụ THẬT). _playNoteFallback = oscillator
|
||||
// beep sai âm (percussion/soundfont).
|
||||
scheduleMidiNoteDispatch(pvTrk, p, brushVelocityRef.current || 0.8, durMs, pvCtx.currentTime, pvCtxInst.program, null, pvCtxInst.ch, pvCtxInst.synthEngine);
|
||||
scheduleMidiNoteDispatch(pvTrk, p, brushVelocityRef.current || 0.8, durMs, pvCtx.currentTime, pvCtxInst.program, null, pvCtxInst.ch, pvCtxInst.synthEngine, true);
|
||||
previewPitchRef.current = p;
|
||||
}
|
||||
}
|
||||
@@ -18659,10 +18673,9 @@ const App = () => {
|
||||
}
|
||||
bTracks.forEach(function (bt) {
|
||||
if (!bt.isArmed && !bPrArmed[bt.id]) return;
|
||||
// V45.23: mute/solo gate preview bridge NOTE_ON — track
|
||||
// muted/solo-out khong duoc phat (cung semantics
|
||||
// computeTrackAudibleGain nhu scheduleMidiNoteDispatch).
|
||||
if (computeTrackAudibleGain(bTracks, bt) <= 0) { try { if (window.__dbgLog) window.__dbgLog('solo_guard', 'block midi ch=' + bt.id + ' p=' + pitch); } catch (e) {} return; }
|
||||
// V45.24: PR preview doc lap voi mute/solo MAIN/SECTION —
|
||||
// khoi phuc volume track (bo qua mute/solo) khi o PR tab.
|
||||
if (_isPrTabNow) { try { ensureTrackPrAudible(bt, bTracks, activeTrackNodesRef.current[bt.id]); } catch (e) {} }
|
||||
if (window.triggerMidiVuActivity) {
|
||||
window.triggerMidiVuActivity(bt.id, scaledVel);
|
||||
}
|
||||
@@ -18679,8 +18692,8 @@ const App = () => {
|
||||
if (arSubs.length > 0) {
|
||||
arSubs.forEach(function(as) {
|
||||
var asTrk = allTracks.find(function(t) { return t.id === as.trackId; });
|
||||
// V45.23: mute/solo gate preview (SonicSF fallback).
|
||||
if (!asTrk || computeTrackAudibleGain(allTracks, asTrk) <= 0) return;
|
||||
// V45.24: PR preview doc lap voi mute/solo MAIN/SECTION.
|
||||
if (_isPrTabNow && asTrk) { try { ensureTrackPrAudible(asTrk, allTracks, activeTrackNodesRef.current[as.trackId]); } catch (e) {} }
|
||||
var asCh = asTrk ? assignTrackMidiChannel(asTrk, allTracks) : 0;
|
||||
var asProg = as.instrumentProgram;
|
||||
var asSe = as.synth_engine;
|
||||
@@ -18703,8 +18716,8 @@ const App = () => {
|
||||
}
|
||||
// Route to ALL armed tracks (not just the first one)
|
||||
armedTracks.forEach(function(at) {
|
||||
// V45.23: mute/solo gate preview (SonicSF fallback).
|
||||
if (computeTrackAudibleGain(allTracks, at) <= 0) return;
|
||||
// V45.24: PR preview doc lap voi mute/solo MAIN/SECTION.
|
||||
if (_isPrTabNow) { try { ensureTrackPrAudible(at, allTracks, activeTrackNodesRef.current[at.id]); } catch (e) {} }
|
||||
var atCh = assignTrackMidiChannel(at, allTracks);
|
||||
var atProg = at.instrumentProgram;
|
||||
var atSe = at.synth_engine;
|
||||
@@ -19029,7 +19042,15 @@ const App = () => {
|
||||
const wasAudible = trackAudibleRef.current[t.id] !== false;
|
||||
const node = activeTrackNodesRef.current[t.id];
|
||||
const audibleGain = computeTrackAudibleGain(effective, t);
|
||||
if (node) setTrackNodeGain(node, audibleGain);
|
||||
// V45.24: PR tab doc lap voi mute/solo MAIN/SECTION — track dang co
|
||||
// sub-tab PIANO_ROLL PLAY thi KHONG ap mute/solo realtime (gain=0,
|
||||
// CC7=0, kill note) de PR khong bi cam; mute/solo chi anh huong
|
||||
// MAIN/SECTION playback.
|
||||
const prPlayingForTrack = !!(subTabsRef.current || []).some(function (s) { return s.type === 'PIANO_ROLL' && s.trackId === t.id && s.isPlaying; });
|
||||
const _volDb = Number(t.volumeDb);
|
||||
const _volLin = (isNaN(_volDb) || !isFinite(_volDb)) ? 1.0 : (_volDb <= -50 ? 0 : Math.pow(10, _volDb / 20));
|
||||
const effGain = prPlayingForTrack ? _volLin : audibleGain;
|
||||
if (node) setTrackNodeGain(node, effGain);
|
||||
// Section sub-nodes (key: <parent>_sub_<trackId>) — mute/solo của
|
||||
// SECTION-TAB phải áp gain cho node play section — CHỈ trong section
|
||||
// (activeTracksRef = tracks section), KHÔNG đụng track MAIN.
|
||||
@@ -19040,19 +19061,19 @@ const App = () => {
|
||||
}
|
||||
});
|
||||
if (node && node.sfEntry) {
|
||||
try { node.sfEntry.gain.setTargetAtTime(audibleGain, getAudioContext().currentTime, 0.02); } catch (e) {}
|
||||
try { node.sfEntry.gain.setTargetAtTime(effGain, getAudioContext().currentTime, 0.02); } catch (e) {}
|
||||
}
|
||||
// MIDI/bridge tracks: CC7 (channel volume) realtime — even for notes
|
||||
// already sounding. Bridge active → qua SonicMidiRouter (bridge); bridge
|
||||
// down → SonicSF (WASM). CC7 = VOLUME của track (không chỉ 100/0).
|
||||
if (isMidiSoundTrack(t)) {
|
||||
try { sendMidiVolumeCC7(t, effective, audible); } catch (e) {}
|
||||
try { sendMidiVolumeCC7(t, effective, audible || prPlayingForTrack); } catch (e) {}
|
||||
}
|
||||
// D18: track vừa MẤT tiếng (mute/solo) — dừng note đang giữ ngay. CC7=0
|
||||
// (sendMidiVolumeCC7) không ăn với VST như Qin (bỏ qua channel volume)
|
||||
// → note kêu tiếp (tremolo) khi solo track khác. NOTE_OFF từng pitch đang
|
||||
// giữ + CC123/CC120 all-notes-off trên channel track đó.
|
||||
if (!audible && wasAudible && isMidiSoundTrack(t)) {
|
||||
if (!audible && wasAudible && !prPlayingForTrack && isMidiSoundTrack(t)) {
|
||||
try {
|
||||
const _isPerc = !!(t.synth_engine && t.synth_engine.soundfont_bank === 128) || !!t.is_percussion;
|
||||
try { if (window.__dbgLog) window.__dbgLog('solo_kill', 'track=' + t.id + ' tset=' + (((timelinePitchesRef.current || {})[t.id] || new Set()).size) + ' held=' + ((activeMidiPitchesRef.current || new Set()).size)); } catch (e) {}
|
||||
@@ -19076,7 +19097,7 @@ const App = () => {
|
||||
} catch (e) {}
|
||||
}
|
||||
if (audible && !wasAudible) becameAudible = true;
|
||||
trackAudibleRef.current[t.id] = audible;
|
||||
trackAudibleRef.current[t.id] = prPlayingForTrack ? true : audible;
|
||||
});
|
||||
// A track that just became audible may have had its audio sources dropped
|
||||
// by a loop restart while it was inaudible (startTrackPlayback skips
|
||||
@@ -19833,10 +19854,16 @@ const App = () => {
|
||||
const audible = computeTrackAudibleGain(list, t) > 0;
|
||||
const node = activeTrackNodesRef.current[t.id];
|
||||
const audibleGain = computeTrackAudibleGain(list, t);
|
||||
if (node) setTrackNodeGain(node, audibleGain);
|
||||
// V45.24: PR tab doc lap — track co sub-tab PR PLAY thi khong ap
|
||||
// mute/solo realtime (gain=0/CC7=0) trong sync effect.
|
||||
const prPlayingForTrack = !!(subTabsRef.current || []).some(function (s) { return s.type === 'PIANO_ROLL' && s.trackId === t.id && s.isPlaying; });
|
||||
const _volDb = Number(t.volumeDb);
|
||||
const _volLin = (isNaN(_volDb) || !isFinite(_volDb)) ? 1.0 : (_volDb <= -50 ? 0 : Math.pow(10, _volDb / 20));
|
||||
const effGain = prPlayingForTrack ? _volLin : audibleGain;
|
||||
if (node) setTrackNodeGain(node, effGain);
|
||||
if (node && node.sfEntry) {
|
||||
try {
|
||||
const g = (typeof audibleGain === 'number' && isFinite(audibleGain) && !isNaN(audibleGain)) ? audibleGain : 1.0;
|
||||
const g = (typeof effGain === 'number' && isFinite(effGain) && !isNaN(effGain)) ? effGain : 1.0;
|
||||
node.sfEntry.gain.setTargetAtTime(g, getAudioContext().currentTime, 0.02);
|
||||
} catch (e) {}
|
||||
}
|
||||
@@ -19844,9 +19871,9 @@ const App = () => {
|
||||
// so FluidSynth/bridge-rendered notes respect mute/solo too. Bridge
|
||||
// active → SonicMidiRouter; down → SonicSF. CC7 = VOLUME track.
|
||||
if (isMidiSoundTrack(t)) {
|
||||
try { sendMidiVolumeCC7(t, list, audible); } catch (e) {}
|
||||
try { sendMidiVolumeCC7(t, list, audible || prPlayingForTrack); } catch (e) {}
|
||||
}
|
||||
trackAudibleRef.current[t.id] = audible;
|
||||
trackAudibleRef.current[t.id] = prPlayingForTrack ? true : audible;
|
||||
});
|
||||
updateSfRouting();
|
||||
}, [tracks, sessionTabs]);
|
||||
@@ -23925,9 +23952,9 @@ const App = () => {
|
||||
setActiveMidiPitches(new Set(activeMidiPitchesRef.current));
|
||||
vkbHeldRef.current.add(pitch);
|
||||
targets.forEach(function (t) {
|
||||
// V45.23: mute/solo gate preview keybed — track muted/solo-out khong phat
|
||||
// (dung semantics computeTrackAudibleGain nhu scheduleMidiNoteDispatch).
|
||||
if (computeTrackAudibleGain(allTracks, t) <= 0) return;
|
||||
// V45.24: PR preview doc lap voi mute/solo MAIN/SECTION — khoi phuc
|
||||
// volume track khi o PR tab.
|
||||
if (_isPrTabNow) { try { ensureTrackPrAudible(t, allTracks, activeTrackNodesRef.current[t.id]); } catch (e) {} }
|
||||
const ctx = resolveTrackInstrumentCtx(t, allTracks);
|
||||
ensureSonicInstrument(ctx);
|
||||
if (window.triggerMidiVuActivity) window.triggerMidiVuActivity(t.id, Math.round(vel * 127));
|
||||
@@ -25554,7 +25581,7 @@ const App = () => {
|
||||
// SonicSF (FluidSynth WASM). Giữ setTimeout scheduling (như SonicSF cũ);
|
||||
// bridge tự render note-off sau duration. A11 (sample-accurate) sẽ thay
|
||||
// setTimeout bằng sampleOffset đẩy thẳng vào SHM.
|
||||
const scheduleMidiNoteDispatch = (track, pitch, velocity, durMs, startTime, program, destNode, ch, synthEngine) => {
|
||||
const scheduleMidiNoteDispatch = (track, pitch, velocity, durMs, startTime, program, destNode, ch, synthEngine, prNote) => {
|
||||
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
||||
const ctx = getAudioContext();
|
||||
const startAt = startTime || ctx.currentTime; // undefined/null = phát ngay
|
||||
@@ -25576,9 +25603,13 @@ const App = () => {
|
||||
if (!guardPlay()) return;
|
||||
// D18b: mute/solo guard tai thoi diem fire — note-on dang cho cua track
|
||||
// vua tro nen inaudible (solo track khac / mute) khong duoc phat ra.
|
||||
// V45.24: note PR tab (prNote=true) BO QUA guard + khoi phuc volume —
|
||||
// mute/solo chi cuc bo MAIN/SECTION, PR khong bao gio bi cam.
|
||||
const _list = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : (tracksRef.current && tracksRef.current.length ? tracksRef.current : activeTracks);
|
||||
const _trk = _list.find(t => t.id === trkId) || track;
|
||||
if (!_trk || computeTrackAudibleGain(_list, _trk) <= 0) { try { if (window.__dbgLog) window.__dbgLog('solo_guard', 'block ch=' + trkId + ' p=' + (pitch || 60)); } catch (e) {} return; }
|
||||
if (prNote) {
|
||||
try { ensureTrackPrAudible(_trk, _list, destNode); } catch (e) {}
|
||||
} else if (!_trk || computeTrackAudibleGain(_list, _trk) <= 0) { try { if (window.__dbgLog) window.__dbgLog('solo_guard', 'block ch=' + trkId + ' p=' + (pitch || 60)); } catch (e) {} return; }
|
||||
// D18c: track pitch dang keu (timeline) - de applyAllTrackMuteSolo kill
|
||||
// dung note khi track thanh inaudible giua chung (Qin VST2 tremolo).
|
||||
try { var _tps = timelinePitchesRef.current; (_tps[trkId] || (_tps[trkId] = new Set())).add(pitch || 60); } catch (e) {}
|
||||
@@ -26190,7 +26221,7 @@ const App = () => {
|
||||
const scheduledTime = startWallTime + effectiveStart;
|
||||
const durMs = effectiveDur * 1000;
|
||||
if (window.SonicSF) {
|
||||
scheduleMidiNoteDispatch(track, note.pitch || 60, note.velocity || 0.8, durMs, scheduledTime, instrumentProgram, destNode, mainCh, synthEngine);
|
||||
scheduleMidiNoteDispatch(track, note.pitch || 60, note.velocity || 0.8, durMs, scheduledTime, instrumentProgram, destNode, mainCh, synthEngine, true);
|
||||
}
|
||||
scheduleNoteExpressionCC11(track, note, mainCh, startWallTime, effectiveStart, effectiveDur);
|
||||
}
|
||||
@@ -26215,7 +26246,7 @@ const App = () => {
|
||||
var schedTime = startWallTime + effStart;
|
||||
var durMs = effDur * 1000;
|
||||
if (window.SonicSF) {
|
||||
scheduleMidiNoteDispatch(ghostTrack, note.pitch || 60, note.velocity || 0.8, durMs, schedTime, ghostProg, ghostDest, ghostCh, ghostSynth);
|
||||
scheduleMidiNoteDispatch(ghostTrack, note.pitch || 60, note.velocity || 0.8, durMs, schedTime, ghostProg, ghostDest, ghostCh, ghostSynth, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -151,7 +151,11 @@ function isMidiSoundTrack(t){return!!(t&&(t.midiItems&&t.midiItems.length>0||t.t
|
||||
// gainNode KHÔNG ảnh hưởng âm bridge. Bridge active → gửi qua
|
||||
// SonicMidiRouter.pushEvent (đúng channel allocation của note-on, trackId làm
|
||||
// channel); bridge down → SonicSF (WASM local) fallback.
|
||||
function sendMidiVolumeCC7(t,list,audible){const volDb=typeof t.volumeDb==='number'&&isFinite(t.volumeDb)?t.volumeDb:0;const volLin=volDb<=-50?0:Math.pow(10,volDb/20);const cc7=audible?Math.max(1,Math.min(127,Math.round(volLin*100))):0;try{if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive&&window.SonicMidiRouter.isBridgeActive()){const isPerc=!!(t.synth_engine&&t.synth_engine.soundfont_bank===128)||!!t.is_percussion;window.SonicMidiRouter.pushEvent({cmd:'CC',channel:t.id,pitch:7,velocity:0,data2:cc7,percussion:isPerc});return;}}catch(e){}try{if(window.SonicSF&&typeof window.SonicSF.controllerChange==='function'){const ch=ensureTrackMidiChannel(t,list);window.SonicSF.controllerChange(ch,7,cc7);}}catch(e){}}// ── CC11 expression (realtime) ──────────────────────────────────────────
|
||||
function sendMidiVolumeCC7(t,list,audible){const volDb=typeof t.volumeDb==='number'&&isFinite(t.volumeDb)?t.volumeDb:0;const volLin=volDb<=-50?0:Math.pow(10,volDb/20);const cc7=audible?Math.max(1,Math.min(127,Math.round(volLin*100))):0;try{if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive&&window.SonicMidiRouter.isBridgeActive()){const isPerc=!!(t.synth_engine&&t.synth_engine.soundfont_bank===128)||!!t.is_percussion;window.SonicMidiRouter.pushEvent({cmd:'CC',channel:t.id,pitch:7,velocity:0,data2:cc7,percussion:isPerc});return;}}catch(e){}try{if(window.SonicSF&&typeof window.SonicSF.controllerChange==='function'){const ch=ensureTrackMidiChannel(t,list);window.SonicSF.controllerChange(ch,7,cc7);}}catch(e){}}// V45.24: PR tab audio DOC LAP voi mute/solo cua MAIN/SECTION — khoi phuc
|
||||
// volume track (theo volumeDb, bo qua mute/solo) truoc khi note PR phat.
|
||||
// sendMidiVolumeCC7(t, list, true) gui CC7=vol (channel khong con bi mute/solo
|
||||
// tu toggle MAIN); setTrackNodeGain phuc hoi gainNode (SonicSF fallback dest).
|
||||
function ensureTrackPrAudible(t,list,node){if(!t)return;try{const volDb=Number(t.volumeDb);const volLin=isNaN(volDb)||!isFinite(volDb)?1.0:volDb<=-50?0:Math.pow(10,volDb/20);if(node)setTrackNodeGain(node,volLin);sendMidiVolumeCC7(t,list,true);}catch(e){}}// ── CC11 expression (realtime) ──────────────────────────────────────────
|
||||
// Mỗi note có expression_curve → chuỗi CC11 theo grid 16ms. Timer đăng ký
|
||||
// vào _cc11Timers để stopAllPlayback hủy sạch; epoch guard (_nativePlayEpoch)
|
||||
// chống timer stale sau stop/loop restart (pattern scheduleMidiNoteDispatch).
|
||||
@@ -340,7 +344,7 @@ else if(fxRt.chainKey!==ck){fxRtSetChain(chain);fxRt.chainKey=ck;return;}else re
|
||||
// node → bypass vô hiệu. Mỗi fxRtStop() bump startToken → mọi checkpoint
|
||||
// dưới đây thấy mismatch → hủy session vừa tạo, không nối node.
|
||||
const token=++fxRt.startToken;fxRt.starting=true;const ctx=getAudioContext();if(!ctx||!ctx.audioWorklet||!masterBus||!masterBus.output||!masterBus.analyser||!masterBus.masterFxGain||!masterBus.masterFxDryGain)return;const chainPayload=chain.map(s=>s.type==='vst3'?{type:'vst3',path:s.path,name:s.name||s.path,preset_b64:s.preset_b64||'',bypass:!!s.bypass||!!(window.currentMasteringSettings||{}).vstBypass}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});let st;try{st=await window.SonicAPI.fxRealtimeStart({fx_chain:chainPayload,sample_rate:ctx.sampleRate||44100});}catch(e){console.warn('[FX RT] start fail:',e);showToast('Không khởi động VST FX realtime: '+(e.message||e),'warning');fxRt.starting=false;return;}if(token!==fxRt.startToken){// Bypass/stop giữa lúc engine đang load chain — hủy session vừa tạo.
|
||||
console.warn('[FX RT] start cancelled mid-flight — session stopped');try{window.SonicAPI.fxRealtimeStop({session_id:st.session_id}).catch(()=>{});}catch(e2){}fxRt.starting=false;return;}fxRt.sessionId=st.session_id;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608311716`);if(token!==fxRt.startToken){fxRt.starting=false;return;}fxRt.node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';// WS to 'localhost' bi tre ~300ms: Chromium thu ::1 truoc (Windows khong
|
||||
console.warn('[FX RT] start cancelled mid-flight — session stopped');try{window.SonicAPI.fxRealtimeStop({session_id:st.session_id}).catch(()=>{});}catch(e2){}fxRt.starting=false;return;}fxRt.sessionId=st.session_id;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608311725`);if(token!==fxRt.startToken){fxRt.starting=false;return;}fxRt.node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';// WS to 'localhost' bi tre ~300ms: Chromium thu ::1 truoc (Windows khong
|
||||
// co listener IPv6 -> SYN hang ~2s) roi moi fallback 127.0.0.1. Dung
|
||||
// thang 127.0.0.1 (loopback) -> WS open ~5ms. Server chap nhan loopback.
|
||||
const wsHost=u.hostname==='localhost'||u.hostname==='::1'?'127.0.0.1'+(u.port?':'+u.port:''):u.host;const tok=localStorage.getItem('sonic_token')||'';const wsUrl=`${wsProto}${wsHost}${st.ws_url}${tok?'?token='+encodeURIComponent(tok):''}`;const ws=new WebSocket(wsUrl);fxRt.ws=ws;ws.binaryType='arraybuffer';ws.onopen=()=>{if(token!==fxRt.startToken){try{ws.close();}catch(e){}fxRt.starting=false;return;}fxRt.active=true;fxRt.flushing=false;fxRt.starting=false;fxRt.inQueue=[];// bỏ audio stale sản xuất trước khi WS open (~4s load VST) — hết lag khởi đầu
|
||||
@@ -804,7 +808,7 @@ let bestIdx=0,bestDist=99;for(let i=0;i<7;i++){let d=Math.abs(pc-srcTones[i]);if
|
||||
React.useEffect(()=>{const handleWheelRaw=e=>{if(e.ctrlKey){e.preventDefault();const zoomFactor=e.deltaY<0?1.15:0.85;setRollZoom(prev=>Math.max(15,Math.min(250,prev*zoomFactor)));}};const container=gridScrollRef.current;if(container){container.addEventListener('wheel',handleWheelRaw,{passive:false});}return()=>{if(container){container.removeEventListener('wheel',handleWheelRaw);}};},[]);// Alt + Scroll event listener: fast‑forward playhead + play notes
|
||||
React.useEffect(()=>{const handleCanvasWheel=e=>{const canvas=canvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const mx=e.clientX-rect.left;const my=e.clientY-rect.top;const pitch=127-Math.floor(my/NoteHeight);if(e.shiftKey){e.preventDefault();// Shift+scroll on note → change velocity of single note or all selected
|
||||
const scrollBeat=mx/pixelsPerBeat-renderBeatOffset;const clickedNote=notes.find(n=>pitch===n.pitch&&scrollBeat>=n.start_beat&&scrollBeat<n.start_beat+n.duration_beats);if(clickedNote){const delta=e.deltaY<0?0.05:-0.05;if(selectedNoteIds.length>0){setNotes(prev=>prev.map(n=>selectedNoteIds.includes(n.id)?{...n,velocity:Math.max(0.05,Math.min(1,(n.velocity||0.8)+delta))}:n));}else{setNotes(prev=>prev.map(n=>n.id===clickedNote.id?{...n,velocity:Math.max(0.05,Math.min(1,(n.velocity||0.8)+delta))}:n));}}else{// Shift+scroll on empty space → horizontal scroll
|
||||
const container=gridScrollRef.current;if(container)container.scrollLeft+=e.deltaY;}return;}if(e.altKey){e.preventDefault();const scrollDelta=e.deltaY;const beatSec=60.0/(parseInt(bpm)||120);const step=scrollDelta<0?-0.25:0.25;const currentBeat=(st.currentTime||0)/beatSec;const maxBeats=totalBeats;const newBeat=Math.max(0,Math.min(maxBeats,currentBeat+step));const newTime=newBeat*beatSec;setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:newTime}:s));if(window.SonicSF){const ctx=getAudioContext();const playing=notes.filter(n=>currentBeat<n.start_beat&&newBeat>=n.start_beat);var pvTrk=activeTracks.find(function(t){return t.id===st.trackId;});var pvCtx=resolveTrackInstrumentCtx(pvTrk,activeTracks);ensureSonicInstrument(pvCtx);playing.forEach(n=>{if(isStandaloneSf()&&isSfTrackEngine(pvCtx.synthEngine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())){playNativeSfNote(pvTrk,n.pitch,n.velocity||0.8,200,undefined,'pv_'+st.trackId);return;}if(isStandaloneSf()&&isVstTrackEngine(pvCtx.synthEngine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())){playNativeVstNote(pvTrk,n.pitch,n.velocity||0.8,200,undefined,'pv_'+st.trackId);return;}scheduleMidiNoteDispatch(pvTrk,n.pitch,n.velocity||0.8,200,ctx.currentTime,pvCtx.program,null,pvCtx.ch,pvCtx.synthEngine);});}}};const canvas=canvasRef.current;if(canvas){canvas.addEventListener('wheel',handleCanvasWheel,{passive:false});}return()=>{if(canvas){canvas.removeEventListener('wheel',handleCanvasWheel);}};},[notes,st.currentTime,pixelsPerBeat,st.id,totalBeats,bpm]);React.useLayoutEffect(()=>{const canvas=canvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const h=128*NoteHeight;canvas.width=viewWidth*dpr;canvas.height=h*dpr;ctx.scale(dpr,dpr);// Scale highlight: rows thuộc scale (root+scale) sáng vàng; ngoài scale
|
||||
const container=gridScrollRef.current;if(container)container.scrollLeft+=e.deltaY;}return;}if(e.altKey){e.preventDefault();const scrollDelta=e.deltaY;const beatSec=60.0/(parseInt(bpm)||120);const step=scrollDelta<0?-0.25:0.25;const currentBeat=(st.currentTime||0)/beatSec;const maxBeats=totalBeats;const newBeat=Math.max(0,Math.min(maxBeats,currentBeat+step));const newTime=newBeat*beatSec;setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:newTime}:s));if(window.SonicSF){const ctx=getAudioContext();const playing=notes.filter(n=>currentBeat<n.start_beat&&newBeat>=n.start_beat);var pvTrk=activeTracks.find(function(t){return t.id===st.trackId;});var pvCtx=resolveTrackInstrumentCtx(pvTrk,activeTracks);ensureSonicInstrument(pvCtx);playing.forEach(n=>{if(isStandaloneSf()&&isSfTrackEngine(pvCtx.synthEngine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())){playNativeSfNote(pvTrk,n.pitch,n.velocity||0.8,200,undefined,'pv_'+st.trackId);return;}if(isStandaloneSf()&&isVstTrackEngine(pvCtx.synthEngine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())){playNativeVstNote(pvTrk,n.pitch,n.velocity||0.8,200,undefined,'pv_'+st.trackId);return;}scheduleMidiNoteDispatch(pvTrk,n.pitch,n.velocity||0.8,200,ctx.currentTime,pvCtx.program,null,pvCtx.ch,pvCtx.synthEngine,true);});}}};const canvas=canvasRef.current;if(canvas){canvas.addEventListener('wheel',handleCanvasWheel,{passive:false});}return()=>{if(canvas){canvas.removeEventListener('wheel',handleCanvasWheel);}};},[notes,st.currentTime,pixelsPerBeat,st.id,totalBeats,bpm]);React.useLayoutEffect(()=>{const canvas=canvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const h=128*NoteHeight;canvas.width=viewWidth*dpr;canvas.height=h*dpr;ctx.scale(dpr,dpr);// Scale highlight: rows thuộc scale (root+scale) sáng vàng; ngoài scale
|
||||
// dim hơn (spec 20:12 — Scale Highlight & Lock)
|
||||
const scHighlight=scaleWithRoot();// Draw background rows
|
||||
for(let pitch=0;pitch<128;pitch++){const y=(127-pitch)*NoteHeight;const isBlack=[1,3,6,8,10].includes(pitch%12);const inScale=scHighlight?scHighlight.includes(pitch%12):null;ctx.fillStyle=isBlack?inScale===false?'#141419':'#1f1f25':inScale===false?'#1b1b20':inScale===true?'#2c2a1e':'#25252a';ctx.fillRect(0,y,viewWidth,NoteHeight);ctx.strokeStyle='#2d2d35';ctx.lineWidth=0.5;ctx.beginPath();ctx.moveTo(0,y+NoteHeight);ctx.lineTo(viewWidth,y+NoteHeight);ctx.stroke();}// Draw snap lines
|
||||
@@ -849,7 +853,7 @@ React.useEffect(function(){setFocusItemId(st.target_id);},[st.id,st.target_id]);
|
||||
if(e.button===2){e.preventDefault();const clickedNote=notes.find(n=>{return pitch===n.pitch&&beat>=n.start_beat&&beat<n.start_beat+n.duration_beats;});if(clickedNote){pushToUndo(notes);setNotes(prev=>prev.filter(n=>n.id!==clickedNote.id));setSelectedNoteIds(prev=>prev.filter(id=>id!==clickedNote.id));swallowContextMenuRef.current=true;showToast('Đã xóa nốt!','info');}else{rightClickDragRef.current={active:true,startX:e.clientX,startY:e.clientY};}return;}if(e.button!==0)return;// Only handle left click
|
||||
// Check if clicking on an existing note
|
||||
const clickedNoteIdx=notes.findIndex(n=>{return pitch===n.pitch&&beat>=n.start_beat&&beat<n.start_beat+n.duration_beats;});// Click on note → play note with SoundFont
|
||||
if(clickedNoteIdx!==-1&&!e.ctrlKey&&!e.shiftKey&&!e.altKey){if(window.SonicSF){const ctx=getAudioContext();var clTrk=activeTracks.find(function(t){return t.id===st.trackId;});var clCtx=resolveTrackInstrumentCtx(clTrk,activeTracks);ensureSonicInstrument(clCtx);if(isStandaloneSf()&&isSfTrackEngine(clCtx.synthEngine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())){playNativeSfNote(clTrk,notes[clickedNoteIdx].pitch,notes[clickedNoteIdx].velocity||0.8,300,undefined,'pv_'+st.trackId);}else if(isStandaloneSf()&&isVstTrackEngine(clCtx.synthEngine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())){playNativeVstNote(clTrk,notes[clickedNoteIdx].pitch,notes[clickedNoteIdx].velocity||0.8,300,undefined,'pv_'+st.trackId);}else{scheduleMidiNoteDispatch(clTrk,notes[clickedNoteIdx].pitch,notes[clickedNoteIdx].velocity||0.8,300,ctx.currentTime,clCtx.program,null,clCtx.ch,clCtx.synthEngine);}}}// Shift+click: toggle selection (multi-select) — user 09:30 (trước đây là
|
||||
if(clickedNoteIdx!==-1&&!e.ctrlKey&&!e.shiftKey&&!e.altKey){if(window.SonicSF){const ctx=getAudioContext();var clTrk=activeTracks.find(function(t){return t.id===st.trackId;});var clCtx=resolveTrackInstrumentCtx(clTrk,activeTracks);ensureSonicInstrument(clCtx);if(isStandaloneSf()&&isSfTrackEngine(clCtx.synthEngine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())){playNativeSfNote(clTrk,notes[clickedNoteIdx].pitch,notes[clickedNoteIdx].velocity||0.8,300,undefined,'pv_'+st.trackId);}else if(isStandaloneSf()&&isVstTrackEngine(clCtx.synthEngine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())){playNativeVstNote(clTrk,notes[clickedNoteIdx].pitch,notes[clickedNoteIdx].velocity||0.8,300,undefined,'pv_'+st.trackId);}else{scheduleMidiNoteDispatch(clTrk,notes[clickedNoteIdx].pitch,notes[clickedNoteIdx].velocity||0.8,300,ctx.currentTime,clCtx.program,null,clCtx.ch,clCtx.synthEngine,true);}}}// Shift+click: toggle selection (multi-select) — user 09:30 (trước đây là
|
||||
// Ctrl+click — Ctrl giờ dành cho COPY-drag)
|
||||
if(e.shiftKey&&!e.altKey&&!e.ctrlKey){if(clickedNoteIdx!==-1){const clickedNote=notes[clickedNoteIdx];if(selectedNoteIds.includes(clickedNote.id)){setSelectedNoteIds(prev=>prev.filter(id=>id!==clickedNote.id));}else{setSelectedNoteIds(prev=>[...prev,clickedNote.id]);}return;}}// Ctrl+click NOTE + drag → COPY nhanh nhóm notes/note đến vị trí mới
|
||||
// (cùng pitch ban đầu; drag đổi vị trí + pitch) — user 09:30
|
||||
@@ -867,14 +871,14 @@ pushToUndo(notes);const start=getSnapBeat(beat,snapValue);const initialDur=lastN
|
||||
if(previewNodesRef.current){try{previewNodesRef.current.osc.stop();}catch(e){}try{previewNodesRef.current.osc.disconnect();}catch(e){}try{previewNodesRef.current.gain.disconnect();}catch(e){}previewNodesRef.current=null;}var dwTrk=activeTracks.find(function(t){return t.id===st.trackId;});var dwCh=dwTrk?assignTrackMidiChannel(dwTrk,activeTracks):0;var dwPitch=snapToScaleRef.current?snapPitchToScale(pitch,selectedScaleRef.current):pitch;var dwDurMs=Math.max(100,Math.round(initialDur*(60/bpm)*1000));if(isStandaloneSf()&&isSfTrackEngine(dwTrk&&dwTrk.synth_engine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())){playNativeSfNote(dwTrk,dwPitch,brushVelocityRef.current||0.8,dwDurMs,undefined,'pvdraw_'+st.trackId);}else if(isStandaloneSf()&&isVstTrackEngine(dwTrk&&dwTrk.synth_engine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())){playNativeVstNote(dwTrk,dwPitch,brushVelocityRef.current||0.8,dwDurMs,undefined,'pvdraw_'+st.trackId);}else if(window.SonicSF&&window.SonicSF.playNote){const ctx=getAudioContext();// playNote (FluidSynth — nhạc cụ THẬT của track). _playNoteFallback chỉ
|
||||
// là oscillator beep (sai âm với percussion/soundfont — user: note vẽ
|
||||
// mới nghe nhạc cụ track trước).
|
||||
scheduleMidiNoteDispatch(dwTrk,dwPitch,brushVelocityRef.current||0.8,dwDurMs,ctx.currentTime,dwTrk?dwTrk.instrumentProgram:undefined,null,dwCh,dwTrk?dwTrk.synth_engine:undefined);}}};const handleGridMouseMove=e=>{const canvas=canvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const beat=x/pixelsPerBeat-renderBeatOffset;const pitch=127-Math.floor(y/NoteHeight);if(selectionMarquee){const snappedBeat=getSnapBeat(beat,snapValue);const marquee={...selectionMarquee,currentBeat:snappedBeat,currentPitch:pitch};setSelectionMarquee(marquee);const minBeat=Math.min(marquee.startBeat,marquee.currentBeat);const maxBeat=Math.max(marquee.startBeat,marquee.currentBeat);const minPitch=Math.min(marquee.startPitch,marquee.currentPitch);const maxPitch=Math.max(marquee.startPitch,marquee.currentPitch);const insideIds=notes.filter(n=>{const withinPitch=n.pitch>=minPitch&&n.pitch<=maxPitch;if(!withinPitch)return false;const noteEnd=n.start_beat+n.duration_beats;if(marquee.startBeat<=marquee.currentBeat){// Left to right: select if any overlap
|
||||
scheduleMidiNoteDispatch(dwTrk,dwPitch,brushVelocityRef.current||0.8,dwDurMs,ctx.currentTime,dwTrk?dwTrk.instrumentProgram:undefined,null,dwCh,dwTrk?dwTrk.synth_engine:undefined,true);}}};const handleGridMouseMove=e=>{const canvas=canvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const beat=x/pixelsPerBeat-renderBeatOffset;const pitch=127-Math.floor(y/NoteHeight);if(selectionMarquee){const snappedBeat=getSnapBeat(beat,snapValue);const marquee={...selectionMarquee,currentBeat:snappedBeat,currentPitch:pitch};setSelectionMarquee(marquee);const minBeat=Math.min(marquee.startBeat,marquee.currentBeat);const maxBeat=Math.max(marquee.startBeat,marquee.currentBeat);const minPitch=Math.min(marquee.startPitch,marquee.currentPitch);const maxPitch=Math.max(marquee.startPitch,marquee.currentPitch);const insideIds=notes.filter(n=>{const withinPitch=n.pitch>=minPitch&&n.pitch<=maxPitch;if(!withinPitch)return false;const noteEnd=n.start_beat+n.duration_beats;if(marquee.startBeat<=marquee.currentBeat){// Left to right: select if any overlap
|
||||
return n.start_beat<=maxBeat&¬eEnd>=minBeat;}else{// Right to left: select only if fully covered
|
||||
return n.start_beat>=minBeat&¬eEnd<=maxBeat;}}).map(n=>n.id);setSelectedNoteIds(insideIds);return;}// Right-click drag → erase sweep
|
||||
const rc=rightClickDragRef.current;if(rc.active&&(Math.abs(e.clientX-rc.startX)>5||Math.abs(e.clientY-rc.startY)>5)){rc.active=false;swallowContextMenuRef.current=true;notesBeforeDragRef.current=JSON.parse(JSON.stringify(notes));setDraggedNote({mode:'erase_sweep',visitedPitches:[]});return;}if(!draggedNote){let foundIdx=-1;for(let i=0;i<notes.length;i++){const n=notes[i];if(pitch===n.pitch){const noteRightBeat=n.start_beat+n.duration_beats;const pixelDist=Math.abs((noteRightBeat-beat)*pixelsPerBeat);if(pixelDist<6&&beat>=n.start_beat){foundIdx=i;break;}}}if(foundIdx!==-1){canvas.style.cursor='ew-resize';setHoveredResizeIdx(foundIdx);}else{canvas.style.cursor=activeRollTool==='eraser'?'pointer':'crosshair';setHoveredResizeIdx(-1);}return;}if(draggedNote.mode==='draw'){const snappedPitch=snapToScaleRef.current?snapPitchToScale(pitch,selectedScaleRef.current):pitch;const lastPitch=draggedNote.lastDrawnPitch!==undefined?draggedNote.lastDrawnPitch:draggedNote.startOffsetPitch;const pitchChanged=snappedPitch!==lastPitch;const noteBeats=draggedNote.noteStartBeats||[];const defaultDur=getSnapDuration(snapValue);function playDrawPreview(p,durMs){stopPreviewNote();// Đồng bộ mastering + routing SF trước khi preview draw (âm qua
|
||||
// mastering FX của main out khi chain bật)
|
||||
try{if(window.__ensureMasteringRouting)window.__ensureMasteringRouting();}catch(er){}var pvTrk=activeTracks.find(function(t){return t.id===st.trackId;});var pvCtxInst=resolveTrackInstrumentCtx(pvTrk,activeTracks);if(isStandaloneSf()&&isSfTrackEngine(pvCtxInst.synthEngine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())){playNativeSfNote(pvTrk,p,brushVelocityRef.current||0.8,durMs,undefined,'pvdraw_'+st.trackId);}else if(isStandaloneSf()&&isVstTrackEngine(pvCtxInst.synthEngine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())){playNativeVstNote(pvTrk,p,brushVelocityRef.current||0.8,durMs,undefined,'pvdraw_'+st.trackId);}else if(window.SonicSF&&window.SonicSF.playNote){var pvCtx=getAudioContext();// playNote (FluidSynth — nhạc cụ THẬT). _playNoteFallback = oscillator
|
||||
// beep sai âm (percussion/soundfont).
|
||||
scheduleMidiNoteDispatch(pvTrk,p,brushVelocityRef.current||0.8,durMs,pvCtx.currentTime,pvCtxInst.program,null,pvCtxInst.ch,pvCtxInst.synthEngine);previewPitchRef.current=p;}}if(pitchChanged){const brushIds=draggedNote.brushIds||[];if(brushIds.length>0&¬eBeats.length>0){const prevNoteId=brushIds[brushIds.length-1];const prevNoteBeat=noteBeats[noteBeats.length-1];const prevDur=Math.max(0.125,beat-prevNoteBeat);setNotes(prev=>prev.map(n=>{if(n.id!==prevNoteId)return n;return{...n,duration_beats:prevDur};}));}const newNote={id:'note_'+Date.now()+Math.random().toString(36).substr(2,8)+'_'+(noteBeats.length+1),pitch:snappedPitch,start_beat:beat,duration_beats:defaultDur,velocity:brushVelocityRef.current,pan:0.0};setNotes(prev=>[...prev,newNote]);setSelectedNoteIds(prev=>[...prev,newNote.id]);draggedNote.brushIds=[...brushIds,newNote.id];draggedNote.lastDrawnPitch=snappedPitch;draggedNote.noteStartBeats=[...noteBeats,beat];playDrawPreview(snappedPitch,Math.max(100,Math.round(defaultDur*(60/bpm)*1000)));}else{const brushIds=draggedNote.brushIds||[];const lastBrushId=brushIds.length>0?brushIds[brushIds.length-1]:draggedNote.drawNoteId;const lastNoteBeat=noteBeats.length>0?noteBeats[noteBeats.length-1]:draggedNote.startOffsetBeat;if(lastBrushId){const extDur=Math.max(0.125,beat-lastNoteBeat);setNotes(prev=>prev.map(n=>{if(n.id!==lastBrushId)return n;return{...n,duration_beats:extDur};}));playDrawPreview(snappedPitch,Math.max(100,Math.round(extDur*(60/bpm)*1000)));}}const container=gridScrollRef.current;if(container){const cr=container.getBoundingClientRect();const visTop=container.scrollTop;const visBot=visTop+container.clientHeight;const pitchPixel=(127-snappedPitch)*NoteHeight;const safeMargin=NoteHeight*2;if(pitchPixel<visTop+safeMargin){const target=Math.max(0,pitchPixel-safeMargin);if(container.scrollTop!==target)container.scrollTop=target;if(!brushAutoScrollRef.current||brushAutoScrollRef.current.direction!=='up'){if(brushAutoScrollRef.current)clearInterval(brushAutoScrollRef.current.id);brushAutoScrollRef.current={direction:'up',id:setInterval(()=>{if(gridScrollRef.current)gridScrollRef.current.scrollTop=Math.max(0,gridScrollRef.current.scrollTop-Math.max(1,Math.floor(NoteHeight*0.5)));},16)};}}else if(pitchPixel+NoteHeight>visBot-safeMargin){const target=Math.min(container.scrollHeight-container.clientHeight,pitchPixel-container.clientHeight+safeMargin+NoteHeight);if(container.scrollTop!==target)container.scrollTop=target;if(!brushAutoScrollRef.current||brushAutoScrollRef.current.direction!=='down'){if(brushAutoScrollRef.current)clearInterval(brushAutoScrollRef.current.id);brushAutoScrollRef.current={direction:'down',id:setInterval(()=>{if(gridScrollRef.current)gridScrollRef.current.scrollTop=Math.min(gridScrollRef.current.scrollHeight-gridScrollRef.current.clientHeight,gridScrollRef.current.scrollTop+Math.max(1,Math.floor(NoteHeight*0.5)));},16)};}}else{if(brushAutoScrollRef.current){clearInterval(brushAutoScrollRef.current.id);brushAutoScrollRef.current=null;}}}return;}if(draggedNote.mode==='erase_sweep'){const erased=draggedNote.erasedIds||[];const target=notes.find(n=>n.pitch===pitch&&beat>=n.start_beat&&beat<n.start_beat+n.duration_beats);if(target&&!erased.includes(target.id)){draggedNote.erasedIds=[...erased,target.id];setNotes(prev=>prev.filter(n=>n.id!==target.id));setSelectedNoteIds(prev=>prev.filter(id=>id!==target.id));}return;}if(draggedNote.mode==='scale'){const newEnd=getSnapBeat(Math.max(draggedNote.firstStart+0.125,beat),snapValue);const range=draggedNote.originalEnd-draggedNote.firstStart;if(range<=0)return;const scaleFactor=Math.max(0.01,(newEnd-draggedNote.firstStart)/range);const ids=draggedNote.selectedNoteIds||[];const firstStart=draggedNote.firstStart;const notesBefore=notesBeforeDragRef.current;if(!notesBefore)return;setNotes(prev=>prev.map(n=>{if(!ids.includes(n.id))return n;const orig=notesBefore?notesBefore.find(o=>o.id===n.id):null;const origStart=orig?orig.start_beat:n.start_beat;const origDur=orig?orig.duration_beats:n.duration_beats;const relStart=origStart-firstStart;const relEnd=relStart+origDur;return{...n,start_beat:firstStart+relStart*scaleFactor,duration_beats:Math.max(0.125,relEnd*scaleFactor-relStart*scaleFactor)};}));return;}if(draggedNote.mode==='resize'){const newDuration=getSnapBeat(Math.max(0.125,beat-draggedNote.originalStart),snapValue);setNotes(prev=>prev.map((n,idx)=>{if(idx!==draggedNote.idx)return n;return{...n,duration_beats:newDuration};}));}else if(draggedNote.mode==='move'){const refOrigStart=draggedNote.clickedOriginalStartBeat;if(refOrigStart===undefined)return;const deltaBeat=getSnapBeat(beat-draggedNote.startOffsetBeat,snapValue)-refOrigStart;// Clamp so no note goes past beat 0
|
||||
scheduleMidiNoteDispatch(pvTrk,p,brushVelocityRef.current||0.8,durMs,pvCtx.currentTime,pvCtxInst.program,null,pvCtxInst.ch,pvCtxInst.synthEngine,true);previewPitchRef.current=p;}}if(pitchChanged){const brushIds=draggedNote.brushIds||[];if(brushIds.length>0&¬eBeats.length>0){const prevNoteId=brushIds[brushIds.length-1];const prevNoteBeat=noteBeats[noteBeats.length-1];const prevDur=Math.max(0.125,beat-prevNoteBeat);setNotes(prev=>prev.map(n=>{if(n.id!==prevNoteId)return n;return{...n,duration_beats:prevDur};}));}const newNote={id:'note_'+Date.now()+Math.random().toString(36).substr(2,8)+'_'+(noteBeats.length+1),pitch:snappedPitch,start_beat:beat,duration_beats:defaultDur,velocity:brushVelocityRef.current,pan:0.0};setNotes(prev=>[...prev,newNote]);setSelectedNoteIds(prev=>[...prev,newNote.id]);draggedNote.brushIds=[...brushIds,newNote.id];draggedNote.lastDrawnPitch=snappedPitch;draggedNote.noteStartBeats=[...noteBeats,beat];playDrawPreview(snappedPitch,Math.max(100,Math.round(defaultDur*(60/bpm)*1000)));}else{const brushIds=draggedNote.brushIds||[];const lastBrushId=brushIds.length>0?brushIds[brushIds.length-1]:draggedNote.drawNoteId;const lastNoteBeat=noteBeats.length>0?noteBeats[noteBeats.length-1]:draggedNote.startOffsetBeat;if(lastBrushId){const extDur=Math.max(0.125,beat-lastNoteBeat);setNotes(prev=>prev.map(n=>{if(n.id!==lastBrushId)return n;return{...n,duration_beats:extDur};}));playDrawPreview(snappedPitch,Math.max(100,Math.round(extDur*(60/bpm)*1000)));}}const container=gridScrollRef.current;if(container){const cr=container.getBoundingClientRect();const visTop=container.scrollTop;const visBot=visTop+container.clientHeight;const pitchPixel=(127-snappedPitch)*NoteHeight;const safeMargin=NoteHeight*2;if(pitchPixel<visTop+safeMargin){const target=Math.max(0,pitchPixel-safeMargin);if(container.scrollTop!==target)container.scrollTop=target;if(!brushAutoScrollRef.current||brushAutoScrollRef.current.direction!=='up'){if(brushAutoScrollRef.current)clearInterval(brushAutoScrollRef.current.id);brushAutoScrollRef.current={direction:'up',id:setInterval(()=>{if(gridScrollRef.current)gridScrollRef.current.scrollTop=Math.max(0,gridScrollRef.current.scrollTop-Math.max(1,Math.floor(NoteHeight*0.5)));},16)};}}else if(pitchPixel+NoteHeight>visBot-safeMargin){const target=Math.min(container.scrollHeight-container.clientHeight,pitchPixel-container.clientHeight+safeMargin+NoteHeight);if(container.scrollTop!==target)container.scrollTop=target;if(!brushAutoScrollRef.current||brushAutoScrollRef.current.direction!=='down'){if(brushAutoScrollRef.current)clearInterval(brushAutoScrollRef.current.id);brushAutoScrollRef.current={direction:'down',id:setInterval(()=>{if(gridScrollRef.current)gridScrollRef.current.scrollTop=Math.min(gridScrollRef.current.scrollHeight-gridScrollRef.current.clientHeight,gridScrollRef.current.scrollTop+Math.max(1,Math.floor(NoteHeight*0.5)));},16)};}}else{if(brushAutoScrollRef.current){clearInterval(brushAutoScrollRef.current.id);brushAutoScrollRef.current=null;}}}return;}if(draggedNote.mode==='erase_sweep'){const erased=draggedNote.erasedIds||[];const target=notes.find(n=>n.pitch===pitch&&beat>=n.start_beat&&beat<n.start_beat+n.duration_beats);if(target&&!erased.includes(target.id)){draggedNote.erasedIds=[...erased,target.id];setNotes(prev=>prev.filter(n=>n.id!==target.id));setSelectedNoteIds(prev=>prev.filter(id=>id!==target.id));}return;}if(draggedNote.mode==='scale'){const newEnd=getSnapBeat(Math.max(draggedNote.firstStart+0.125,beat),snapValue);const range=draggedNote.originalEnd-draggedNote.firstStart;if(range<=0)return;const scaleFactor=Math.max(0.01,(newEnd-draggedNote.firstStart)/range);const ids=draggedNote.selectedNoteIds||[];const firstStart=draggedNote.firstStart;const notesBefore=notesBeforeDragRef.current;if(!notesBefore)return;setNotes(prev=>prev.map(n=>{if(!ids.includes(n.id))return n;const orig=notesBefore?notesBefore.find(o=>o.id===n.id):null;const origStart=orig?orig.start_beat:n.start_beat;const origDur=orig?orig.duration_beats:n.duration_beats;const relStart=origStart-firstStart;const relEnd=relStart+origDur;return{...n,start_beat:firstStart+relStart*scaleFactor,duration_beats:Math.max(0.125,relEnd*scaleFactor-relStart*scaleFactor)};}));return;}if(draggedNote.mode==='resize'){const newDuration=getSnapBeat(Math.max(0.125,beat-draggedNote.originalStart),snapValue);setNotes(prev=>prev.map((n,idx)=>{if(idx!==draggedNote.idx)return n;return{...n,duration_beats:newDuration};}));}else if(draggedNote.mode==='move'){const refOrigStart=draggedNote.clickedOriginalStartBeat;if(refOrigStart===undefined)return;const deltaBeat=getSnapBeat(beat-draggedNote.startOffsetBeat,snapValue)-refOrigStart;// Clamp so no note goes past beat 0
|
||||
const minOrigStart=Math.min(...draggedNote.selectedNotesOffset.map(o=>o.originalStartBeat));const clampedDeltaBeat=minOrigStart+deltaBeat<0?-minOrigStart:deltaBeat;const deltaPitch=Math.round(pitch-draggedNote.startOffsetPitch);setNotes(prev=>prev.map(n=>{const offset=draggedNote.selectedNotesOffset.find(o=>o.id===n.id);if(!offset)return n;return{...n,start_beat:getSnapBeat(Math.max(0,offset.originalStartBeat+clampedDeltaBeat),snapValue),pitch:Math.max(0,Math.min(127,offset.originalPitch+deltaPitch))};}));}};const handleGridMouseUp=()=>{if(draggedNote&&draggedNote.mode==='draw'){const dn=draggedNote;const brushIds=dn.brushIds||[];const lastBrushId=brushIds.length>0?brushIds[brushIds.length-1]:dn.drawNoteId;if(lastBrushId){const lastNote=notes.find(n=>n.id===lastBrushId);if(lastNote)lastNoteDurationRef.current=lastNote.duration_beats;}}setDraggedNote(null);// Marquee (Ctrl+drag): CHỌN notes nằm trong vùng (user 09:15 — trước đây
|
||||
// chỉ vẽ highlight, không set selectedNoteIds → Ctrl+X báo "chọn notes")
|
||||
if(selectionMarquee){const minBeat=Math.min(selectionMarquee.startBeat,selectionMarquee.currentBeat);const maxBeat=Math.max(selectionMarquee.startBeat,selectionMarquee.currentBeat);const minPitch=Math.min(selectionMarquee.startPitch,selectionMarquee.currentPitch);const maxPitch=Math.max(selectionMarquee.startPitch,selectionMarquee.currentPitch);const inMarquee=notes.filter(n=>{const center=n.start_beat+n.duration_beats/2;return center>=minBeat&¢er<=maxBeat&&n.pitch>=minPitch&&n.pitch<=maxPitch;});setSelectedNoteIds(inMarquee.map(n=>n.id));}setSelectionMarquee(null);rightClickDragRef.current={active:false,startX:0,startY:0};if(brushAutoScrollRef.current){clearInterval(brushAutoScrollRef.current.id);brushAutoScrollRef.current=null;}stopPreviewNote();previewPitchRef.current=null;};const handleContextMenu=e=>{e.preventDefault();if(swallowContextMenuRef.current){swallowContextMenuRef.current=false;return;}const pos={x:e.clientX,y:e.clientY,parentKey:null};scaleMenuOriginRef.current={x:pos.x,y:pos.y};setScaleMenuPos(pos);};const ccDragRef=React.useRef(null);const brushAutoScrollRef=React.useRef(null);const rightClickDragRef=React.useRef({active:false,startX:0,startY:0});const swallowContextMenuRef=React.useRef(false);// Status hint động (user 09:40): theo dõi Shift/Ctrl + mouse trong piano roll
|
||||
@@ -1297,16 +1301,15 @@ if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){// D1: bridg
|
||||
var bTracks=activeTracksRef.current||[];// ARM piano roll (sub-tab isArmed, cuc bo) cung tinh nhu ARM
|
||||
// track - preview note tu piano roll tab phai toi track dang
|
||||
// edit du track main KHONG ARM (ARM piano roll khong truyen).
|
||||
var bPrArmed={};if(_isPrTabNow){try{(subTabsRef.current||[]).forEach(function(s){if(s.type==='PIANO_ROLL'&&s.isArmed&&s.trackId)bPrArmed[s.trackId]=true;});}catch(e){}}bTracks.forEach(function(bt){if(!bt.isArmed&&!bPrArmed[bt.id])return;// V45.23: mute/solo gate preview bridge NOTE_ON — track
|
||||
// muted/solo-out khong duoc phat (cung semantics
|
||||
// computeTrackAudibleGain nhu scheduleMidiNoteDispatch).
|
||||
if(computeTrackAudibleGain(bTracks,bt)<=0){try{if(window.__dbgLog)window.__dbgLog('solo_guard','block midi ch='+bt.id+' p='+pitch);}catch(e){}return;}if(window.triggerMidiVuActivity){window.triggerMidiVuActivity(bt.id,scaledVel);}try{heldMidiNotesRef.current[bt.id]=(heldMidiNotesRef.current[bt.id]||0)+1;}catch(err){}try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_ON',channel:bt.id,pitch:pitch,velocity:scaledVel/127,percussion:!!(bt.synth_engine&&bt.synth_engine.soundfont_bank===128),live:true});}catch(e){}});}else if(window.SonicSF){var allTracks=activeTracksRef.current||[];var arSubs=_isPrTabNow&&subTabsRef&&subTabsRef.current?subTabsRef.current.filter(function(s){return s.type==='PIANO_ROLL'&&s.isArmed;}):[];var armedTracks=allTracks.filter(function(t){return t.isArmed;});// Piano Roll arming has priority: route to each armed sub-tab's parent track
|
||||
if(arSubs.length>0){arSubs.forEach(function(as){var asTrk=allTracks.find(function(t){return t.id===as.trackId;});// V45.23: mute/solo gate preview (SonicSF fallback).
|
||||
if(!asTrk||computeTrackAudibleGain(allTracks,asTrk)<=0)return;var asCh=asTrk?assignTrackMidiChannel(asTrk,allTracks):0;var asProg=as.instrumentProgram;var asSe=as.synth_engine;if(window.triggerMidiVuActivity){window.triggerMidiVuActivity(as.trackId,scaledVel);}// Giữ VU theo trường độ: tăng counter note đang giữ
|
||||
var bPrArmed={};if(_isPrTabNow){try{(subTabsRef.current||[]).forEach(function(s){if(s.type==='PIANO_ROLL'&&s.isArmed&&s.trackId)bPrArmed[s.trackId]=true;});}catch(e){}}bTracks.forEach(function(bt){if(!bt.isArmed&&!bPrArmed[bt.id])return;// V45.24: PR preview doc lap voi mute/solo MAIN/SECTION —
|
||||
// khoi phuc volume track (bo qua mute/solo) khi o PR tab.
|
||||
if(_isPrTabNow){try{ensureTrackPrAudible(bt,bTracks,activeTrackNodesRef.current[bt.id]);}catch(e){}}if(window.triggerMidiVuActivity){window.triggerMidiVuActivity(bt.id,scaledVel);}try{heldMidiNotesRef.current[bt.id]=(heldMidiNotesRef.current[bt.id]||0)+1;}catch(err){}try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_ON',channel:bt.id,pitch:pitch,velocity:scaledVel/127,percussion:!!(bt.synth_engine&&bt.synth_engine.soundfont_bank===128),live:true});}catch(e){}});}else if(window.SonicSF){var allTracks=activeTracksRef.current||[];var arSubs=_isPrTabNow&&subTabsRef&&subTabsRef.current?subTabsRef.current.filter(function(s){return s.type==='PIANO_ROLL'&&s.isArmed;}):[];var armedTracks=allTracks.filter(function(t){return t.isArmed;});// Piano Roll arming has priority: route to each armed sub-tab's parent track
|
||||
if(arSubs.length>0){arSubs.forEach(function(as){var asTrk=allTracks.find(function(t){return t.id===as.trackId;});// V45.24: PR preview doc lap voi mute/solo MAIN/SECTION.
|
||||
if(_isPrTabNow&&asTrk){try{ensureTrackPrAudible(asTrk,allTracks,activeTrackNodesRef.current[as.trackId]);}catch(e){}}var asCh=asTrk?assignTrackMidiChannel(asTrk,allTracks):0;var asProg=as.instrumentProgram;var asSe=as.synth_engine;if(window.triggerMidiVuActivity){window.triggerMidiVuActivity(as.trackId,scaledVel);}// Giữ VU theo trường độ: tăng counter note đang giữ
|
||||
// (tick giữ peak cho tới khi note-off)
|
||||
try{heldMidiNotesRef.current[as.trackId]=(heldMidiNotesRef.current[as.trackId]||0)+1;}catch(err){}if(isStandaloneSf()&&isSfTrackEngine(asSe)){playNativeSfNote(asTrk,pitch,scaledVel/127,60000,undefined,'midi_'+as.trackId+'_'+pitch);}else if(isStandaloneSf()&&isVstTrackEngine(asSe)){playNativeVstNote(asTrk,pitch,scaledVel/127,60000,undefined,'midi_'+as.trackId+'_'+pitch);}else{window.SonicSF.playNote(pitch,scaledVel,60000,undefined,asProg,null,asCh,asSe);}});}// Route to ALL armed tracks (not just the first one)
|
||||
armedTracks.forEach(function(at){// V45.23: mute/solo gate preview (SonicSF fallback).
|
||||
if(computeTrackAudibleGain(allTracks,at)<=0)return;var atCh=assignTrackMidiChannel(at,allTracks);var atProg=at.instrumentProgram;var atSe=at.synth_engine;var atDest=activeTrackNodesRef.current[at.id]?.gainNode||null;if(window.triggerMidiVuActivity){window.triggerMidiVuActivity(at.id,scaledVel);}// Giữ VU theo trường độ: tăng counter note đang giữ
|
||||
armedTracks.forEach(function(at){// V45.24: PR preview doc lap voi mute/solo MAIN/SECTION.
|
||||
if(_isPrTabNow){try{ensureTrackPrAudible(at,allTracks,activeTrackNodesRef.current[at.id]);}catch(e){}}var atCh=assignTrackMidiChannel(at,allTracks);var atProg=at.instrumentProgram;var atSe=at.synth_engine;var atDest=activeTrackNodesRef.current[at.id]?.gainNode||null;if(window.triggerMidiVuActivity){window.triggerMidiVuActivity(at.id,scaledVel);}// Giữ VU theo trường độ: tăng counter note đang giữ
|
||||
try{heldMidiNotesRef.current[at.id]=(heldMidiNotesRef.current[at.id]||0)+1;}catch(err){}if(isStandaloneSf()&&isSfTrackEngine(atSe)){playNativeSfNote(at,pitch,scaledVel/127,60000,undefined,'midi_'+at.id+'_'+pitch);}else if(isStandaloneSf()&&isVstTrackEngine(atSe)){playNativeVstNote(at,pitch,scaledVel/127,60000,undefined,'midi_'+at.id+'_'+pitch);}else{window.SonicSF.playNote(pitch,scaledVel,60000,undefined,atProg,atDest,atCh,atSe);}});}}else if(cmd===0x8||cmd===0x9&&rawVel===0){activeMidiPitchesRef.current.delete(pitch);setActiveMidiPitches(new Set(activeMidiPitchesRef.current));const current=lastMidiNoteRef.current;if(current&¤t.pitch===pitch){const lenSec=(performance.now()-current.startTime)/1000;lastMidiNoteRef.current={...current,length:lenSec};setLastMidiNote(prev=>prev&&prev.pitch===pitch?{...prev,length:lenSec,time:Date.now()}:prev);}// Stop the note on ALL tracks (not just armed) to prevent stuck notes
|
||||
// when ARM is toggled off while a key is held
|
||||
if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){// D1: bridge note-off qua router (channel trùng NOTE_ON — track.id).
|
||||
@@ -1353,20 +1356,24 @@ const applyAllTrackMuteSolo=(trackId,patch)=>{const list=activeTracksRef.current
|
||||
// audible -> D18b guard (25423) + scheduler (25498) + toggleTrackSoloEvaluate
|
||||
// (setTimeout 60ms restart) thay trang thai PRE-solo -> Qin note van fire
|
||||
// (tremolo). Render ke tiep tu dong dong bo lai ref.
|
||||
activeTracksRef.current=effective;try{if(window.__dbgLog)window.__dbgLog('solo_apply','track='+trackId+' patch='+(patch?JSON.stringify(patch):'-')+' n='+effective.length+' solo=['+effective.filter(function(t){return t.solo;}).map(function(t){return t.id;}).join(',')+'] muted=['+effective.filter(function(t){return t.muted;}).map(function(t){return t.id;}).join(',')+']');}catch(e){}let becameAudible=false;effective.forEach(t=>{const audible=computeTrackAudibleGain(effective,t)>0;const wasAudible=trackAudibleRef.current[t.id]!==false;const node=activeTrackNodesRef.current[t.id];const audibleGain=computeTrackAudibleGain(effective,t);if(node)setTrackNodeGain(node,audibleGain);// Section sub-nodes (key: <parent>_sub_<trackId>) — mute/solo của
|
||||
activeTracksRef.current=effective;try{if(window.__dbgLog)window.__dbgLog('solo_apply','track='+trackId+' patch='+(patch?JSON.stringify(patch):'-')+' n='+effective.length+' solo=['+effective.filter(function(t){return t.solo;}).map(function(t){return t.id;}).join(',')+'] muted=['+effective.filter(function(t){return t.muted;}).map(function(t){return t.id;}).join(',')+']');}catch(e){}let becameAudible=false;effective.forEach(t=>{const audible=computeTrackAudibleGain(effective,t)>0;const wasAudible=trackAudibleRef.current[t.id]!==false;const node=activeTrackNodesRef.current[t.id];const audibleGain=computeTrackAudibleGain(effective,t);// V45.24: PR tab doc lap voi mute/solo MAIN/SECTION — track dang co
|
||||
// sub-tab PIANO_ROLL PLAY thi KHONG ap mute/solo realtime (gain=0,
|
||||
// CC7=0, kill note) de PR khong bi cam; mute/solo chi anh huong
|
||||
// MAIN/SECTION playback.
|
||||
const prPlayingForTrack=!!(subTabsRef.current||[]).some(function(s){return s.type==='PIANO_ROLL'&&s.trackId===t.id&&s.isPlaying;});const _volDb=Number(t.volumeDb);const _volLin=isNaN(_volDb)||!isFinite(_volDb)?1.0:_volDb<=-50?0:Math.pow(10,_volDb/20);const effGain=prPlayingForTrack?_volLin:audibleGain;if(node)setTrackNodeGain(node,effGain);// Section sub-nodes (key: <parent>_sub_<trackId>) — mute/solo của
|
||||
// SECTION-TAB phải áp gain cho node play section — CHỈ trong section
|
||||
// (activeTracksRef = tracks section), KHÔNG đụng track MAIN.
|
||||
Object.keys(activeTrackNodesRef.current).forEach(function(k){if(k.endsWith('_sub_'+t.id)){const sn=activeTrackNodesRef.current[k];if(sn&&sn.gainNode)setTrackNodeGain(sn,audibleGain);}});if(node&&node.sfEntry){try{node.sfEntry.gain.setTargetAtTime(audibleGain,getAudioContext().currentTime,0.02);}catch(e){}}// MIDI/bridge tracks: CC7 (channel volume) realtime — even for notes
|
||||
Object.keys(activeTrackNodesRef.current).forEach(function(k){if(k.endsWith('_sub_'+t.id)){const sn=activeTrackNodesRef.current[k];if(sn&&sn.gainNode)setTrackNodeGain(sn,audibleGain);}});if(node&&node.sfEntry){try{node.sfEntry.gain.setTargetAtTime(effGain,getAudioContext().currentTime,0.02);}catch(e){}}// MIDI/bridge tracks: CC7 (channel volume) realtime — even for notes
|
||||
// already sounding. Bridge active → qua SonicMidiRouter (bridge); bridge
|
||||
// down → SonicSF (WASM). CC7 = VOLUME của track (không chỉ 100/0).
|
||||
if(isMidiSoundTrack(t)){try{sendMidiVolumeCC7(t,effective,audible);}catch(e){}}// D18: track vừa MẤT tiếng (mute/solo) — dừng note đang giữ ngay. CC7=0
|
||||
if(isMidiSoundTrack(t)){try{sendMidiVolumeCC7(t,effective,audible||prPlayingForTrack);}catch(e){}}// D18: track vừa MẤT tiếng (mute/solo) — dừng note đang giữ ngay. CC7=0
|
||||
// (sendMidiVolumeCC7) không ăn với VST như Qin (bỏ qua channel volume)
|
||||
// → note kêu tiếp (tremolo) khi solo track khác. NOTE_OFF từng pitch đang
|
||||
// giữ + CC123/CC120 all-notes-off trên channel track đó.
|
||||
if(!audible&&wasAudible&&isMidiSoundTrack(t)){try{const _isPerc=!!(t.synth_engine&&t.synth_engine.soundfont_bank===128)||!!t.is_percussion;try{if(window.__dbgLog)window.__dbgLog('solo_kill','track='+t.id+' tset='+((timelinePitchesRef.current||{})[t.id]||new Set()).size+' held='+(activeMidiPitchesRef.current||new Set()).size);}catch(e){}const _held=activeMidiPitchesRef.current||new Set();// D18c: timeline MIDI notes (per-track) dang keu - NOTE_OFF tung pitch;
|
||||
if(!audible&&wasAudible&&!prPlayingForTrack&&isMidiSoundTrack(t)){try{const _isPerc=!!(t.synth_engine&&t.synth_engine.soundfont_bank===128)||!!t.is_percussion;try{if(window.__dbgLog)window.__dbgLog('solo_kill','track='+t.id+' tset='+((timelinePitchesRef.current||{})[t.id]||new Set()).size+' held='+(activeMidiPitchesRef.current||new Set()).size);}catch(e){}const _held=activeMidiPitchesRef.current||new Set();// D18c: timeline MIDI notes (per-track) dang keu - NOTE_OFF tung pitch;
|
||||
// truoc day chi kill preview keyboard (activeMidiPitchesRef) -> note
|
||||
// timeline (Qin VST2) cu ngan/loop (tremolo) khi solo track khac.
|
||||
const _tSet=(timelinePitchesRef.current||{})[t.id];if(_tSet&&_tSet.size){_tSet.forEach(function(_p){window.SonicMidiRouter.pushEvent({cmd:'NOTE_OFF',channel:t.id,pitch:_p,velocity:0,percussion:_isPerc,live:true});});}_held.forEach(function(_p){window.SonicMidiRouter.pushEvent({cmd:'NOTE_OFF',channel:t.id,pitch:_p,velocity:0,percussion:_isPerc,live:true});});window.SonicMidiRouter.pushEvent({cmd:'CC',channel:t.id,pitch:123,velocity:0,data2:0,percussion:_isPerc});window.SonicMidiRouter.pushEvent({cmd:'CC',channel:t.id,pitch:120,velocity:0,data2:0,percussion:_isPerc});if(heldMidiNotesRef.current)delete heldMidiNotesRef.current[t.id];if(timelinePitchesRef.current)delete timelinePitchesRef.current[t.id];}catch(e){}}if(audible&&!wasAudible)becameAudible=true;trackAudibleRef.current[t.id]=audible;});// A track that just became audible may have had its audio sources dropped
|
||||
const _tSet=(timelinePitchesRef.current||{})[t.id];if(_tSet&&_tSet.size){_tSet.forEach(function(_p){window.SonicMidiRouter.pushEvent({cmd:'NOTE_OFF',channel:t.id,pitch:_p,velocity:0,percussion:_isPerc,live:true});});}_held.forEach(function(_p){window.SonicMidiRouter.pushEvent({cmd:'NOTE_OFF',channel:t.id,pitch:_p,velocity:0,percussion:_isPerc,live:true});});window.SonicMidiRouter.pushEvent({cmd:'CC',channel:t.id,pitch:123,velocity:0,data2:0,percussion:_isPerc});window.SonicMidiRouter.pushEvent({cmd:'CC',channel:t.id,pitch:120,velocity:0,data2:0,percussion:_isPerc});if(heldMidiNotesRef.current)delete heldMidiNotesRef.current[t.id];if(timelinePitchesRef.current)delete timelinePitchesRef.current[t.id];}catch(e){}}if(audible&&!wasAudible)becameAudible=true;trackAudibleRef.current[t.id]=prPlayingForTrack?true:audible;});// A track that just became audible may have had its audio sources dropped
|
||||
// by a loop restart while it was inaudible (startTrackPlayback skips
|
||||
// muted/soloed tracks, so unmute alone cannot resurrect them). Re-schedule
|
||||
// from the current playhead — the same approach toggleTrackSoloEvaluate
|
||||
@@ -1441,10 +1448,12 @@ const all=[...(sessionTabs||[]).reduce((acc,s)=>acc.concat(s.tracks||[]),[]),...
|
||||
// KHÔNG được trigger re-sync/re-route (tránh mất âm khi play sau đó —
|
||||
// updateSfRouting chạy thừa có thể đặt SF destination sai thời điểm).
|
||||
const audioSig=list.map(t=>(t.muted?'1':'0')+(t.solo?'1':'0')+':'+(t.volumeDb??0)+':'+(t.audioBypass?'1':'0')+(t.midiBypass?'1':'0')+':'+(t.midiItems||[]).length+':'+(t.clips||[]).length+':'+(t.sections||[]).length).join('|');if(trackAudioSyncSigRef.current===audioSig)return;trackAudioSyncSigRef.current=audioSig;list.forEach(t=>{// ♪ state → SF mastering route (sfRouteGain/sfDryGain), live on existing nodes
|
||||
const sn=activeTrackNodesRef.current[t.id];if(sn&&sn.sfRouteGain&&sn.sfDryGain&&sn.sfRouteGain.gain.value>0!==!effMidiBypass(t)){const _b=effMidiBypass(t);sn.sfRouteGain.gain.value=_b?0:1;sn.sfDryGain.gain.value=_b?1:0;}const sig=(t.muted?'1':'0')+(t.solo?'1':'0')+':'+(t.volumeDb??0);if(trackMuteSoloSigRef.current[t.id]===sig)return;trackMuteSoloSigRef.current[t.id]=sig;const audible=computeTrackAudibleGain(list,t)>0;const node=activeTrackNodesRef.current[t.id];const audibleGain=computeTrackAudibleGain(list,t);if(node)setTrackNodeGain(node,audibleGain);if(node&&node.sfEntry){try{const g=typeof audibleGain==='number'&&isFinite(audibleGain)&&!isNaN(audibleGain)?audibleGain:1.0;node.sfEntry.gain.setTargetAtTime(g,getAudioContext().currentTime,0.02);}catch(e){}}// MIDI/bridge tracks: mirror the gain decision onto CC7 (channel volume)
|
||||
const sn=activeTrackNodesRef.current[t.id];if(sn&&sn.sfRouteGain&&sn.sfDryGain&&sn.sfRouteGain.gain.value>0!==!effMidiBypass(t)){const _b=effMidiBypass(t);sn.sfRouteGain.gain.value=_b?0:1;sn.sfDryGain.gain.value=_b?1:0;}const sig=(t.muted?'1':'0')+(t.solo?'1':'0')+':'+(t.volumeDb??0);if(trackMuteSoloSigRef.current[t.id]===sig)return;trackMuteSoloSigRef.current[t.id]=sig;const audible=computeTrackAudibleGain(list,t)>0;const node=activeTrackNodesRef.current[t.id];const audibleGain=computeTrackAudibleGain(list,t);// V45.24: PR tab doc lap — track co sub-tab PR PLAY thi khong ap
|
||||
// mute/solo realtime (gain=0/CC7=0) trong sync effect.
|
||||
const prPlayingForTrack=!!(subTabsRef.current||[]).some(function(s){return s.type==='PIANO_ROLL'&&s.trackId===t.id&&s.isPlaying;});const _volDb=Number(t.volumeDb);const _volLin=isNaN(_volDb)||!isFinite(_volDb)?1.0:_volDb<=-50?0:Math.pow(10,_volDb/20);const effGain=prPlayingForTrack?_volLin:audibleGain;if(node)setTrackNodeGain(node,effGain);if(node&&node.sfEntry){try{const g=typeof effGain==='number'&&isFinite(effGain)&&!isNaN(effGain)?effGain:1.0;node.sfEntry.gain.setTargetAtTime(g,getAudioContext().currentTime,0.02);}catch(e){}}// MIDI/bridge tracks: mirror the gain decision onto CC7 (channel volume)
|
||||
// so FluidSynth/bridge-rendered notes respect mute/solo too. Bridge
|
||||
// active → SonicMidiRouter; down → SonicSF. CC7 = VOLUME track.
|
||||
if(isMidiSoundTrack(t)){try{sendMidiVolumeCC7(t,list,audible);}catch(e){}}trackAudibleRef.current[t.id]=audible;});updateSfRouting();},[tracks,sessionTabs]);const prevActiveTabRef=useRef(activeTab);useEffect(()=>{const prevTab=prevActiveTabRef.current;prevActiveTabRef.current=activeTab;updateSfRouting();// Log chẩn đoán zoom khi đổi tab (user: zoom reset khi chuyển tab)
|
||||
if(isMidiSoundTrack(t)){try{sendMidiVolumeCC7(t,list,audible||prPlayingForTrack);}catch(e){}}trackAudibleRef.current[t.id]=prPlayingForTrack?true:audible;});updateSfRouting();},[tracks,sessionTabs]);const prevActiveTabRef=useRef(activeTab);useEffect(()=>{const prevTab=prevActiveTabRef.current;prevActiveTabRef.current=activeTab;updateSfRouting();// Log chẩn đoán zoom khi đổi tab (user: zoom reset khi chuyển tab)
|
||||
if(prevTab!==activeTab){console.log('[Zoom] đổi tab',prevTab,'→',activeTab,'| zoom=',zoom,'| minZoom=',(minZoom||0).toFixed(2));}// Tab DEACTIVE → stop âm của tab đó (user requirement): rời khỏi sub-tab
|
||||
// (PIANO_ROLL/section/audio tab) đang play → stop playback của tab đó.
|
||||
// Tab mới hoạt động bình thường; MAIN giữ hành vi cũ (mở piano-roll lúc
|
||||
@@ -1795,9 +1804,9 @@ const vkbNoteOn=(pitch,vel)=>{try{if(window.__ensureMasteringRouting)window.__en
|
||||
const _prTabNow=(subTabsRef.current||[]).find(function(s){return s.id===activeTabRef.current;});const _isPrTabNow=!!(_prTabNow&&_prTabNow.type==='PIANO_ROLL');const prArmedIds={};try{if(_isPrTabNow){(subTabsRef.current||[]).forEach(function(s){if(s.type==='PIANO_ROLL'&&s.isArmed&&s.trackId)prArmedIds[s.trackId]=true;});}}catch(e){}const armed=allTracks.filter(function(t){return t.isArmed||prArmedIds[t.id];});const targets=armed.length?armed:_isPrTabNow?allTracks:[];if(!armed.length&&!window.__vkbNoArmToast){window.__vkbNoArmToast=true;showToast(_isPrTabNow?'Không có track nào ARM — chơi vào tất cả track.':'Không có track nào ARM — preview MIDI câm (MAIN/SECTION).','info');}// DIAG Bug 3/4 (preview khong am): trang thai routing tai note-on.
|
||||
try{let _pdDest='none';if(window.AudioRoutingEngine&&window.AudioRoutingEngine._connected){_pdDest=window.AudioRoutingEngine._dest&&window.AudioRoutingEngine._dest.constructor?window.AudioRoutingEngine._dest.constructor.name:'null';_pdDest+='/'+window.AudioRoutingEngine._trackId;}else{_pdDest='NOT_CONNECTED';}const _pdD=window.BridgeAudioNode?window.BridgeAudioNode.getDiag():null;const _pdTab=activeTabRef.current;const _pdSub=subTabsRef.current?(subTabsRef.current.find(s=>s.id===_pdTab)||{}).type:null;_dbgLog('preview_diag','armed='+armed.length+' trg='+targets.length+' prTab='+(_isPrTabNow?1:0)+' tab='+_pdTab+' sub='+(_pdSub||'none')+' play='+(isPlayingRef.current?1:0)+' br='+(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()?1:0)+' fxRt='+(fxRt.active?1:0)+' dest='+_pdDest+' bd='+(_pdD?'i='+_pdD.initialized+' wl='+_pdD.useWorklet+' nd='+_pdD.hasWorkletNode+' pre='+_pdD.preBufferLen+' ch='+_pdD.chunksLen+' g='+_pdD.gain:'MISSING')+' mb='+!!masterBus);}catch(e){}// DIAG Bug 3/4: arm bo dem audio frames (500ms sau note-on) de biet
|
||||
// engine co gui audio ve JS khong (tach engine vs JS routing).
|
||||
try{window.__dbgPreviewAudioArmed={count:0,nz:0,t0:performance.now()};}catch(e){}activeMidiPitchesRef.current.add(pitch);setActiveMidiPitches(new Set(activeMidiPitchesRef.current));vkbHeldRef.current.add(pitch);targets.forEach(function(t){// V45.23: mute/solo gate preview keybed — track muted/solo-out khong phat
|
||||
// (dung semantics computeTrackAudibleGain nhu scheduleMidiNoteDispatch).
|
||||
if(computeTrackAudibleGain(allTracks,t)<=0)return;const ctx=resolveTrackInstrumentCtx(t,allTracks);ensureSonicInstrument(ctx);if(window.triggerMidiVuActivity)window.triggerMidiVuActivity(t.id,Math.round(vel*127));try{heldMidiNotesRef.current[t.id]=(heldMidiNotesRef.current[t.id]||0)+1;}catch(err){}const percussion=!!(ctx&&ctx.synthEngine&&ctx.synthEngine.soundfont_bank===128);if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){try{if(window.__ensureBridgeProgram)window.__ensureBridgeProgram(t.id,ctx.program,ctx.synthEngine);}catch(er){}try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_ON',channel:t.id,pitch:pitch,velocity:vel,percussion:percussion,live:true});}catch(e){}}else if(isStandaloneSf()&&(isSfTrackEngine(ctx.synthEngine)||isVstTrackEngine(ctx.synthEngine))){if(isSfTrackEngine(ctx.synthEngine))playNativeSfNote(t,pitch,vel,60000,undefined,'vkb_'+t.id+'_'+pitch);else playNativeVstNote(t,pitch,vel,60000,undefined,'vkb_'+t.id+'_'+pitch);}else if(window.SonicSF){const dest=activeTrackNodesRef.current[t.id]&&activeTrackNodesRef.current[t.id].gainNode||null;window.SonicSF.playNote(pitch,Math.max(1,Math.round(vel*127)),60000,undefined,ctx.program,dest,ctx.ch,ctx.synthEngine);}});// Live record: forward tới mọi recorder đang chạy (timeline take / piano roll)
|
||||
try{window.__dbgPreviewAudioArmed={count:0,nz:0,t0:performance.now()};}catch(e){}activeMidiPitchesRef.current.add(pitch);setActiveMidiPitches(new Set(activeMidiPitchesRef.current));vkbHeldRef.current.add(pitch);targets.forEach(function(t){// V45.24: PR preview doc lap voi mute/solo MAIN/SECTION — khoi phuc
|
||||
// volume track khi o PR tab.
|
||||
if(_isPrTabNow){try{ensureTrackPrAudible(t,allTracks,activeTrackNodesRef.current[t.id]);}catch(e){}}const ctx=resolveTrackInstrumentCtx(t,allTracks);ensureSonicInstrument(ctx);if(window.triggerMidiVuActivity)window.triggerMidiVuActivity(t.id,Math.round(vel*127));try{heldMidiNotesRef.current[t.id]=(heldMidiNotesRef.current[t.id]||0)+1;}catch(err){}const percussion=!!(ctx&&ctx.synthEngine&&ctx.synthEngine.soundfont_bank===128);if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){try{if(window.__ensureBridgeProgram)window.__ensureBridgeProgram(t.id,ctx.program,ctx.synthEngine);}catch(er){}try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_ON',channel:t.id,pitch:pitch,velocity:vel,percussion:percussion,live:true});}catch(e){}}else if(isStandaloneSf()&&(isSfTrackEngine(ctx.synthEngine)||isVstTrackEngine(ctx.synthEngine))){if(isSfTrackEngine(ctx.synthEngine))playNativeSfNote(t,pitch,vel,60000,undefined,'vkb_'+t.id+'_'+pitch);else playNativeVstNote(t,pitch,vel,60000,undefined,'vkb_'+t.id+'_'+pitch);}else if(window.SonicSF){const dest=activeTrackNodesRef.current[t.id]&&activeTrackNodesRef.current[t.id].gainNode||null;window.SonicSF.playNote(pitch,Math.max(1,Math.round(vel*127)),60000,undefined,ctx.program,dest,ctx.ch,ctx.synthEngine);}});// Live record: forward tới mọi recorder đang chạy (timeline take / piano roll)
|
||||
if(activeMIDIRecordersRef.current){const msg={data:[0x90,pitch,Math.max(1,Math.round(vel*127))]};for(const trackId in activeMIDIRecordersRef.current){const rec=activeMIDIRecordersRef.current[trackId];if(rec){try{rec.handleMIDIMessage(msg,null);}catch(e){}}}}};const vkbNoteOff=pitch=>{vkbHeldRef.current.delete(pitch);activeMidiPitchesRef.current.delete(pitch);setActiveMidiPitches(new Set(activeMidiPitchesRef.current));const allTracks=activeTracksRef.current||[];allTracks.forEach(function(t){const ctx=resolveTrackInstrumentCtx(t,allTracks);const percussion=!!(ctx&&ctx.synthEngine&&ctx.synthEngine.soundfont_bank===128);if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_OFF',channel:t.id,pitch:pitch,velocity:0,percussion:percussion,live:true});}catch(e){}}else if(window.SonicSF&&window.SonicSF.stopNote){if(isStandaloneSf()){stopNativeSfNote('vkb_'+t.id+'_'+pitch);stopNativeVstNote('vkb_'+t.id+'_'+pitch);}window.SonicSF.stopNote(ctx.ch,pitch);}try{if(heldMidiNotesRef.current[t.id]!==undefined){heldMidiNotesRef.current[t.id]=Math.max(0,heldMidiNotesRef.current[t.id]-1);if(heldMidiNotesRef.current[t.id]===0)delete heldMidiNotesRef.current[t.id];}}catch(err){}});if(activeMIDIRecordersRef.current){const msg={data:[0x80,pitch,0]};for(const trackId in activeMIDIRecordersRef.current){const rec=activeMIDIRecordersRef.current[trackId];if(rec){try{rec.handleMIDIMessage(msg,null);}catch(e){}}}}};vkbNoteOnRef.current=vkbNoteOn;vkbNoteOffRef.current=vkbNoteOff;window.__toggleVkb=function(){setVkbOpen(function(p){const next=!p;if(!next&&vkbHeldRef.current.size){Array.from(vkbHeldRef.current).forEach(function(pitch){try{vkbNoteOffRef.current(pitch);}catch(e){}});}return next;});};window.__toggleChordsPanel=function(){setChordsPanelOpen(function(p){return!p;});};// ── Server-side upload ──
|
||||
// WAV encoder tối thiểu — upload buffer-only clip khi LƯU (nếu chưa có
|
||||
// serverFileId — upload sớm thất bại/race → clip mất sau reload).
|
||||
@@ -1983,7 +1992,7 @@ fxRtTrackReconcile(trackId,node,track);}catch(e){console.warn('rebuildTrackFxGra
|
||||
// Cần activeTrackNodesRef/isPlayingRef. Session giống master (fxRtStart):
|
||||
// fetch start → AudioWorkletNode 'sf-fx-realtime' → WS; WS open → splice
|
||||
// worklet vào chain hiện tại (đọc mods MỚI NHẤT — an toàn với rebuild).
|
||||
const fxRtTrackStart=async(trackId,track,path)=>{const key=trackId+':'+path;const chain=fxRtTrackChain(track);if(!chain.length)return;if(fxRtTracks[key]){if(!fxRtTracks[key].node||fxRtTracks[key].node.context!==getAudioContext())fxRtTrackStop(trackId,path,true);else return;}const ctx=getAudioContext();if(!ctx||!ctx.audioWorklet)return;const chainPayload=chain.map(s=>s.type==='vst3'?{type:'vst3',path:s.path,name:s.name||s.path,preset_b64:s.preset_b64||'',bypass:!!s.bypass||!!(track&&track.vstFxBypass)}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});let st;try{st=await window.SonicAPI.fxRealtimeStart({fx_chain:chainPayload,sample_rate:ctx.sampleRate||44100});}catch(e){console.warn('[FX RT track] start fail',trackId,path,e);return;}const entry={sessionId:st.session_id,ws:null,node:null,ready:false,path,spliced:null,chainKey:fxRtTrackChainKey(chain)+'|vb:'+(track&&track.vstFxBypass?'1':'0'),paramsBySlot:chain.map(s=>fxRtEntryParams(s,null)),inQueue:[],pumpTimer:null,lastInSent:0,flushing:false,flushTimer:null};fxRtTracks[key]=entry;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608311716`);const node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});entry.node=node;const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';// WS to 'localhost' bi tre ~300ms (Chromium thu ::1 truoc roi fallback) - dung 127.0.0.1
|
||||
const fxRtTrackStart=async(trackId,track,path)=>{const key=trackId+':'+path;const chain=fxRtTrackChain(track);if(!chain.length)return;if(fxRtTracks[key]){if(!fxRtTracks[key].node||fxRtTracks[key].node.context!==getAudioContext())fxRtTrackStop(trackId,path,true);else return;}const ctx=getAudioContext();if(!ctx||!ctx.audioWorklet)return;const chainPayload=chain.map(s=>s.type==='vst3'?{type:'vst3',path:s.path,name:s.name||s.path,preset_b64:s.preset_b64||'',bypass:!!s.bypass||!!(track&&track.vstFxBypass)}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});let st;try{st=await window.SonicAPI.fxRealtimeStart({fx_chain:chainPayload,sample_rate:ctx.sampleRate||44100});}catch(e){console.warn('[FX RT track] start fail',trackId,path,e);return;}const entry={sessionId:st.session_id,ws:null,node:null,ready:false,path,spliced:null,chainKey:fxRtTrackChainKey(chain)+'|vb:'+(track&&track.vstFxBypass?'1':'0'),paramsBySlot:chain.map(s=>fxRtEntryParams(s,null)),inQueue:[],pumpTimer:null,lastInSent:0,flushing:false,flushTimer:null};fxRtTracks[key]=entry;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608311725`);const node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});entry.node=node;const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';// WS to 'localhost' bi tre ~300ms (Chromium thu ::1 truoc roi fallback) - dung 127.0.0.1
|
||||
const wsHost=u.hostname==='localhost'||u.hostname==='::1'?'127.0.0.1'+(u.port?':'+u.port:''):u.host;const tok=localStorage.getItem('sonic_token')||'';const ws=new WebSocket(`${wsProto}${wsHost}${st.ws_url}${tok?'?token='+encodeURIComponent(tok):''}`);entry.ws=ws;ws.binaryType='arraybuffer';ws.onopen=()=>{entry.ready=true;entry.inQueue=[];// bỏ audio stale trước khi WS open — hết lag khởi đầu
|
||||
fxRtTrackSplice(trackId,path);try{(entry.paramsBySlot||[]).forEach((pl,slot)=>pl.forEach(p=>ws.send(JSON.stringify({cmd:'set_param',slot,key:p.key,value:p.value}))));}catch(e){}};ws.onmessage=ev=>{if(typeof ev.data==='string'){try{const d=JSON.parse(ev.data);if(d&&d.cmd==='flush_done'){entry.flushing=false;if(entry.flushTimer){clearTimeout(entry.flushTimer);entry.flushTimer=null;}if(entry.node)entry.node.port.postMessage({type:'flush_done'});}}catch(e){}return;}if(!(ev.data instanceof ArrayBuffer)||!entry.node)return;if(entry.flushing)return;// V24: output stale trong luc flush — drop
|
||||
const f=new Float32Array(ev.data);const half=f.length/2;const l=new Float32Array(half);const r=new Float32Array(half);for(let i=0;i<half;i++){l[i]=f[i*2];r[i]=f[i*2+1];}entry.node.port.postMessage({type:'out',l,r});};ws.onclose=()=>fxRtTrackStop(trackId,path,true);ws.onerror=()=>{try{ws.close();}catch(e){}};entry.node.port.onmessage=ev=>{const d=ev.data;if(!d||!entry.ws)return;if(d.type==='flush'){// V24: nhu master — worklet underrun-resume → flush engine ring
|
||||
@@ -2103,7 +2112,7 @@ if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){const isPerc
|
||||
// SonicSF (FluidSynth WASM). Giữ setTimeout scheduling (như SonicSF cũ);
|
||||
// bridge tự render note-off sau duration. A11 (sample-accurate) sẽ thay
|
||||
// setTimeout bằng sampleOffset đẩy thẳng vào SHM.
|
||||
const scheduleMidiNoteDispatch=(track,pitch,velocity,durMs,startTime,program,destNode,ch,synthEngine)=>{if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){const ctx=getAudioContext();const startAt=startTime||ctx.currentTime;// undefined/null = phát ngay
|
||||
const scheduleMidiNoteDispatch=(track,pitch,velocity,durMs,startTime,program,destNode,ch,synthEngine,prNote)=>{if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){const ctx=getAudioContext();const startAt=startTime||ctx.currentTime;// undefined/null = phát ngay
|
||||
const delayMs=Math.max(0,(startAt-ctx.currentTime)*1000);// Guard chống note-on trễ sau Stop: CHỈ khi là timeline (startTime thật).
|
||||
const guardPlay=startTime!=null?function(){return isPlayingRef.current||subTabsRef.current&&subTabsRef.current.some(s=>s.isPlaying)||isExportingRef.current;}:function(){return true;};const trkId=track?track.id:0;// Percussion (bank 128) → router allocateChannel ch 9; melodic → round-robin.
|
||||
const isPerc=!!(synthEngine&&synthEngine.soundfont_bank===128);// D8: track đổi instrument → gửi CC0/CC32 (bank THẬT từ synthEngine) +
|
||||
@@ -2111,7 +2120,9 @@ const isPerc=!!(synthEngine&&synthEngine.soundfont_bank===128);// D8: track đ
|
||||
if(program!==undefined&&program!==null){try{if(window.__ensureBridgeProgram)window.__ensureBridgeProgram(trkId,program,synthEngine);}catch(e){}}// V11: epoch guard — hủy timer cũ khi stop/loop restart (xem ref).
|
||||
const epoch=bridgeNoteEpochRef.current;setTimeout(function(){if(epoch!==bridgeNoteEpochRef.current)return;if(!guardPlay())return;// D18b: mute/solo guard tai thoi diem fire — note-on dang cho cua track
|
||||
// vua tro nen inaudible (solo track khac / mute) khong duoc phat ra.
|
||||
const _list=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracksRef.current&&tracksRef.current.length?tracksRef.current:activeTracks;const _trk=_list.find(t=>t.id===trkId)||track;if(!_trk||computeTrackAudibleGain(_list,_trk)<=0){try{if(window.__dbgLog)window.__dbgLog('solo_guard','block ch='+trkId+' p='+(pitch||60));}catch(e){}return;}// D18c: track pitch dang keu (timeline) - de applyAllTrackMuteSolo kill
|
||||
// V45.24: note PR tab (prNote=true) BO QUA guard + khoi phuc volume —
|
||||
// mute/solo chi cuc bo MAIN/SECTION, PR khong bao gio bi cam.
|
||||
const _list=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracksRef.current&&tracksRef.current.length?tracksRef.current:activeTracks;const _trk=_list.find(t=>t.id===trkId)||track;if(prNote){try{ensureTrackPrAudible(_trk,_list,destNode);}catch(e){}}else if(!_trk||computeTrackAudibleGain(_list,_trk)<=0){try{if(window.__dbgLog)window.__dbgLog('solo_guard','block ch='+trkId+' p='+(pitch||60));}catch(e){}return;}// D18c: track pitch dang keu (timeline) - de applyAllTrackMuteSolo kill
|
||||
// dung note khi track thanh inaudible giua chung (Qin VST2 tremolo).
|
||||
try{var _tps=timelinePitchesRef.current;(_tps[trkId]||(_tps[trkId]=new Set())).add(pitch||60);}catch(e){}var _rt=window.SonicMidiRouter;try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_ON',channel:trkId,pitch:pitch||60,velocity:velocity||0.8,percussion:isPerc});}catch(e){console.warn('[Midi] pushEvent error:',e&&e.message);}},delayMs);scheduleNoteOff('B:'+trkId+':'+(pitch||60),function(){if(epoch!==bridgeNoteEpochRef.current)return;if(!guardPlay())return;try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_OFF',channel:trkId,pitch:pitch||60,velocity:0,percussion:isPerc});}catch(e){}// D18c: xoa khoi per-track set (note da tat - khong con can kill).
|
||||
try{var _tps2=timelinePitchesRef.current;if(_tps2[trkId])_tps2[trkId].delete(pitch||60);}catch(e){}},delayMs,durMs||1000);return;}// Native VSTi (standalone browser, bridge inactive) → midi-render thay vì
|
||||
@@ -2238,8 +2249,8 @@ try{if(window.__ensureBridgeProgram)window.__ensureBridgeProgram(st.trackId,inst
|
||||
// future (silence when pressing play). Ghost notes are already relative to
|
||||
// the item window, so no absolute-session offset is applied anywhere here.
|
||||
if(isStandaloneSf()&&(isSfTrackEngine(synthEngine)||isVstTrackEngine(synthEngine))&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())){scheduleNativeTrack(track,offsetSeconds,context,destNode,bpm,{notesOverride:midiNotes,playheadNow:()=>startOffsetTimeRef.current+(getAudioContext().currentTime-startAudioTimeRef.current),sources:activeSourcesRef.current,isActive:()=>!!isPlayingRef.current});return;// native render cả tab → audio buffer (không per-note WASM)
|
||||
}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){scheduleMidiNoteDispatch(track,note.pitch||60,note.velocity||0.8,durMs,scheduledTime,instrumentProgram,destNode,mainCh,synthEngine);}scheduleNoteExpressionCC11(track,note,mainCh,startWallTime,effectiveStart,effectiveDur);}});// 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){scheduleMidiNoteDispatch(ghostTrack,note.pitch||60,note.velocity||0.8,durMs,schedTime,ghostProg,ghostDest,ghostCh,ghostSynth);}}});});};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
|
||||
}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){scheduleMidiNoteDispatch(track,note.pitch||60,note.velocity||0.8,durMs,scheduledTime,instrumentProgram,destNode,mainCh,synthEngine,true);}scheduleNoteExpressionCC11(track,note,mainCh,startWallTime,effectiveStart,effectiveDur);}});// 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){scheduleMidiNoteDispatch(ghostTrack,note.pitch||60,note.velocity||0.8,durMs,schedTime,ghostProg,ghostDest,ghostCh,ghostSynth,true);}}});});};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).
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<script src="/static/js/services/promptTemplateManager.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/chordTheory.js?v=202608290945"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608311716" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608311725" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202608290945">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
Reference in New Issue
Block a user