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:
+247
-85
@@ -42,6 +42,48 @@ const assignTrackMidiChannel = (track, tracks) => {
|
||||
return ch;
|
||||
};
|
||||
|
||||
// ── Instrument context (PIANO ROLL play — nguồn duy nhất, mọi nơi dùng) ──
|
||||
// Track "đã loaded instrument" = track.instrumentProgram (GM preset) HOẶC
|
||||
// track.synth_engine (soundfont: soundfont_id/bank/program). Resolve thành
|
||||
// context thống nhất cho schedulePianoRollMidi + preview (wheel/click/keybed)
|
||||
// để note PHẢI chơi đúng instrument của track — không phụ thuộc 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 bảo FluidSynth channel của track đã select ĐÚNG instrument trước khi
|
||||
// notes bắn. Fire-and-forget: playNote tự load + retry nếu SF chưa load xong
|
||||
// (dedup sẵn trong loadSoundFont) — không chặn, 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 → mặc đị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ả nền): sfload SF mới bất đồng bộ làm
|
||||
// WASM heap 256MB đầy → FluidSynth stall → CÂM TOÀN CỤC (mọi âm thanh
|
||||
// chết sau khi mở tab). playNote TỰ load + retry đúng lúc note cần.
|
||||
};
|
||||
|
||||
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 thật trong ref (SF notes đã schedule tới nó —
|
||||
// ghi đè bằng silent node làm mất 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 KHOẢNG PHÁT
|
||||
// (theo lịch) nhưng master output im lặng liên tục ~750ms → graph bị hỏng
|
||||
// (BiquadFilter "state is bad" — node cache dính) → rebuild nodes.
|
||||
// Đoạn im lặng TỰ NHIÊN (intro/rest — mọi source nằm ngoài khoảng phát)
|
||||
// KHÔNG trigger rebuild (cooldown 3s phòng trigger lặp).
|
||||
if (isPlaying && activeTabRef.current === 'main' && masterBus && masterBus.analyser && activeSourcesRef.current.length > 0) {
|
||||
// Áp cho main + sub-tab AUDIO. PIANO_ROLL tab KHÔNG có nguồn liên tục
|
||||
// (chỉ notes MIDI schedule rời rạc) → rest/im >750ms là TỰ NHIÊN, watchdog
|
||||
// sẽ rebuild loop + panic hủy notes chờ → CÂM TOÀN CỤC → exempt hoàn toàn
|
||||
// (các fix setValueAtTime/NaN guard đã hết "state is bad" — watchdog chỉ
|
||||
// còn là lớp cứu cuối 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 thật đang trong khoảng 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 hiện tại (main hoặc 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 lại: phải schedule notes MỚI (âm piano roll mất ở lần loop 2
|
||||
// nếu chỉ chạy 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 tới NODE CỦA
|
||||
// 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, mất 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 (nguồn duy nhất) — track "đã
|
||||
// loaded instrument" (instrumentProgram GM hoặc synth_engine soundfont)
|
||||
// phải chơi ĐÚNG instrument đó. ensureSonicInstrument select channel đúng
|
||||
// trước khi notes bắn (fire-and-forget — playNote tự load+retry nếu SF
|
||||
// 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));
|
||||
// BẮT BUỘC start rAF loop updatePlayhead — nếu không, playhead sub-tab
|
||||
// không di chuyển (và loop/stop không bao giờ chạy).
|
||||
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); }
|
||||
// Dừng triệt để: noteoff từng note + hủy 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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user