npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@audio/midi-render

v0.1.0

Published

MIDI → PCM — GM-mapped synth render, no soundfont, no samples

Readme

@audio/midi-render npm MIT

MIDI → PCM — General MIDI synth render, no soundfont, no samples

npm install @audio/midi-render
import render, { GM, voiceFor } from '@audio/midi-render'

A synth-GM: every one of the 128 General MIDI Level 1 programs and every percussion key (35-81) maps to an @audio/synth-* generator — FM (Chowning 1973), Karplus-Strong plucked strings (Karplus & Strong 1983), and modal resonator banks (Fletcher & Rossing, The Physics of Musical Instruments, 1998) — instead of a sampled/SF2 bank. Parses with @audio/midi-parse, allocates polyphony per channel with @audio/synth-poly (note-stealing), mixes to stereo. Zero asset weight: the whole instrument set ships as code.

import render from '@audio/midi-render'
import { readFileSync } from 'fs'

let smf = readFileSync('song.mid')
let { channelData, sampleRate, duration } = render(smf)
// channelData: [Float32Array, Float32Array] (L, R) — write to WAV/whatever with @audio/encode-wav
// override an instrument, or inspect the map
import render, { GM, voiceFor } from '@audio/midi-render'
import bell from '@audio/synth-fm'

render(smf, { programs: { 8: (freq, opts) => bell(freq, { ...opts, index: 12 }) } })
console.log(GM.programs[0].name)         // 'Acoustic Grand Piano'
console.log(voiceFor(0)(440, { fs: 44100, duration: 1, velocity: 0.8 }).length)

| Param | Default | | |---|---|---| | fs | 44100 | Sample rate, Hz | | channels | 2 | Output channel count, 1 or 2 | | voices | 32 | Max simultaneous notes per (channel, program) or (channel, drum key) — oldest is stolen with a 5ms fade | | tail | 1.0 | Seconds of silence appended after the render's natural end | | gain | 0.5 | Overall gain multiplier, applied before normalization | | normalize | true | Peak-normalize to −1 dBFS if the mix exceeds it — never boosts a quieter mix | | programs | — | Record<program, Voice> — override the GM program map | | drums | — | Record<key, Voice> — override the GM percussion map | | tempoScale | 1 | Stretches/compresses the whole schedule (2 = twice as fast) | | transpose | 0 | Semitones added to every non-percussion note | | bendRange | 2 | Pitch-bend sensitivity in semitones (GM default) |

The GM map

