No audio libraries, no DSP dependencies, no build step. A Standard MIDI File is parsed by hand, rendered by five hand-written synthesis engines, and sung over by a hand-written formant voice synthesiser — everything down to the WAV bytes.
Runs on Node's native TypeScript (22.6+): node synth.ts. That's it.
.mid ──> midiParse.ts ──> Song{notes[]} ──┬──> synth.ts ──> 5 engines + FX ──> chiptune.wav
(or songData.ts, pre-parsed) │
└──> chorus.ts ──> backing (synth.ts)
+ sung lead (voice.ts) ──> chorus.wav
| File | What it is |
|---|---|
midiParse.ts |
Standard-MIDI-File parser → flat Note[] (start, dur, pitch, velocity in seconds) |
synth.ts |
Music synthesiser: 5 engines, vibrato, sympathetic resonance, delay, reverb, mono/stereo |
voice.ts |
Klatt-style formant voice: speaks and sings, French phonemes |
chorus.ts |
Both together: sings a song's melody over its own backing track |
songData.ts |
A song pre-parsed and embedded (base64 float32) so it runs with no .mid present |
prototype/ |
The original Python version, kept for reference |
Docs: synth · voice · findings
node synth.ts --play # the bundled song, additive organ
node synth.ts --voice rhodes --stereo --reverb --play # FM electric piano, wide
node synth.ts --voice string --sympathetic --play # plucked strings that ring together
node synth.ts some.mid --voice acid --delay --play # any MIDI file
node voice.ts --say "bonjour tout le monde" --play # speech (crude French G2P)
node voice.ts --phonemes "b O~ Z u R" --play # exact phonemes
node voice.ts --sing "Z @ . v w a . l a . m E R" \
--notes "66:0.6,67:0.6,69:0.4,71:1.6" \
--transpose -12 --formants 0.88 --play # singing, male
node chorus.ts --play # sung chorus over the backingOutput is always a .wav in the working directory; --play pipes it to aplay
(Linux/ALSA — swap for afplay/ffplay elsewhere).
Five synthesis engines (details) — additive, 2-op FM,
subtractive (polyBLEP + resonant filter), formant/vocal, and Karplus–Strong
physical modelling. 13 named voices via --voice.
Effects — vibrato, delay, Schroeder reverb, stereo ping-pong, and sympathetic string resonance (a bank of tuned resonators, in the song's own key, excited by transients and coupled octave-to-fifth).
A voice synthesiser (details) — a real source-filter model: Rosenberg glottal pulse → nasal pole/zero → 5-formant Klatt cascade → lip radiation, with a parallel frication branch. Vowels, fricatives, nasals, stops (closure/burst/VOT), liquids, a uvular French R, coarticulation, prosody, melisma, and a male/female vocal-tract scale.
docs/findings.md is the most interesting file here: every bug that mattered, what it sounded like, and what it actually was. Most of them were phonetics errors wearing DSP clothing.
web/ is a self-contained, no-samples web MIDI player built from the same
engine: a static page that parses .mid files and synthesises them live with
the Web Audio API — a mixer, per-channel GM instruments, an effects rack
(reverb / delay / chorus), a per-voice instrument editor, and a playable
keyboard (computer keys or a real Web MIDI device). No sound-fonts, no samples;
every sound is generated by the hand-written synth.
node build.mjs # dev build -> web/app.js (bundles songData.ts)
node build.mjs --serve # dev + watch + http://localhost:8080
node build.mjs --release # self-contained release -> dist/The release build writes a static dist/ folder (index.html + minified
app.js + LICENSE + README.txt) that you serve with any static web server.
It bundles an original, public-domain demo song
(web/demoSong.ts) instead of songData.ts, so the
distributable carries none of the copyrighted note data. To share it, zip the
dist/ folder. See dist/README.txt (created by the build)
for end-user instructions.
Code is MIT licensed — see LICENSE. (Prefer a different licence
for your own fork? Swap that file.)
songData.ts and the lyrics in chorus.ts are derived from a commercial
recording and are not ours to license. They're here as test data for a
personal experiment. Don't redistribute them — and note the web release above
deliberately leaves them out.