FEAT: thêm nút bypass cho track strip để bypass không qua mastering panel
This commit is contained in:
+87
-8
@@ -62,7 +62,37 @@ const assignTrackMidiChannel = (track, tracks) => {
|
||||
// Storage for server-side file IDs mapped to track IDs
|
||||
let serverFileIdMap = {};
|
||||
let audioCtx;
|
||||
let masterBus = null; // { input, compressor, analyser, output, masteringActive }
|
||||
let masterBus = null; // { input, compressor, analyser, output, masteringActive, dryInput, dryOutput }
|
||||
|
||||
// Per-track mastering-bypass state (trackId -> bool), kept in sync with the
|
||||
// tracks state so ANY audio path can route without holding the track object.
|
||||
const trackMasteringBypassMap = {};
|
||||
|
||||
// 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) {
|
||||
const bypass = !!(track && track.masteringBypass);
|
||||
const routeGain = ctx.createGain();
|
||||
const dryGain = ctx.createGain();
|
||||
const masterDest = bus ? bus.input : ctx.destination;
|
||||
const dryDest = (bus && bus.dryInput) ? bus.dryInput : ctx.destination;
|
||||
routeGain.gain.value = bypass ? 0 : 1;
|
||||
dryGain.gain.value = bypass ? 1 : 0;
|
||||
routeGain.connect(masterDest);
|
||||
dryGain.connect(dryDest);
|
||||
return { routeGain, dryGain };
|
||||
}
|
||||
|
||||
// Live-toggle a route with a short crossfade (click-free).
|
||||
function setMasteringRoute(route, bypass) {
|
||||
if (!route || !audioCtx) return;
|
||||
const t = audioCtx.currentTime;
|
||||
const on = !!bypass;
|
||||
route.routeGain.gain.cancelScheduledValues(t);
|
||||
route.dryGain.gain.cancelScheduledValues(t);
|
||||
route.routeGain.gain.setTargetAtTime(on ? 0 : 1, t, 0.02);
|
||||
route.dryGain.gain.setTargetAtTime(on ? 1 : 0, t, 0.02);
|
||||
}
|
||||
|
||||
function makeDistortionCurve(k) {
|
||||
const n_samples = 44100;
|
||||
@@ -288,6 +318,15 @@ function initMasterBus(ctx) {
|
||||
const output = ctx.createGain();
|
||||
output.gain.value = 1.0;
|
||||
|
||||
// Per-track mastering-bypass dry bus: tracks with bypass ON feed into
|
||||
// dryInput -> dryOutput -> output, skipping the mastering modules
|
||||
// (EQ / Imager / Maximizer) while still passing the master volume fader
|
||||
// and the master output metering.
|
||||
const dryInput = ctx.createGain();
|
||||
const dryOutput = ctx.createGain();
|
||||
dryInput.connect(dryOutput);
|
||||
dryOutput.connect(output);
|
||||
|
||||
const analyser = ctx.createAnalyser();
|
||||
analyser.fftSize = 256;
|
||||
|
||||
@@ -307,6 +346,8 @@ function initMasterBus(ctx) {
|
||||
analyser,
|
||||
output,
|
||||
masteringActive: false,
|
||||
dryInput,
|
||||
dryOutput,
|
||||
|
||||
// Analysers for metering
|
||||
inputAnalyser,
|
||||
@@ -754,6 +795,7 @@ const MixerStrip = ({ track, index, onUpdateTrack, trackVuRefs }) => {
|
||||
const dbLabel = (track.volumeDb == null || track.volumeDb <= -50) ? '-inf' : (track.volumeDb > 0 ? '+' : '') + (track.volumeDb || 0).toFixed(1) + 'dB';
|
||||
const isMuted = track.muted;
|
||||
const isSoloed = track.solo;
|
||||
const isBypassed = track.masteringBypass;
|
||||
const vol = track.volumeDb != null ? track.volumeDb : 0;
|
||||
var pct = Math.max(0, Math.min(100, (vol + 60) / 72 * 100));
|
||||
var vuColor = pct >= 80 ? '#ef4444' : pct >= 50 ? '#eab308' : '#22c55e';
|
||||
@@ -776,7 +818,17 @@ const MixerStrip = ({ track, index, onUpdateTrack, trackVuRefs }) => {
|
||||
onClick: e => { e.stopPropagation(); if (onUpdateTrack) onUpdateTrack(track.id, { solo: !track.solo }); },
|
||||
title: "Solo",
|
||||
className: "w-5 h-5 flex items-center justify-center rounded-sm text-[10px] font-mono font-bold border transition " + (isSoloed ? 'bg-yellow-400 text-black border-yellow-300' : 'bg-[#3a3a3a] text-zinc-400 border-black/60 hover:text-zinc-100')
|
||||
}, "S")),
|
||||
}, "S"), React.createElement("button", {
|
||||
onClick: e => {
|
||||
e.stopPropagation();
|
||||
const next = !track.masteringBypass;
|
||||
if (onUpdateTrack) onUpdateTrack(track.id, { masteringBypass: next });
|
||||
// Live audio re-route (applies immediately to playing tracks).
|
||||
if (window.__setTrackMasteringBypass) window.__setTrackMasteringBypass(track.id, next);
|
||||
},
|
||||
title: "Bypass Mastering: bật thì track KHÔNG qua EQ/Imager/Maximizer ở Main out",
|
||||
className: "w-5 h-5 flex items-center justify-center rounded-sm text-[10px] font-mono font-bold border transition " + (isBypassed ? 'bg-sky-400 text-black border-sky-300' : 'bg-[#3a3a3a] text-zinc-400 border-black/60 hover:text-zinc-100')
|
||||
}, "B")),
|
||||
React.createElement("div", {
|
||||
className: "flex-1 flex items-stretch justify-center gap-1 px-1 py-1 min-h-0"
|
||||
}, React.createElement("div", {
|
||||
@@ -7986,6 +8038,7 @@ const serializeTracksList = (tracksList, secondsPerBar) => {
|
||||
pan: t.pan || 0.0,
|
||||
mute: t.muted || false,
|
||||
solo: t.solo || false,
|
||||
mastering_bypass: t.masteringBypass || false,
|
||||
instrument_id: t.instrumentId || null,
|
||||
instrument_program: t.instrumentProgram !== undefined ? t.instrumentProgram : null,
|
||||
instrument_name: t.instrumentName || null,
|
||||
@@ -8061,6 +8114,7 @@ const deserializeTracksList = (schemaTracks, secondsPerBar, sectionStore) => {
|
||||
pan: t.pan || 0.0,
|
||||
muted: t.mute || false,
|
||||
solo: t.solo || false,
|
||||
masteringBypass: t.mastering_bypass || false,
|
||||
color: t.color || (t.id === '1' ? '#0f766e' : '#1d4ed8'),
|
||||
startTime: t.start_time || 0,
|
||||
height: t.height || 140,
|
||||
@@ -11720,6 +11774,20 @@ const App = () => {
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// Live per-track mastering bypass: updates the routing map + re-routes any
|
||||
// active track node immediately (called from MixerStrip's B button).
|
||||
window.__setTrackMasteringBypass = function(trackId, bypass) {
|
||||
trackMasteringBypassMap[trackId] = !!bypass;
|
||||
const node = activeTrackNodesRef.current[trackId];
|
||||
if (node && node.route) setMasteringRoute(node.route, !!bypass);
|
||||
};
|
||||
|
||||
// Keep the routing map in sync with the tracks state (loads, undo, AI ops…).
|
||||
useEffect(() => {
|
||||
(tracks || []).forEach(t => { trackMasteringBypassMap[t.id] = !!t.masteringBypass; });
|
||||
(sessionTabs || []).forEach(st => (st.tracks || []).forEach(t => { trackMasteringBypassMap[t.id] = !!t.masteringBypass; }));
|
||||
}, [tracks, sessionTabs]);
|
||||
window.__toggleMediaExplorerRef = function() {
|
||||
setShowMediaExplorer(function(p) {
|
||||
const next = !p;
|
||||
@@ -12413,6 +12481,10 @@ const App = () => {
|
||||
showToast('Đã khôi phục dự án "' + lastName + '" (' + restoredItemCount + ' items).', 'info');
|
||||
} catch(e) {
|
||||
console.warn('restoreLastSessionProject failed:', e);
|
||||
// Stale session id (project deleted / DB reset): clear it so the error
|
||||
// does not repeat on every page load.
|
||||
localStorage.removeItem('sonic_project_id');
|
||||
localStorage.removeItem('sonic_project_name');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15270,7 +15342,10 @@ const App = () => {
|
||||
source.connect(volumeGainNode);
|
||||
volumeGainNode.connect(pannerNode);
|
||||
pannerNode.connect(fadeGainNode);
|
||||
fadeGainNode.connect(masterBus ? masterBus.input : context.destination);
|
||||
// Route through mastering chain unless this track has mastering bypass ON.
|
||||
const route = createMasteringRoute(context, { masteringBypass: !!trackMasteringBypassMap[st.trackId] }, masterBus);
|
||||
fadeGainNode.connect(route.routeGain);
|
||||
fadeGainNode.connect(route.dryGain);
|
||||
source.start(context.currentTime, offsetBuffer);
|
||||
activeSourcesRef.current = [source];
|
||||
activeTrackNodesRef.current[st.trackId] = {
|
||||
@@ -15548,13 +15623,15 @@ const App = () => {
|
||||
pannerNode.pan.setValueAtTime((track.pan ?? 0) / 100, context.currentTime);
|
||||
// Ensure master bus is initialized for MAIN OUT routing
|
||||
if (!masterBus) initMasterBus(context);
|
||||
// Route through master bus if available, else direct to destination
|
||||
const dest = masterBus ? masterBus.input : context.destination;
|
||||
|
||||
const analyserNode = context.createAnalyser();
|
||||
analyserNode.fftSize = 256;
|
||||
pannerNode.connect(analyserNode);
|
||||
analyserNode.connect(dest);
|
||||
// Dual mastering route: routeGain -> mastering chain (normal), dryGain ->
|
||||
// dry bus (bypass). Live-toggled via setMasteringRoute(node.route, ...).
|
||||
const route = createMasteringRoute(context, track, masterBus);
|
||||
analyserNode.connect(route.routeGain);
|
||||
analyserNode.connect(route.dryGain);
|
||||
|
||||
let fxStopFn;
|
||||
if (track.fxType === 'chorus') {
|
||||
@@ -15570,7 +15647,7 @@ const App = () => {
|
||||
} else {
|
||||
gainNode.connect(pannerNode);
|
||||
}
|
||||
node = { gainNode, pannerNode, fxStopFn, analyserNode };
|
||||
node = { gainNode, pannerNode, fxStopFn, analyserNode, route };
|
||||
activeTrackNodesRef.current[track.id] = node;
|
||||
}
|
||||
return node.gainNode;
|
||||
@@ -18137,7 +18214,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, color: colors[idx % colors.length],
|
||||
muted: false, solo: false, masteringBypass: false, color: colors[idx % colors.length],
|
||||
markers: [], serverFileId: null, clips: [], sections: [],
|
||||
midiItems: [midiItem],
|
||||
isArmed: false, monitoringEnabled: true,
|
||||
@@ -18199,6 +18276,7 @@ const App = () => {
|
||||
pan: 0,
|
||||
muted: false,
|
||||
solo: false,
|
||||
masteringBypass: false,
|
||||
color: selectColor,
|
||||
markers: [],
|
||||
serverFileId: null,
|
||||
@@ -18386,6 +18464,7 @@ const App = () => {
|
||||
pan: 0,
|
||||
muted: false,
|
||||
solo: false,
|
||||
masteringBypass: false,
|
||||
color: selectColor,
|
||||
markers: [],
|
||||
serverFileId: null,
|
||||
|
||||
Reference in New Issue
Block a user