live-coding audio language

Write sound.
Change it while it plays.

niminal is a language and a DSP engine built together. Describe instruments, patterns and scenes, hear every edit land on the beat, then render the very same file to audio.

saw_lead.nml
// The basic synth from the spec, playing a short phrase.
tempo 100bpm
instr saw_lead(freq: hz, amp: db = -6db) {
level = env[0 5ms 1 200ms 0.6 | 300ms 0]
osc(saw, freq)
.lpf(cutoff: 2khz, res: 0.2)
.gain(amp) * level
}
saw_lead(freq: a3) for 1beat
at +1beat saw_lead(freq: c4) for 1beat
at +2beats saw_lead(freq: e4, amp: -9db) for 1beat
at +3beats saw_lead(freq: a4) for 2beats
rendered by niminal-live render
01

Live

Send code to a running server from VS Code, the REPL or a socket. Changes land on the beat you choose, and every input is logged by sample.

02

Offline

The same file renders to audio with one command. Sessions replay from their log to identical samples.

03

Extensible

Write functions and instruments in the language, down to the sample. They get types, units, hover docs and diagnostics like the built-ins.

04

Musical

Rational time, mini-notation, clips, scenes and arrangements. One pattern cycle is one bar.

what it sounds like

Every clip on this page is rendered from the code beside it.

instruments and buses

Signal flow you can read.

Instruments are plain expressions with units that are checked at compile time: hz, db, ms, deg. Buses and fx declarations wire up effects, and declaration order doesn't matter.

room.nml
tempo 110bpm
bus space
fx room {
echoes = space.delay(time: 3/16beat, feedback: 0.4).lpf(cutoff: 3khz)
out = (echoes + space.reverb(room: 0.8, damp: 0.4)).gain(0.8)
}
fx lead_fx {
out = lead_fx_audio.gain(0.8)
}
bus lead_fx_audio: mono
instr lead_voice(freq: hz) {
voice = pluck(bright: 0.6, freq: freq)
lead_fx_audio += voice
}
clip lead { instrument: lead_voice }
instr pluck(freq: hz, amp: db = -6db, bright: 0..1 = 0.5) {
level = env[0 2ms 1 600ms.exp 0]
out = osc(saw, freq)
.lpf(cutoff: freq * (2 + bright * 6), res: 0.3)
.gain(amp) * level
space += out.gain(-9db)
}
lead(freq: c4) for 1/2beat
at +1/2beat lead(freq: eb4) for 1/2beat
at +1beat lead(freq: g4) for 1/2beat
at +3/2beat lead(freq: c5) for 2beats
at +0beat pluck(freq: c2, amp: -3db) for 4beats

patterns, clips and scenes

Perform with the music, not around it.

Mini-notation for rhythm, functions like every, spread_evenly and transpose for variation, and scenes you launch quantized to the bar.

groove.nml
config { channels: stereo }
tempo 110bpm
bus space: stereo
fx space_return {
out = space.reverb(room: 0.8, damp: 0.4).gain(0.8)
}
instr pluck(freq: hz, bright: 0..1 = 0.5, az: deg = 0deg) {
level = perc(decay: 400ms)
voice = osc(saw, freq).lpf(cutoff: freq * (2 + bright * 8), res: 0.25) * level
out = voice.pan(angle: az)
space += out.gain(-18db)
}
clip bass {
instrument: pluck(bright: 0.2)
notes: [c2 ~ c2 eb2 ~ g1 bb1 ~]
gain: [0db -6db -3db]
bright: [0.2 0.5 0.8]
}
clip lead {
instrument: pluck(bright: 0.8)
notes: [c4 e4 g4 b4].every(2, reverse)
}
clip bass_chorus = bass.transpose(+5st)
answer = [c4 e4 g4 b4].every(2, reverse).transpose(+5st).drop(25%)
spread = [c5].spread_evenly(hits: 4, steps: 8)
clip lead_chorus {
instrument: pluck(bright: 0.8)
notes: answer
}
clip lead_outro {
instrument: pluck(bright: 0.8)
notes: spread
}
scene verse { bass, lead }
scene chorus { bass_chorus, lead_chorus }
scene outro { lead_outro }
arrangement song {
at +0bars verse.start
at +4bars verse.stop
at +4bars chorus.start
at +8bars bass_chorus.mute
at +10bars bass_chorus.unmute
at +12bars chorus.stop
at +12bars outro.start
at +14bars hush
}
song.start

arrangements and samples

From sketch to finished piece.

Drum kits load from a folder, sections play in order, and niminal-live render can bounce the whole song or just bars 5 to 9.

song.nml
config { channels: stereo }
tempo 110bpm
drums = kit("drums")
bus space: stereo
instr pluck(freq: hz, bright: 0..1 = 0.5) {
voice = osc(saw, freq).lpf(cutoff: freq * (2 + bright * 6), res: 0.25) * env[0 2ms 1 400ms.exp 0]
out = voice.pan(angle: 0deg)
space += out.gain(-12db)
}
fx hall { out = space.reverb(room: 0.8, damp: 0.4).gain(0.8) }
bassline = [c2 ~ c2 eb2 ~ g1 bb1 ~]
groove = [kick ~ ~ kick snare ~ kick ~ kick ~ ~ kick snare ~ ~ snare]
tune = [c4 e4 g4 b4].every(2, reverse)
clip beat { instrument: drums, notes: groove }
clip hats { instrument: drums(gain: -8db), notes: [hat hat hat hat hat hat hat hat] }
clip bass { instrument: pluck(bright: 0.2), notes: bassline }
clip bass_chorus = bass.transpose(+5st)
clip lead { instrument: pluck(bright: 0.7), notes: tune }
scene intro { bass }
scene verse { bass, beat, hats }
scene chorus { bass_chorus, beat, hats, lead }
scene outro { bass }
arrangement song {
sections: [intro.over(2bars) verse.over(4bars) chorus.over(4bars) verse.over(4bars) outro.over(2bars)]
}

the loop

Start a server. Send it code. Hear it change.

# start the audio server
$ niminal server

# in another terminal: play a piece, and listen for changes
$ niminal-live live examples/groove.nml --midi

# change the music, landing four bars from now
$ niminal-live send 'chorus.start' --quantize "next 4bars"

# later: replay the whole session, sample for sample
$ niminal replay ~/.niminal/sessions/1760112000.log --out set.wav
editor

VS Code

Highlighting, evaluate block or file, diagnostics, and a scenes panel you can launch with Cmd+1..9.

lsp

Language server

Completion, hover, go to definition and an outline from niminal-live lsp, for any editor.

control

MIDI and OSC

Bind a control to a knob with ctl x = midi.cc(21) or an OSC address. Inputs are logged and replay too.

engine

Rust, no allocations on the audio thread

Compilation happens at eval time; landing a change only moves things. The engine builds for WebAssembly.

Make some noise.