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:
+56
-39
@@ -442,6 +442,37 @@ function setTrackNodeGain(node, gainLinear) {
|
|||||||
node.gainNode.gain.setTargetAtTime(g, t, 0.02);
|
node.gainNode.gain.setTargetAtTime(g, t, 0.02);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track thực sự phát âm MIDI/bridge (cần CC7 để đổi volume/mute/solo): có MIDI
|
||||||
|
// items, type MIDI, hoặc 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 NHẤT đổi âm bridge — bridge trộn MỌI
|
||||||
|
// channel vào MỘT shared audio sink (bridgeAudioNode), per-track WebAudio
|
||||||
|
// gainNode KHÔNG ảnh hưởng âm bridge. Bridge active → gửi qua
|
||||||
|
// SonicMidiRouter.pushEvent (đúng channel allocation của 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
|
// 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
|
// tracks only — audio clips, MIDI items, and section-item bounds. Section-TAB
|
||||||
// content is deliberately IGNORED here: when played inside the main session the
|
// content is deliberately IGNORED here: when played inside the main session the
|
||||||
@@ -16095,27 +16126,11 @@ const App = () => {
|
|||||||
if (node && node.sfEntry) {
|
if (node && node.sfEntry) {
|
||||||
try { node.sfEntry.gain.setTargetAtTime(audibleGain, getAudioContext().currentTime, 0.02); } catch (e) {}
|
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/bridge tracks: CC7 (channel volume) realtime — even for notes
|
||||||
// the per-track gain cannot silence them. Every MIDI track owns a
|
// already sounding. Bridge active → qua SonicMidiRouter (bridge); bridge
|
||||||
// dedicated channel (ensureTrackMidiChannel) → mute/solo via CC7 (channel
|
// down → SonicSF (WASM). CC7 = VOLUME của track (không chỉ 100/0).
|
||||||
// volume) applies realtime, even to notes already sounding.
|
if (isMidiSoundTrack(t)) {
|
||||||
// CC7 = VOLUME của track (không chỉ 100/0) — volume slider MIDI track tác
|
try { sendMidiVolumeCC7(t, effective, audible); } catch (e) {}
|
||||||
// dụng qua channel volume (yêu cầu channel RIÊNG cho từng 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) {}
|
|
||||||
}
|
}
|
||||||
if (audible && !wasAudible) becameAudible = true;
|
if (audible && !wasAudible) becameAudible = true;
|
||||||
trackAudibleRef.current[t.id] = audible;
|
trackAudibleRef.current[t.id] = audible;
|
||||||
@@ -16791,24 +16806,11 @@ const App = () => {
|
|||||||
node.sfEntry.gain.setTargetAtTime(g, getAudioContext().currentTime, 0.02);
|
node.sfEntry.gain.setTargetAtTime(g, getAudioContext().currentTime, 0.02);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
// MIDI tracks: mirror the gain decision onto the channel CC7 volume so
|
// MIDI/bridge tracks: mirror the gain decision onto CC7 (channel volume)
|
||||||
// FluidSynth-rendered notes respect mute/solo too. CC7 = VOLUME track
|
// so FluidSynth/bridge-rendered notes respect mute/solo too. Bridge
|
||||||
// (không chỉ 100/0) — volume slider MIDI tác dụng qua channel volume.
|
// active → SonicMidiRouter; down → SonicSF. CC7 = VOLUME track.
|
||||||
if ((t.midiItems && t.midiItems.length > 0) || t.type === 'MIDI') {
|
if (isMidiSoundTrack(t)) {
|
||||||
try {
|
try { sendMidiVolumeCC7(t, list, audible); } catch (e) {}
|
||||||
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) {}
|
|
||||||
}
|
}
|
||||||
trackAudibleRef.current[t.id] = audible;
|
trackAudibleRef.current[t.id] = audible;
|
||||||
});
|
});
|
||||||
@@ -24392,6 +24394,17 @@ const App = () => {
|
|||||||
if (sn && sn.gainNode) sn.gainNode.gain.setValueAtTime(volLinear, ctx.currentTime);
|
if (sn && sn.gainNode) sn.gainNode.gain.setValueAtTime(volLinear, ctx.currentTime);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// MIDI/bridge track: gainNode WebAudio không đổi âm bridge (bridge dùng MỘT
|
||||||
|
// shared sink) → gửi CC7 realtime qua SonicMidiRouter (bridge active) hoặc
|
||||||
|
// 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 updateTrackPan = (trackId, val) => {
|
||||||
const beforeSnap = captureTrackSnapshot(trackId);
|
const beforeSnap = captureTrackSnapshot(trackId);
|
||||||
@@ -29697,6 +29710,7 @@ STRICT CONSTRAINTS:
|
|||||||
onClick: e => {
|
onClick: e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleTrackMute(track.id);
|
toggleTrackMute(track.id);
|
||||||
|
if (window.__applyTrackMuteSolo) window.__applyTrackMuteSolo(track.id, { muted: !track.muted });
|
||||||
},
|
},
|
||||||
title: "Mute",
|
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'}`
|
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 => {
|
onClick: e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleTrackSoloEvaluate(track.id);
|
toggleTrackSoloEvaluate(track.id);
|
||||||
|
if (window.__applyTrackMuteSolo) window.__applyTrackMuteSolo(track.id, { solo: !track.solo });
|
||||||
},
|
},
|
||||||
title: "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'}`
|
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 => {
|
onClick: e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleTrackMute(vTrack.id);
|
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'}`
|
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", {
|
}, "M"), /*#__PURE__*/React.createElement("button", {
|
||||||
onClick: e => {
|
onClick: e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleTrackSoloEvaluate(vTrack.id);
|
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'}`
|
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", {
|
}, "S"))), /*#__PURE__*/React.createElement("div", {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user