FIX: thêm nút bypass cho midi items và audio items, Section item
This commit is contained in:
+76
-37
@@ -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 = mọi 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,
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user