Files
SonicForgeStudio/md/31_MIDI_FIX.md
T

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 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**.
[ 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 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:

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