FIX: thêm nút bypass cho midi items và audio items, Section item

This commit is contained in:
2026-08-03 22:21:50 +07:00
parent 1af2119444
commit 01fcf51fea
4 changed files with 148 additions and 68 deletions
+76 -37
View File
@@ -1143,7 +1143,7 @@ const MixerStrip = ({ track, index, onUpdateTrack, trackVuRefs }) => {
if (window.__setTrackBypass) window.__setTrackBypass(track.id, 'audio', next);
else if (window.__setTrackMasteringBypass) window.__setTrackMasteringBypass(track.id, next);
},
title: "Bypass Audio (clips + sections): XÁM = bypass đang bật (bỏ FX + mastering), SÁNG XANH = xử lý bình thường",
title: "A = Mastering FX Chain cho audio items (clips + sections): XÁM = bypass mastering (vẫn qua track FX Rack), SÁNG XANH = qua mastering",
className: "w-5 h-5 flex items-center justify-center rounded-sm text-[10px] font-mono font-bold border transition " + (isAudioBypassed ? 'bg-[#3a3a3a] text-zinc-400 border-black/60 hover:text-zinc-100' : 'bg-sky-400 text-black border-sky-300')
}, "A"), React.createElement("button", {
onClick: e => {
@@ -1153,7 +1153,7 @@ const MixerStrip = ({ track, index, onUpdateTrack, trackVuRefs }) => {
if (window.__setTrackBypass) window.__setTrackBypass(track.id, 'midi', next);
else if (window.__setTrackMasteringBypass) window.__setTrackMasteringBypass(track.id, next);
},
title: "Bypass MIDI (soundfont): XÁM = bypass đang bật (bỏ FX + mastering), SÁNG TÍM = xử lý bình thường",
title: "♪ = Mastering FX Chain cho MIDI (soundfont): XÁM = bypass mastering (vẫn qua track FX Rack), SÁNG TÍM = qua mastering",
className: "w-5 h-5 flex items-center justify-center rounded-sm text-[10px] font-mono font-bold border transition " + (isMidiBypassed ? 'bg-[#3a3a3a] text-zinc-400 border-black/60 hover:text-zinc-100' : 'bg-fuchsia-400 text-black border-fuchsia-300')
}, "\u266A")),
React.createElement("div", {
@@ -1514,7 +1514,6 @@ const TrackStripConsole = ({ track, index, onUpdateTrack, trackVuRefs }) => {
const [pan, setPan] = React.useState(0.0);
const [panLabel, setPanLabel] = React.useState('center');
const [isPhaseInverted, setIsPhaseInverted] = React.useState(false);
const [isFxActive, setIsFxActive] = React.useState(true);
const panPointerRef = React.useRef(null);
const setVuCanvas = React.useCallback(function(el) {
if (el) trackVuRefs.current[track.id + '_mixer'] = el;
@@ -1659,7 +1658,7 @@ const TrackStripConsole = ({ track, index, onUpdateTrack, trackVuRefs }) => {
else if (window.__setTrackMasteringBypass) window.__setTrackMasteringBypass(track.id, next);
},
className: "btn-daw h-[24px] rounded flex items-center justify-center font-extrabold text-[9px] " + (track.audioBypass ? " text-slate-500" : " text-sky-300 bg-sky-500/30 border-sky-400/70"),
title: "Bypass Audio (clips + sections): XÁM = bypass đang bật (bỏ FX + mastering), SÁNG XANH = xử lý bình thường"
title: "A = Mastering FX Chain cho audio items (clips + sections): XÁM = bypass mastering (vẫn qua track FX Rack), SÁNG XANH = qua mastering"
}, "A"),
React.createElement("button", {
onClick: function(e) {
@@ -1670,7 +1669,7 @@ const TrackStripConsole = ({ track, index, onUpdateTrack, trackVuRefs }) => {
else if (window.__setTrackMasteringBypass) window.__setTrackMasteringBypass(track.id, next);
},
className: "btn-daw h-[24px] rounded flex items-center justify-center font-extrabold text-[10px] " + (track.midiBypass ? " text-slate-500" : " text-fuchsia-300 bg-fuchsia-500/30 border-fuchsia-400/70"),
title: "Bypass MIDI (soundfont): XÁM = bypass đang bật (bỏ FX + mastering), SÁNG TÍM = xử lý bình thường"
title: "♪ = Mastering FX Chain cho MIDI (soundfont): XÁM = bypass mastering (vẫn qua track FX Rack), SÁNG TÍM = qua mastering"
}, "\u266A"),
React.createElement("button", {
onClick: function() { if (window.__openFxRack) window.__openFxRack(track.id, track.name); },
@@ -1678,9 +1677,15 @@ const TrackStripConsole = ({ track, index, onUpdateTrack, trackVuRefs }) => {
title: "Track FX Chain (mastering_expand.md §II.4)"
}, "FX"),
React.createElement("button", {
onClick: function() { setIsFxActive(function(p) { return !p; }); },
className: "btn-daw h-[24px] rounded flex items-center justify-center text-[8px]" + (isFxActive ? " text-emerald-400" : " text-slate-500"),
title: "Toggle FX Power"
onClick: function() {
var next = track.fxActive === false;
if (onUpdateTrack) onUpdateTrack(track.id, { fxActive: next });
// Live re-route: rebuild both FX chains (audio + SF) so EVERY item
// (midi/section/audioclip) immediately follows the FX power state.
if (window.__rebuildTrackFxGraph) window.__rebuildTrackFxGraph(track.id);
},
className: "btn-daw h-[24px] rounded flex items-center justify-center text-[8px]" + (track.fxActive !== false ? " text-emerald-400" : " text-slate-500"),
title: "Toggle FX Power: ON (sáng) = MỌI items (MIDI/section/audioclip) qua FX Rack Panel (nút A/♪ chỉ ảnh hưởng Mastering FX Chain); OFF (tối) = bỏ toàn bộ track FX"
}, React.createElement("i", { className: "fa-solid fa-power-off" })),
React.createElement("button", {
className: "btn-daw h-[24px] rounded text-slate-400 flex items-center justify-center",
@@ -8437,6 +8442,7 @@ const serializeTracksList = (tracksList, secondsPerBar) => {
mastering_bypass: t.audioBypass || false,
audio_bypass: t.audioBypass || false,
midi_bypass: t.midiBypass || false,
fx_active: t.fxActive !== false,
instrument_id: t.instrumentId || null,
instrument_program: t.instrumentProgram !== undefined ? t.instrumentProgram : null,
instrument_name: t.instrumentName || null,
@@ -8516,6 +8522,7 @@ const deserializeTracksList = (schemaTracks, secondsPerBar, sectionStore) => {
masteringBypass: (t.audio_bypass !== undefined ? t.audio_bypass : (t.mastering_bypass || false)),
audioBypass: (t.audio_bypass !== undefined ? t.audio_bypass : (t.mastering_bypass || false)),
midiBypass: (t.midi_bypass !== undefined ? t.midi_bypass : (t.mastering_bypass || false)),
fxActive: t.fx_active !== undefined ? t.fx_active : true,
color: t.color || (t.id === '1' ? '#0f766e' : '#1d4ed8'),
startTime: t.start_time || 0,
height: t.height || 140,
@@ -12960,6 +12967,12 @@ const App = () => {
});
} else if (which === 'midi') {
trackMidiBypassMap[trackId] = !!on;
// bypasses Mastering FX Chain for the soundfont ONLY the SF path
// keeps flowing through its own sfModules (track FX Rack, PWR-controlled).
const node = activeTrackNodesRef.current[trackId];
if (node && node.sfRouteGain && node.sfDryGain) {
setMasteringRoute({ routeGain: node.sfRouteGain, dryGain: node.sfDryGain, _trackId: trackId }, !!on);
}
try { if (updateSfRoutingRef.current) updateSfRoutingRef.current(); } catch (e) { }
}
};
@@ -13533,6 +13546,12 @@ const App = () => {
});
const list = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : all;
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;
}
const sig = (t.muted ? '1' : '0') + (t.solo ? '1' : '0') + ':' + (t.volumeDb ?? 0);
if (trackMuteSoloSigRef.current[t.id] === sig) return;
trackMuteSoloSigRef.current[t.id] = sig;
@@ -16939,12 +16958,13 @@ const App = () => {
pannerNode.connect(scopeSplitter);
scopeSplitter.connect(scopeAnalyserL, 0);
scopeSplitter.connect(scopeAnalyserR, 1);
// Dual mastering route: routeGain -> mastering chain (normal, post-FX),
// dryGain -> dry bus (bypass, tapped PRE-FX so the bypassed channel skips
// BOTH the track FX chain and the mastering chain at Main out).
// Dual mastering route: routeGain -> mastering chain (normal), dryGain ->
// dry bus (skip mastering ONLY). BOTH tap POST-FX (analyser) so the A
// button bypasses JUST the Mastering FX Chain the track FX Rack chain
// (fxEntry modules) is controlled ONLY by the PWR button (fxActive).
const route = createMasteringRoute(context, track, masterBus);
analyserNode.connect(route.routeGain);
gainNode.connect(route.dryGain);
analyserNode.connect(route.dryGain);
let fxStopFn;
// Track FX chain (unified_fx_rack_panel.md §II): reusable module instances
@@ -16955,21 +16975,24 @@ const App = () => {
const fxLegacyIn = context.createGain();
gainNode.connect(fxEntry);
const fxChain = track.fxChain || [];
const chainMods = fxChain.filter(m => m && m.active !== false).map(m => {
// FX Power (nút PWR): ON = mi items con (midi/section/audioclip) qua FX
// Rack Panel chain; OFF = b toàn b track FX (fxChain + chorus/reverb).
const fxEnabled = track.fxActive !== false;
const chainMods = fxEnabled ? fxChain.filter(m => m && m.active !== false).map(m => {
try { return createTrackFxModule(m.type || m, context, m.params); } catch (e) { return null; }
}).filter(Boolean);
}).filter(Boolean) : [];
let fxChainTail = fxEntry;
chainMods.forEach(mod => {
fxChainTail.connect(mod.input);
fxChainTail = mod.output;
});
fxChainTail.connect(fxLegacyIn);
if (track.fxType === 'chorus') {
if (fxEnabled && track.fxType === 'chorus') {
const fxInput = context.createGain();
fxLegacyIn.connect(fxInput);
const chorus = createChorusNode(context, fxInput, pannerNode);
fxStopFn = chorus.stop;
} else if (track.fxType === 'reverb') {
} else if (fxEnabled && track.fxType === 'reverb') {
const fxInput = context.createGain();
fxLegacyIn.connect(fxInput);
createReverbNode(context, fxInput, pannerNode);
@@ -16982,7 +17005,7 @@ const App = () => {
// fully separate from the audio-clips chain so the A (audio) bypass
// NEVER affects the soundfont instrument, and the bypass never touches
// audio clips/sections.
let sfEntry = null, sfOut = null, sfPan = null, sfAnalyser = null;
let sfEntry = null, sfOut = null, sfPan = null, sfAnalyser = null, sfRouteGain = null, sfDryGain = null;
if ((track.midiItems && track.midiItems.length > 0)) {
sfEntry = context.createGain();
sfOut = context.createGain();
@@ -16998,11 +17021,26 @@ const App = () => {
// gain (volume/mute/solo) instead.
sfEntry.gain.value = 1;
sfOut.connect(sfPan);
sfPan.connect(masterBus.input);
// Feed the soundfont into the shared scope analysers too, so the Wave
// Observer shows the FULL post-FX track signal: audioclips + section
// items (pannerNode) AND midi items (sfOut) mixed.
sfOut.connect(scopeSplitter);
// 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);
sfRouteGain = context.createGain();
sfDryGain = context.createGain();
sfRouteGain.gain.value = sfBypass ? 0 : 1;
sfDryGain.gain.value = sfBypass ? 1 : 0;
sfPan.connect(sfRouteGain);
sfPan.connect(sfDryGain);
sfRouteGain.connect(masterBus.input);
sfDryGain.connect(masterBus.dryInput);
sfOut.connect(sfAnalyser);
const sfMods = fxChain.filter(m => m && m.active !== false).map(m => {
const sfMods = fxEnabled ? fxChain.filter(m => m && m.active !== false).map(m => {
try { return createTrackFxModule(m.type || m, context, m.params); } catch (e) { return null; }
}).filter(Boolean);
}).filter(Boolean) : [];
let sfTail = sfEntry;
sfMods.forEach(mod => {
sfTail.connect(mod.input);
@@ -17010,7 +17048,7 @@ const App = () => {
});
sfTail.connect(sfOut);
}
node = { gainNode, pannerNode, fxStopFn, analyserNode, route, fxEntry, fxLegacyIn, scopeAnalyserL, scopeAnalyserR, sfEntry, sfOut, sfPan, sfAnalyser };
node = { gainNode, pannerNode, fxStopFn, analyserNode, route, fxEntry, fxLegacyIn, scopeAnalyserL, scopeAnalyserR, sfEntry, sfOut, sfPan, sfAnalyser, sfRouteGain, sfDryGain };
// Realtime mute/solo: apply the track's current mute/solo/volume state to
// the fresh node so items of muted/soloed tracks start correctly.
const trackList = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : [track];
@@ -17037,9 +17075,10 @@ const App = () => {
const track = list.find(t => t.id === trackId);
if (!track) return;
node.fxEntry.disconnect();
const chainMods = (track.fxChain || []).filter(m => m && m.active !== false).map(m => {
const fxEnabled = track.fxActive !== false;
const chainMods = fxEnabled ? (track.fxChain || []).filter(m => m && m.active !== false).map(m => {
try { return createTrackFxModule(m.type || m, getAudioContext(), m.params); } catch (e) { return null; }
}).filter(Boolean);
}).filter(Boolean) : [];
let tail = node.fxEntry;
chainMods.forEach(mod => {
tail.connect(mod.input);
@@ -17049,9 +17088,9 @@ const App = () => {
// Rebuild the independent SF chain too (same modules, separate instances)
if (node.sfEntry && node.sfOut) {
node.sfEntry.disconnect();
const sfMods = (track.fxChain || []).filter(m => m && m.active !== false).map(m => {
const sfMods = fxEnabled ? (track.fxChain || []).filter(m => m && m.active !== false).map(m => {
try { return createTrackFxModule(m.type || m, getAudioContext(), m.params); } catch (e) { return null; }
}).filter(Boolean);
}).filter(Boolean) : [];
let sfTail = node.sfEntry;
sfMods.forEach(mod => {
sfTail.connect(mod.input);
@@ -17082,14 +17121,9 @@ const App = () => {
if (midiAudible.length === 1) {
const t = midiAudible[0];
const node = activeTrackNodesRef.current[t.id];
// MIDI bypass ON: route the soundfont straight to the dry bus (skips
// the track FX chain AND the mastering chain at Main out), exactly like
// the audio-group bypass CC7 keeps mute/solo working.
if (trackMidiBypassMap[t.id] && masterBus && masterBus.dryInput && window.SonicSF && window.SonicSF.setOutputDestination) {
window.SonicSF.setOutputDestination(masterBus.dryInput);
return;
}
// Independent SF FX path unaffected by the audio (A) bypass toggle.
// SF ALWAYS enters the track's own FX chain (sfEntry sfModules when
// PWR ON). The button only switches the post-FX route (sfRouteGain /
// sfDryGain) to skip or include the Mastering FX Chain.
if (node && node.sfEntry && window.SonicSF && window.SonicSF.setOutputDestination) {
window.SonicSF.setOutputDestination(node.sfEntry);
return;
@@ -17112,8 +17146,8 @@ const App = () => {
window.__getTrackScopeAnalysers = (trackId) => {
const node = activeTrackNodesRef.current[trackId];
if (!node) return null;
// MIDI tracks: show the soundfont's own analyser (post-FX).
if (node.sfAnalyser) return { L: node.sfAnalyser, R: node.sfAnalyser, sr: getAudioContext().sampleRate };
// The shared scope splitter carries the FULL post-FX track signal:
// pannerNode (audioclips + section items) mixed with sfOut (midi items).
if (!node.scopeAnalyserL || !node.scopeAnalyserR) return null;
return { L: node.scopeAnalyserL, R: node.scopeAnalyserR, sr: getAudioContext().sampleRate };
};
@@ -17397,7 +17431,10 @@ const App = () => {
source.buffer = clip.buffer;
source.playbackRate.value = clip.speed || 1.0;
source.connect(gainNode);
gainNode.connect(pannerNode);
// NOTE: do NOT connect gainNode pannerNode directly here the track
// node already wires gainNode fxEntry [FX modules] fxLegacyIn
// chorus/reverb pannerNode. A direct tap would BYPASS the FX Rack
// chain for audio clips (double path: with + without FX).
const clipStart = clip.startTime || 0;
const clipDuration = clip.buffer.duration / (clip.speed || 1.0);
const clipEnd = clipStart + clipDuration;
@@ -19698,7 +19735,7 @@ const App = () => {
id: 'midi_track_' + now + '_' + idx,
name: midiItem.name || (midiResult.length > 1 ? 'MIDI Track ' + (idx + 1) : (file.name || 'MIDI').replace(/\.midi?$/i, '')),
buffer: null, startTime: 0, volumeDb: 0, pan: 0,
muted: false, solo: false, masteringBypass: false, audioBypass: false, midiBypass: false, color: colors[idx % colors.length],
muted: false, solo: false, masteringBypass: false, audioBypass: false, midiBypass: false, fxActive: true, color: colors[idx % colors.length],
markers: [], serverFileId: null, clips: [], sections: [],
midiItems: [midiItem],
isArmed: false, monitoringEnabled: true,
@@ -19763,6 +19800,7 @@ const App = () => {
masteringBypass: false,
audioBypass: false,
midiBypass: false,
fxActive: true,
color: selectColor,
markers: [],
serverFileId: null,
@@ -19953,6 +19991,7 @@ const App = () => {
masteringBypass: false,
audioBypass: false,
midiBypass: false,
fxActive: true,
color: selectColor,
markers: [],
serverFileId: null,