From 08cd4d72cfb667b4c2bfe507259b7374d63a48a8 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Mon, 27 Jul 2026 21:28:43 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20s=E1=BB=ADa=20l=E1=BB=97i=20khi=20set=20?= =?UTF-8?q?instrument=20cho=20track=202=20th=C3=AC=20khi=20v=E1=BA=BD=20mi?= =?UTF-8?q?di=20note=20=E1=BB=9F=20track=201=20c=C5=A9ng=20ph=C3=A1t=20?= =?UTF-8?q?=C3=A2m=20thanh=20t=E1=BB=AB=20track=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/storage/sonicforge.db | Bin 110592 -> 110592 bytes md/41_INSTRUMENT.md | 230 ++++++++++++++++++++++++++++++++++++++ md/42_DOUBLEINST.md | 120 ++++++++++++++++++++ 3 files changed, 350 insertions(+) create mode 100644 md/41_INSTRUMENT.md create mode 100644 md/42_DOUBLEINST.md diff --git a/app/storage/sonicforge.db b/app/storage/sonicforge.db index bbd260a87435c4f09fe822809f13707b2af0960f..121e53984c4132aa725f7d3570e0814f85c1057d 100644 GIT binary patch delta 2298 zcmeHH+fN)-80RdPhzcxF8d(+#TZ>l6aL$}LXU?I>gcKYUrobpsoe ziOBGDD(1ZZ0c!JU49G?cOwV?|>3tP^Iw?{zkxE*6s3$ZLvm>LSK9lu?vUWIQTSL*X z9qPjx<7LN${Jj>)U9b0H9U2R-H@Vpi9S>o9QlE&#Eb4R&pgK{m>wA!&*dx={(cl_A{s5bD0eYxkmU#R z3*BA@hG{OApotvMS^|#G1^qiPCL6~}zB&#kbK^XmPFm8ra0&(Bexa*@Zp*E1=67$# z7&T;^$487D{QO{#j|dnmgz7~(>fbK(G<)uXi=-V%r-!6Ph@+iEfks_3%C2J;iPKP? z!5zJqf>VwYlcRB&6&BH9W-S1+|E&_~=7+{9x^F*YK{R0;Y7Y`$%>x+Xn}I`eft zUBVZu-ChAxaG56YNMe*&#DG5*0|(%5D^XTs^#mn#`I;M2uUKi}$46+lXS?#waAI;T@vV`4G}cUFUn zkA#|A3G$!xOa4Uim@zz*%bkUpjiNsl+x+wQcKKAp;Z0cz;Q~kP^n{Mbp?x!0O*Eks zNESN5nU^k8v59ptjP2<3v~6)UrC=py27J8Q33u-w@UoapA4^T7vx#gP{A(9c`5yhr1dgCY@qTOlINk!(u7Eqg0DpG6!6~6C&7fG#;PI$YCWIwI2diR5OK#&GEG$ zys^H|UH6=u3}Wl2ygxY|D-%qoF>g d32N$8V9DGkhhHD>eLi@!|Mu0}%eUUW`VabTu-yOv delta 2536 zcmeHI&2Jk;6wh;$s4Z$^=YtZ*Ew-apRhzo=u{*n)9{Pnq2#LA}Dh`G1+Ho2?w$~qP z#|adb3vfXMMrGgtq)C&u5E|O5iIOOh_z#LiFBFMzMu2*Of;cd1yLE!9-jVvG=RNp0 z^M3Em?>BRPt@Zp`>v#|B_%LuBc3eL*J=X%!CWu~t`+3;1vfH!8eC6={#7*tyeGucv zAP7XjLFm{VjwY?F9i$@k5>PR2!WI#H0rtwj@1BA|`I$Hl2l%=9t(#R%F^FMk{Db+) zeX6EntQoAN7$ZiwFPuv{@u6_PM*G4=$0|5xEMYm}eiYHfw^yKx+Y5c|)ipX&%DMb# zn(2;7#PV~{vnire{_%2HymJW-)IpT#C1;c*3R#o#?-u==P({#7 zkd?CtCSwJYiN9ySzX?ZZt%uty+dUMEdy}xgR@|Uei#fyPR3&d};;qwFuqt`VsZB$@ zVMApG8dFFvp_;n*X&U_eqvbtfb_OIt<`VQ)2V>|c9re01Cgb-0LN5W4NX3 zOLRpqv+M|p=S@ubu2nzRSGwCa!mCtkc!HF4@vNd50}@X~WD5MPG=eZ=B0mkm|IVIJ zLX$bnw6dj|SWHYp57$@sh<7hN=u0uMGC(sG%N{YYD(0@rNm4oXdzYkiI+hGv?Or1^ z=Va7uDw#DgYlu$OH;Rr%80Eh&_PPc9qWc(Z6Ym{_o&1~Cyf}FZx|*XeMvuCBC*t4) z_;_%BFCc#RR!07g-wKN(6A-AXogr*UwJR++A}23Cn}BS$>jK?S)sa$OU(Z1kxmTgH zxv8niI|Xar8@HQ-&st8>&Q_gJbge+JO4r4MvsVlQsiKI01LzZSUqbT~OAmBq0gKgj@@XOG_+m?Ld%`dA$mIMe!telY^$`F&eBJtq} zmJAx9xt{9v5GF28!~R+=qA8VOb+kClM#Sx}AkZ%DM6t0x++*%+G)gJL#j&`=(nTM9@v=yq@wl?U{ zbX@!>-PlYbgu3l@iNB`cko19^h$0VeOu=Ud2gjtGqbeyu&1!FWmCVX@7Y)wP1AH7sMvk_YL`mA+e zP_0tYv_dgUP3+#E{ub#TtnyEmgQEX3Y_A_?x43f|cGpb7FcF?BaQdM|#NZXck2gk0 za(^QKC-Q%2f&ZKQtvOJukB8zu9`fypmkNbM)-j!ACK2ve2u=e6VSDFSKrYeKE3nD;` Scope: Track 1 + └── └── Inactive: Item 2 -> Scope: Track 2 + +``` + +--- + +## 2. MULTI-ITEM PIANO ROLL TAB STATE STRUCTURE + +When a user opens one or more `MIDIItems` inside the same Piano Roll Tab, the Tab Context State maintains a list of open item IDs along with an `active_item_id` representing the item currently selected in the toolbar dropdown: + +```javascript +// Tab Context state structure for a Piano Roll Tab editing multiple items +const multiItemPianoRollTabContext = { + tab_id: "tab_pianoroll_multi_editor", + title: "Piano Roll Editor", + type: "PIANO_ROLL_TAB", + + // 1. Array of all MIDI Item IDs currently loaded in this Tab + open_item_ids: ["item_01", "item_02"], + + // 2. ID of the item currently selected for direct editing via the Toolbar Dropdown + active_item_id: "item_01", + + // 3. Dynamic Computed Context (Derived State based on active_item_id) + active_scope: { + item_id: "item_01", + parent_track_id: "track_01", // Reverse pointer to Track 1 + midi_channel: 0, // Dedicated MIDI Channel for Track 1 + current_synth_engine: { + type: "soundfont", + plugin_id: "dsk_asian_dreamz", + soundfont_bank: 0, + soundfont_program: 0 // Pipa + } + } +}; + +``` + +--- + +## 3. ISOLATED INSTRUMENT SELECTION WORKFLOW + +### Step 1: User switches `active_item_id` in the Toolbar Dropdown + +When the user selects `MIDI Item 2` from the Piano Roll Tab dropdown: + +1. The Tab Controller receives a `SWITCH_PIANO_ROLL_ACTIVE_ITEM` event. +2. The system queries `MIDI Item 2` for its `parent_track_id` (e.g., returning `"track_02"`). +3. The controller reads the current `synth_engine` configuration directly from `Track 2`. +4. The Piano Roll Tab's Synth button label updates to reflect `Track 2`'s instrument (or displays `🎵 None (Default Synth)` if unassigned). + +### Step 2: User changes the instrument in the Piano Roll Synth Menu + +When the user opens the Synth menu on the Piano Roll toolbar and selects a new instrument (e.g., selecting `Vital VST3` or `Saxophone SoundFont`): + +1. The UI resolves the current `active_item_id` (`"item_01"`). +2. The UI queries the parent track ID: `targetTrackId = getItemParentTrackId(active_item_id)`. +3. The system dispatches an isolated mutation action: +```javascript +dispatch({ + type: "UPDATE_TRACK_SYNTH_ENGINE", + payload: { + track_id: targetTrackId, // Modifies ONLY "track_01"; "track_02" remains untouched + synth_engine: { + type: "vst3", + plugin_id: "Vital", + soundfont_bank: 0, + soundfont_program: 0 + } + } +}); + +``` + + + +--- + +## 4. AUDIO CHANNEL ISOLATION + +To ensure that triggering notes on `Item 1` plays the Pipa sound while `Item 2` plays the Vital patch without audio cross-talk, dedicated MIDI channels are assigned per track: + +### MIDI Channel Binding Rules + +| Track | MIDI Item | Parent Track ID | Dedicated MIDI Channel | Applied Instrument | +| --- | --- | --- | --- | --- | +| **Track 1** | MIDI Item 1 | `track_01` | Channel 0 | DSK_Asian_DreamZ (Pipa) | +| **Track 2** | MIDI Item 2 | `track_02` | Channel 1 | Vital.vst3 | + +* **When configuring Instrument for Track 1:** +The Client SoundEngine/Wasm routes configuration changes exclusively to Channel 0: +```javascript +soundFontPlayerInstance.selectInstrument(channel = 0, bank = 0, program = 0); + +``` + + +* **When configuring Instrument for Track 2:** +The Client SoundEngine/Wasm routes configuration changes exclusively to Channel 1: +```javascript +soundFontPlayerInstance.selectInstrument(channel = 1, bank = 0, program = 56); + +``` + + +* **When previewing notes in the Piano Roll:** +* If `MIDI Item 1` is active $\rightarrow$ Dispatch `noteOn(channel = 0, pitch, velocity)`. +* If `MIDI Item 2` is active $\rightarrow$ Dispatch `noteOn(channel = 1, pitch, velocity)`. + + + +--- + +## 5. CORE SERVICE IMPLEMENTATION (`pianoRollTabService.js`) + +```javascript +// app/static/js/services/pianoRollTabService.js + +/** + * State Manager and Dispatcher for Multi-Item Piano Roll Tabs + */ +export class PianoRollTabManager { + constructor(sessionState, soundEngine) { + this.sessionState = sessionState; + this.soundEngine = soundEngine; + } + + /** + * Retrieves parent Track by Item ID + */ + getParentTrackByItemId(itemId) { + for (const track of this.sessionState.main_session.tracks) { + const item = track.items.find(i => i.id === itemId); + if (item) return track; + } + return null; + } + + /** + * Updates instrument configuration from the Piano Roll Tab Toolbar + * @param {string} activeItemId - Currently selected Item ID in dropdown + * @param {Object} newSynthConfig - New Synth configuration object + */ + setInstrumentFromPianoRoll(activeItemId, newSynthConfig) { + const parentTrack = this.getParentTrackByItemId(activeItemId); + + if (!parentTrack) { + console.error(`[PianoRoll] Parent Track not found for Item ID: ${activeItemId}`); + return; + } + + console.log(`[PianoRoll] Applying new instrument to Track "${parentTrack.name}" (ID: ${parentTrack.id})`); + + // 1. Update state exclusively on the parent Track + parentTrack.synth_engine = { ...newSynthConfig }; + + // 2. Resolve parent Track's dedicated MIDI Channel + const trackIndex = this.sessionState.main_session.tracks.findIndex(t => t.id === parentTrack.id); + const dedicatedMidiChannel = trackIndex % 16; // Assign channels 0-15 + + // 3. Dispatch instrument change to the Client Sound Engine ONLY FOR THIS CHANNEL + if (this.soundEngine) { + this.soundEngine.selectInstrument( + dedicatedMidiChannel, + newSynthConfig.soundfont_bank || 0, + newSynthConfig.soundfont_program || 0 + ); + } + + // 4. Dispatch UI re-render event + window.dispatchEvent(new CustomEvent('DAW_STATE_UPDATED', { detail: this.sessionState })); + } + + /** + * Switches the active item in the toolbar dropdown + */ + switchActiveItem(tabContext, newActiveItemId) { + tabContext.active_item_id = newActiveItemId; + + const parentTrack = this.getParentTrackByItemId(newActiveItemId); + if (parentTrack) { + tabContext.active_scope = { + item_id: newActiveItemId, + parent_track_id: parentTrack.id, + current_synth_engine: parentTrack.synth_engine || { type: "none" } + }; + } + + return tabContext; + } +} + +``` + +--- + +## 6. UI/UX SAFETY CHECKS & ERROR PREVENTION + +* **Explicit Parent Track Indicators on Toolbar:** +Next to the MIDI Item dropdown, the Piano Roll toolbar explicitly displays context labels: +`[ Item Selector: MIDI Item 1 ▾ ] ── (Belongs to: Track 1)` +The Synth Selector button displays: `[ 🎵 Synth (Track 1): DSK_Pipa ▾ ]`. +* **Isolated Event Bus Mutators:** +When invoking `setTrackInstrument`, global setters such as `setAllTracksInstrument()` or `global_synth_engine` mutations are strictly forbidden. Every state mutation function requires an explicit `track_id` parameter. +* **Multi-Item Ghost Notes Rendering:** +The item selected in the dropdown (`active_item_id`) is the sole editable item. All other items listed in `open_item_ids` render automatically as read-only Ghost Notes for visual reference without mixing note data or instrument parameters. \ No newline at end of file diff --git a/md/42_DOUBLEINST.md b/md/42_DOUBLEINST.md new file mode 100644 index 0000000..259263f --- /dev/null +++ b/md/42_DOUBLEINST.md @@ -0,0 +1,120 @@ +# DIAGNOSIS & BUG FIX: ENABLING ARM ON TRACK 1 CAUSES TRACK 1 INSTRUMENT TO OVERRIDE TRACK 2 + +--- + +## 1. ROOT CAUSE ANALYSIS + +The issue where arming Track 1 causes Track 1's instrument to override or mute Track 2 stems from two common architectural bugs: + +### 🔴 Cause 1: MIDI Channel Collision (Most Common) + +* **Current State:** Both Track 1 and Track 2 share the default MIDI Channel (`Channel 0`) on the Synth Engine (`SpessaSynth` / `FluidSynth`). +* **Bug Sequence:** +1. Initially, Track 2 assigns its instrument patch to `Channel 0`. +2. When you arm Track 1, the UI issues a patch change command: `selectInstrument(channel = 0, bank_track1, program_track1)`. +3. This call **overwrites** `Channel 0`'s instrument patch with Track 1's instrument. +4. When the Timeline plays back over Track 2, Track 2 still reads notes on `Channel 0`. Consequently, all notes on Track 2 play using Track 1's instrument sound, or get muted completely if the voice allocation limit is exceeded. + + + +### 🔴 Cause 2: Hardcoded Live MIDI Channel Handler + +* When pressing keys on a hardware MIDI Keyboard, the `onmidimessage` handler sends a fixed `noteOn(0, pitch, velocity)` call to `Channel 0`. +* If Track 1 is armed and triggers `programChange(0, prog1)` while Track 2 on the timeline also feeds notes into `Channel 0`, live previews and timeline playback collide on the exact same audio channel. + +--- + +## 2. TECHNICAL SOLUTION & FIX CODEBASE + +To allow two tracks to play completely distinct instruments simultaneously—even with ARM Live Monitoring active—the system must enforce **Dedicated MIDI Channel Binding**: + +### Step 1: Assign an Independent MIDI Channel Per Track (`sessionStore`) + +During initialization or when adding a track to the session, allocate a distinct MIDI channel (from 0 to 15) to each track: + +```javascript +// Assigns a dedicated MIDI Channel based on the Track's index in the session +export function getDedicatedMIDIChannel(trackIndex) { + // Channel 9 (10th channel) is reserved for Percussion/Drums + if (trackIndex === 9) return 10; + return trackIndex % 16; +} + +``` + +### Step 2: Update Patch Selection Commands to Target Only the Assigned Track Channel + +When selecting an instrument or when Track 1 is armed, apply patch changes exclusively to Track 1's assigned MIDI channel: + +```javascript +// app/static/js/services/soundfontPlayer.js + +export function setTrackInstrument(track, trackIndex, soundEngine) { + const dedicatedChannel = getDedicatedMIDIChannel(trackIndex); + const synthConfig = track.synth_engine || {}; + + const bank = synthConfig.soundfont_bank || 0; + const program = synthConfig.soundfont_program || 0; + + // Change patch ONLY on this track's assigned channel; do NOT touch other channels + soundEngine.selectInstrument(dedicatedChannel, bank, program); + + console.log(`[DAW Router] Track "${track.name}" mapped to Channel ${dedicatedChannel} (Bank:${bank}, Program:${program})`); +} + +``` + +### Step 3: Route Live Hardware MIDI Keyboard Signals to the Armed Track's Assigned Channel + +When the hardware MIDI keyboard emits events, identify the currently armed track and route `noteOn` / `noteOff` messages directly to that track's designated MIDI channel: + +```javascript +// app/static/js/services/midiHandler.js + +export function handleLiveMIDIMessage(event, sessionState, soundEngine) { + if (!event || !event.data || event.data.length < 3) return; + + const [statusByte, pitch, velocityByte] = event.data; + const command = statusByte >> 4; + + // 1. Locate the currently ARMED [R] track on the UI + const armedTrackIndex = sessionState.main_session.tracks.findIndex(t => t.is_armed); + + if (armedTrackIndex === -1) { + // No track armed -> Suppress live preview + return; + } + + // 2. Resolve the dedicated MIDI channel for the armed track + const targetChannel = getDedicatedMIDIChannel(armedTrackIndex); + + // 3. Route live Note On / Note Off messages to the resolved target channel + if (command === 0x9 && velocityByte > 0) { + soundEngine.noteOn(targetChannel, pitch, velocityByte / 127.0); + } else if (command === 0x8 || (command === 0x9 && velocityByte === 0)) { + soundEngine.noteOff(targetChannel, pitch); + } +} + +``` + +--- + +## 3. STANDARD AUDIO ROUTING MATRIX + +| Object / Criteria | Track 1 (Violin) | Track 2 (Piano) | +| --- | --- | --- | +| **ARM State** | 🔴 ARMED (ON) | ⚪ DISARMED (OFF) | +| **Assigned MIDI Channel** | `Channel 0` | `Channel 1` | +| **Synth Command** | `selectInstrument(ch=0, bank=0, prog=40)` | `selectInstrument(ch=1, bank=0, prog=0)` | +| **Live Keyboard Source** | Keypress on SE49 $\rightarrow$ `noteOn(ch=0, pitch, vel)` | Does not receive live key events | +| **Timeline Play Source** | Emits notes from Item 1 $\rightarrow$ `noteOn(ch=0)` | Emits notes from Item 2 $\rightarrow$ `noteOn(ch=1)` | +| **Audio Output Result** | Smooth Violin output | Simultaneous Piano output without voice overriding | + +--- + +## 4. VERIFICATION & BUG FIX CHECKLIST + +* [ ] Console logs on project load confirm that Track 1 and Track 2 reside on separate channels (`Channel 0` and `Channel 1`). +* [ ] Arming Track 1 $\rightarrow$ Playing keys on SE49 outputs Track 1's instrument sound. +* [ ] Pressing Timeline Play $\rightarrow$ Track 2 outputs its assigned instrument sound on `Channel 1` in parallel with Track 1. \ No newline at end of file