6.5 KiB
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 position0: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**.
[ 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:
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
requestAnimationFramerender loop whileisRecording === true. - The renderer must query the
activeNotesMap (currently held keys) in addition to finalizedrecordedNotes. - 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:
// 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:
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:
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
activeRecordingItemon record start. - Confirm
requestAnimationFrameis 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.