174 lines
6.5 KiB
Markdown
174 lines
6.5 KiB
Markdown
# DIAGNOSTIC REPORT: WHY MIDI SIGNAL IS RECEIVED BUT NOT RECORDED / RENDERED
|
|
|
|
---
|
|
|
|
## 1. Executive Summary & Root Cause Analysis
|
|
|
|
Based on the DAW UI screenshot provided, the system is successfully receiving MIDI hardware signals (as indicated by the active VU meter on Track 01 set to `MIDIIN2 (SE49)`), but no MIDI data is being written or displayed on the timeline.
|
|
|
|
This issue occurs due to four architectural and state-management gaps in the current implementation.
|
|
|
|
---
|
|
|
|
## 2. Detailed Root Causes
|
|
|
|
### Root Cause 1: Global Transport Record vs. Track Arm Disconnect
|
|
|
|
* **Observed State:** Track 01 has its individual Arm `[R]` button active (red indicator ON). However, the Global Transport Record button (red circle on the top toolbar) is inactive/stopped at time position `0:01.951`.
|
|
* **Technical Issue:** Arming a track only enables Live Monitoring (routing MIDI input to the virtual synth engine for real-time audio playback). Recording MIDI into timeline buffers requires both **Track Arm = `true**` AND **Transport Engine State = `RECORDING**`.
|
|
|
|
```text
|
|
[ Track Armed ] + [ Transport STOPPED ] --> Live Monitoring ONLY (VU meter lights up, no recording)
|
|
[ Track Armed ] + [ Transport RECORDING ] --> Live Monitoring + Event Buffer Write + Canvas Redraw
|
|
|
|
```
|
|
|
|
### Root Cause 2: Gate Condition in `handleMIDIMessage`
|
|
|
|
In the client recording engine (`ClientMIDIRecorder`), incoming MIDI events trigger live synth audio, but note recording is gated behind a transport flag:
|
|
|
|
```javascript
|
|
handleMIDIMessage(event) {
|
|
// BUG: If global transport is not in RECORD mode, execution stops here.
|
|
// Synth gets triggered elsewhere, but recordedNotes array remains empty.
|
|
if (!this.isRecording) return;
|
|
|
|
const [status, pitch, velocity] = event.data;
|
|
// ... logic to write to activeNotes and recordedNotes
|
|
}
|
|
|
|
```
|
|
|
|
### Root Cause 3: Absence of Real-Time Canvas Redraw Loop
|
|
|
|
For notes to render dynamically inside the MIDI Item clip as keys are pressed:
|
|
|
|
* The UI Canvas must run a `requestAnimationFrame` render loop while `isRecording === true`.
|
|
* The renderer must query the `activeNotes` Map (currently held keys) in addition to finalized `recordedNotes`.
|
|
* If the UI only renders on static session updates (e.g., when clicking or stopping transport), live notes will not appear on screen during playback.
|
|
|
|
### Root Cause 4: Track Target ID Unbound to Input Stream
|
|
|
|
If multiple tracks exist, `ClientMIDIRecorder` must know which `track_id` is currently armed and matched to device `MIDIIN2 (SE49)`. If events arrive without a target track context, they cannot be routed into the target `MIDIItem.source_data.notes` array.
|
|
|
|
---
|
|
|
|
## 3. Technical Solutions & Code Adjustments
|
|
|
|
### Step 1: Ensure Dual-Stage Recording State Verification
|
|
|
|
Update the transport control logic so pressing **Record + Play** on the top toolbar initializes active record buffers on all armed tracks:
|
|
|
|
```javascript
|
|
// Transport Controller
|
|
function startTransportRecording() {
|
|
const armedTracks = session.tracks.filter(t => t.is_armed);
|
|
|
|
if (armedTracks.length === 0) {
|
|
console.warn("No tracks armed for recording.");
|
|
startPlaybackOnly();
|
|
return;
|
|
}
|
|
|
|
// Activate global transport record state
|
|
transport.isRecording = true;
|
|
transport.isPlaying = true;
|
|
|
|
// Initialize temporary recording items on each armed track
|
|
armedTracks.forEach(track => {
|
|
const newRecordingItem = {
|
|
id: `rec_item_${Date.now()}`,
|
|
type: "MIDI_ITEM",
|
|
start_bar: transport.currentBar,
|
|
duration_bars: 0.1, // Expands dynamically during recording
|
|
clip_start_offset_bars: 0.0,
|
|
source_data: { total_buffer_bars: 8.0, notes: [] }
|
|
};
|
|
|
|
track.activeRecordingItem = newRecordingItem;
|
|
midiRecorder.start(track.id, transport.currentBar);
|
|
});
|
|
|
|
// Start UI animation loop for live waveform/note preview
|
|
requestAnimationFrame(renderLiveRecordingUI);
|
|
}
|
|
|
|
```
|
|
|
|
### Step 2: Live MIDI Note Binding & Duration Expansion
|
|
|
|
Update `ClientMIDIRecorder` to feed both the active buffer and the active recording clip:
|
|
|
|
```javascript
|
|
handleMIDIMessage(event) {
|
|
const [status, pitch, velocity] = event.data;
|
|
const command = status >> 4;
|
|
|
|
// 1. Always trigger Live Audio Preview (VU Meter + Synth Node)
|
|
this.triggerSynthPreview(pitch, velocity);
|
|
|
|
// 2. Gate recording buffer write behind global transport record state
|
|
if (!transport.isRecording || !this.targetTrack) return;
|
|
|
|
const currentBeat = this.calculateLatencyCompensatedBeat();
|
|
|
|
// Command 0x9: Note On
|
|
if (command === 0x9 && velocity > 0) {
|
|
const liveNote = {
|
|
id: `note_${Date.now()}_${pitch}`,
|
|
pitch: pitch,
|
|
start_beat: currentBeat,
|
|
duration_beats: 0.25, // Default initial length until Note Off
|
|
velocity: velocity / 127.0
|
|
};
|
|
|
|
this.activeNotes.set(pitch, liveNote);
|
|
this.targetTrack.activeRecordingItem.source_data.notes.push(liveNote);
|
|
}
|
|
// Command 0x8: Note Off
|
|
else if (command === 0x8 || (command === 0x9 && velocity === 0)) {
|
|
if (this.activeNotes.has(pitch)) {
|
|
const note = this.activeNotes.get(pitch);
|
|
note.duration_beats = Math.max(0.125, currentBeat - note.start_beat);
|
|
this.activeNotes.delete(pitch);
|
|
}
|
|
}
|
|
}
|
|
|
|
```
|
|
|
|
### Step 3: Real-Time UI Canvas Render Loop
|
|
|
|
Add real-time item length expansion and live note drawing on the main canvas during recording:
|
|
|
|
```javascript
|
|
function renderLiveRecordingUI() {
|
|
if (!transport.isRecording) return;
|
|
|
|
const currentBar = transport.getCurrentBarPosition();
|
|
|
|
session.tracks.forEach(track => {
|
|
if (track.is_armed && track.activeRecordingItem) {
|
|
const item = track.activeRecordingItem;
|
|
|
|
// Expand item duration on timeline as playhead moves forward
|
|
item.duration_bars = Math.max(0.5, currentBar - item.start_bar);
|
|
|
|
// Draw item bounding box and active/completed MIDI note rectangles
|
|
drawTimelineItem(trackCanvasCtx, item);
|
|
}
|
|
});
|
|
|
|
requestAnimationFrame(renderLiveRecordingUI);
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Checklist to Fix in Your Application
|
|
|
|
* [ ] Check if clicking top toolbar **Record + Play** sets `transport.isRecording = true`.
|
|
* [ ] Verify that Track 01 generates a temporary `activeRecordingItem` on record start.
|
|
* [ ] Confirm `requestAnimationFrame` is re-rendering the canvas continuously while transport is moving.
|
|
* [ ] Ensure incoming MIDI events on `MIDIIN2 (SE49)` push notes into Track 01's item note array rather than just playing the synth. |