fix: sửa lỗi khi set instrument cho track 2 thì khi vẽ midi note ở track 1 cũng phát âm thanh từ track 2
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user