fix(ui): per-track volume/mute/solo now works over bridge — send CC7 via SonicMidiRouter pushEvent instead of local WASM controllerChange

This commit is contained in:
2026-08-14 10:56:24 +07:00
parent 35499b614c
commit d42abb2a41
2 changed files with 82 additions and 58 deletions
+56 -39
View File
@@ -442,6 +442,37 @@ function setTrackNodeGain(node, gainLinear) {
node.gainNode.gain.setTargetAtTime(g, t, 0.02);
}
// Track thc s phát âm MIDI/bridge (cn CC7 đ đi volume/mute/solo): có MIDI
// items, type MIDI, hoc instrument (VST3/soundfont qua bridge).
function isMidiSoundTrack(t) {
return !!(t && ((t.midiItems && t.midiItems.length > 0) || t.type === 'MIDI' || t.instrumentId ||
(t.synth_engine && (t.synth_engine.soundfont_id || t.synth_engine.plugin_path))));
}
// MIDI volume realtime: CC7 là đưng DUY NHT đi âm bridge bridge trn MI
// channel vào MT shared audio sink (bridgeAudioNode), per-track WebAudio
// gainNode KHÔNG nh hưng âm bridge. Bridge active gi qua
// SonicMidiRouter.pushEvent (đúng channel allocation ca note-on, trackId làm
// channel); bridge down SonicSF (WASM local) fallback.
function sendMidiVolumeCC7(t, list, audible) {
const volDb = (typeof t.volumeDb === 'number' && isFinite(t.volumeDb)) ? t.volumeDb : 0;
const volLin = volDb <= -50 ? 0 : Math.pow(10, volDb / 20);
const cc7 = audible ? Math.max(1, Math.min(127, Math.round(volLin * 100))) : 0;
try {
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive && window.SonicMidiRouter.isBridgeActive()) {
const isPerc = !!(t.synth_engine && t.synth_engine.soundfont_bank === 128) || !!t.is_percussion;
window.SonicMidiRouter.pushEvent({ cmd: 'CC', channel: t.id, pitch: 7, velocity: 0, data2: cc7, percussion: isPerc });
return;
}
} catch (e) {}
try {
if (window.SonicSF && typeof window.SonicSF.controllerChange === 'function') {
const ch = ensureTrackMidiChannel(t, list);
window.SonicSF.controllerChange(ch, 7, cc7);
}
} catch (e) {}
}
// MAIN SESSION end-time (seconds): endtime of the items ON the session's own
// tracks only audio clips, MIDI items, and section-item bounds. Section-TAB
// content is deliberately IGNORED here: when played inside the main session the
@@ -16095,27 +16126,11 @@ const App = () => {
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
// volume) applies realtime, even to notes already sounding.
// CC7 = VOLUME ca track (không ch 100/0) volume slider MIDI track tác
// dng qua channel volume (yêu cu channel RIÊNG cho tng track).
if ((t.midiItems && t.midiItems.length > 0) || t.type === 'MIDI') {
try {
if (window.SonicSF && typeof window.SonicSF.controllerChange === 'function') {
const ch = ensureTrackMidiChannel(t, effective);
var cc7 = 100;
if (audible) {
var volDb = (typeof t.volumeDb === 'number' && isFinite(t.volumeDb)) ? t.volumeDb : 0;
var volLin = volDb <= -50 ? 0 : Math.pow(10, volDb / 20);
cc7 = Math.max(1, Math.min(127, Math.round(volLin * 100)));
} else {
cc7 = 0;
}
window.SonicSF.controllerChange(ch, 7, cc7);
}
} catch (e) {}
// MIDI/bridge tracks: CC7 (channel volume) realtime even for notes
// already sounding. Bridge active qua SonicMidiRouter (bridge); bridge
// down SonicSF (WASM). CC7 = VOLUME ca track (không ch 100/0).
if (isMidiSoundTrack(t)) {
try { sendMidiVolumeCC7(t, effective, audible); } catch (e) {}
}
if (audible && !wasAudible) becameAudible = true;
trackAudibleRef.current[t.id] = audible;
@@ -16791,24 +16806,11 @@ const App = () => {
node.sfEntry.gain.setTargetAtTime(g, 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. CC7 = VOLUME track
// (không ch 100/0) volume slider MIDI tác dng qua channel volume.
if ((t.midiItems && t.midiItems.length > 0) || t.type === 'MIDI') {
try {
if (window.SonicSF && typeof window.SonicSF.controllerChange === 'function') {
const ch = ensureTrackMidiChannel(t, list);
var cc7 = 100;
if (audible) {
var volDb = (typeof t.volumeDb === 'number' && isFinite(t.volumeDb)) ? t.volumeDb : 0;
var volLin = volDb <= -50 ? 0 : Math.pow(10, volDb / 20);
cc7 = Math.max(1, Math.min(127, Math.round(volLin * 100)));
} else {
cc7 = 0;
}
window.SonicSF.controllerChange(ch, 7, cc7);
}
} catch (e) {}
// MIDI/bridge tracks: mirror the gain decision onto CC7 (channel volume)
// so FluidSynth/bridge-rendered notes respect mute/solo too. Bridge
// active SonicMidiRouter; down SonicSF. CC7 = VOLUME track.
if (isMidiSoundTrack(t)) {
try { sendMidiVolumeCC7(t, list, audible); } catch (e) {}
}
trackAudibleRef.current[t.id] = audible;
});
@@ -24392,6 +24394,17 @@ const App = () => {
if (sn && sn.gainNode) sn.gainNode.gain.setValueAtTime(volLinear, ctx.currentTime);
}
});
// MIDI/bridge track: gainNode WebAudio không đi âm bridge (bridge dùng MT
// shared sink) gi CC7 realtime qua SonicMidiRouter (bridge active) hoc
// SonicSF (WASM fallback).
const trkList = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : tracks;
const trk = trkList.find(x => x.id === trackId);
if (trk && isMidiSoundTrack(trk)) {
try {
const nt = { ...trk, volumeDb: val };
sendMidiVolumeCC7(nt, trkList, computeTrackAudibleGain(trkList, nt) > 0);
} catch (e) {}
}
};
const updateTrackPan = (trackId, val) => {
const beforeSnap = captureTrackSnapshot(trackId);
@@ -29697,6 +29710,7 @@ STRICT CONSTRAINTS:
onClick: e => {
e.stopPropagation();
toggleTrackMute(track.id);
if (window.__applyTrackMuteSolo) window.__applyTrackMuteSolo(track.id, { muted: !track.muted });
},
title: "Mute",
className: `px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 ${track.muted ? 'bg-red-950 text-red-400 border-red-700' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`
@@ -29704,6 +29718,7 @@ STRICT CONSTRAINTS:
onClick: e => {
e.stopPropagation();
toggleTrackSoloEvaluate(track.id);
if (window.__applyTrackMuteSolo) window.__applyTrackMuteSolo(track.id, { solo: !track.solo });
},
title: "Solo",
className: `px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 ${track.solo ? 'bg-amber-950 text-amber-400 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`
@@ -30270,12 +30285,14 @@ STRICT CONSTRAINTS:
onClick: e => {
e.stopPropagation();
toggleTrackMute(vTrack.id);
if (window.__applyTrackMuteSolo) window.__applyTrackMuteSolo(vTrack.id, { muted: !vTrack.muted });
},
className: `px-1.5 py-0.5 text-xs rounded font-mono font-bold border ${vTrack.muted ? 'bg-red-950 text-red-400 border-red-700' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`
}, "M"), /*#__PURE__*/React.createElement("button", {
onClick: e => {
e.stopPropagation();
toggleTrackSoloEvaluate(vTrack.id);
if (window.__applyTrackMuteSolo) window.__applyTrackMuteSolo(vTrack.id, { solo: !vTrack.solo });
},
className: `px-1.5 py-0.5 text-xs rounded font-mono font-bold border ${vTrack.solo ? 'bg-amber-950 text-amber-400 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`
}, "S"))), /*#__PURE__*/React.createElement("div", {
File diff suppressed because one or more lines are too long