# TECHNICAL SPECIFICATION: INSTRUMENT MANAGEMENT & DATA ISOLATION FOR MULTI-ITEM PIANO ROLL TABS This document specifies the data architecture, state flow, and audio channel routing rules required to ensure that when a single Piano Roll Tab opens multiple `MIDIItems` simultaneously from different Tracks, modifying the instrument/synth engine for the active item applies exclusively to its parent track without affecting any other items or tracks. --- ## 1. DATA OWNERSHIP HIERARCHY To prevent cross-track instrument configuration leakage, the system enforces **Track-Level Ownership**: * **Track (`TrackState`):** The sole owner of synth engine configurations (`synth_engine`), assigned MIDI channel (`midi_channel`), and mixing parameters (`volume_db`, `pan`). * **MIDI Item (`MIDIItemState`):** Contains **no** independent synth configuration parameters. An item holds only its array of notes (`source_data.notes`) and a mandatory parent reference pointer (`parent_track_id`). ```text [ Main Session / Project State ] │ ├── Track 1 (id: "track_01", midi_channel: 0, synth_engine: "DSK_Pipa") │ └── Item 1 (id: "item_01", parent_track_id: "track_01") ──┐ │ │ └── Track 2 (id: "track_02", midi_channel: 1, synth_engine: "None")│ └── Item 2 (id: "item_02", parent_track_id: "track_02") ──┼─► [ Piano Roll Tab Context ] │ (Active Item Selector Dropdown) │ ├── Selected: Item 1 -> 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.