fix(media-explorer): bridge preview channel 15 + NOTE_ON timing; synth dropdown portal
This commit is contained in:
+19
-6
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user