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
|
||||
// tracks state so ANY audio path can route without holding the track object.
|
||||
const trackMasteringBypassMap = {};
|
||||
const trackAudioBypassMap = {};
|
||||
const trackMidiBypassMap = {};
|
||||
|
||||
// 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);
|
||||
// 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 dryGain = ctx.createGain();
|
||||
const masterDest = bus ? bus.input : ctx.destination;
|
||||
@@ -80,18 +90,30 @@ function createMasteringRoute(ctx, track, bus) {
|
||||
dryGain.gain.value = bypass ? 1 : 0;
|
||||
routeGain.connect(masterDest);
|
||||
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) {
|
||||
if (!route || !audioCtx) return;
|
||||
const t = audioCtx.currentTime;
|
||||
if (!route) return;
|
||||
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);
|
||||
try {
|
||||
const ctx = (typeof getAudioContext === 'function') ? getAudioContext() : null;
|
||||
if (!ctx) return;
|
||||
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
|
||||
@@ -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 isMuted = track.muted;
|
||||
const isSoloed = track.solo;
|
||||
const isBypassed = track.masteringBypass;
|
||||
const isAudioBypassed = !!track.audioBypass;
|
||||
const isMidiBypassed = !!track.midiBypass;
|
||||
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';
|
||||
@@ -1114,14 +1137,25 @@ const MixerStrip = ({ track, index, onUpdateTrack, trackVuRefs }) => {
|
||||
}, "S"), React.createElement("button", {
|
||||
onClick: e => {
|
||||
e.stopPropagation();
|
||||
const next = !track.masteringBypass;
|
||||
if (onUpdateTrack) onUpdateTrack(track.id, { masteringBypass: next });
|
||||
const next = !(track.audioBypass ?? track.masteringBypass);
|
||||
if (onUpdateTrack) onUpdateTrack(track.id, { audioBypass: next });
|
||||
// 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",
|
||||
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")),
|
||||
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 " + (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 => {
|
||||
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", {
|
||||
className: "flex-1 flex items-stretch justify-center gap-1 px-1 py-1 min-h-0"
|
||||
}, React.createElement("div", {
|
||||
@@ -1472,7 +1506,7 @@ const TrackStripConsole = ({ track, index, onUpdateTrack, trackVuRefs }) => {
|
||||
var trackColor = track.color || '#06b6d4';
|
||||
var isMuted = track.muted;
|
||||
var isSoloed = track.solo;
|
||||
var isBypassed = track.masteringBypass;
|
||||
var isBypassed = !!(track.audioBypass ?? track.masteringBypass);
|
||||
var isArmed = track.isArmed;
|
||||
var trackName = track.name || 'Track ' + (index + 1);
|
||||
var isMicActive = track.inputSource?.deviceType === 'MICROPHONE';
|
||||
@@ -1618,14 +1652,26 @@ const TrackStripConsole = ({ track, index, onUpdateTrack, trackVuRefs }) => {
|
||||
React.createElement("button", {
|
||||
onClick: function(e) {
|
||||
e.stopPropagation();
|
||||
var next = !track.masteringBypass;
|
||||
if (onUpdateTrack) onUpdateTrack(track.id, { masteringBypass: next });
|
||||
var next = !(track.audioBypass ?? track.masteringBypass);
|
||||
if (onUpdateTrack) onUpdateTrack(track.id, { audioBypass: next });
|
||||
// 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" : ""),
|
||||
title: "Bypass: track KHÔNG qua FX + mastering ở Main out"
|
||||
}, React.createElement("i", { className: "fa-solid fa-bars-staggered text-[8px]" })),
|
||||
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"
|
||||
}, "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", {
|
||||
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" : ""),
|
||||
@@ -8388,7 +8434,9 @@ const serializeTracksList = (tracksList, secondsPerBar) => {
|
||||
pan: t.pan || 0.0,
|
||||
mute: t.muted || 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_program: t.instrumentProgram !== undefined ? t.instrumentProgram : null,
|
||||
instrument_name: t.instrumentName || null,
|
||||
@@ -8465,7 +8513,9 @@ const deserializeTracksList = (schemaTracks, secondsPerBar, sectionStore) => {
|
||||
pan: t.pan || 0.0,
|
||||
muted: t.mute || 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'),
|
||||
startTime: t.start_time || 0,
|
||||
height: t.height || 140,
|
||||
@@ -12893,12 +12943,34 @@ const App = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// Live per-track mastering bypass: updates the routing map + re-routes any
|
||||
// active track node immediately (called from MixerStrip's B button).
|
||||
// Live per-track bypass (split into two buttons per user request):
|
||||
// - '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) {
|
||||
trackAudioBypassMap[trackId] = !!bypass;
|
||||
trackMasteringBypassMap[trackId] = !!bypass;
|
||||
trackMidiBypassMap[trackId] = !!bypass;
|
||||
const node = activeTrackNodesRef.current[trackId];
|
||||
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
|
||||
@@ -12913,7 +12985,11 @@ const App = () => {
|
||||
const audible = computeTrackAudibleGain(effective, t) > 0;
|
||||
const wasAudible = trackAudibleRef.current[t.id] !== false;
|
||||
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
|
||||
// the per-track gain cannot silence them. Every MIDI track owns a
|
||||
// dedicated channel (ensureTrackMidiChannel) → mute/solo via CC7 (channel
|
||||
@@ -13450,7 +13526,11 @@ const App = () => {
|
||||
// sessionTabs declarations (TDZ-safe).
|
||||
useEffect(() => {
|
||||
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;
|
||||
list.forEach(t => {
|
||||
const sig = (t.muted ? '1' : '0') + (t.solo ? '1' : '0') + ':' + (t.volumeDb ?? 0);
|
||||
@@ -13458,7 +13538,11 @@ const App = () => {
|
||||
trackMuteSoloSigRef.current[t.id] = sig;
|
||||
const audible = computeTrackAudibleGain(list, t) > 0;
|
||||
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
|
||||
// FluidSynth-rendered notes respect mute/solo too.
|
||||
if ((t.midiItems && t.midiItems.length > 0) || t.type === 'MIDI') {
|
||||
@@ -16558,7 +16642,7 @@ const App = () => {
|
||||
volumeGainNode.connect(pannerNode);
|
||||
pannerNode.connect(fadeGainNode);
|
||||
// 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.dryGain);
|
||||
source.start(context.currentTime, offsetBuffer);
|
||||
@@ -16893,12 +16977,50 @@ const App = () => {
|
||||
} else {
|
||||
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
|
||||
// the fresh node so items of muted/soloed tracks start correctly.
|
||||
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;
|
||||
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();
|
||||
}
|
||||
return node.gainNode;
|
||||
@@ -16924,6 +17046,22 @@ const App = () => {
|
||||
tail = mod.output;
|
||||
});
|
||||
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) {
|
||||
console.warn('rebuildTrackFxGraph error:', e);
|
||||
}
|
||||
@@ -16942,7 +17080,20 @@ const App = () => {
|
||||
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);
|
||||
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) {
|
||||
window.SonicSF.setOutputDestination(node.gainNode);
|
||||
return;
|
||||
@@ -16960,7 +17111,10 @@ const App = () => {
|
||||
window.__updateSfRouting = updateSfRouting;
|
||||
window.__getTrackScopeAnalysers = (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 };
|
||||
};
|
||||
const getOrCreateSubTrackNode = (track, subTrack, context) => {
|
||||
@@ -19544,7 +19698,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, 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: [],
|
||||
midiItems: [midiItem],
|
||||
isArmed: false, monitoringEnabled: true,
|
||||
@@ -19607,6 +19761,8 @@ const App = () => {
|
||||
muted: false,
|
||||
solo: false,
|
||||
masteringBypass: false,
|
||||
audioBypass: false,
|
||||
midiBypass: false,
|
||||
color: selectColor,
|
||||
markers: [],
|
||||
serverFileId: null,
|
||||
@@ -19795,6 +19951,8 @@ const App = () => {
|
||||
muted: false,
|
||||
solo: false,
|
||||
masteringBypass: false,
|
||||
audioBypass: false,
|
||||
midiBypass: false,
|
||||
color: selectColor,
|
||||
markers: [],
|
||||
serverFileId: null,
|
||||
@@ -23026,7 +23184,7 @@ const App = () => {
|
||||
// selection exists yet. Previously this always overwrote the
|
||||
// selection with 0 → (contentEnd + 2 bars).
|
||||
const hasSel = (selectionMode === 'local' && selLeft !== null && selRight !== null && selRight > selLeft)
|
||||
|| (selectionStart !== null && selectionEnd !== null && selectionEnd > selectionStart);
|
||||
|| (selectionStart !== null && selectionEnd !== null);
|
||||
if (!hasSel) {
|
||||
const bpmVal = parseInt(bpm) || 120;
|
||||
const secPerBar = (60.0 / bpmVal) * 4;
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user