Files
SonicForgeStudio/md/41_INSTRUMENT.md
T

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:

  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:
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 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)

// 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.