FIX: lỗi mở MIDI item ở trong PIANO ROLL TAB là bị lỗi âm thanh của main session

This commit is contained in:
2026-08-04 22:07:48 +07:00
parent 0272912cff
commit 856a8183b6
5 changed files with 521 additions and 136 deletions
+247 -85
View File
@@ -42,6 +42,48 @@ const assignTrackMidiChannel = (track, tracks) => {
return ch;
};
// Instrument context (PIANO ROLL play ngun duy nht, mi nơi dùng)
// Track "đã loaded instrument" = track.instrumentProgram (GM preset) HOC
// track.synth_engine (soundfont: soundfont_id/bank/program). Resolve thành
// context thng nht cho schedulePianoRollMidi + preview (wheel/click/keybed)
// đ note PHI chơi đúng instrument ca track không ph thuc st snapshot.
const resolveTrackInstrumentCtx = (track, tracks) => {
const all = tracks || [];
const ch = track ? assignTrackMidiChannel(track, all) : 0;
const se = track ? track.synth_engine : undefined;
const isSf = !!(se && (se.type === 'soundfont' || se.soundfont_id));
if (isSf) {
return {
ch,
program: undefined, // SF path synthEngine đưc ưu tiên trong _playNoteFluid
synthEngine: se,
sfId: se.soundfont_id,
bank: se.soundfont_bank || 0,
prog: se.soundfont_program || 0
};
}
if (track && track.instrumentProgram !== undefined) {
return { ch, program: track.instrumentProgram, synthEngine: undefined, sfId: undefined, bank: 0, prog: track.instrumentProgram };
}
return { ch, program: undefined, synthEngine: undefined, sfId: undefined, bank: 0, prog: 0 };
};
// Đm bo FluidSynth channel ca track đã select ĐÚNG instrument trưc khi
// notes bn. Fire-and-forget: playNote t load + retry nếu SF chưa load xong
// (dedup sn trong loadSoundFont) không chn, không gây stall khi m tab.
const ensureSonicInstrument = (ctx) => {
try {
if (!window.SonicSF || !window.SonicSF.selectInstrument) return;
if (ctx.sfId) {
window.SonicSF.selectInstrument(ctx.ch, ctx.bank, ctx.prog, ctx.sfId);
} else if (ctx.program !== undefined) {
window.SonicSF.selectInstrument(ctx.ch, 0, ctx.program, null);
}
} catch (e) {
console.warn('[Instrument] ensureSonicInstrument error:', e);
}
};
// Handle ?sfs=<encoded> from double-clicking a .sfs file (opens domain -> loads project)
(function handleSfsDeepLink() {
@@ -124,7 +166,8 @@ function computeTrackAudibleGain(trackList, track) {
if (track.muted) return 0;
const hasSolo = (trackList || []).some(t => t.solo);
if (hasSolo && !track.solo) return 0;
const volDb = track.volumeDb ?? 0;
const volDb = Number(track.volumeDb);
if (isNaN(volDb) || !isFinite(volDb)) return 1.0;
return volDb <= -50 ? 0 : Math.pow(10, volDb / 20);
}
@@ -132,8 +175,9 @@ function computeTrackAudibleGain(trackList, track) {
function setTrackNodeGain(node, gainLinear) {
if (!node || !node.gainNode || !audioCtx) return;
const t = audioCtx.currentTime;
const g = (typeof gainLinear === 'number' && isFinite(gainLinear) && !isNaN(gainLinear)) ? gainLinear : 1.0;
node.gainNode.gain.cancelScheduledValues(t);
node.gainNode.gain.setTargetAtTime(gainLinear, t, 0.02);
node.gainNode.gain.setTargetAtTime(g, t, 0.02);
}
// MAIN SESSION end-time (seconds): endtime of the items ON the session's own
@@ -218,6 +262,7 @@ function makeDistortionCurve(k) {
}
function applyMasteringSettings(s) {
console.log('[Mastering] applyMasteringSettings active=', !!(s && s.masterConnected), 'bypass=', !!(s && s.isBypassed), 'eqActive=', !!(s && s.eqActive), 'masterBus=', !!masterBus);
if (!masterBus || !audioCtx || !s) return;
// getAudioContext() invokes this on EVERY call (stopAll, play, VU, ).
// Re-applying identical values in rapid bursts is "fast parameter automation"
@@ -241,14 +286,17 @@ function applyMasteringSettings(s) {
// cancelScheduledValues + a slower time constant keeps rapid slider drags
// from piling up automation events on the biquad filters (the trigger for
// Chromium's "BiquadFilterNode: state is bad").
// Guard NaN: setValueAtTime(NaN) trên biquad Chromium "BiquadFilterNode:
// state is bad" + câm. Field settings undefined/NaN mc đnh 0.
const _g = (v, lo, hi) => (typeof v === 'number' && isFinite(v)) ? clamp(v, lo, hi) : 0;
masterBus.eqLowFilter.gain.cancelScheduledValues(now);
masterBus.eqLowFilter.gain.setTargetAtTime(s.eqActive ? clamp(s.eqLowGain, -24, 24) : 0, now, 0.05);
masterBus.eqLowFilter.gain.setValueAtTime(s.eqActive ? _g(s.eqLowGain, -24, 24) : 0, now);
masterBus.eqMid1Filter.gain.cancelScheduledValues(now);
masterBus.eqMid1Filter.gain.setTargetAtTime(s.eqActive ? clamp(s.eqMid1Gain, -24, 24) : 0, now, 0.05);
masterBus.eqMid1Filter.gain.setValueAtTime(s.eqActive ? _g(s.eqMid1Gain, -24, 24) : 0, now);
masterBus.eqMid2Filter.gain.cancelScheduledValues(now);
masterBus.eqMid2Filter.gain.setTargetAtTime(s.eqActive ? clamp(s.eqMid2Gain, -24, 24) : 0, now, 0.05);
masterBus.eqMid2Filter.gain.setValueAtTime(s.eqActive ? _g(s.eqMid2Gain, -24, 24) : 0, now);
masterBus.eqHighFilter.gain.cancelScheduledValues(now);
masterBus.eqHighFilter.gain.setTargetAtTime(s.eqActive ? clamp(s.eqHighGain, -24, 24) : 0, now, 0.05);
masterBus.eqHighFilter.gain.setValueAtTime(s.eqActive ? _g(s.eqHighGain, -24, 24) : 0, now);
// 2. Imager Settings (Mid/Side width per band imager_spec.md)
// Width % semantics per the guide: 0% = MONO (S × 0), 100% = original
@@ -675,7 +723,7 @@ const EQPRO_DEFAULT_BANDS = [
{ type: 'highshelf', freq: 10000, gain: 0, q: 0.7, active: true }
];
function eqproFreqToX(f, w) { return w * (Math.log10(f / EQPRO_F_MIN) / Math.log10(EQPRO_F_MAX / EQPRO_F_MIN)); }
function eqproClamp(v, lo, hi) { return Math.min(hi, Math.max(lo, v)); }
function eqproClamp(v, lo, hi) { return (typeof v === 'number' && isFinite(v)) ? Math.min(hi, Math.max(lo, v)) : lo; }
function eqproXToFreq(x, w) { return EQPRO_F_MIN * Math.pow(EQPRO_F_MAX / EQPRO_F_MIN, eqproClamp(x, 0, w) / w); }
function eqproGainToY(g, h) { return (h / 2) - (g * ((h / 2) / EQPRO_MAX_DB)); }
function eqproYToGain(y, h) { return ((h / 2) - y) * (EQPRO_MAX_DB / (h / 2)); }
@@ -750,10 +798,11 @@ function createEqProModule(ctx, params) {
filters.forEach(f => { try { f.disconnect(); } catch (e) { } });
filters.length = 0;
let tail = input;
const maxF = (ctx.sampleRate || 44100) * 0.45;
bands.forEach(b => {
const f = ctx.createBiquadFilter();
f.type = b.type || 'peaking';
f.frequency.value = eqproClamp(b.freq !== undefined ? b.freq : 1000, EQPRO_F_MIN, EQPRO_F_MAX);
f.frequency.value = eqproClamp(b.freq !== undefined ? b.freq : 1000, EQPRO_F_MIN, Math.min(EQPRO_F_MAX, maxF));
f.Q.value = eqproClamp(b.q !== undefined ? b.q : 1, 0.1, 18);
f.gain.value = (b.active !== false) ? ((b.gain || 0) * amount / 100) : 0;
tail.connect(f);
@@ -768,8 +817,9 @@ function createEqProModule(ctx, params) {
Object.assign(b, patch);
const f = filters[i]; if (!f) return;
const now = ctx.currentTime;
const maxF = (ctx.sampleRate || 44100) * 0.45;
if (patch.type !== undefined) f.type = patch.type;
if (patch.freq !== undefined) f.frequency.setValueAtTime(eqproClamp(b.freq, EQPRO_F_MIN, EQPRO_F_MAX), now);
if (patch.freq !== undefined) f.frequency.setValueAtTime(eqproClamp(b.freq, EQPRO_F_MIN, Math.min(EQPRO_F_MAX, maxF)), now);
if (patch.q !== undefined) f.Q.setValueAtTime(eqproClamp(b.q, 0.1, 18), now);
if (patch.gain !== undefined || patch.active !== undefined) f.gain.setValueAtTime((b.active !== false) ? ((b.gain || 0) * amount / 100) : 0, now);
};
@@ -797,32 +847,39 @@ function createEqProModule(ctx, params) {
}
function createTrackFxModule(type, ctx, params) {
const num = (v, def) => {
const n = Number(v);
return isFinite(n) && !isNaN(n) ? n : def;
};
const maxF = (ctx.sampleRate || 44100) * 0.45;
const clampF = v => Math.max(20, Math.min(v, maxF));
const input = ctx.createGain();
const output = ctx.createGain();
const p = params || {};
let nodes = {};
if (type === 'compressor') {
const comp = ctx.createDynamicsCompressor();
comp.threshold.value = p.threshold !== undefined ? p.threshold : -16;
comp.knee.value = 8; comp.ratio.value = p.ratio !== undefined ? p.ratio : 3;
comp.threshold.value = num(p.threshold, -16);
comp.knee.value = 8; comp.ratio.value = num(p.ratio, 3);
comp.attack.value = 0.02; comp.release.value = 0.25;
const makeup = ctx.createGain(); makeup.gain.value = p.makeup !== undefined ? Math.pow(10, p.makeup / 20) : 1.0;
const makeup = ctx.createGain(); makeup.gain.value = Math.pow(10, num(p.makeup, 0) / 20);
input.connect(comp); comp.connect(makeup); makeup.connect(output);
nodes = { comp, makeup };
} else if (type === 'limiter') {
const lim = ctx.createDynamicsCompressor();
lim.threshold.value = p.ceiling !== undefined ? p.ceiling : -1.0;
lim.threshold.value = num(p.ceiling, -1.0);
lim.knee.value = 0; lim.ratio.value = 20;
lim.attack.value = 0.001; lim.release.value = 0.05;
input.connect(lim); lim.connect(output);
nodes = { lim };
} else if (type === 'exciter') {
const hp = ctx.createBiquadFilter();
hp.type = 'highpass'; hp.frequency.value = 2000; hp.Q.value = 0.7;
hp.type = 'highpass'; hp.frequency.value = clampF(2000); hp.Q.value = 0.7;
const shaper = ctx.createWaveShaper();
shaper.curve = makeDistortionCurve(3); shaper.oversample = '4x';
const dry = ctx.createGain(); dry.gain.value = 1.0;
const wet = ctx.createGain(); wet.gain.value = ((p.drive !== undefined ? p.drive : 40) / 100) * 0.6;
const wet = ctx.createGain(); wet.gain.value = (num(p.drive, 40) / 100) * 0.6;
input.connect(dry); dry.connect(output);
input.connect(hp); hp.connect(shaper); shaper.connect(wet); wet.connect(output);
nodes = { hp, shaper, dry, wet };
@@ -830,8 +887,8 @@ function createTrackFxModule(type, ctx, params) {
const split = ctx.createChannelSplitter(2);
const merge = ctx.createChannelMerger(2);
const gLL = ctx.createGain(), gRL = ctx.createGain(), gLR = ctx.createGain(), gRR = ctx.createGain();
const midLin = Math.pow(10, (p.mid !== undefined ? p.mid : 0) / 20);
const sideLin = Math.pow(10, (p.side !== undefined ? p.side : 0) / 20);
const midLin = Math.pow(10, num(p.mid, 0) / 20);
const sideLin = Math.pow(10, num(p.side, 0) / 20);
const a = (midLin + sideLin) / 2, b = (midLin - sideLin) / 2;
gLL.gain.value = a; gRR.gain.value = a; gRL.gain.value = b; gLR.gain.value = b;
input.connect(split);
@@ -845,14 +902,14 @@ function createTrackFxModule(type, ctx, params) {
return createEqProModule(ctx, params);
} else {
// 'eq' or default: 4-band EQ (params.g1..g4 = band gains in dB)
const f1 = ctx.createBiquadFilter(); f1.type = 'lowshelf'; f1.frequency.value = 100;
const f2 = ctx.createBiquadFilter(); f2.type = 'peaking'; f2.frequency.value = 800; f2.Q.value = 0.7;
const f3 = ctx.createBiquadFilter(); f3.type = 'peaking'; f3.frequency.value = 3200; f3.Q.value = 1.2;
const f4 = ctx.createBiquadFilter(); f4.type = 'highshelf'; f4.frequency.value = 10000;
f1.gain.value = p.g1 !== undefined ? p.g1 : 0;
f2.gain.value = p.g2 !== undefined ? p.g2 : 0;
f3.gain.value = p.g3 !== undefined ? p.g3 : 0;
f4.gain.value = p.g4 !== undefined ? p.g4 : 0;
const f1 = ctx.createBiquadFilter(); f1.type = 'lowshelf'; f1.frequency.value = clampF(100);
const f2 = ctx.createBiquadFilter(); f2.type = 'peaking'; f2.frequency.value = clampF(800); f2.Q.value = 0.7;
const f3 = ctx.createBiquadFilter(); f3.type = 'peaking'; f3.frequency.value = clampF(3200); f3.Q.value = 1.2;
const f4 = ctx.createBiquadFilter(); f4.type = 'highshelf'; f4.frequency.value = clampF(10000);
f1.gain.value = num(p.g1, 0);
f2.gain.value = num(p.g2, 0);
f3.gain.value = num(p.g3, 0);
f4.gain.value = num(p.g4, 0);
input.connect(f1); f1.connect(f2); f2.connect(f3); f3.connect(f4); f4.connect(output);
nodes = { f1, f2, f3, f4 };
}
@@ -7067,9 +7124,10 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
currentBeat < n.start_beat && newBeat >= n.start_beat
);
var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
var pvCh = pvTrk ? assignTrackMidiChannel(pvTrk, activeTracks) : 0;
var pvCtx = resolveTrackInstrumentCtx(pvTrk, activeTracks);
ensureSonicInstrument(pvCtx);
playing.forEach(n => {
window.SonicSF.playNote(n.pitch, (n.velocity || 0.8) * 127, 200, ctx.currentTime, st.instrumentProgram, null, pvCh, pvTrk ? pvTrk.synth_engine : undefined);
window.SonicSF.playNote(n.pitch, (n.velocity || 0.8) * 127, 200, ctx.currentTime, pvCtx.program, null, pvCtx.ch, pvCtx.synthEngine);
});
}
}
@@ -7401,8 +7459,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (window.SonicSF) {
const ctx = getAudioContext();
var clTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
var clCh = clTrk ? assignTrackMidiChannel(clTrk, activeTracks) : 0;
window.SonicSF.playNote(notes[clickedNoteIdx].pitch, 100, 300, ctx.currentTime, st.instrumentProgram, null, clCh, clTrk ? clTrk.synth_engine : undefined);
var clCtx = resolveTrackInstrumentCtx(clTrk, activeTracks);
ensureSonicInstrument(clCtx);
window.SonicSF.playNote(notes[clickedNoteIdx].pitch, 100, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine);
}
}
@@ -7721,9 +7780,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (window.SonicSF && window.SonicSF._playNoteFallback) {
var pvCtx = getAudioContext();
var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
var pvCh = pvTrk ? assignTrackMidiChannel(pvTrk, activeTracks) : 0;
var pvCtxInst = resolveTrackInstrumentCtx(pvTrk, activeTracks);
var pvVel = Math.round(brushVelocityRef.current * 127);
var pvNodes = window.SonicSF._playNoteFallback(p, pvVel, durMs, pvCtx.currentTime, pvTrk ? pvTrk.instrumentProgram : undefined, null, pvCh, pvTrk ? pvTrk.synth_engine : undefined);
var pvNodes = window.SonicSF._playNoteFallback(p, pvVel, durMs, pvCtx.currentTime, pvCtxInst.program, null, pvCtxInst.ch, pvCtxInst.synthEngine);
if (pvNodes) previewNodesRef.current = pvNodes;
previewPitchRef.current = p;
}
@@ -8038,8 +8097,8 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
const renderKeybed = () => {
var kbTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
var kbCh = kbTrk ? assignTrackMidiChannel(kbTrk, activeTracks) : 0;
var kbSynth = kbTrk ? kbTrk.synth_engine : undefined;
var kbCtx = resolveTrackInstrumentCtx(kbTrk, activeTracks);
ensureSonicInstrument(kbCtx);
const keys = [];
for (let pitch = 127; pitch >= PITCH_START; pitch--) {
const isBlack = [1, 3, 6, 8, 10].includes(pitch % 12);
@@ -8061,7 +8120,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
window.triggerMidiVuActivity(st.trackId, 100);
}
if (window.SonicSF) {
window.SonicSF.playNote(pitch, 100, 500, undefined, st.instrumentProgram, null, kbCh, kbSynth);
window.SonicSF.playNote(pitch, 100, 500, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
}
} catch (err) {
console.error('playNote error:', err);
@@ -8074,7 +8133,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
window.triggerMidiVuActivity(st.trackId, 100);
}
if (window.SonicSF) {
window.SonicSF.playNote(pitch, 100, 200, undefined, st.instrumentProgram, null, kbCh, kbSynth);
window.SonicSF.playNote(pitch, 100, 200, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
}
} catch (err) { console.error('playNote error:', err); }
}
@@ -8924,6 +8983,7 @@ const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsLi
duration: st.duration || 4,
instrument_program: st.instrumentProgram,
instrument_name: st.instrumentName,
synth_engine: st.synth_engine || null,
current_time: st.currentTime || 0,
color: st.color || null
};
@@ -8995,6 +9055,7 @@ const deserializeProjectFromSchema = (schemaObj) => {
duration: st.duration || 4,
instrumentProgram: st.instrument_program,
instrumentName: st.instrument_name,
synth_engine: st.synth_engine || null,
currentTime: st.current_time || 0,
color: st.color || null
};
@@ -10625,9 +10686,10 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
const f = filters[i];
if (!f) return;
try {
f.frequency.setTargetAtTime(bd.freq, now, 0.02);
f.gain.setTargetAtTime(bd.gain, now, 0.02);
f.Q.setTargetAtTime(bd.q, now, 0.02);
// setValueAtTime (không automation) tránh "BiquadFilterNode: state is bad"
f.frequency.setValueAtTime(bd.freq, now);
f.gain.setValueAtTime(bd.gain, now);
f.Q.setValueAtTime(bd.q, now);
} catch (e) {}
});
}
@@ -14425,7 +14487,10 @@ const App = () => {
const audibleGain = computeTrackAudibleGain(list, t);
if (node) setTrackNodeGain(node, audibleGain);
if (node && node.sfEntry) {
try { node.sfEntry.gain.setTargetAtTime(audibleGain, getAudioContext().currentTime, 0.02); } catch (e) {}
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 tracks: mirror the gain decision onto the channel CC7 volume so
// FluidSynth-rendered notes respect mute/solo too.
@@ -14442,6 +14507,10 @@ const App = () => {
updateSfRouting();
}, [tracks, sessionTabs]);
useEffect(() => {
updateSfRouting();
}, [activeTab]);
const [selectionCleared, setSelectionCleared] = useState(false); // LOOP_EDITOR_2.md §4.2
const midiVuActivityRef = useRef({});
@@ -15910,17 +15979,29 @@ const App = () => {
const midiItem = (track.midiItems || []).find(m => m.id === midiItemId);
if (!midiItem) return;
// Clean stop any ongoing playback to avoid stuck playheads and audio routes
stopAllPlayback();
const ctx = getAudioContext();
const silentBuffer = ctx.createBuffer(1, 128, ctx.sampleRate);
const existing = subTabs.find(s => s.type === 'PIANO_ROLL' && s.target_id === midiItemId);
if (existing) {
setSubTabs(prev => prev.map(s => s.id === existing.id ? {
...s,
buffer: s.buffer || silentBuffer,
instrumentProgram: track.instrumentProgram !== undefined ? track.instrumentProgram : (track.synth_engine ? track.synth_engine.soundfont_program : undefined),
instrumentName: track.instrumentName,
instrumentId: track.instrumentId,
synth_engine: track.synth_engine,
} : s));
setActiveTab(existing.id);
showToast('Piano Roll cho nốt MIDI đã được mở.', 'info');
return;
}
const tabId = 'midi_' + Date.now();
const tabLabel = `Piano Roll: ${midiItem.name || 'MIDI'}`;
const ctx = getAudioContext();
const silentBuffer = ctx.createBuffer(1, 128, ctx.sampleRate);
const newTab = {
id: tabId,
label: tabLabel,
@@ -15948,17 +16029,9 @@ const App = () => {
};
setSubTabs(prev => [...prev, newTab]);
setActiveTab(tabId);
if (track.synth_engine && window.SonicSF && window.SonicSF.selectInstrument) {
var se = track.synth_engine;
var sfId = se.soundfont_id;
if (sfId) {
var allTrks = activeTracksRef.current || activeTracks;
// Use the track's dedicated channel (same as every playback path) so the
// preload matches where piano-roll notes will actually sound.
var seCh = assignTrackMidiChannel(track, allTrks);
window.SonicSF.selectInstrument(seCh, se.soundfont_bank || 0, se.soundfont_program || 0, sfId);
}
}
// KHÔNG loadSoundFont đây (k c nn): sfload SF mi bt đng b làm
// WASM heap 256MB đy FluidSynth stall CÂM TOÀN CC (mi âm thanh
// chết sau khi m tab). playNote T load + retry đúng lúc note cn.
};
const handleUpdateMidiNotes = (tabId, notes) => {
@@ -17553,11 +17626,15 @@ const App = () => {
fadeGainNode.connect(route.dryGain);
source.start(context.currentTime, offsetBuffer);
activeSourcesRef.current = [source];
activeTrackNodesRef.current[st.trackId] = {
gainNode: volumeGainNode,
pannerNode,
source
};
// PIANO_ROLL: GI track node tht trong ref (SF notes đã schedule ti nó
// ghi đè bng silent node làm mt mastering route/updateSfRouting đúng).
if (st.type !== 'PIANO_ROLL') {
activeTrackNodesRef.current[st.trackId] = {
gainNode: volumeGainNode,
pannerNode,
source
};
}
// Realtime mute/solo for the newly created playback chain.
if (window.__applyTrackMuteSolo) window.__applyTrackMuteSolo(st.trackId, null);
startOffsetTimeRef.current = offsetWallTime;
@@ -17649,12 +17726,32 @@ const App = () => {
// Master-silence watchdog: đang play + có source ĐANG TRONG KHONG PHÁT
// (theo lch) nhưng master output im lng liên tc ~750ms graph b hng
// (BiquadFilter "state is bad" node cache dính) rebuild nodes.
// Đon im lng T NHIÊN (intro/rest mi source nm ngoài khong phát)
// KHÔNG trigger rebuild (cooldown 3s phòng trigger lp).
if (isPlaying && activeTabRef.current === 'main' && masterBus && masterBus.analyser && activeSourcesRef.current.length > 0) {
// Áp cho main + sub-tab AUDIO. PIANO_ROLL tab KHÔNG có ngun liên tc
// (ch notes MIDI schedule ri rc) rest/im >750ms là T NHIÊN, watchdog
// s rebuild loop + panic hy notes ch CÂM TOÀN CC exempt hoàn toàn
// (các fix setValueAtTime/NaN guard đã hết "state is bad" watchdog ch
// còn là lp cu cui cho main/audio-tab).
const _anySubPlaying = subTabsRef.current.some(s => s.isPlaying);
const activeSub = subTabsRef.current.find(s => s.id === activeTabRef.current);
const isPianoRoll = activeSub && activeSub.type === 'PIANO_ROLL';
if (!isPianoRoll && (isPlaying || _anySubPlaying) && masterBus && masterBus.analyser && activeSourcesRef.current.length > 0) {
try {
// "Đáng l đang có âm" quyết đnh watchdog có đưc rebuild không:
// - Main / sub-tab audio: source tht đang trong khong phát.
const ctxNow = getAudioContext().currentTime;
const anyPlaying = activeSourcesRef.current.some(s => typeof s.startTime === 'number' && ctxNow >= s.startTime && ctxNow <= s.startTime + (s.buffer ? s.buffer.duration : 0) + 0.1);
let anyPlaying = false;
if (_anySubPlaying) {
const _subs = subTabsRef.current || [];
for (let _si = 0; _si < _subs.length; _si++) {
const s = _subs[_si];
if (!s.isPlaying) continue;
if (s.buffer) {
if (activeSourcesRef.current.some(src => typeof src.startTime === 'number' && ctxNow >= src.startTime && ctxNow <= src.startTime + (src.buffer ? src.buffer.duration : 0) + 0.1)) { anyPlaying = true; break; }
}
}
} else {
anyPlaying = activeSourcesRef.current.some(s => typeof s.startTime === 'number' && ctxNow >= s.startTime && ctxNow <= s.startTime + (s.buffer ? s.buffer.duration : 0) + 0.1);
}
if (anyPlaying) {
const d = new Uint8Array(128);
masterBus.analyser.getByteTimeDomainData(d);
@@ -17672,9 +17769,25 @@ const App = () => {
stopAllPlayback();
Object.keys(activeTrackNodesRef.current).forEach(k => { const n = activeTrackNodesRef.current[k]; try { if (n && n.gainNode && n.gainNode.disconnect) n.gainNode.disconnect(); } catch (e) {} });
activeTrackNodesRef.current = {};
try { initMasterBus(); } catch (e) { console.warn('[Recovery] initMasterBus error:', e); }
setIsPlaying(true);
startTrackPlayback(pt);
try { initMasterBus(getAudioContext()); } catch (e) { console.warn('[Recovery] initMasterBus error:', e); }
// Resume ĐÚNG chế đ play hin ti (main hoc sub-tab piano roll)
const curTab = activeTabRef.current;
const subSt = subTabsRef.current.find(s => s.id === curTab);
if (subSt && (subSt.type === 'PIANO_ROLL' || subSt.buffer)) {
const resumeAt = subSt.currentTime || 0;
if (subSt.type === 'PIANO_ROLL') schedulePianoRollMidi(subSt, resumeAt);
startSubTabPlayback(subSt, resumeAt);
setSubTabs(prev => prev.map(s => s.id === curTab ? {
...s,
buffer: s.buffer || getAudioContext().createBuffer(1, 128, getAudioContext().sampleRate),
isPlaying: true,
currentTime: resumeAt
} : s));
animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
} else {
setIsPlaying(true);
startTrackPlayback(pt);
}
} catch (e) { console.warn('[Recovery] rebuild error:', e); }
}
} else {
@@ -17762,6 +17875,7 @@ const App = () => {
stopAllPlayback();
setSubTabs(prev => prev.map(s => s.id === activeTabRef.current ? {
...s,
buffer: s.buffer || getAudioContext().createBuffer(1, 128, getAudioContext().sampleRate),
currentTime: start,
isPlaying: true
} : s));
@@ -17788,9 +17902,13 @@ const App = () => {
if (st.isLooping) {
setSubTabs(prev => prev.map(s => s.id === activeTabRef.current ? {
...s,
buffer: s.buffer || getAudioContext().createBuffer(1, 128, getAudioContext().sampleRate),
currentTime: 0,
isPlaying: true
} : s));
// Loop li: phi schedule notes MI (âm piano roll mt ln loop 2
// nếu ch chy silent buffer).
if (st.type === 'PIANO_ROLL') schedulePianoRollMidi(st, 0);
startSubTabPlayback(st, 0);
animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
} else {
@@ -18049,7 +18167,10 @@ const App = () => {
const nodeAudibleGain = computeTrackAudibleGain(trackList, track);
setTrackNodeGain(node, nodeAudibleGain);
if (node.sfEntry) {
try { node.sfEntry.gain.setTargetAtTime(nodeAudibleGain, context.currentTime, 0.02); } catch (e) {}
try {
const g = (typeof nodeAudibleGain === 'number' && isFinite(nodeAudibleGain) && !isNaN(nodeAudibleGain)) ? nodeAudibleGain : 1.0;
node.sfEntry.gain.setTargetAtTime(g, context.currentTime, 0.02);
} catch (e) {}
}
activeTrackNodesRef.current[track.id] = node;
console.log('[Bypass] node created track', track.id, 'initial audioBypass=', !!trackAudioBypassMap[track.id], 'routeGain=', node.route.routeGain.gain.value, 'dryGain=', node.route.dryGain.gain.value);
@@ -18113,6 +18234,25 @@ const App = () => {
const updateSfRouting = () => {
try {
const list = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : tracks;
// PIANO_ROLL tab đang active ưu tiên route FluidSynth ti NODE CA
// TRACK đang edit (sfEntry FX Rack riêng, fallback gainNode) đ fader/
// pan/FX áp đúng cho notes đang nghe k c khi project có >1 track MIDI
// audible (bình thưng rơi v masterBus.input, mt FX track).
const _activeTabId = activeTabRef.current;
const _activeSub = subTabsRef.current ? subTabsRef.current.find(s => s.id === _activeTabId) : null;
if (_activeSub && _activeSub.type === 'PIANO_ROLL') {
const _prNode = activeTrackNodesRef.current[_activeSub.trackId];
if (_prNode && window.SonicSF && window.SonicSF.setOutputDestination) {
if (_prNode.sfEntry) {
window.SonicSF.setOutputDestination(_prNode.sfEntry);
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);
if (midiAudible.length === 1) {
const t = midiAudible[0];
@@ -18579,12 +18719,20 @@ const App = () => {
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);
const instrumentProgram = track ? track.instrumentProgram : undefined;
const synthEngine = track ? track.synth_engine : undefined;
// Instrument context resolve t TRACK live (ngun duy nht) track "đã
// loaded instrument" (instrumentProgram GM hoc synth_engine soundfont)
// phi chơi ĐÚNG instrument đó. ensureSonicInstrument select channel đúng
// trưc khi notes bn (fire-and-forget playNote t load+retry nếu SF
// chưa xong).
const instCtx = resolveTrackInstrumentCtx(track, activeTracksRef.current || []);
const instrumentProgram = instCtx.program;
const synthEngine = instCtx.synthEngine;
const mainCh = instCtx.ch;
var allTracks = activeTracksRef.current || [];
var mainCh = track ? assignTrackMidiChannel(track, allTracks) : 0;
// The piano-roll playhead (st.currentTime) and transport are item-relative:
ensureSonicInstrument(instCtx);
console.log('[Play] PianoRoll schedule:', st.id, '| notes=', midiNotes.length, '| track=', st.trackId, '| program=', instrumentProgram, '| sf=', instCtx.sfId ? instCtx.sfId : '-', '| dest=', destNode ? 'ok' : 'null', '| ch=', mainCh, '| sfEngine=', synthEngine ? 'yes' : 'no', '| bypassA=', !!trackAudioBypassMap[st.trackId], '| bypassMidi=', !!trackMidiBypassMap[st.trackId]);
// 0 = item start. Notes must be scheduled relative to the item too, otherwise
// items placed later in the project play `item.startTime` seconds in the
// future (silence when pressing play). Ghost notes are already relative to
@@ -18631,6 +18779,7 @@ const App = () => {
});
};
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);
@@ -18642,21 +18791,28 @@ const App = () => {
isPlaying: false
} : s));
} else {
stopAllPlayback();
const startOffset = st.currentTime || 0;
if (st.type === 'PIANO_ROLL') {
schedulePianoRollMidi(st, startOffset);
startSubTabPlayback(st, startOffset);
} else {
startSubTabPlayback(st, startOffset);
try {
stopAllPlayback();
const startOffset = st.currentTime || 0;
if (st.type === 'PIANO_ROLL') {
schedulePianoRollMidi(st, startOffset);
startSubTabPlayback(st, startOffset);
} else {
startSubTabPlayback(st, startOffset);
}
setSubTabs(prev => prev.map(s => s.id === activeTab ? {
...s,
buffer: s.buffer || getAudioContext().createBuffer(1, 128, getAudioContext().sampleRate),
isPlaying: true,
currentTime: startOffset
} : s));
// BT BUC start rAF loop updatePlayhead nếu không, playhead sub-tab
// không di chuyn (và loop/stop không bao gi chy).
animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
} catch (e) {
console.error('[Play] Piano Roll play error:', e);
showToast('Lỗi phát Piano Roll: ' + e.message, 'error');
}
setSubTabs(prev => prev.map(s => s.id === activeTab ? {
...s,
isPlaying: true,
currentTime: startOffset
} : s));
}
return;
}
@@ -18716,6 +18872,7 @@ const App = () => {
if (isPlaying || subTabs.some(s => s.isPlaying)) stopAllPlayback();
};
const stopAllPlayback = () => {
try {
activeSourcesRef.current.forEach(src => {
try {
src.stop();
@@ -18723,12 +18880,17 @@ const App = () => {
});
activeSourcesRef.current = [];
Object.values(activeTrackNodesRef.current).forEach(n => {
if (n.fxStopFn) n.fxStopFn();
if (n.fxStopFn) { try { n.fxStopFn(); } catch (e) {} }
});
activeTrackNodesRef.current = {};
stopMidiCapture();
if (window.SonicSF) {
window.SonicSF.stopAll();
try { window.SonicSF.stopAll(); } catch (e) { console.warn('[Stop] stopAll error:', e); }
// Dng trit đ: noteoff tng note + hy scheduled note-on (hết âm stuck)
if (window.SonicSF.panic) { try { window.SonicSF.panic(); } catch (e) { console.warn('[Stop] panic error:', e); } }
}
} catch (e) {
console.warn('[Stop] stopAllPlayback error:', e);
}
setIsPlaying(false);
setSubTabs(prev => prev.map(s => ({
File diff suppressed because one or more lines are too long
+66 -11
View File
@@ -69,9 +69,12 @@
try {
if (_gainNode) {
_gainNode.disconnect();
_gainNode.connect(node || (window.masterBus ? window.masterBus.input : ((_audioCtx || window.__sharedAudioCtx).destination)));
const dest = node || (window.masterBus ? window.masterBus.input : ((_audioCtx || window.__sharedAudioCtx).destination));
_gainNode.connect(dest);
console.log('[SonicSF] setOutputDestination to:', node ? 'track node (sfEntry)' : 'masterBus.input');
} else {
_pendingOutputDestination = node || null;
console.log('[SonicSF] setOutputDestination pending:', node ? 'track node (sfEntry)' : 'null');
}
} catch (e) {
console.warn('[SonicSF] setOutputDestination error:', e);
@@ -269,6 +272,12 @@
_doLoadSoundFont: async function (sfId) {
try {
// KHÔNG unload SF cũ khi sfload SF mới: unload làm handle cũ
// thành rác trong khi channel state vẫn trỏ tới → program_select
// bị skip (progAlreadySet) → noteon trên handle đã unload →
// "Instrument not found ... substituted prog 0". Heap 256MB đủ
// cho vài SF (SGM + latin = 2 handle — log OK). SF cũ khi cần
// lại chỉ được sfload lại nếu map bị xóa (không xảy ra ở đây).
var cache = window.SonicSFStorage;
var buf = cache ? await cache.getBuffer(sfId) : null;
if (buf) {
@@ -449,10 +458,11 @@
},
_playNoteFluid: function (note, velocity, durationMs, startTime, program, channel, synthEngine) {
var midiPitch = Math.min(127, Math.max(0, parseInt(note) || 60));
var midiVel = Math.min(127, Math.max(1, Math.floor(
typeof velocity === 'number' ? (velocity > 1 ? velocity : velocity * 127) : 100
)));
var parsedPitch = parseInt(note);
var midiPitch = isNaN(parsedPitch) ? 60 : Math.min(127, Math.max(0, parsedPitch));
var rawVel = (typeof velocity === 'number' && isFinite(velocity)) ? (velocity > 1 ? velocity : velocity * 127) : 100;
if (isNaN(rawVel)) rawVel = 100;
var midiVel = Math.min(127, Math.max(1, Math.floor(rawVel)));
var _origChannel = channel;
var usedBank = 0, usedProg = 0;
if (synthEngine) {
@@ -485,8 +495,10 @@
var self = this;
var doNote = function () {
try {
var finalBank = usedBank;
var finalProg = usedProg;
var finalBank = parseInt(usedBank);
if (isNaN(finalBank) || !isFinite(finalBank)) finalBank = 0;
var finalProg = parseInt(usedProg);
if (isNaN(finalProg) || !isFinite(finalProg)) finalProg = 0;
var finalSfId = synthEngine ? synthEngine.soundfont_id : undefined;
var cachedCh = _channels[ch];
// The note's own synth engine (track instrument) is
@@ -495,15 +507,17 @@
// re-picked instrument plays the wrong soundfont. Without an
// engine, fall back to the soundfont configured on the channel.
if (!synthEngine && cachedCh && cachedCh.sfId !== undefined) {
finalBank = cachedCh.bank;
finalProg = cachedCh.program;
finalBank = parseInt(cachedCh.bank) || 0;
finalProg = parseInt(cachedCh.program) || 0;
finalSfId = cachedCh.sfId;
}
// Ensure the soundfont is actually loaded before the note plays.
// Quick instrument pick on a track does not pre-load it, so load
// lazily here and retry the note once the font is ready.
if (finalSfId && !_sfHandleMap.has(finalSfId)) {
console.log('[SonicSF] soundfont not loaded yet, loading:', finalSfId);
self.loadSoundFont(finalSfId).then(function (ok) {
console.log('[SonicSF] loadSoundFont result:', ok, 'for:', finalSfId);
if (ok) doNote();
});
return;
@@ -512,9 +526,13 @@
// instrument for each item regardless of processing order.
// Skip if the channel already has this exact instrument (avoids
// per-note soundfont reloads that cause audible crackle/glitches).
var progAlreadySet = cachedCh && cachedCh.program === finalProg && cachedCh.bank === finalBank && cachedCh.sfId === finalSfId;
// ⚠️ Chỉ skip khi handle SF vẫn CÒN HỢP LỆ trong map — nếu
// không → vẫn program_select lại (tránh dùng handle đã unload).
var progAlreadySet = cachedCh && cachedCh.program === finalProg && cachedCh.bank === finalBank && cachedCh.sfId === finalSfId
&& (finalSfId ? _sfHandleMap.has(finalSfId) : true);
if ((synthEngine || program !== undefined) && !progAlreadySet) {
var sfHandle = finalSfId ? _sfHandleMap.get(finalSfId) : undefined;
console.log('[SonicSF] selectProgram for channel:', ch, 'sfHandle:', sfHandle, 'bank:', finalBank, 'prog:', finalProg);
if (sfHandle !== undefined) {
try {
_fluidModule._fluid_synth_program_select(_synthPtr, ch, sfHandle, finalBank, finalProg);
@@ -528,6 +546,7 @@
_channels[ch].program = finalProg;
_channels[ch].sfId = finalSfId;
}
console.log('[SonicSF] noteon channel:', ch, 'pitch:', midiPitch, 'vel:', midiVel, 'sfId:', finalSfId);
_fluidModule._fluid_synth_noteon(_synthPtr, ch, midiPitch, midiVel);
var noteMapKey = (_origChannel !== undefined ? _origChannel : 0) + ':' + midiPitch;
if (!_activeNotes[noteMapKey]) _activeNotes[noteMapKey] = [];
@@ -558,6 +577,31 @@
}
},
// Hủy mọi note-on được schedule (tương lai) + note-off mọi notes đang
// ngân — gọi khi STOP/PAUSE để hết "âm thanh bị stuck" (note-on chưa
// bắn vẫn bắn sau khi dừng; notes durationMs>=60000 không có note-off
// tự động → ngân vô hạn → VU master nhảy dù không play).
panic: function () {
_scheduledNotes.forEach(function (sn) { if (sn.on) { clearTimeout(sn.on); sn.on = null; } });
_scheduledNotes = [];
if (_initialized && _fluidModule) {
// noteoff TỪNG note đang ngân (binding _fluid_synth_noteoff chắc
// chắn tồn tại — đã dùng cho duration hết) — all_notes_off có
// thể không có trong WASM exports (catch nuốt → notes kẹt).
Object.keys(_activeNotes).forEach(function (key) {
var parts = key.split(':');
var pitch = parseInt(parts[1], 10);
(_activeNotes[key] || []).forEach(function (ch) {
try { _fluidModule._fluid_synth_noteoff(_synthPtr, ch, pitch); } catch (e) {}
});
});
try {
for (var c = 0; c < 16; c++) _fluidModule._fluid_synth_all_notes_off(_synthPtr, c);
} catch (e) {}
}
_activeNotes = {};
},
_playNoteFallback: function (note, velocity, durationMs, startTime, program, destinationNode, channel, synthEngine) {
var ctx = getCtx();
var freq = 440 * Math.pow(2, (note - 69) / 12);
@@ -592,7 +636,8 @@
osc.frequency.setValueAtTime(freq, 0);
var startAt = startTime !== undefined ? startTime : ctx.currentTime;
var durSec = durationMs / 1000;
var vel = typeof velocity === 'number' ? (velocity > 1 ? velocity / 127 : velocity) : 0.8;
var vel = (typeof velocity === 'number' && isFinite(velocity) && !isNaN(velocity)) ? (velocity > 1 ? velocity / 127 : velocity) : 0.8;
if (isNaN(vel)) vel = 0.8;
var targetGain = vel * volFactor;
noteGain.gain.setValueAtTime(0, startAt);
noteGain.gain.linearRampToValueAtTime(targetGain, startAt + attackTime);
@@ -613,6 +658,15 @@
stopAll: function () {
if (_initialized && _fluidModule) {
// noteoff từng note đang ngân (binding chắc chắn tồn tại) —
// phòng all_notes_off không có trong WASM exports.
Object.keys(_activeNotes).forEach(function (key) {
var parts = key.split(':');
var pitch = parseInt(parts[1], 10);
(_activeNotes[key] || []).forEach(function (ch) {
try { _fluidModule._fluid_synth_noteoff(_synthPtr, ch, pitch); } catch (e) {}
});
});
for (var ch = 0; ch < 16; ch++) {
try { _fluidModule._fluid_synth_all_notes_off(_synthPtr, ch); } catch (e) {}
}
@@ -631,6 +685,7 @@
} catch (e) {}
});
Object.keys(_activeOscillators).forEach(function (k) { delete _activeOscillators[k]; });
_activeNotes = {};
},
saveToIndexedDB: async function (name, arrayBuffer) {
+2 -2
View File
@@ -16,7 +16,7 @@
<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=202608031400"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202608042158"></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>
@@ -24,7 +24,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=202608038600" defer></script>
<script src="/static/js/app.precompiled.js?v=202608042200" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {