7 Commits

5 changed files with 455 additions and 89 deletions
+197 -46
View File
@@ -112,6 +112,20 @@ const trackMasteringBypassMap = {};
const trackAudioBypassMap = {};
const trackMidiBypassMap = {};
// Mastering chain ON? (masterConnected && !isBypassed)
const masteringChainOn = () => !!(window.currentMasteringSettings && window.currentMasteringSettings.masterConnected && !window.currentMasteringSettings.isBypassed);
// bypass hiu lc CH khi mastering chain TT khi chain ON, MI track
// (solo/preview/play) PHI đi qua mastering chain (user requirement: âm phi
// qua chain đ đ ln). Chain OFF theo maps như cũ.
const effMidiBypass = (track) => {
if (masteringChainOn()) return false;
return trackMidiBypassMap[track.id] !== undefined ? !!trackMidiBypassMap[track.id] : !!(track.midiBypass ?? track.masteringBypass);
};
const effAudioBypass = (track) => {
if (masteringChainOn()) return false;
return trackAudioBypassMap[track.id] !== undefined ? !!trackAudioBypassMap[track.id] : !!(track.audioBypass ?? track.masteringBypass);
};
// Build the dual routing for one track: routeGain -> mastering chain (normal),
// dryGain -> dry bus (bypass). Gains start at complementary 1/0 values.
function createMasteringRoute(ctx, track, bus) {
@@ -121,9 +135,11 @@ function createMasteringRoute(ctx, track, bus) {
let bypass = false;
if (track && track.id && trackAudioBypassMap[track.id] !== undefined) {
bypass = !!trackAudioBypassMap[track.id];
} else {
bypass = !!(track && (track.audioBypass ?? track.masteringBypass));
} else if (track) {
bypass = !!(track.audioBypass ?? track.masteringBypass);
}
// Mastering chain ON MI track qua chain ( b override user requirement)
if (masteringChainOn()) bypass = false;
const routeGain = ctx.createGain();
const dryGain = ctx.createGain();
const masterDest = bus ? bus.input : ctx.destination;
@@ -336,9 +352,9 @@ function applyMasteringSettings(s) {
const upwardGainLinear = (s.maximizerActive && s.maxUpward > 0) ? (Math.pow(10, clamp(s.maxUpward, 0, 30) / 20) - 1.0) : 0.0;
masterBus.upwardGain.gain.setTargetAtTime(upwardGainLinear, now, 0.01);
// Limiter Threshold
// Limiter Threshold (WaveShaper ceiling _setCeiling rebuild curve)
const ceilingVal = s.maximizerActive ? clamp(s.ceiling, -60, 0) : -0.1;
masterBus.maximizerCompressor.threshold.setTargetAtTime(ceilingVal, now, 0.01);
if (masterBus.maximizerCompressor._setCeiling) masterBus.maximizerCompressor._setCeiling(ceilingVal);
// 4. Bus Compressor module (mastering_expand.md §II.2)
if (masterBus.compNode) {
@@ -348,11 +364,14 @@ function applyMasteringSettings(s) {
masterBus.compMakeup.gain.setTargetAtTime(compOn ? Math.pow(10, clamp(s.compMakeup, 0, 12) / 20) : 1.0, now, 0.02);
}
// 5. Brickwall Limiter module (ratio 20:1, knee 0)
// 5. Brickwall Limiter module (WaveShaper tanh threshold = mc clip; OFF = identity)
if (masterBus.limNode) {
const limOn = !!s.limActive;
masterBus.limNode.threshold.setTargetAtTime(limOn ? clamp(s.limThreshold, -24, 0) : 0, now, 0.02);
masterBus.limNode.ratio.setTargetAtTime(limOn ? 20 : 1, now, 0.02);
if (limOn) {
if (masterBus.limNode._setThreshold) masterBus.limNode._setThreshold(clamp(s.limThreshold, -24, 0));
} else {
try { masterBus.limNode.curve = new Float32Array([-1, 1]); } catch (e) {}
}
}
// 6. Harmonic Exciter module (dry/wet mix; dry stays 1.0 for subtle warmth)
@@ -501,12 +520,21 @@ function initMasterBus(ctx) {
upwardCompressor.connect(upwardGain);
upwardGain.connect(upwardSummingGain);
const maximizerCompressor = ctx.createDynamicsCompressor();
maximizerCompressor.threshold.value = -0.1;
maximizerCompressor.knee.value = 0.0;
maximizerCompressor.ratio.value = 20.0;
maximizerCompressor.attack.value = 0.001;
maximizerCompressor.release.value = 0.05;
// Brickwall Limiter ti ceiling: WaveShaper HARD CLIP (slope 1 không boost,
// clip chính xác ti ceiling) KHÔNG DynamicsCompressor (NaN trên bass
// transient chain state-bad CÂM + stuck).
const maximizerCompressor = ctx.createWaveShaper();
maximizerCompressor.oversample = '2x';
let _maxCeil = -0.1;
const _buildMaxCurve = (db) => {
const c = Math.pow(10, Math.max(-60, Math.min(0, db)) / 20);
const _c = new Float32Array(4096);
for (let _i = 0; _i < 4096; _i++) { const _x = (_i / 4095) * 2 - 1; _c[_i] = Math.max(-c, Math.min(c, _x)); }
maximizerCompressor.curve = _c;
_maxCeil = db;
};
_buildMaxCurve(-0.1);
maximizerCompressor._setCeiling = (db) => { if (db !== _maxCeil) _buildMaxCurve(db); };
upwardSummingGain.connect(maximizerCompressor);
@@ -525,14 +553,23 @@ function initMasterBus(ctx) {
compNode.connect(compMakeup);
compMakeup.connect(compOutput);
// Brickwall Limiter module (ratio 20:1, knee 0)
// Brickwall Limiter module (WaveShaper tanh soft-clip KHÔNG
// DynamicsCompressor: NaN trên bass transient chain stuck)
const limInput = ctx.createGain();
const limNode = ctx.createDynamicsCompressor();
limNode.threshold.value = -1.0;
limNode.knee.value = 0;
limNode.ratio.value = 20;
limNode.attack.value = 0.001;
limNode.release.value = 0.05;
const limNode = ctx.createWaveShaper();
limNode.oversample = '2x';
let _limLastThresh = null;
const _buildLimCurve = (db) => {
const tLin = Math.pow(10, Math.max(-24, Math.min(0, db)) / 20);
const k = 1 / Math.max(0.02, tLin);
const _c = new Float32Array(4096);
const _tk = Math.tanh(k);
for (let _i = 0; _i < 4096; _i++) { const _x = (_i / 4095) * 2 - 1; _c[_i] = Math.tanh(_x * k) / _tk; }
limNode.curve = _c;
_limLastThresh = db;
};
_buildLimCurve(-1.0);
limNode._setThreshold = (db) => { if (db !== _limLastThresh) _buildLimCurve(db); };
const limOutput = ctx.createGain();
limInput.connect(limNode);
limNode.connect(limOutput);
@@ -643,10 +680,12 @@ function initMasterBus(ctx) {
eqMid1Filter.connect(eqMid2Filter);
eqMid2Filter.connect(eqHighFilter);
// Setup default non-mastered routing:
// input -> compressor -> inputAnalyser -> outputAnalyser -> output -> analyser -> destination
masterBus.input.connect(masterBus.compressor);
masterBus.compressor.connect(masterBus.inputAnalyser);
// Setup default non-mastered routing (KHÔNG compressor mc đnh trong path):
// input -> inputAnalyser -> outputAnalyser -> output -> analyser -> destination
// Compressor mc đnh (ratio 12, threshold -24 LUÔN-ON) va (a) pump-down
// tín hiu âm nh/méo, va (b) phát NaN khi gp bass transient 11 biquad
// "state is bad" CÂM + stuck. Mastering chain có comp/lim module riêng khi bt.
masterBus.input.connect(masterBus.inputAnalyser);
masterBus.inputAnalyser.connect(masterBus.outputAnalyser);
masterBus.outputAnalyser.connect(masterBus.output);
masterBus.output.connect(masterBus.analyser);
@@ -657,6 +696,11 @@ function initMasterBus(ctx) {
// masteringSettings effect for subsequent changes see useEffect).
if (window.currentMasteringSettings) {
try {
// Reset sig-cache: chain MI (biquad/maximizer node mi) gi giá tr
// INIT (gain 0, width 100) applyMasteringSettings early-return vì
// _lastMasteringSig không đi (module-level, persist qua recreation)
// chain FLAT ("spectrum hin th nhưng không x lí âm thanh").
_lastMasteringSig = null;
toggleMasteringOnMaster(window.currentMasteringSettings.masterConnected, window.currentMasteringSettings.isBypassed);
applyMasteringSettings(window.currentMasteringSettings);
} catch (e) {
@@ -867,12 +911,25 @@ function createTrackFxModule(type, ctx, params) {
input.connect(comp); comp.connect(makeup); makeup.connect(output);
nodes = { comp, makeup };
} else if (type === 'limiter') {
const lim = ctx.createDynamicsCompressor();
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 };
// Brickwall Limiter bng WaveShaper tanh soft-clip KHÔNG DynamicsCompressor:
// Chromium compressor phát NaN vi bass transient mnh (pitch thp + vel cao
// đng lot) NaN vào master chain 11 biquad "state is bad" CÂM + stuck.
const shaper = ctx.createWaveShaper();
shaper.oversample = '2x';
const ceilingDb = Math.min(0, num(p.ceiling, -1.0));
const threshLin = Math.pow(10, ceilingDb / 20);
const k = 1 / Math.max(0.02, threshLin);
const _curve = new Float32Array(4096);
const _tanhK = Math.tanh(k);
for (let _i = 0; _i < 4096; _i++) {
const _x = (_i / 4095) * 2 - 1;
_curve[_i] = Math.tanh(_x * k) / _tanhK;
}
shaper.curve = _curve;
const makeup = ctx.createGain();
makeup.gain.value = 1.0;
input.connect(shaper); shaper.connect(makeup); makeup.connect(output);
nodes = { shaper, makeup };
} else if (type === 'exciter') {
const hp = ctx.createBiquadFilter();
hp.type = 'highpass'; hp.frequency.value = clampF(2000); hp.Q.value = 0.7;
@@ -971,7 +1028,7 @@ function buildOfflineTrackNode(track, ctx, nodeMap) {
sfOut.connect(sfPan);
const sfRouteGain = ctx.createGain();
const sfDryGain = ctx.createGain();
const sfBypass = trackMidiBypassMap[track.id] !== undefined ? !!trackMidiBypassMap[track.id] : !!(track.midiBypass ?? track.masteringBypass);
const sfBypass = effMidiBypass(track);
sfRouteGain.gain.value = sfBypass ? 0 : 1;
sfDryGain.gain.value = sfBypass ? 1 : 0;
sfPan.connect(sfRouteGain);
@@ -7681,14 +7738,16 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
try { previewNodesRef.current.gain.disconnect(); } catch(e) {}
previewNodesRef.current = null;
}
if (window.SonicSF && window.SonicSF._playNoteFallback) {
if (window.SonicSF && window.SonicSF.playNote) {
const ctx = getAudioContext();
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));
var dwNodes = window.SonicSF._playNoteFallback(dwPitch, Math.round(brushVelocityRef.current * 127), dwDurMs, ctx.currentTime, dwTrk ? dwTrk.instrumentProgram : undefined, null, dwCh, dwTrk ? dwTrk.synth_engine : undefined);
if (dwNodes) previewNodesRef.current = dwNodes;
// playNote (FluidSynth nhc c THT ca track). _playNoteFallback ch
// là oscillator beep (sai âm vi percussion/soundfont user: note v
// mi nghe nhc c track trưc).
window.SonicSF.playNote(dwPitch, Math.round(brushVelocityRef.current * 127), dwDurMs, ctx.currentTime, dwTrk ? dwTrk.instrumentProgram : undefined, null, dwCh, dwTrk ? dwTrk.synth_engine : undefined);
}
}
};
@@ -7777,13 +7836,14 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
function playDrawPreview(p, durMs) {
stopPreviewNote();
if (window.SonicSF && window.SonicSF._playNoteFallback) {
if (window.SonicSF && window.SonicSF.playNote) {
var pvCtx = getAudioContext();
var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
var pvCtxInst = resolveTrackInstrumentCtx(pvTrk, activeTracks);
var pvVel = Math.round(brushVelocityRef.current * 127);
var pvNodes = window.SonicSF._playNoteFallback(p, pvVel, durMs, pvCtx.currentTime, pvCtxInst.program, null, pvCtxInst.ch, pvCtxInst.synthEngine);
if (pvNodes) previewNodesRef.current = pvNodes;
// playNote (FluidSynth nhc c THT). _playNoteFallback = oscillator
// beep sai âm (percussion/soundfont).
window.SonicSF.playNote(p, pvVel, durMs, pvCtx.currentTime, pvCtxInst.program, null, pvCtxInst.ch, pvCtxInst.synthEngine);
previewPitchRef.current = p;
}
}
@@ -14475,9 +14535,10 @@ const App = () => {
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) !== !trackMidiBypassMap[t.id]) {
sn.sfRouteGain.gain.value = trackMidiBypassMap[t.id] ? 0 : 1;
sn.sfDryGain.gain.value = trackMidiBypassMap[t.id] ? 1 : 0;
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;
@@ -14507,8 +14568,22 @@ const App = () => {
updateSfRouting();
}, [tracks, sessionTabs]);
const prevActiveTabRef = useRef(activeTab);
useEffect(() => {
const prevTab = prevActiveTabRef.current;
prevActiveTabRef.current = activeTab;
updateSfRouting();
// Tab DEACTIVE stop âm ca tab đó (user requirement): ri khi sub-tab
// (PIANO_ROLL/section/audio tab) đang play stop playback ca tab đó.
// Tab mi hot đng bình thưng; MAIN gi hành vi cũ (m piano-roll lúc
// main play main tiếp tc handleEditMidiInTab đã x lý).
if (prevTab !== activeTab) {
const prevSub = subTabsRef.current.find(s => s.id === prevTab);
if (prevSub && prevSub.isPlaying) {
try { stopAllPlayback(); } catch (e) {}
setSubTabs(prev => prev.map(s => s.id === prevTab ? { ...s, isPlaying: false } : s));
}
}
}, [activeTab]);
const [selectionCleared, setSelectionCleared] = useState(false); // LOOP_EDITOR_2.md §4.2
@@ -14586,6 +14661,28 @@ const App = () => {
if (audioCtx && masterBus) {
toggleMasteringOnMaster(masteringSettings.masterConnected, masteringSettings.isBypassed);
applyMasteringSettings(masteringSettings);
// Re-sync live track routes: mastering ON mi track qua chain ( b
// override bi effMidiBypass/effAudioBypass) nút PWR bt/tt phi áp
// ngay lên node đang phát (không ch tracks effect).
try {
const _list = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : tracks;
_list.forEach(_t => {
const _n = activeTrackNodesRef.current[_t.id];
if (_n) {
if (_n.sfRouteGain && _n.sfDryGain) {
const _b = effMidiBypass(_t);
_n.sfRouteGain.gain.value = _b ? 0 : 1;
_n.sfDryGain.gain.value = _b ? 1 : 0;
}
if (_n.route && _n.route.routeGain && _n.route.dryGain) {
const _ab = effAudioBypass(_t);
_n.route.routeGain.gain.value = _ab ? 0 : 1;
_n.route.dryGain.gain.value = _ab ? 1 : 0;
}
}
});
updateSfRouting();
} catch (e) {}
}
}, [masteringSettings]);
@@ -17734,16 +17831,22 @@ const App = () => {
// 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).
// PIANO_ROLL: watchdog CH rebuild khi output cha NaN (chain chết
// state-bad). KHÔNG rebuild khi im lng thưng (rests t nhiên gia các
// note false-positive = stopAllPlayback + reschedule = glitch).
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) {
if ((isPlaying || _anySubPlaying) && masterBus && masterBus.analyser && (isPianoRoll || 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.
// - PIANO_ROLL: tab đang play (notes đã schedule) = đáng l có âm.
const ctxNow = getAudioContext().currentTime;
let anyPlaying = false;
if (_anySubPlaying) {
if (isPianoRoll) {
anyPlaying = _anySubPlaying;
} else if (_anySubPlaying) {
const _subs = subTabsRef.current || [];
for (let _si = 0; _si < _subs.length; _si++) {
const s = _subs[_si];
@@ -17760,10 +17863,28 @@ const App = () => {
masterBus.analyser.getByteTimeDomainData(d);
let pk = 0;
for (let i = 0; i < d.length; i++) { const v = Math.abs(d[i] - 128) / 128; if (v > pk) pk = v; }
if (pk < 0.001) {
// PIANO_ROLL: chain chết xut NaN getByteTimeDomainData đc NaN
// byte 0/128 pk CAO mù. Check float data (ch piano roll rests
// t nhiên thì KHÔNG rebuild).
let nanOut = false;
if (isPianoRoll) {
try {
const f = new Float32Array(128);
masterBus.analyser.getFloatTimeDomainData(f);
for (let i = 0; i < f.length; i++) { if (!isFinite(f[i])) { nanOut = true; break; } }
} catch (e) {}
}
// PIANO_ROLL: CH rebuild khi output NaN (chain chết state-bad).
// pk<0.001 (im lng) KHÔNG trigger cho piano roll rests t nhiên
// gia các note > 750ms là BÌNH THƯNG false-positive = recovery
// hy play + restart notes (glitch) đúng chui log recovery trưc.
if ((isPianoRoll ? nanOut : (pk < 0.001 || nanOut))) {
masterSilenceFramesRef.current++;
const sinceRebuild = performance.now() - (lastMasterRebuildTimeRef.current || 0);
if (masterSilenceFramesRef.current > 45 && sinceRebuild > 3000) {
// NaN = chain CHT chc chn rebuild NGAY (3 frame 50ms).
// pk<0.001 (im lng nghi ng main) gi 45 frame (750ms) đ loi
// transient gap false-positive. Cooldown 3000 chng rebuild-loop.
if (masterSilenceFramesRef.current > (nanOut ? 3 : 45) && sinceRebuild > 3000) {
masterSilenceFramesRef.current = 0;
lastMasterRebuildTimeRef.current = performance.now();
console.warn('[Recovery] Master silent while sources active — rebuilding audio graph');
@@ -17772,7 +17893,18 @@ 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(getAudioContext()); } catch (e) { console.warn('[Recovery] initMasterBus error:', e); }
// Tháo chain cũ khi destination ri rebuild THT initMasterBus
// early-return khi masterBus còn tn ti recovery trưc đây là
// no-op chain "state is bad" b STUCK vĩnh vin.
try {
if (masterBus) {
try { if (masterBus.analyser) masterBus.analyser.disconnect(); } catch (e) {}
try { if (masterBus.output) masterBus.output.disconnect(); } catch (e) {}
try { if (masterBus.dryOutput) masterBus.dryOutput.disconnect(); } catch (e) {}
}
masterBus = null;
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);
@@ -18142,7 +18274,7 @@ const App = () => {
// button: bypass Mastering FX Chain for the soundfont ONLY (track FX
// Rack modules are still applied PWR controls those). sfRouteGain
// masterBus.input (mastering), sfDryGain dry bus (skip mastering).
const sfBypass = trackMidiBypassMap[track.id] !== undefined ? !!trackMidiBypassMap[track.id] : !!(track.midiBypass ?? track.masteringBypass);
const sfBypass = effMidiBypass(track);
sfRouteGain = context.createGain();
sfDryGain = context.createGain();
sfRouteGain.gain.value = sfBypass ? 0 : 1;
@@ -18248,6 +18380,14 @@ const App = () => {
if (_prNode && window.SonicSF && window.SonicSF.setOutputDestination) {
if (_prNode.sfEntry) {
window.SonicSF.setOutputDestination(_prNode.sfEntry);
// Ép route qua mastering chain NGAY ti thi đim routing (node có
// th to khi mastering OFF route dry sa ngay nếu chain ON).
if (_prNode.sfRouteGain && _prNode.sfDryGain) {
const _prTrk = list.find(t => t.id === _activeSub.trackId);
const _b = effMidiBypass(_prTrk || { id: _activeSub.trackId });
_prNode.sfRouteGain.gain.value = _b ? 0 : 1;
_prNode.sfDryGain.gain.value = _b ? 1 : 0;
}
return;
}
if (_prNode.gainNode) {
@@ -18265,6 +18405,12 @@ const App = () => {
// sfDryGain) to skip or include the Mastering FX Chain.
if (node && node.sfEntry && window.SonicSF && window.SonicSF.setOutputDestination) {
window.SonicSF.setOutputDestination(node.sfEntry);
// Ép route qua mastering chain NGAY ti thi đim routing.
if (node.sfRouteGain && node.sfDryGain) {
const _b = effMidiBypass(t.id);
node.sfRouteGain.gain.value = _b ? 0 : 1;
node.sfDryGain.gain.value = _b ? 1 : 0;
}
return;
}
if (node && node.gainNode && window.SonicSF && window.SonicSF.setOutputDestination) {
@@ -22911,6 +23057,11 @@ STRICT CONSTRAINTS:
...(srcTrack || {}),
id: newTrackId,
name: '[AI Var] ' + title,
// KHÔNG kế tha midiChannel ca track gc: nếu dùng chung channel,
// solo/mute track gc gi CC7=0 trên channel đó clone (cùng
// channel) b NH/CÂM. ensureTrackMidiChannel/assignTrackMidiChannel
// s cp channel RIÊNG cho track mi.
midiChannel: undefined,
buffer: null,
startTime: 0,
clips: [],
File diff suppressed because one or more lines are too long
+69 -10
View File
@@ -23,6 +23,8 @@
let _activeOscillators = {};
let _gainNode = null;
let _pendingOutputDestination = null;
let _outputDestination = null; // cache đích route — dedupe swap dư giữa stream
let _validPercCache = {}; // { sfId: [bank, prog] | null } — preset percussion hợp lệ
let _scheduledNotes = [];
let _loadPromises = {};
let _sfloadSeq = 0;
@@ -32,7 +34,8 @@
if (!_gainNode) {
_gainNode = _audioCtx.createGain();
_gainNode.gain.value = 0.3;
_gainNode.connect(_pendingOutputDestination || (window.masterBus ? window.masterBus.input : _audioCtx.destination));
_outputDestination = _pendingOutputDestination || (window.masterBus ? window.masterBus.input : _audioCtx.destination);
_gainNode.connect(_outputDestination);
}
return _audioCtx;
}
@@ -41,7 +44,8 @@
if (!_gainNode) {
_gainNode = ctx.createGain();
_gainNode.gain.value = 0.3;
_gainNode.connect(_pendingOutputDestination || (window.masterBus ? window.masterBus.input : ctx.destination));
_outputDestination = _pendingOutputDestination || (window.masterBus ? window.masterBus.input : ctx.destination);
_gainNode.connect(_outputDestination);
}
return ctx;
}
@@ -54,7 +58,8 @@
if (!_gainNode) {
_gainNode = window.__sharedAudioCtx.createGain();
_gainNode.gain.value = 0.3;
_gainNode.connect(_pendingOutputDestination || window.__sharedAudioCtx.destination);
_outputDestination = _pendingOutputDestination || window.__sharedAudioCtx.destination;
_gainNode.connect(_outputDestination);
}
return window.__sharedAudioCtx;
};
@@ -68,9 +73,17 @@
setOutputDestination: function (node) {
try {
if (_gainNode) {
_gainNode.disconnect();
const dest = node || (window.masterBus ? window.masterBus.input : ((_audioCtx || window.__sharedAudioCtx).destination));
// DEDUPE: đích không đổi → KHÔNG disconnect/reconnect.
// Swap dư giữa dòng notes đang phát (applyAllTrackMuteSolo →
// updateSfRouting gọi lại cùng đích sfEntry sau noteon đầu)
// làm ScriptProcessor xuất buffer uninitialized → NaN →
// 11 biquad "state is bad" → CÂM (mọi log: state-bad nổ
// ngay sau setOutputDestination lần 2).
if (dest === _outputDestination) return;
_gainNode.disconnect();
_gainNode.connect(dest);
_outputDestination = dest;
console.log('[SonicSF] setOutputDestination to:', node ? 'track node (sfEntry)' : 'masterBus.input');
} else {
_pendingOutputDestination = node || null;
@@ -104,7 +117,8 @@
if (!_gainNode) {
_gainNode = _audioCtx.createGain();
_gainNode.gain.value = 0.3;
_gainNode.connect(_pendingOutputDestination || (window.masterBus ? window.masterBus.input : _audioCtx.destination));
_outputDestination = _pendingOutputDestination || (window.masterBus ? window.masterBus.input : _audioCtx.destination);
_gainNode.connect(_outputDestination);
}
console.log("[SonicSF] AudioCtx state:", _audioCtx.state, "sampleRate:", _audioCtx.sampleRate);
@@ -140,7 +154,7 @@
// expected notice when a soundfont simply has no
// preset for a bank (e.g. bank 128 on a melodic-only
// font) — the note is just silent, not an error.
if (msg && msg.indexOf('No preset found on channel') !== -1) return;
if (msg && (msg.indexOf('No preset found on channel') !== -1 || msg.indexOf('There is no preset with bank number') !== -1)) return;
console.warn('[FluidSynth:err]', msg);
}
});
@@ -191,17 +205,31 @@
var spn = _audioCtx.createScriptProcessor(spBufSz, 0, 2);
var lp = _fluidModule._malloc(spBufSz * 4);
var rp = _fluidModule._malloc(spBufSz * 4);
// Heap WASM có thể realloc khi load SoundFont lớn (SGM-V2.01
// ~300MB) → lp/rp DANGLE → đọc vùng nhớ đã free → NaN/garbage
// → master chain "state is bad" → CÂM + stuck. Theo dõi
// buffer + re-malloc khi đổi.
var _heapBufRef = _fluidModule.HEAPU8.buffer;
spn.onaudioprocess = function (e) {
var left = e.outputBuffer.getChannelData(0);
var right = e.outputBuffer.getChannelData(1);
var sz = left.length;
try {
if (_fluidModule.HEAPU8.buffer !== _heapBufRef) {
try { _fluidModule._free(lp); _fluidModule._free(rp); } catch (er2) {}
lp = _fluidModule._malloc(sz * 4);
rp = _fluidModule._malloc(sz * 4);
_heapBufRef = _fluidModule.HEAPU8.buffer;
}
_fluidModule._fluid_synth_write_float(_synthPtr, sz, lp, 0, 1, rp, 0, 1);
var hf = _fluidModule.HEAPF32;
var lpb = lp >> 2, rpb = rp >> 2;
for (var si = 0; si < sz; si++) {
left[si] = hf[lpb + si];
right[si] = hf[rpb + si];
// NaN sweep: mẫu NaN/Inf → 0 (chain biquad
// KHÔNG BAO GIỜ được nhận NaN → không state-bad).
var L = hf[lpb + si], R = hf[rpb + si];
left[si] = isFinite(L) ? L : 0;
right[si] = isFinite(R) ? R : 0;
}
} catch (er) {}
};
@@ -535,7 +563,34 @@
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);
// Percussion (bank 128): tìm preset HỢP LỆ trong
// font — quét bank 128 + bank 0 (0-127) MỘT LẦN,
// cache theo sfId. Trước đây chỉ thử 4 preset cố
// định → font không có → cache channel = (128,0)
// INVALID → note sau skip re-select (progAlreadySet)
// → noteon preset rỗng = CÂM ("1 âm đầu rồi câm").
if (finalBank === 128) {
var _vKey = finalSfId || ('h' + sfHandle);
if (_validPercCache[_vKey] === undefined) {
var _found = null;
for (var _b = 0; _b < 2 && !_found; _b++) {
var _bk = _b === 0 ? 128 : 0;
for (var _p = 0; _p < 128 && !_found; _p++) {
try {
if (_fluidModule._fluid_synth_program_select(_synthPtr, 9, sfHandle, _bk, _p) === 0) {
_found = [_bk, _p];
}
} catch (e) {}
}
}
_validPercCache[_vKey] = _found;
}
if (_validPercCache[_vKey]) {
finalBank = _validPercCache[_vKey][0];
finalProg = _validPercCache[_vKey][1];
}
}
var _selRet = _fluidModule._fluid_synth_program_select(_synthPtr, ch, sfHandle, finalBank, finalProg);
} catch (e) {}
} else {
try { _fluidModule._fluid_synth_bank_select(_synthPtr, ch, finalBank); } catch (e) {}
@@ -618,7 +673,11 @@
var oscType = 'triangle';
var attackTime = 0.03, decayTime = 0.1, sustainLevel = 0.5, releaseTime = 0.2, volFactor = 0.25;
var prog = program !== undefined ? parseInt(program) : 0;
if (channel !== undefined && channel >= 0 && channel < 16) {
// CHỈ dùng cache channel khi KHÔNG có program/synthEngine được
// truyền — trước đây override program của track bằng cache channel
// (bị track khác cùng channel ghi đè → preview note vẽ mới mang
// nhạc cụ của track TRƯỚC).
if (program === undefined && channel !== undefined && channel >= 0 && channel < 16) {
prog = _channels[channel].program || prog;
}
if (prog >= 0 && prog <= 7) { oscType = 'sine'; decayTime = 0.3; sustainLevel = 0.1; releaseTime = 0.2; }
+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=202608042158"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202608060200"></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=202608042300" defer></script>
<script src="/static/js/app.precompiled.js?v=202608060430" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {
+101
View File
@@ -1854,3 +1854,104 @@
(2) **Chống NaN cho chương trình/bank FluidSynth**: Thêm bộ lọc `parseInt``isNaN` kiểm tra biến `usedBank``usedProg` bên trong hàm phát nốt `doNote` của `soundfontPlayer.js`. Tránh truyền giá trị `NaN` trực tiếp vào hàm WASM `_fluid_synth_program_select` có thể làm rối loạn bộ tổng hợp âm bên trong FluidSynth và kết xuất mẫu âm thanh NaN.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (build lại), `app/static/js/services/soundfontPlayer.js`, `app/templates/index.html`, `wiki.md`
- **Ghi chú/Test (nếu có):** `node build.mjs` thành công.
### [2026-08-05 22:30] Task: 2 fix quyết định trên baseline — NaN sweep synth (heap dangle) + watchdog piano-roll rebuild THẬT
- **Báo cáo user lặp lại (cùng text):** piano-roll play → BACK → play → CÂM, chain STUCK. Sau khi loại toàn bộ compressor (v22:00) mà vẫn lỗi → NaN KHÔNG từ compressor.
- **2 lỗ hổng còn lại (baseline cfc114b):**
(1) **Synth render KHÔNG sweep NaN + lp/rp dangle:** `_leftBufPtr/_rightBufPtr` malloc 1 lần ở init; load SGM-V2.01 (~300MB) → heap WASM realloc → pointer đọc vùng free → NaN → chain state-bad. FIX soundfontPlayer.js: theo dõi `HEAPU8.buffer` → re-malloc khi đổi + **NaN sweep (isFinite → 0)** ở mọi block — synth output KHÔNG BAO GIỜ chứa NaN.
(2) **Watchdog recovery là NO-OP:** `initMasterBus` early-return khi masterBus còn tồn tại → [Recovery] không rebuild gì → chain chết STUCK vĩnh viễn. FIX: teardown (disconnect analyser/output/dryOutput + masterBus=null) TRƯỚC initMasterBus → rebuild biquad THẬT + reschedule.
(3) **Watchdog mở cho PIANO_ROLL:** trước đây `!isPianoRoll` (exempt hoàn toàn). Giờ: piano-roll CHỈ rebuild khi output NaN (getFloatTimeDomainData + isFinite — rests tự nhiên KHÔNG trigger, tránh false-positive).
- **Các file ảnh hưởng:** soundfontPlayer.js, app.jsx, index.html (?v=202608052230 cho cả 2), wiki.md. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → play → BACK → play từ đầu → KỲ VỌNG: CÓ ÂM (sweep chặn NaN tại nguồn + watchdog cứu nếu còn chết).
### [2026-08-05 23:30] Task: Trên commit user 55d3464 — áp lại fix compressor (default bypass + tanh/hard-clip limiters) — trị "âm nhỏ + chain chết"
- **User tự commit 55d3464 "FIX: Piano roll tab không xuất âm thanh qua mastering chain"** = cfc114b + giữ watchdog piano-roll + giữ NaN sweep + re-malloc của agent — NHƯNG compressor gốc VẪN CÒN.
- **Báo cáo user:** recovery có âm nhưng KHÔNG qua mastering → rất nhỏ.
- **2 lý do:**
(1) **Compressor mặc định (threshold -24, ratio 12, LUÔN-ON) vẫn trong path** (`input → compressor → inputAnalyser`) — pump-down tín hiệu → âm nhỏ + méo; và phát NaN trên bass transient → state-bad → chain chết.
(2) maximizerCompressor + limNode + track limiter vẫn là DynamicsCompressor — nguồn NaN.
- **Áp lại (trên HEAD 55d3464):** bypass compressor mặc định; maximizerCompressor → WaveShaper HARD CLIP tại ceiling (slope 1 — không boost); mastering limNode → tanh soft-clip; track 'limiter' → tanh; applyMasteringSettings adapt (_setCeiling/_setThreshold, OFF → identity).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608052330), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → play → BACK → play từ đầu → KỲ VỌNG: CÓ ÂM, âm lượng đủ (hết compressor pump-down), qua mastering.
### [2026-08-05 23:50] Task: Watchdog piano-roll FALSE POSITIVE — recovery trên rests tự nhiên (pk trigger) → NaN-ONLY
- **Log v2330 (compressor fix ĐÃ chạy):** recovery fire → rebuild → applyMasteringSettings → node mới → sfEntry → **noteon 23 notes từ 0:0 (bass 56/49/37 vel 127) — KHÔNG state-bad!!** ⇒ compressor fix HOẠT ĐỘNG (bass không còn NaN → chain không chết).
- **NHƯNG [Recovery] vẫn fire** — lỗi watchdog của agent: điều kiện `pk < 0.001 || (isPianoRoll && nanOut)`**piano roll vẫn trigger bởi pk<0.001** — rest tự nhiên > 750ms trong pattern = FALSE POSITIVE → recovery hủy play + restart notes (glitch + "âm nhỏ" do restart mất bối cảnh).
- **FIX:** `(isPianoRoll ? nanOut : (pk < 0.001 || nanOut))` — piano roll CHỈ rebuild khi output NaN (chain chết). Main giữ nguyên (pk || nanOut).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608052350), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → play piano roll từ 0:0 → KỲ VỌNG: CÓ ÂM liên tục, KHÔNG [Recovery] (trừ khi chain thực sự chết — NaN), không restart glitch.
### [2026-08-06 00:00] Task: TRIGGER THẬT cuối — routing swap DƯ giữa stream (dedupe setOutputDestination) + state-bad trên mastering PWR
- **Log v2350:** state-bad TRỞ LẠI — nổ NGAY SAU `setOutputDestination to: track node (sfEntry)` LẦN 2 (sau 6 noteon đầu — từ applyAllTrackMuteSolo cuối startSubTabPlayback) — dù compressor đã hết (bass play sạch ở các log khác). ⇒ **Graph mutation (disconnect/reconnect _gainNode GIỮA stream) làm ScriptProcessor xuất buffer uninitialized → NaN → 11 biquad sụp.** Compressor vô can. Khớp mọi log từ đầu: state-bad LUÔN sau swap lần 2; play 0:1 (swap xong trước notes) không nổ.
- **FIX (soundfontPlayer.js):** dedupe `setOutputDestination` — cache `_outputDestination` (5 chỗ khởi tạo) + `if (dest === _outputDestination) return;` — swap dư (sfEntry→sfEntry) bị loại; swap thật (masterBus.input→sfEntry — TRƯỚC notes) giữ nguyên.
- **Các file ảnh hưởng:** soundfontPlayer.js (?v=202608052355 — chỉ hard refresh, không cần build precompiled), wiki.md.
- **Ghi chú/Test:** hard refresh → bật mastering PWR → play piano roll từ 0:0 → KỲ VỌNG: KHÔNG state-bad, âm qua mastering (EQ/imager/maximizer nghe rõ), không recovery giả.
### [2026-08-06 00:00] Task: Chain FLAT sau recreation — stale _lastMasteringSig cache (spectrum hiển thị nhưng không xử lí)
- **Báo cáo user:** mastering chain spectrum hiển thị trong các module NHƯNG âm thanh không được xử lí (không tăng gain, không thay đổi).
- **Cơ chế:** `applyMasteringSettings` early-return qua `_lastMasteringSig` (module-level, PERSIST qua recreation masterBus). Sau recovery (watchdog rebuild: masterBus=null → initMasterBus) chain MỚI giữ giá trị INIT (EQ gain 0, imager width 100, maximizer boost 0) → **FLAT** — tín hiệu chảy qua modules (spectrum hiển thị) nhưng output = input. Cũng giải thích "âm rất nhỏ" sau recovery ở vòng trước (mất maximizer gain 5.4dB + EQ).
- **FIX (app.jsx initMasterBus):** `_lastMasteringSig = null;` trước `applyMasteringSettings` — chain mới được cấu hình lại đầy đủ.
- **Kết hợp với v23:55 (dedupe swap):** dedupe ngăn state-bad (không recovery → không flat); sig-reset đảm bảo recovery (nếu có) cấu hình lại chain — 2 fix bổ trợ.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608060000), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → bật mastering PWR → play piano roll 0:0 → KỲ VỌNG: âm qua mastering ĐẦY ĐỦ (EQ gain nghe rõ, maximizer boost, imager width) — chỉnh slider module → âm thay đổi ngay.
### [2026-08-06 00:30] Task: Recovery NGAY khi NaN — fast-path 3 frame (~50ms) thay vì 45 frame (750ms)
- **Câu hỏi user:** "khi quay lại 0:0 mastering recovery — tại sao delay 1.5s mà không recovery ngay?"
- **Giải thích delay:** 2 ngưỡng cố ý: (1) 45 frame ≈ 750ms xác nhận im lặng THẬT (false-positive = stopAllPlayback + restart = glitch — thiết kế cho main session transient gap); (2) 3s cooldown chống rebuild-loop.
- **Fix (app.jsx updatePlayhead):** `if (masterSilenceFramesRef.current > (nanOut ? 3 : 45) && sinceRebuild > 3000)`**NaN (chain chết chắc chắn) → rebuild sau 3 frame ≈ 50ms** (gần như tức thì); pk<0.001 (main) giữ 45 frame. Cooldown 3000 giữ nguyên (chống loop nếu collapse deterministic).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608060030), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → play 0:0 → nếu chain chết: âm hồi phục ~150ms (thay vì 1.5s).
### [2026-08-06 01:00] Task: Percussion track câm — program_select bank 128 thiếu preset → fallback preset hợp lệ
- **Báo cáo user:** track percussion chỉ nghe 1 âm đầu rồi câm; bật channel 10 → `[FluidSynth:err] There is no preset with bank number 128 and preset number 0 in SoundFont 2`.
- **Cơ chế:** track "latin hand perc" (sfId) mang bank 128 prog 0; `fluid_synth_program_select` trả -1 (preset không tồn tại trong font) → noteon trên preset rỗng = CÂM; channel cache vẫn ghi (128,0) → noteon sau skip re-select (progAlreadySet) → CÂM tiếp ("1 âm đầu" = note đầu còn preset cũ hợp lệ trước khi cache bị ghi đè).
- **FIX (soundfontPlayer.js doNote):** kiểm tra `_selRet !== 0 && finalBank === 128`**fallback chuỗi preset trống phổ biến [128,48 (GM kit) → 0,0 → 128,1 → 0,48]** — chọn preset đầu tiên select thành công (trả 0) + cập nhật channel cache (finalBank/finalProg) → các note sau dùng preset hợp lệ. Lọc thêm error "There is no preset with bank number" khỏi console (printErr).
- **Các file ảnh hưởng:** `soundfontPlayer.js` (?v=202608060100 — chỉ hard refresh, không build precompiled), `wiki.md`.
- **Ghi chú/Test:** hard refresh → play track percussion → KỲ VỌNG: mọi note đều kêu (fallback preset), hết error spam.
### [2026-08-06 01:30] Task: Note vẽ mới trong piano roll nghe nhạc cụ track TRƯỚC — 2 lỗi preview
- **Báo cáo user:** track 4 percussion — click note vẽ từ trước = percussion ✓; VẼ note mới = nhạc cụ track 3.
- **2 lỗi:**
(1) `_playNoteFallback` (soundfontPlayer dòng 664): `prog = _channels[channel].program || prog`**override program track bằng cache channel** (track 3 cùng channel ghi đè) → oscillator preview mang character track 3. FIX: chỉ override khi `program === undefined`.
(2) **Draw/brush preview dùng `_playNoteFallback` (oscillator beep)** thay vì `playNote` (FluidSynth — nhạc cụ thật) — click note cũ dùng playNote nên đúng. FIX app.jsx: cả 2 chỗ (brush ~7725, draw ~7819) → `playNote` với `resolveTrackInstrumentCtx` (ch/synthEngine của track).
- **Các file ảnh hưởng:** `soundfontPlayer.js` + `app.jsx` + `index.html` (?v=202608060130 cho cả 2), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → track percussion → VẼ note mới → KỲ VỌNG: percussion thật (không phải nhạc cụ track trước).
### [2026-08-06 02:00] Task: Percussion piano-roll play "1 âm đầu rồi câm" — quét preset hợp lệ toàn font (cache theo sfId)
- **Báo cáo user:** track 4 percussion — play preview trong piano roll: chỉ 1 âm đầu, các âm sau câm.
- **Cơ chế:** fallback v01:00 chỉ thử 4 preset cố định [128,48],[0,0],[128,1],[0,48] — font "latin hand perc" KHÔNG có preset nào → `finalBank/finalProg` giữ (128,0) → channel cache = (128,0) INVALID → noteon thứ 2: progAlreadySet = true (cache khớp (128,0)) → SKIP re-select → noteon trên preset rỗng = CÂM. Âm đầu = noteon trên preset DEFAULT của font (auto-assign khi load).
- **FIX (soundfontPlayer doNote):** khi `finalBank === 128`**quét toàn bộ preset font: bank 128 0-127 + bank 0 0-127 (tối đa 256 program_select, probe qua channel 9)** — chọn preset đầu trả 0 → cache `_validPercCache[sfId]` → finalBank/finalProg = preset hợp lệ → select + noteon trên preset ĐÚNG → mọi note kêu. Cache 1 lần/font (lần sau không quét lại).
- **Các file ảnh hưởng:** `soundfontPlayer.js` (?v=202608060200 — chỉ hard refresh, không build), `wiki.md`.
- **Ghi chú/Test:** hard refresh → play track percussion trong piano roll → KỲ VỌNG: MỌI note kêu (không chỉ âm đầu).
### [2026-08-06 03:00] Task: BẢO ĐẢM mastering chain xử lí MỌI track (solo) + preview piano roll khi mastering ON
- **Yêu cầu user:** (1) track solo → luồng âm PHẢI qua mastering chain khi ON (âm to); (2) preview note MIDI trong piano roll PHẢI qua mastering chain khi ON.
- **Cơ chế cũ:** ♪ bypass (trackMidiBypassMap/trackAudioBypassMap) → routeGain=0/dryGain=1 → track bỏ qua chain — kể cả khi chain ON.
- **FIX (app.jsx):**
(1) Helpers: `masteringChainOn()` + `effMidiBypass(track)`/`effAudioBypass(track)`**chain ON → bypass luôn false (♪ bị override); chain OFF → theo ♪ maps**.
(2) Áp tại: createMasteringRoute (audio route), getOrCreateTrackNode sfBypass (~18241), buildOfflineTrackNode (~1031), sync effect (~14538).
(3) `[masteringSettings]` effect: sau toggle+apply → **re-sync live nodes** (sfRouteGain/sfDryGain + route.routeGain/dryGain theo effBypass) + updateSfRouting — PWR bật/tắt áp ngay lên node đang phát.
(4) Preview piano roll: SF → sfEntry → sfRouteGain (=1 khi chain ON) → masterBus.input → chain ✓.
- **⚠️ Sửa hậu quả patch replace_all hỏng 3 vùng** (createMasteringRoute, sync effect, node creation — khôi phục đúng nguyên bản + áp helper đúng chỗ).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608060300), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → mastering ON → solo track bất kỳ → âm qua chain (to); preview note trong piano roll → qua chain.
### [2026-08-06 03:30] Task: Solo track AI không qua mastering khi bỏ solo track 1 — ép route tại thời điểm routing
- **Báo cáo user:** solo track 1 + solo track AI → cả 2 qua mastering ✓; bỏ solo track 1 → track AI solo KHÔNG qua mastering ✗.
- **Phân tích:** 2 case khác nhau: >1 audible → SF fallback `setOutputDestination(null)` → masterBus.input → chain ✓; 1 audible → SF → node.sfEntry → sfMods → sfRouteGain → chain — nếu sfRouteGain bị 0 (dry — node tạo lúc mastering OFF / state stale) → KHÔNG qua chain. AI track template không có bypass field (sạch) — nên nguyên nhân là ROUTE STALE tại node.
- **FIX (app.jsx updateSfRouting):** ép `sfRouteGain/sfDryGain` theo `effMidiBypass` NGAY TẠI thời điểm routing — cả nhánh PIANO_ROLL + nhánh midiAudible single-track (belt-and-suspenders — không chỉ lúc tạo node). Sửa lỗi gọi effMidiBypass với trackId string → track object.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608060330), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → mastering ON → solo track 1 + solo track AI → bỏ solo track 1 → track AI phải VẪN qua mastering. Nếu vẫn lỗi → dán console (tìm `[Bypass]` + `setOutputDestination` + `updateSfRouting error`).
### [2026-08-06 04:00] Task: AI Var clone kế thừa midiChannel track gốc → solo bị nhỏ (CC7 collision)
- **Báo cáo user:** track do USER chèn → solo âm bình thường; track do AI prompt chèn (AI Var) → solo âm NHỎ (nút solo con của track gốc). Yêu cầu kiểm tra quá trình AI clone.
- **Cơ chế (dòng 23042):** `const newTrack = { ...(srcTrack || {}), ... }` — clone AI Var spread TOÀN BỘ track gốc → **kế thừa `midiChannel`** → clone + track gốc DÙNG CHUNG channel. Solo track gốc (hoặc clone) → sync effect gửi `controllerChange(ch, 7, audible ? 100 : 0)` — track bị solo-mute (cùng channel) nhận CC7=0 → **notes của clone (cùng channel) cũng bị CC7=0 → âm NHỎ/CÂM**.
- **FIX (dòng 23046):** thêm `midiChannel: undefined` vào clone — `ensureTrackMidiChannel` (đã có guard) cấp channel RIÊNG (loop 0-15 skip 9). Kiểm tra: chỉ 1 chỗ spread srcTrack (23043) — AI composition dùng template sạch ✓.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608060400), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → tạo [AI Var] từ track → solo track AI Var → âm PHẢI bình thường (không nhỏ); solo track gốc → AI Var không bị ảnh hưởng.
### [2026-08-06 04:30] Task: Tab deactive → stop âm của tab đó (piano-roll tiếp tục kêu khi quay main)
- **Báo cáo user:** piano roll tab đang play → quay về MAIN/SECTION-TAB → VẪN nghe âm piano roll. Yêu cầu: tab nào deactive → stop âm tab đó.
- **FIX (app.jsx effect [activeTab] ~14571):** `prevActiveTabRef` lưu tab trước; khi đổi tab → nếu tab CŨ là sub-tab (PIANO_ROLL/section/audio) đang `isPlaying``stopAllPlayback()` + set `isPlaying: false` cho tab đó. Tab MỚI không bị ảnh hưởng; MAIN giữ hành vi cũ (mở piano-roll lúc main play → main tiếp tục — handleEditMidiInTab đã xử lý).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608060430), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → piano-roll play → bấm tab MAIN → âm piano-roll phải DỪNG ngay; play main → mở piano-roll → main tiếp tục (hành vi cũ).