fix(media-explorer): bridge preview channel 15 + NOTE_ON timing; synth dropdown portal

This commit is contained in:
2026-08-16 16:58:18 +07:00
parent 574c2f885d
commit 223023615d
3 changed files with 32 additions and 10 deletions
+19 -6
View File
@@ -13657,6 +13657,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
synthListRef.current = synthList;
const synthInstRef = React.useRef(null);
synthInstRef.current = synthInst;
const btnSynthRef = React.useRef(null);
const [treeWidth, setTreeWidth] = React.useState(function() {
var saved = localStorage.getItem('studio_media_explorer_tree_width');
return saved ? parseInt(saved) : 176;
@@ -14272,7 +14273,13 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
const delayMs = Math.max(0, (at - ctx.currentTime) * 1000);
const pitch = note.pitch || 60;
const vel = note.velocity != null ? note.velocity : 0.8;
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: ch, pitch: pitch, velocity: vel, percussion: isPerc, live: true });
// NOTE_ON cũng delay theo beat (giống scheduleMidiNoteDispatch) — gửi
// ngay t=0 khi needLoad=true sẽ mất note vì SF2 chưa load xong trong
// bridge (uiWorker async bên C++).
setTimeout(() => {
if (selectTokenRef.current !== (o.token || selectTokenRef.current)) return;
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: ch, pitch: pitch, velocity: vel, percussion: isPerc, live: true });
}, delayMs);
setTimeout(() => {
if (selectTokenRef.current !== (o.token || selectTokenRef.current)) return;
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: ch, pitch: pitch, velocity: 0, percussion: isPerc, live: true });
@@ -15145,12 +15152,18 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
<i className="fa-solid fa-bolt text-[9px]"></i> <span>Auto-Play</span>
</button>
<div className="relative">
<button id="btnSynth" className={`h-6 px-2 border rounded-sm font-bold text-[10px] flex items-center gap-1 ${synthInst ? 'bg-gradient-to-r from-violet-600 to-fuchsia-600 text-white border-slate-700' : 'bg-[#e0e0e0] text-slate-600 border-[#707070]'}`} title="Chọn instrument để preview MIDI" onClick={toggleSynthDropdown}>
<button id="btnSynth" className={`h-6 px-2 border rounded-sm font-bold text-[10px] flex items-center gap-1 ${synthInst ? 'bg-gradient-to-r from-violet-600 to-fuchsia-600 text-white border-slate-700' : 'bg-[#e0e0e0] text-slate-600 border-[#707070]'}`} ref={btnSynthRef} title="Chọn instrument để preview MIDI" onClick={toggleSynthDropdown}>
<i className="fa-solid fa-music text-[9px]"></i> <span>Synth{synthInst ? ': ' + (synthInst.name || '?') : ''}</span>
<i className="fa-solid fa-caret-down text-[8px]"></i>
</button>
{synthOpen && (
<div className="absolute bottom-8 left-0 z-40 w-64 bg-white border border-[#808080] shadow-xl rounded-sm text-xs text-slate-800 font-sans max-h-72 overflow-y-auto">
{synthOpen && (() => {
// Portal ra document.body: dropdown absolute bottom-8 bị clip bởi
// overflow-hidden của root Media Explorer khi list dài (max-h-72
// trồi lên trên) → header + ô tìm kiếm mất khi mở lần 2+.
const btnRect = btnSynthRef.current ? btnSynthRef.current.getBoundingClientRect() : null;
return ReactDOM.createPortal(
<div className="fixed z-40 w-64 bg-white border border-[#808080] shadow-xl rounded-sm text-xs text-slate-800 font-sans max-h-72 overflow-y-auto"
style={{ left: btnRect ? btnRect.left : 0, bottom: btnRect ? (window.innerHeight - btnRect.top) + 8 : 0 }}>
<div className="sticky top-0 bg-[#e0e0e0] px-2 py-1 font-bold border-b border-[#a0a0a0] flex items-center justify-between z-20">
<span>Select Instrument</span>
<button onClick={() => setSynthOpen(false)} className="text-slate-500 hover:text-slate-900"><i className="fa-solid fa-xmark"></i></button>
@@ -15199,8 +15212,8 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
{!synthLoading && filteredSynthList && filteredSynthList.length === 0 && synthFilter && (
<div className="px-2 py-2 text-slate-400 italic">Không tìm thấy nhạc cụ trùng khớp</div>
)}
</div>
)}
</div>, document.body);
})()}
</div>
<div className="flex items-center gap-1" title="Tempo preview MIDI">
<span className="font-mono text-[10px] text-slate-700">Tempo:</span>
File diff suppressed because one or more lines are too long
+4 -1
View File
@@ -3,7 +3,7 @@
// bridge connected -> NativeBridgeService.dispatchMidiEvent (Rust SHM -> C++ bridge)
// bridge down -> onFallback callback (app.jsx wires SonicSF/Carla path).
(function () {
var MELODIC_CHANNELS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15]; // skip 9 (percussion)
var MELODIC_CHANNELS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14]; // skip 9 (percussion); 15 reserved for Media Explorer preview
var router = {
bridgeConnected: false,
@@ -24,6 +24,9 @@
/** trackId -> MIDI channel; percussion (bank 128) -> ch 9. */
allocateChannel: function (trackId, isPercussion) {
// Media Explorer preview dùng channel 15 cố định — tránh đè channel
// melodic track (0-8,10-14) / percussion (9) đang giữ instrument.
if (trackId === 'media_explorer') return 15;
if (this._channels[trackId]) return this._channels[trackId].ch;
var ch;
if (isPercussion) {