8.7 KiB
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).
[ 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:
// 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:
- The Tab Controller receives a
SWITCH_PIANO_ROLL_ACTIVE_ITEMevent. - The system queries
MIDI Item 2for itsparent_track_id(e.g., returning"track_02"). - The controller reads the current
synth_engineconfiguration directly fromTrack 2. - 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):
- The UI resolves the current
active_item_id("item_01"). - The UI queries the parent track ID:
targetTrackId = getItemParentTrackId(active_item_id). - The system dispatches an isolated mutation action:
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:
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:
soundFontPlayerInstance.selectInstrument(channel = 1, bank = 0, program = 56);
- When previewing notes in the Piano Roll:
- If
MIDI Item 1is active\rightarrowDispatchnoteOn(channel = 0, pitch, velocity). - If
MIDI Item 2is active\rightarrowDispatchnoteOn(channel = 1, pitch, velocity).
5. CORE SERVICE IMPLEMENTATION (pianoRollTabService.js)
// 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 assetAllTracksInstrument()orglobal_synth_enginemutations are strictly forbidden. Every state mutation function requires an explicittrack_idparameter. - Multi-Item Ghost Notes Rendering:
The item selected in the dropdown (
active_item_id) is the sole editable item. All other items listed inopen_item_idsrender automatically as read-only Ghost Notes for visual reference without mixing note data or instrument parameters.