Skip to main content

MIDI Playback

Waveform Playlist supports MIDI file loading and playback through the optional @waveform-playlist/midi package. MIDI tracks render as piano roll visualizations and play back using SoundFont samples or Tone.js PolySynth synthesis.

Installation​

npm install @waveform-playlist/midi @tonejs/midi

@tonejs/midi is a regular dependency and will be installed automatically. The package is separate from the core library so users who only need audio don't pay the bundle cost (~8-12 KB gzipped).

Basic Usage​

Use the useMidiTracks hook to load .mid files:

import { useMidiTracks } from '@waveform-playlist/midi';
import { WaveformPlaylistProvider, Waveform, PlayButton, StopButton } from '@waveform-playlist/browser';

function MidiPlayer() {
const { tracks, loading, error } = useMidiTracks(
[{ src: '/music/song.mid' }],
{ sampleRate: 48000 },
);

if (loading) return <div>Loading MIDI...</div>;
if (error) return <div>Error: {error}</div>;

return (
<WaveformPlaylistProvider tracks={tracks} controls={{ show: true, width: 200 }}>
<PlayButton /> <StopButton />
<Waveform />
</WaveformPlaylistProvider>
);
}

A single .mid file typically produces multiple ClipTrack objects — one per MIDI channel (e.g., Piano, Bass, Drums). All tracks are returned at once after loading completes.

MidiTrackConfig Options​

interface MidiTrackConfig {
src?: string; // URL to .mid file
midiNotes?: MidiNoteData[]; // Pre-parsed notes (skip fetch+parse)
name?: string; // Track display name
muted?: boolean;
soloed?: boolean;
volume?: number; // Default: 1.0
pan?: number; // Default: 0
color?: string;
startTime?: number; // Clip position in seconds (default: 0)
duration?: number; // Override clip duration in seconds
flatten?: boolean; // Merge all MIDI tracks into one (default: false)
}

interface UseMidiTracksOptions {
sampleRate: number; // Required — pass AudioContext.sampleRate
}

Pre-Parsed Notes​

If you already have note data (e.g., from a custom parser or algorithm), skip the fetch step:

const { tracks } = useMidiTracks(
[
{
midiNotes: [
{ midi: 60, name: 'C4', time: 0, duration: 0.5, velocity: 0.8 },
{ midi: 64, name: 'E4', time: 0.5, duration: 0.5, velocity: 0.7 },
],
name: 'Melody',
duration: 4,
},
],
{ sampleRate: 48000 },
);

Flatten Mode​

By default, each MIDI channel becomes a separate track. Use flatten: true to merge all channels into one visual track:

const { tracks } = useMidiTracks(
[{ src: '/music/song.mid', flatten: true }],
{ sampleRate: 48000 },
);

SoundFont Playback​

For realistic instrument sounds, load a SoundFont file and pass the cache to the provider:

import { SoundFontCache } from '@waveform-playlist/playout';

// Load the SoundFont (do this once, e.g., in a hook or effect)
const cache = new SoundFontCache();
await cache.load('/soundfonts/piano.sf2');

// Pass to provider — MIDI tracks automatically use the SoundFont samples
<WaveformPlaylistProvider
tracks={tracks}
soundFontCache={cache}
>
<Waveform />
</WaveformPlaylistProvider>

Without a soundFontCache, MIDI tracks fall back to Tone.js PolySynth synthesis. SoundFont playback uses the midiProgram field on each clip to select the correct instrument samples.

Mixing MIDI and Audio Tracks​

MIDI and audio tracks can be played together. Both useMidiTracks and useAudioTracks return ClipTrack[], so merge them into a single array:

import { useMidiTracks } from '@waveform-playlist/midi';
import { useAudioTracks, WaveformPlaylistProvider, Waveform } from '@waveform-playlist/browser';

function MixedPlayer() {
const { tracks: midiTracks, loading: midiLoading } = useMidiTracks(
[{ src: '/music/song.mid' }],
{ sampleRate: 48000 },
);

const { tracks: audioTracks, loading: audioLoading } = useAudioTracks([
{ src: '/audio/vocals.mp3', name: 'Vocals' },
]);

if (midiLoading || audioLoading) return <div>Loading...</div>;

const allTracks = [...midiTracks, ...audioTracks];

return (
<WaveformPlaylistProvider tracks={allTracks}>
<Waveform />
</WaveformPlaylistProvider>
);
}

Pure Parsing (No React)​

The parseMidiFile function works without React — useful for Node.js scripts or non-React apps:

import { parseMidiFile } from '@waveform-playlist/midi';

const response = await fetch('/music/song.mid');
const buffer = await response.arrayBuffer();
const parsed = parseMidiFile(buffer);

console.log(parsed.name); // Song name
console.log(parsed.bpm); // Tempo
console.log(parsed.tracks.length); // Number of tracks

for (const track of parsed.tracks) {
console.log(track.name, track.notes.length, 'notes');
}

There's also parseMidiUrl for a fetch+parse convenience:

import { parseMidiUrl } from '@waveform-playlist/midi';

const parsed = await parseMidiUrl('/music/song.mid');

Track Naming​

Track names are derived from the MIDI file content:

  • Channel 9 → "Drums" (GM percussion convention)
  • Program > 0 → Title-cased GM instrument name (e.g., "Electric Bass (Finger)")
  • Program 0 → Track name from the MIDI file, or "Acoustic Grand Piano", or "Channel N"

When loading user-dropped files with a config name, individual track names from the MIDI file are used directly (not prefixed with the config name).