| Programs | Family | Synthesis | |---|---|---| | 0-3 | Piano (acoustic) | 2-op FM hammer transient + synth-modal 'string' body; t60 scales with pitch (8s @ MIDI36 → 1.5s @ MIDI96, linear) — inharmonicity 0.0002 | | 4-5 | Piano (electric) | synth-fm's epiano() preset, two ratio/index voicings | | 6-7 | Harpsichord/Clavinet | synth-pluck (Karplus-Strong), bright/fast damping | | 8-15 | Chromatic percussion | synth-modal 'bar'/'plate' (Fletcher & Rossing mode tables) or synth-fm's bell(); vibraphone gets a tremolo LFO, marimba/xylophone use strike position | | 16-23 | Organ | 9-partial additive drawbar (Hammond footages 16'-1', ratios 1/2/3/4/6/8/10/12/16) with slow vibrato, sustain-while-held envelope; accordion/harmonica = sawtooth + reed FM | | 24-31 | Guitar | synth-pluck, damping/lowpass/drive per articulation (nylon 0.994 … distortion tanh drive 8) | | 32-39 | Bass | synth-pluck (acoustic/electric/slap, damping 0.993-0.999 + percussive click) or oscillator + ADSR (synth bass 1/2) | | 40-55 | Strings/ensemble | Detuned oscillator pairs/triples (±5-10 cents) through a slow ADSR; pizzicato = pluck, timpani = synth-drum membrane(), choir/voice = a wide detuned pad (no formant/vowel synth in the ecosystem — documented fallback) | | 56-63 | Brass | synth-fm with a rising modulation index (index climbs toward indexFloor over the attack — Chowning 1973's brass patch; fm()'s exponential decay toward a floor runs upward just as well as downward) | | 64-71 | Reed (sax/oboe/bassoon) | synth-fm 2-op, ratio ≈ 2; clarinet is a plain square wave (a cylindrical closed-reed bore suppresses even harmonics — the same spectrum a square wave has) | | 72-79 | Pipe (flute family) | Sine + vibrato + pink-noise breath (−16 to −36 dB below the tone, per instrument) | | 80-87 | Synth lead | Oscillator + fast ADSR, one or two per patch (fifths, sub-oscillator bass+lead) | | 88-95 | Synth pad | Detuned oscillator stack, long attack | | 96-103 | Synth FX | bell()/noise textures — deliberately loose; "keep sane," not a faithful FX patch | | 104-111 | Ethnic | synth-pluck/synth-fm/detuned-pad approximations; bagpipe is a sustained reed drone | | 112-119 | Percussive (tuned) | synth-modal bar/plate or synth-drum membrane(); reverse cymbal = a time-reversed metal() | | 120-127 | Sound effects | Colored-noise bursts (synth-noise), never silent | | 35-81 (ch. 10) | GM percussion key map | synth-drum's membrane()/metal()/noiseDrum() and synth-modal 'tube-closed' (cowbell/agogo), per-key durations/pitches from the spec (kick 35/36, snare 38/40 = noise+membrane blend, hats 42/44/46, crashes/rides 49-59, toms 41-50 at 80-200 Hz, latin percussion 60-81) |

Every program/key is covered — render() never falls silent on a valid GM number, and an out-of-map percussion key gets a short click rather than nothing.

Velocity maps to amplitude through a v^1.5 power curve (perceptually closer to a loudness control than raw linear velocity), applied once before synth-poly's own linear gain stage.

MIDI control handling

  • Program changes — honored per note, per channel (mid-track changes apply to every note from that point on).
  • CC7 (channel volume) — read per note (the value in effect at the note's start time); default 100/127 per the GM System Level 1 spec's default controller value.
  • CC10 (pan) — equal-power, read once per channel at the value in effect at the channel's first note. Mid-channel pan automation is not applied per note — each channel renders to one mono buffer before panning, so a single pan value applies to the whole channel. Default center (64).
  • CC64 (sustain pedal) — extends a note's held duration until the pedal releases (or the end of the piece, if never released). Applied per note, before synthesis.
  • Pitch bend — read once per note, at the value in effect when the note starts, and applied as a constant per-note transposition (bendRange semitones at full deflection). Bend automation during a held note is not applied — there's no per-sample pitch modulation. Custom RPN 0,0 bend-range messages are not parsed; only the bendRange option changes the default ±2 semitones.
  • Percussion (channel 10) — program changes are ignored; the fixed GM1 percussion map always applies (matching GM Level 1, which defines a single kit).

Performance

Rendering is deterministic per (instrument, freq, duration) — repeated notes (very common in real MIDI — the same pitch/duration/instrument recurs constantly) reuse a cached Float32Array within one render() call instead of re-synthesizing. A 2-minute, 8-channel, ~1500-note file renders in ≈4s on a laptop CPU (measured in test.js). Each channel is rendered and mixed down before moving to the next — the renderer never holds every note's audio in memory at once.

Use when: you need audio from a Standard MIDI File and don't want a soundfont/sample-bank dependency — previews, MIDI-to-audio-in-the-browser, test fixtures, procedurally generated music. Not a substitute for a real soundfont/sample player when sample-accurate GM-compatible timbre matters (a game engine's music, a DAW export) — the instrument bank here is a from-scratch synthesis approximation of each GM voice, not a recording of one.


Part of @audio/midi — the midi family umbrella.

MIT © audiojs