MIDI Player

Plays Standard MIDI Files (Format 0/1) through the Juno synth engine using AudioContext-timed scheduling for sample-accurate playback.

Files

static/js/synth/midi-player.js

MidiPlayer API

Constructor

var player = new MidiPlayer(audioContext);

Methods

Method Args Returns Description
load(arrayBuffer) Raw MIDI file data { duration, noteCount, tempo } Parses SMF and returns info
play(startTime, scheduleVoiceFn, releaseVoiceFn) startTime — AudioContext time, scheduleVoiceFn(freq, time, duration) — called for each note, releaseVoiceFn(voice) — called for cleanup Starts incremental scheduling
stop(releaseVoiceFn) releaseVoiceFn(voice) — called for each active voice Stops playback, releases all voices
onProgress(fn) fn(elapsed, total) Progress callback during playback
onStop(fn) fn() Callback when playback finishes naturally

SMF Parser (_parse)

Handles: - Header chunk — format (0/1), number of tracks, division (metrical or SMPTE) - Track chunks — delta-time variable-length quantity, running status - Note on/off — matched by note number + track, durations computed from note-off times - Tempo meta events (0xFF 0x51) — microseconds per quarter note, updates tick-to-second conversion mid-stream - End of track (0xFF 0x2F) — terminates track parsing - Running status correctly scoped to channel events only (0x80–0xEF), meta events (0xFF) do not set running status

Scheduling Strategy

Uses incremental lookahead scheduling to avoid creating all voices upfront:

  1. On play(startTime), a scheduleBatch function runs via setTimeout
  2. Each batch looks ahead 1 second from audioCtx.currentTime
  3. All note events within the lookahead window are scheduled on the AudioContext timeline
  4. After scheduling, another batch is queued 50 ms later
  5. Cleanup timers (setTimeout) release voices after note duration + release

This keeps the number of active voices bounded (typically 20–40) regardless of song length.

Integration in init-juno.js

The Juno page creates a MidiPlayer instance and wires it to three functions:

Function Purpose
scheduleVoice(freq, startTime, duration) Creates voice nodes scheduled at startTime with ADSR envelope ending at startTime + duration + release
midiReleaseVoice(voice) Stops oscillators and disconnects gain (does NOT modify the already-scheduled envelope)
loadMidiFile(url, label) Fetches a MIDI file from the server and parses it

The scheduleVoice function mirrors createVoice (keyboard playback) but all node scheduling uses the startTime parameter instead of audioCtx.currentTime.

Transpose

MIDI notes are transposed by the keyboard's current octave setting (octave):

baseFreq = midiFreq * Math.pow(2, octave - 4) * rangeMultiplier

Built-in MIDI Files

MIDI files placed in /static/midi/ are listed via /static/midi/list.json and displayed as a clickable list in the Juno page sidebar. Users can also load files from their local filesystem via the File... button.