IMPROVE: thêm nút bypass cho midi items và audio items
This commit is contained in:
+195
-37
@@ -67,11 +67,21 @@ let masterBus = null; // { input, compressor, analyser, output, masteringActive,
|
|||||||
// Per-track mastering-bypass state (trackId -> bool), kept in sync with the
|
// 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.
|
// tracks state so ANY audio path can route without holding the track object.
|
||||||
const trackMasteringBypassMap = {};
|
const trackMasteringBypassMap = {};
|
||||||
|
const trackAudioBypassMap = {};
|
||||||
|
const trackMidiBypassMap = {};
|
||||||
|
|
||||||
// Build the dual routing for one track: routeGain -> mastering chain (normal),
|
// Build the dual routing for one track: routeGain -> mastering chain (normal),
|
||||||
// dryGain -> dry bus (bypass). Gains start at complementary 1/0 values.
|
// dryGain -> dry bus (bypass). Gains start at complementary 1/0 values.
|
||||||
function createMasteringRoute(ctx, track, bus) {
|
function createMasteringRoute(ctx, track, bus) {
|
||||||
const bypass = !!(track && track.masteringBypass);
|
// Prefer the live bypass map (synced from the tracks state on every render),
|
||||||
|
// falling back to the track object — this guarantees the A-button toggle is
|
||||||
|
// picked up even if a stale track object is passed in.
|
||||||
|
let bypass = false;
|
||||||
|
if (track && track.id && trackAudioBypassMap[track.id] !== undefined) {
|
||||||
|
bypass = !!trackAudioBypassMap[track.id];
|
||||||
|
} else {
|
||||||
|
bypass = !!(track && (track.audioBypass ?? track.masteringBypass));
|
||||||
|
}
|
||||||
const routeGain = ctx.createGain();
|
const routeGain = ctx.createGain();
|
||||||
const dryGain = ctx.createGain();
|
const dryGain = ctx.createGain();
|
||||||
const masterDest = bus ? bus.input : ctx.destination;
|
const masterDest = bus ? bus.input : ctx.destination;
|
||||||
@@ -80,18 +90,30 @@ function createMasteringRoute(ctx, track, bus) {
|
|||||||
dryGain.gain.value = bypass ? 1 : 0;
|
dryGain.gain.value = bypass ? 1 : 0;
|
||||||
routeGain.connect(masterDest);
|
routeGain.connect(masterDest);
|
||||||
dryGain.connect(dryDest);
|
dryGain.connect(dryDest);
|
||||||
return { routeGain, dryGain };
|
const routeObj = { routeGain, dryGain };
|
||||||
|
routeObj._trackId = track && track.id;
|
||||||
|
routeObj._bypass = bypass;
|
||||||
|
return routeObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Live-toggle a route with a short crossfade (click-free).
|
// Live-toggle a route. HARD switch: cancel any pending automation and assign
|
||||||
|
// .value directly (instant, cannot be delayed by the automation queue).
|
||||||
function setMasteringRoute(route, bypass) {
|
function setMasteringRoute(route, bypass) {
|
||||||
if (!route || !audioCtx) return;
|
if (!route) return;
|
||||||
const t = audioCtx.currentTime;
|
|
||||||
const on = !!bypass;
|
const on = !!bypass;
|
||||||
route.routeGain.gain.cancelScheduledValues(t);
|
try {
|
||||||
route.dryGain.gain.cancelScheduledValues(t);
|
const ctx = (typeof getAudioContext === 'function') ? getAudioContext() : null;
|
||||||
route.routeGain.gain.setTargetAtTime(on ? 0 : 1, t, 0.02);
|
if (!ctx) return;
|
||||||
route.dryGain.gain.setTargetAtTime(on ? 1 : 0, t, 0.02);
|
const t = ctx.currentTime;
|
||||||
|
route.routeGain.gain.cancelScheduledValues(t);
|
||||||
|
route.dryGain.gain.cancelScheduledValues(t);
|
||||||
|
route.routeGain.gain.value = on ? 0 : 1;
|
||||||
|
route.dryGain.gain.value = on ? 1 : 0;
|
||||||
|
route._bypass = on;
|
||||||
|
console.log('[Bypass] track', route._trackId, 'audioBypass=' + on, '→', on ? 'DRY BUS (bỏ mastering + bỏ track FX)' : 'MASTERING CHAIN (qua FX + mastering)');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('setMasteringRoute error:', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Realtime mute/solo: audible linear gain for a track given the full track list
|
// Realtime mute/solo: audible linear gain for a track given the full track list
|
||||||
@@ -1088,7 +1110,8 @@ 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 dbLabel = (track.volumeDb == null || track.volumeDb <= -50) ? '-inf' : (track.volumeDb > 0 ? '+' : '') + (track.volumeDb || 0).toFixed(1) + 'dB';
|
||||||
const isMuted = track.muted;
|
const isMuted = track.muted;
|
||||||
const isSoloed = track.solo;
|
const isSoloed = track.solo;
|
||||||
const isBypassed = track.masteringBypass;
|
const isAudioBypassed = !!track.audioBypass;
|
||||||
|
const isMidiBypassed = !!track.midiBypass;
|
||||||
const vol = track.volumeDb != null ? track.volumeDb : 0;
|
const vol = track.volumeDb != null ? track.volumeDb : 0;
|
||||||
var pct = Math.max(0, Math.min(100, (vol + 60) / 72 * 100));
|
var pct = Math.max(0, Math.min(100, (vol + 60) / 72 * 100));
|
||||||
var vuColor = pct >= 80 ? '#ef4444' : pct >= 50 ? '#eab308' : '#22c55e';
|
var vuColor = pct >= 80 ? '#ef4444' : pct >= 50 ? '#eab308' : '#22c55e';
|
||||||
@@ -1114,14 +1137,25 @@ const MixerStrip = ({ track, index, onUpdateTrack, trackVuRefs }) => {
|
|||||||
}, "S"), React.createElement("button", {
|
}, "S"), React.createElement("button", {
|
||||||
onClick: e => {
|
onClick: e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const next = !track.masteringBypass;
|
const next = !(track.audioBypass ?? track.masteringBypass);
|
||||||
if (onUpdateTrack) onUpdateTrack(track.id, { masteringBypass: next });
|
if (onUpdateTrack) onUpdateTrack(track.id, { audioBypass: next });
|
||||||
// Live audio re-route (applies immediately to playing tracks).
|
// Live audio re-route (applies immediately to playing tracks).
|
||||||
if (window.__setTrackMasteringBypass) window.__setTrackMasteringBypass(track.id, next);
|
if (window.__setTrackBypass) window.__setTrackBypass(track.id, 'audio', next);
|
||||||
|
else if (window.__setTrackMasteringBypass) window.__setTrackMasteringBypass(track.id, next);
|
||||||
},
|
},
|
||||||
title: "Bypass Mastering: bật thì track KHÔNG qua EQ/Imager/Maximizer ở Main out",
|
title: "Bypass Audio (clips + sections): XÁM = bypass đang bật (bỏ FX + mastering), SÁNG XANH = xử lý bình thường",
|
||||||
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')
|
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')
|
||||||
}, "B")),
|
}, "A"), React.createElement("button", {
|
||||||
|
onClick: e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const next = !(track.midiBypass ?? track.masteringBypass);
|
||||||
|
if (onUpdateTrack) onUpdateTrack(track.id, { midiBypass: next });
|
||||||
|
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",
|
||||||
|
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", {
|
React.createElement("div", {
|
||||||
className: "flex-1 flex items-stretch justify-center gap-1 px-1 py-1 min-h-0"
|
className: "flex-1 flex items-stretch justify-center gap-1 px-1 py-1 min-h-0"
|
||||||
}, React.createElement("div", {
|
}, React.createElement("div", {
|
||||||
@@ -1472,7 +1506,7 @@ const TrackStripConsole = ({ track, index, onUpdateTrack, trackVuRefs }) => {
|
|||||||
var trackColor = track.color || '#06b6d4';
|
var trackColor = track.color || '#06b6d4';
|
||||||
var isMuted = track.muted;
|
var isMuted = track.muted;
|
||||||
var isSoloed = track.solo;
|
var isSoloed = track.solo;
|
||||||
var isBypassed = track.masteringBypass;
|
var isBypassed = !!(track.audioBypass ?? track.masteringBypass);
|
||||||
var isArmed = track.isArmed;
|
var isArmed = track.isArmed;
|
||||||
var trackName = track.name || 'Track ' + (index + 1);
|
var trackName = track.name || 'Track ' + (index + 1);
|
||||||
var isMicActive = track.inputSource?.deviceType === 'MICROPHONE';
|
var isMicActive = track.inputSource?.deviceType === 'MICROPHONE';
|
||||||
@@ -1618,14 +1652,26 @@ const TrackStripConsole = ({ track, index, onUpdateTrack, trackVuRefs }) => {
|
|||||||
React.createElement("button", {
|
React.createElement("button", {
|
||||||
onClick: function(e) {
|
onClick: function(e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
var next = !track.masteringBypass;
|
var next = !(track.audioBypass ?? track.masteringBypass);
|
||||||
if (onUpdateTrack) onUpdateTrack(track.id, { masteringBypass: next });
|
if (onUpdateTrack) onUpdateTrack(track.id, { audioBypass: next });
|
||||||
// Live re-route: bypassed channel skips FX + mastering chain at Main out.
|
// Live re-route: bypassed channel skips FX + mastering chain at Main out.
|
||||||
if (window.__setTrackMasteringBypass) window.__setTrackMasteringBypass(track.id, next);
|
if (window.__setTrackBypass) window.__setTrackBypass(track.id, 'audio', next);
|
||||||
|
else if (window.__setTrackMasteringBypass) window.__setTrackMasteringBypass(track.id, next);
|
||||||
},
|
},
|
||||||
className: "btn-daw h-[24px] rounded text-sky-400 flex items-center justify-center" + (isBypassed ? " btn-bypass-active bg-sky-500/20" : ""),
|
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: track KHÔNG qua FX + mastering ở Main out"
|
title: "Bypass Audio (clips + sections): XÁM = bypass đang bật (bỏ FX + mastering), SÁNG XANH = xử lý bình thường"
|
||||||
}, React.createElement("i", { className: "fa-solid fa-bars-staggered text-[8px]" })),
|
}, "A"),
|
||||||
|
React.createElement("button", {
|
||||||
|
onClick: function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
var next = !(track.midiBypass ?? track.masteringBypass);
|
||||||
|
if (onUpdateTrack) onUpdateTrack(track.id, { midiBypass: next });
|
||||||
|
if (window.__setTrackBypass) window.__setTrackBypass(track.id, 'midi', next);
|
||||||
|
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"
|
||||||
|
}, "\u266A"),
|
||||||
React.createElement("button", {
|
React.createElement("button", {
|
||||||
onClick: function() { if (window.__openFxRack) window.__openFxRack(track.id, track.name); },
|
onClick: function() { if (window.__openFxRack) window.__openFxRack(track.id, track.name); },
|
||||||
className: "btn-daw h-[24px] rounded text-slate-300 font-semibold flex items-center justify-center text-[7px]" + ((track.fxChain || []).length > 0 ? " text-cyan-400" : ""),
|
className: "btn-daw h-[24px] rounded text-slate-300 font-semibold flex items-center justify-center text-[7px]" + ((track.fxChain || []).length > 0 ? " text-cyan-400" : ""),
|
||||||
@@ -8388,7 +8434,9 @@ const serializeTracksList = (tracksList, secondsPerBar) => {
|
|||||||
pan: t.pan || 0.0,
|
pan: t.pan || 0.0,
|
||||||
mute: t.muted || false,
|
mute: t.muted || false,
|
||||||
solo: t.solo || false,
|
solo: t.solo || false,
|
||||||
mastering_bypass: t.masteringBypass || false,
|
mastering_bypass: t.audioBypass || false,
|
||||||
|
audio_bypass: t.audioBypass || false,
|
||||||
|
midi_bypass: t.midiBypass || false,
|
||||||
instrument_id: t.instrumentId || null,
|
instrument_id: t.instrumentId || null,
|
||||||
instrument_program: t.instrumentProgram !== undefined ? t.instrumentProgram : null,
|
instrument_program: t.instrumentProgram !== undefined ? t.instrumentProgram : null,
|
||||||
instrument_name: t.instrumentName || null,
|
instrument_name: t.instrumentName || null,
|
||||||
@@ -8465,7 +8513,9 @@ const deserializeTracksList = (schemaTracks, secondsPerBar, sectionStore) => {
|
|||||||
pan: t.pan || 0.0,
|
pan: t.pan || 0.0,
|
||||||
muted: t.mute || false,
|
muted: t.mute || false,
|
||||||
solo: t.solo || false,
|
solo: t.solo || false,
|
||||||
masteringBypass: t.mastering_bypass || false,
|
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)),
|
||||||
color: t.color || (t.id === '1' ? '#0f766e' : '#1d4ed8'),
|
color: t.color || (t.id === '1' ? '#0f766e' : '#1d4ed8'),
|
||||||
startTime: t.start_time || 0,
|
startTime: t.start_time || 0,
|
||||||
height: t.height || 140,
|
height: t.height || 140,
|
||||||
@@ -12893,12 +12943,34 @@ const App = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Live per-track mastering bypass: updates the routing map + re-routes any
|
// Live per-track bypass (split into two buttons per user request):
|
||||||
// active track node immediately (called from MixerStrip's B button).
|
// - 'audio': bypass FX+mastering for AUDIO items (audio clips + section items)
|
||||||
|
// - 'midi' : bypass FX+mastering for MIDI items (soundfont instrument)
|
||||||
|
window.__setTrackBypass = function(trackId, which, on) {
|
||||||
|
if (which === 'audio') {
|
||||||
|
trackAudioBypassMap[trackId] = !!on;
|
||||||
|
trackMasteringBypassMap[trackId] = !!on;
|
||||||
|
// Live re-route the main track node AND every section sub-node of this
|
||||||
|
// track (keys "<trackId>_sub_<subId>") so section items bypass instantly.
|
||||||
|
Object.keys(activeTrackNodesRef.current).forEach(function(k) {
|
||||||
|
if (k === trackId || k.indexOf(trackId + '_sub_') === 0) {
|
||||||
|
const n = activeTrackNodesRef.current[k];
|
||||||
|
if (n && n.route) setMasteringRoute(n.route, !!on);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (which === 'midi') {
|
||||||
|
trackMidiBypassMap[trackId] = !!on;
|
||||||
|
try { if (updateSfRoutingRef.current) updateSfRoutingRef.current(); } catch (e) { }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Legacy single-button bypass (kept for backward compatibility): sets both.
|
||||||
window.__setTrackMasteringBypass = function(trackId, bypass) {
|
window.__setTrackMasteringBypass = function(trackId, bypass) {
|
||||||
|
trackAudioBypassMap[trackId] = !!bypass;
|
||||||
trackMasteringBypassMap[trackId] = !!bypass;
|
trackMasteringBypassMap[trackId] = !!bypass;
|
||||||
|
trackMidiBypassMap[trackId] = !!bypass;
|
||||||
const node = activeTrackNodesRef.current[trackId];
|
const node = activeTrackNodesRef.current[trackId];
|
||||||
if (node && node.route) setMasteringRoute(node.route, !!bypass);
|
if (node && node.route) setMasteringRoute(node.route, !!bypass);
|
||||||
|
try { if (updateSfRoutingRef.current) updateSfRoutingRef.current(); } catch (e) { }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Realtime mute/solo: applies the (patched) mute/solo state to every active
|
// Realtime mute/solo: applies the (patched) mute/solo state to every active
|
||||||
@@ -12913,7 +12985,11 @@ const App = () => {
|
|||||||
const audible = computeTrackAudibleGain(effective, t) > 0;
|
const audible = computeTrackAudibleGain(effective, t) > 0;
|
||||||
const wasAudible = trackAudibleRef.current[t.id] !== false;
|
const wasAudible = trackAudibleRef.current[t.id] !== false;
|
||||||
const node = activeTrackNodesRef.current[t.id];
|
const node = activeTrackNodesRef.current[t.id];
|
||||||
if (node) setTrackNodeGain(node, computeTrackAudibleGain(effective, t));
|
const audibleGain = computeTrackAudibleGain(effective, t);
|
||||||
|
if (node) setTrackNodeGain(node, audibleGain);
|
||||||
|
if (node && node.sfEntry) {
|
||||||
|
try { node.sfEntry.gain.setTargetAtTime(audibleGain, getAudioContext().currentTime, 0.02); } catch (e) {}
|
||||||
|
}
|
||||||
// MIDI items: FluidSynth mixes ALL channels into ONE shared gain node, so
|
// MIDI items: FluidSynth mixes ALL channels into ONE shared gain node, so
|
||||||
// the per-track gain cannot silence them. Every MIDI track owns a
|
// the per-track gain cannot silence them. Every MIDI track owns a
|
||||||
// dedicated channel (ensureTrackMidiChannel) → mute/solo via CC7 (channel
|
// dedicated channel (ensureTrackMidiChannel) → mute/solo via CC7 (channel
|
||||||
@@ -13450,7 +13526,11 @@ const App = () => {
|
|||||||
// sessionTabs declarations (TDZ-safe).
|
// sessionTabs declarations (TDZ-safe).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const all = [...(tracks || []), ...(sessionTabs || []).reduce((acc, s) => acc.concat(s.tracks || []), [])];
|
const all = [...(tracks || []), ...(sessionTabs || []).reduce((acc, s) => acc.concat(s.tracks || []), [])];
|
||||||
all.forEach(t => { trackMasteringBypassMap[t.id] = !!t.masteringBypass; });
|
all.forEach(t => {
|
||||||
|
trackMasteringBypassMap[t.id] = !!t.audioBypass;
|
||||||
|
trackAudioBypassMap[t.id] = !!t.audioBypass;
|
||||||
|
trackMidiBypassMap[t.id] = !!t.midiBypass;
|
||||||
|
});
|
||||||
const list = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : all;
|
const list = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : all;
|
||||||
list.forEach(t => {
|
list.forEach(t => {
|
||||||
const sig = (t.muted ? '1' : '0') + (t.solo ? '1' : '0') + ':' + (t.volumeDb ?? 0);
|
const sig = (t.muted ? '1' : '0') + (t.solo ? '1' : '0') + ':' + (t.volumeDb ?? 0);
|
||||||
@@ -13458,7 +13538,11 @@ const App = () => {
|
|||||||
trackMuteSoloSigRef.current[t.id] = sig;
|
trackMuteSoloSigRef.current[t.id] = sig;
|
||||||
const audible = computeTrackAudibleGain(list, t) > 0;
|
const audible = computeTrackAudibleGain(list, t) > 0;
|
||||||
const node = activeTrackNodesRef.current[t.id];
|
const node = activeTrackNodesRef.current[t.id];
|
||||||
if (node) setTrackNodeGain(node, computeTrackAudibleGain(list, t));
|
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) {}
|
||||||
|
}
|
||||||
// MIDI tracks: mirror the gain decision onto the channel CC7 volume so
|
// MIDI tracks: mirror the gain decision onto the channel CC7 volume so
|
||||||
// FluidSynth-rendered notes respect mute/solo too.
|
// FluidSynth-rendered notes respect mute/solo too.
|
||||||
if ((t.midiItems && t.midiItems.length > 0) || t.type === 'MIDI') {
|
if ((t.midiItems && t.midiItems.length > 0) || t.type === 'MIDI') {
|
||||||
@@ -16558,7 +16642,7 @@ const App = () => {
|
|||||||
volumeGainNode.connect(pannerNode);
|
volumeGainNode.connect(pannerNode);
|
||||||
pannerNode.connect(fadeGainNode);
|
pannerNode.connect(fadeGainNode);
|
||||||
// Route through mastering chain unless this track has mastering bypass ON.
|
// Route through mastering chain unless this track has mastering bypass ON.
|
||||||
const route = createMasteringRoute(context, { masteringBypass: !!trackMasteringBypassMap[st.trackId] }, masterBus);
|
const route = createMasteringRoute(context, { masteringBypass: !!trackAudioBypassMap[st.trackId] }, masterBus);
|
||||||
fadeGainNode.connect(route.routeGain);
|
fadeGainNode.connect(route.routeGain);
|
||||||
fadeGainNode.connect(route.dryGain);
|
fadeGainNode.connect(route.dryGain);
|
||||||
source.start(context.currentTime, offsetBuffer);
|
source.start(context.currentTime, offsetBuffer);
|
||||||
@@ -16893,12 +16977,50 @@ const App = () => {
|
|||||||
} else {
|
} else {
|
||||||
fxLegacyIn.connect(pannerNode);
|
fxLegacyIn.connect(pannerNode);
|
||||||
}
|
}
|
||||||
node = { gainNode, pannerNode, fxStopFn, analyserNode, route, fxEntry, fxLegacyIn, scopeAnalyserL, scopeAnalyserR };
|
// Independent MIDI/soundfont FX path (user request): SF audio flows
|
||||||
|
// gainNode → sfEntry → [own module instances] → sfOut → pan → master bus,
|
||||||
|
// 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;
|
||||||
|
if ((track.midiItems && track.midiItems.length > 0)) {
|
||||||
|
sfEntry = context.createGain();
|
||||||
|
sfOut = context.createGain();
|
||||||
|
sfPan = context.createStereoPanner();
|
||||||
|
sfPan.pan.setValueAtTime((track.pan ?? 0) / 100, context.currentTime);
|
||||||
|
sfAnalyser = context.createAnalyser();
|
||||||
|
sfAnalyser.fftSize = 2048;
|
||||||
|
// NOTE: sfEntry is NOT fed from gainNode — the FluidSynth output is
|
||||||
|
// routed into sfEntry directly by updateSfRouting(). Connecting
|
||||||
|
// gainNode → sfEntry here would LEAK every audio clip/section into the
|
||||||
|
// SF chain → masterBus.input → mastering even when bypassed (double
|
||||||
|
// audio + still processed). sfEntry.gain mirrors the track's audible
|
||||||
|
// gain (volume/mute/solo) instead.
|
||||||
|
sfEntry.gain.value = 1;
|
||||||
|
sfOut.connect(sfPan);
|
||||||
|
sfPan.connect(masterBus.input);
|
||||||
|
sfOut.connect(sfAnalyser);
|
||||||
|
const sfMods = fxChain.filter(m => m && m.active !== false).map(m => {
|
||||||
|
try { return createTrackFxModule(m.type || m, context, m.params); } catch (e) { return null; }
|
||||||
|
}).filter(Boolean);
|
||||||
|
let sfTail = sfEntry;
|
||||||
|
sfMods.forEach(mod => {
|
||||||
|
sfTail.connect(mod.input);
|
||||||
|
sfTail = mod.output;
|
||||||
|
});
|
||||||
|
sfTail.connect(sfOut);
|
||||||
|
}
|
||||||
|
node = { gainNode, pannerNode, fxStopFn, analyserNode, route, fxEntry, fxLegacyIn, scopeAnalyserL, scopeAnalyserR, sfEntry, sfOut, sfPan, sfAnalyser };
|
||||||
// Realtime mute/solo: apply the track's current mute/solo/volume state to
|
// Realtime mute/solo: apply the track's current mute/solo/volume state to
|
||||||
// the fresh node so items of muted/soloed tracks start correctly.
|
// the fresh node so items of muted/soloed tracks start correctly.
|
||||||
const trackList = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : [track];
|
const trackList = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : [track];
|
||||||
setTrackNodeGain(node, computeTrackAudibleGain(trackList, track));
|
const nodeAudibleGain = computeTrackAudibleGain(trackList, track);
|
||||||
|
setTrackNodeGain(node, nodeAudibleGain);
|
||||||
|
if (node.sfEntry) {
|
||||||
|
try { node.sfEntry.gain.setTargetAtTime(nodeAudibleGain, context.currentTime, 0.02); } catch (e) {}
|
||||||
|
}
|
||||||
activeTrackNodesRef.current[track.id] = node;
|
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);
|
||||||
updateSfRouting();
|
updateSfRouting();
|
||||||
}
|
}
|
||||||
return node.gainNode;
|
return node.gainNode;
|
||||||
@@ -16924,6 +17046,22 @@ const App = () => {
|
|||||||
tail = mod.output;
|
tail = mod.output;
|
||||||
});
|
});
|
||||||
tail.connect(node.fxLegacyIn);
|
tail.connect(node.fxLegacyIn);
|
||||||
|
// 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 => {
|
||||||
|
try { return createTrackFxModule(m.type || m, getAudioContext(), m.params); } catch (e) { return null; }
|
||||||
|
}).filter(Boolean);
|
||||||
|
let sfTail = node.sfEntry;
|
||||||
|
sfMods.forEach(mod => {
|
||||||
|
sfTail.connect(mod.input);
|
||||||
|
sfTail = mod.output;
|
||||||
|
});
|
||||||
|
sfTail.connect(node.sfOut);
|
||||||
|
}
|
||||||
|
if (node.sfPan && getAudioContext()) {
|
||||||
|
node.sfPan.pan.setTargetAtTime((track.pan ?? 0) / 100, getAudioContext().currentTime, 0.02);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('rebuildTrackFxGraph error:', e);
|
console.warn('rebuildTrackFxGraph error:', e);
|
||||||
}
|
}
|
||||||
@@ -16942,7 +17080,20 @@ const App = () => {
|
|||||||
const list = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : tracks;
|
const list = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : tracks;
|
||||||
const midiAudible = list.filter(t => (t.midiItems && t.midiItems.length > 0) && computeTrackAudibleGain(list, t) > 0);
|
const midiAudible = list.filter(t => (t.midiItems && t.midiItems.length > 0) && computeTrackAudibleGain(list, t) > 0);
|
||||||
if (midiAudible.length === 1) {
|
if (midiAudible.length === 1) {
|
||||||
const node = activeTrackNodesRef.current[midiAudible[0].id];
|
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.
|
||||||
|
if (node && node.sfEntry && window.SonicSF && window.SonicSF.setOutputDestination) {
|
||||||
|
window.SonicSF.setOutputDestination(node.sfEntry);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (node && node.gainNode && window.SonicSF && window.SonicSF.setOutputDestination) {
|
if (node && node.gainNode && window.SonicSF && window.SonicSF.setOutputDestination) {
|
||||||
window.SonicSF.setOutputDestination(node.gainNode);
|
window.SonicSF.setOutputDestination(node.gainNode);
|
||||||
return;
|
return;
|
||||||
@@ -16960,7 +17111,10 @@ const App = () => {
|
|||||||
window.__updateSfRouting = updateSfRouting;
|
window.__updateSfRouting = updateSfRouting;
|
||||||
window.__getTrackScopeAnalysers = (trackId) => {
|
window.__getTrackScopeAnalysers = (trackId) => {
|
||||||
const node = activeTrackNodesRef.current[trackId];
|
const node = activeTrackNodesRef.current[trackId];
|
||||||
if (!node || !node.scopeAnalyserL || !node.scopeAnalyserR) return null;
|
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 };
|
||||||
|
if (!node.scopeAnalyserL || !node.scopeAnalyserR) return null;
|
||||||
return { L: node.scopeAnalyserL, R: node.scopeAnalyserR, sr: getAudioContext().sampleRate };
|
return { L: node.scopeAnalyserL, R: node.scopeAnalyserR, sr: getAudioContext().sampleRate };
|
||||||
};
|
};
|
||||||
const getOrCreateSubTrackNode = (track, subTrack, context) => {
|
const getOrCreateSubTrackNode = (track, subTrack, context) => {
|
||||||
@@ -19544,7 +19698,7 @@ const App = () => {
|
|||||||
id: 'midi_track_' + now + '_' + idx,
|
id: 'midi_track_' + now + '_' + idx,
|
||||||
name: midiItem.name || (midiResult.length > 1 ? 'MIDI Track ' + (idx + 1) : (file.name || 'MIDI').replace(/\.midi?$/i, '')),
|
name: midiItem.name || (midiResult.length > 1 ? 'MIDI Track ' + (idx + 1) : (file.name || 'MIDI').replace(/\.midi?$/i, '')),
|
||||||
buffer: null, startTime: 0, volumeDb: 0, pan: 0,
|
buffer: null, startTime: 0, volumeDb: 0, pan: 0,
|
||||||
muted: false, solo: false, masteringBypass: false, color: colors[idx % colors.length],
|
muted: false, solo: false, masteringBypass: false, audioBypass: false, midiBypass: false, color: colors[idx % colors.length],
|
||||||
markers: [], serverFileId: null, clips: [], sections: [],
|
markers: [], serverFileId: null, clips: [], sections: [],
|
||||||
midiItems: [midiItem],
|
midiItems: [midiItem],
|
||||||
isArmed: false, monitoringEnabled: true,
|
isArmed: false, monitoringEnabled: true,
|
||||||
@@ -19607,6 +19761,8 @@ const App = () => {
|
|||||||
muted: false,
|
muted: false,
|
||||||
solo: false,
|
solo: false,
|
||||||
masteringBypass: false,
|
masteringBypass: false,
|
||||||
|
audioBypass: false,
|
||||||
|
midiBypass: false,
|
||||||
color: selectColor,
|
color: selectColor,
|
||||||
markers: [],
|
markers: [],
|
||||||
serverFileId: null,
|
serverFileId: null,
|
||||||
@@ -19795,6 +19951,8 @@ const App = () => {
|
|||||||
muted: false,
|
muted: false,
|
||||||
solo: false,
|
solo: false,
|
||||||
masteringBypass: false,
|
masteringBypass: false,
|
||||||
|
audioBypass: false,
|
||||||
|
midiBypass: false,
|
||||||
color: selectColor,
|
color: selectColor,
|
||||||
markers: [],
|
markers: [],
|
||||||
serverFileId: null,
|
serverFileId: null,
|
||||||
@@ -23026,7 +23184,7 @@ const App = () => {
|
|||||||
// selection exists yet. Previously this always overwrote the
|
// selection exists yet. Previously this always overwrote the
|
||||||
// selection with 0 → (contentEnd + 2 bars).
|
// selection with 0 → (contentEnd + 2 bars).
|
||||||
const hasSel = (selectionMode === 'local' && selLeft !== null && selRight !== null && selRight > selLeft)
|
const hasSel = (selectionMode === 'local' && selLeft !== null && selRight !== null && selRight > selLeft)
|
||||||
|| (selectionStart !== null && selectionEnd !== null && selectionEnd > selectionStart);
|
|| (selectionStart !== null && selectionEnd !== null);
|
||||||
if (!hasSel) {
|
if (!hasSel) {
|
||||||
const bpmVal = parseInt(bpm) || 120;
|
const bpmVal = parseInt(bpm) || 120;
|
||||||
const secPerBar = (60.0 / bpmVal) * 4;
|
const secPerBar = (60.0 / bpmVal) * 4;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -24,7 +24,7 @@
|
|||||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
<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/promptTemplateManager.js?v=202607281039"></script>
|
||||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||||
<script src="/static/js/app.precompiled.js?v=202608033500" defer></script>
|
<script src="/static/js/app.precompiled.js?v=202608034300" defer></script>
|
||||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@@ -1,3 +1,49 @@
|
|||||||
|
### [2026-08-03] Task: FIX LEAK — clip audio chảy qua sfEntry → masterBus.input → mastering (nguyên nhân "bypass vẫn qua mastering + tăng gain")
|
||||||
|
- **Tóm tắt thay đổi:** User (bundle mới nhất, log route ĐÚNG: track1 bypass routeGain=0/dryGain=1) vẫn nghe audioclip qua mastering + tăng gain quá mức. **NGUYÊN NHÂN THẬT**: patch "SF path độc lập" trước đó nối `gainNode.connect(sfEntry)` VĨNH VIỄN — clip audio ở gainNode chảy luôn vào đường SF (`sfEntry → sfModules → sfOut → sfPan → masterBus.input → mastering`) → clip nghe **2 đường cùng lúc** (dry + SF path qua mastering) dù bypass bật → tăng gain + vẫn bị mastering xử lý. FIX:
|
||||||
|
1. **XÓA `gainNode.connect(sfEntry)`** — SF output vào sfEntry TRỰC TIẾP qua `setOutputDestination(sfEntry)` (updateSfRouting), không đi qua gainNode → clip không bao giờ chảy vào SF chain.
|
||||||
|
2. **sfEntry.gain mirror audible gain** (volume/mute/solo) ở 3 nơi: getOrCreateTrackNode (khởi tạo), applyAllTrackMuteSolo, effect sync `[tracks, sessionTabs]` — SF vẫn tuân theo fader/mute/solo.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608034300)
|
||||||
|
- **Ghi chú/Test (nếu có):** BUILD OK 935143 bytes, node --check OK, `pytest` 86 passed. Verify: `gainNode.connect(sfEntry)` không còn trong bundle.
|
||||||
|
---
|
||||||
|
- **Tóm tắt thay đổi:** User yêu cầu rõ: **tắt nút A (xám) = phải bypass Mastering** — semantic đã đúng (xám = audioBypass=true = dry). Củng cố thêm: (1) `setMasteringRoute` giờ gán **`.value` TRỰC TIẾP** sau `cancelScheduledValues` (hard switch, không phụ thuộc automation queue — không thể trễ/treo); (2) thêm log `[Bypass] node created track <id> initial audioBypass=... routeGain=... dryGain=...` khi tạo track node — xác minh trạng thái ban đầu của route đúng với map. Rà lại MỌI path playback (startTrackPlayback, startLocalTrackPlayback loop-selection, section sub-nodes) đều qua track route → bypass áp cho audioclip + section ✓.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608034200)
|
||||||
|
- **Ghi chú/Test (nếu có):** BUILD OK 934318 bytes, node --check OK, `pytest` 86 passed. Log mới khi bấm A: `[Bypass] track <id> audioBypass=true → DRY BUS (bỏ mastering + bỏ track FX)`.
|
||||||
|
---
|
||||||
|
- **Tóm tắt thay đổi:** User dán log: `audioBypass=false routeGain.gain=0.000 dryGain.gain=1.000` — giá trị `.value` ĐẢO ngược so với cờ. Nguyên nhân: `setTargetAtTime` là automation TRỄ (timeConstant 0.02) — `.value` đọc ngay sau khi schedule vẫn hiển thị trạng thái CŨ → log gây hiểu lầm (không phải DSP sai). Fix: `setMasteringRoute` dùng `setValueAtTime` tại currentTime (áp dụng TỨC THÌ, không ramp) + log mục tiêu rõ ràng: `[Bypass] track <id> audioBypass=true → DRY BUS (bỏ mastering + bỏ track FX)` / `→ MASTERING CHAIN (qua FX + mastering)` — không còn log giá trị stale. Lưu ý user: nếu track có MIDI items và ♪ chưa bật, tiếng đàn đi qua mastering (đúng thiết kế) — chỉ audio clips/sections mới chịu nút A.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608034100)
|
||||||
|
- **Ghi chú/Test (nếu có):** BUILD OK 934145 bytes, node --check OK, `pytest` 86 passed.
|
||||||
|
---
|
||||||
|
- **Tóm tắt thay đổi:** Log `route is not defined` do patch trước: trong `createMasteringRoute` gán `route._trackId`/`route._bypass` nhưng biến `route` KHÔNG tồn tại trong hàm (hàm trả `{routeGain, dryGain}`; `route` là tên biến ở caller). Lỗi bắn ra MỖI lần tạo track node (play). Fix: tạo `routeObj` trước rồi gán `_trackId`/`_bypass` lên đó; `setMasteringRoute` đọc `route._trackId` hợp lệ (param).
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608034000)
|
||||||
|
- **Ghi chú/Test (nếu có):** BUILD OK 934143 bytes, node --check OK, `pytest` 86 passed.
|
||||||
|
---
|
||||||
|
- **Tóm tắt thay đổi:** User báo nút A vẫn đưa tín hiệu qua Mastering panel. Rà toàn bộ chuỗi: clip → gainNode → (bypass) dryGain → masterBus.dryInput → dryOutput → output — **bỏ hoàn toàn mastering modules** (wiring đã đúng từ Pha H; masterBus: dryInput.connect(dryOutput), dryOutput.connect(output); wet: input → compressor → inputAnalyser → [modules] → outputAnalyser → output). Section items qua sub-node → parent gainNode → cùng route ✓. Củng cố: (1) `setMasteringRoute` dùng `getAudioContext()` (loại bỏ nguy cơ return sớm khi audioCtx null) + lưu `route._trackId/_bypass`; (2) **console.log `[Bypass] track <id> audioBypass=... routeGain.gain=... dryGain.gain=... → dry bus (bỏ mastering)` mỗi lần toggle** — user mở DevTools console để xác nhận đường tín hiệu thực tế. **Lưu ý: MIDI items (♪ chưa bật) ĐI QUA mastering** (masterBus.input → modules) — đúng thiết kế tách A/♪; nếu user test track có MIDI và thấy "vẫn qua mastering", đó là tiếng đàn chứ không phải audio clips.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608033900)
|
||||||
|
- **Ghi chú/Test (nếu có):** BUILD OK 934112 bytes, node --check OK, `pytest` 86 passed.
|
||||||
|
---
|
||||||
|
- **Tóm tắt thay đổi:** Theo yêu cầu user: **nút không sáng (xám) = cho phép bypass đang bật; nút sáng = không bypass (xử lý bình thường)**. Đảo UI cả 2 strip (MixerStrip + TrackStripConsole): A bypass bật → xám, tắt → sáng xanh; ♪ bypass bật → xám, tắt → sáng tím. Củng cố DSP để bypass audio chắc chắn tác dụng:
|
||||||
|
1. `createMasteringRoute` **ưu tiên đọc `trackAudioBypassMap`** (sync từ state mỗi render) trước khi fallback track object — tránh stale object.
|
||||||
|
2. `__setTrackBypass('audio')` giờ **re-route LIVE mọi section sub-node** (`<trackId>_sub_<subId>`) cùng main node → audioclip item + section item bypass ngay khi bấm, kể cả đang play.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608033800)
|
||||||
|
- **Ghi chú/Test (nếu có):** BUILD OK 933696 bytes, node --check OK, `pytest` 86 passed. Tooltip nút ghi rõ: "XÁM = bypass đang bật, SÁNG = xử lý bình thường".
|
||||||
|
---
|
||||||
|
|
||||||
|
### [2026-08-03] Task: Fix màu nút bypass A/♪ + tách hoàn toàn đường SF (MIDI không còn lệ thuộc nút A)
|
||||||
|
- **Tóm tắt thay đổi:**
|
||||||
|
1. **Đường SF độc lập (fix lệ thuộc A)**: trước đây SF chảy qua chain DÙNG CHUNG (gainNode → fxEntry → ... → panner → routeGain) — khi bật nút A, routeGain=0 → MIDI mất FX dù ♪ chưa bật. Fix: track có midiItems giờ dựng **SF chain riêng**: `gainNode → sfEntry → [module instances RIÊNG (cùng loại/params)] → sfOut → sfPan → masterBus.input` — hoàn toàn tách khỏi chuỗi audio. `updateSfRouting`: ♪ off → SF → sfEntry (FX giữ nguyên dù A bật/tắt); ♪ on → SF → masterBus.dryInput. `rebuildTrackFxGraph` rebuild cả 2 chain + sync sfPan.pan. Scope rack: track MIDI hiển thị `sfAnalyser` (post-FX).
|
||||||
|
2. **Màu nút theo yêu cầu**: TrackStripConsole — A/♪ inactive = **xám** (`text-slate-500`), active = **sáng xanh** (A: `text-sky-300 bg-sky-500/30`) / **sáng tím** (♪: `text-fuchsia-300 bg-fuchsia-500/30`). MixerStrip bỏ fallback `?? masteringBypass` khỏi visual (state hoàn toàn độc lập: A chỉ đọc `audioBypass`, ♪ chỉ đọc `midiBypass`).
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608033700)
|
||||||
|
- **Ghi chú/Test (nếu có):** BUILD OK 933049 bytes, node --check OK, `pytest` 86 passed.
|
||||||
|
---
|
||||||
|
|
||||||
|
### [2026-08-03] Task: Tách nút bypass track thành 2 nút (Audio vs MIDI) + fix loop 0→18 khi click tempo track
|
||||||
|
- **Tóm tắt thay đổi:**
|
||||||
|
1. **Tách bypass**: track giờ có `audioBypass` + `midiBypass` (migrate từ `masteringBypass` cũ — deserialize: `audio_bypass ?? mastering_bypass`, `midi_bypass ?? mastering_bypass`; serialize ghi cả 3). UI MixerStrip + TrackStripConsole: thay nút B đơn bằng **nút [A]** (bypass audio items = clips + sections; route kép dry/route như cũ — `trackAudioBypassMap` + `createMasteringRoute` đọc `audioBypass ?? masteringBypass`) và **nút [♪]** (bypass MIDI riêng — `trackMidiBypassMap` + `updateSfRouting`: khi bật, SF output → `masterBus.dryInput` trực tiếp, bỏ FX + mastering nhưng giữ CC7 mute/solo). `window.__setTrackBypass(trackId, 'audio'|'midi', on)`; giữ `__setTrackMasteringBypass` (set cả 2) cho tương thích.
|
||||||
|
2. **Fix loop**: click lên tempo track (hoặc click đơn bất kỳ) tạo selection RỖNG (start==end) → `hasSel` cũ FALSE → nút loop auto-derive 0→18 ghi đè. Fix: `hasSel` giờ nhận mọi selection state có tồn tại (`selectionStart !== null && selectionEnd !== null` — kể cả start==end) → không bao giờ tự derive khi user đã click lane.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608033600)
|
||||||
|
- **Ghi chú/Test (nếu có):** BUILD OK 931110 bytes, node --check OK, `pytest` 86 passed.
|
||||||
|
---
|
||||||
|
|
||||||
### [2026-08-03] Task: Wave Observer + Module Vector Display Canvas trong FX Rack Panel (unified_fx_rack_panel_update.md)
|
### [2026-08-03] Task: Wave Observer + Module Vector Display Canvas trong FX Rack Panel (unified_fx_rack_panel_update.md)
|
||||||
- **Tóm tắt thay đổi:** Tích hợp theo spec cập nhật:
|
- **Tóm tắt thay đổi:** Tích hợp theo spec cập nhật:
|
||||||
1. **Scope analysers per-track (post-FX)**: `getOrCreateTrackNode` thêm `scopeSplitter` + `scopeAnalyserL/R` (fftSize 2048) nối từ `pannerNode` (output context, SAU FX chain — đúng sơ đồ spec §III.3); `analyserNode` chính tăng 256→2048. Expose `window.__getTrackScopeAnalysers(trackId)` → {L, R, sr}.
|
1. **Scope analysers per-track (post-FX)**: `getOrCreateTrackNode` thêm `scopeSplitter` + `scopeAnalyserL/R` (fftSize 2048) nối từ `pannerNode` (output context, SAU FX chain — đúng sơ đồ spec §III.3); `analyserNode` chính tăng 256→2048. Expose `window.__getTrackScopeAnalysers(trackId)` → {L, R, sr}.
|
||||||
|
|||||||
Reference in New Issue
Block a user