Files
SonicForgeStudio/md/42_DOUBLEINST.md
T

5.2 KiB

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:

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

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

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