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 render
01

Live

Send code to a running daemon 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 opcodes and instruments in the language. 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 tracks wire up effects, and declaration order doesn't matter.

room.nml
tempo 110bpm
bus space
track 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)
}
track lead {
instrument = pluck(bright: 0.6)
out = it.gain(0.8)
}
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, euclid and transpose for variation, and scenes you launch quantized to the bar.

groove.nml
config { channels: stereo }
tempo 110bpm
bus space: stereo
instr pluck(freq: hz, bright: 0..1 = 0.5, az: deg = 0deg) {
level = env[0 2ms 1 400ms.exp 0]
voice = osc(saw, freq).lpf(cutoff: freq * (2 + bright * 8), res: 0.25) * level
out = voice.pan(azimuth: az)
space += out.gain(-12db)
}
track hall { out = space.reverb(room: 0.8, damp: 0.4).gain(0.8) }
track bass { instrument = pluck(bright: 0.2) }
track lead { instrument = pluck(bright: 0.8) }
clip riff {
notes: [c2 ~ c2 eb2 ~ g1 bb1 ~]
gain: [0db -6db -3db]
bright: [0.2 0.5 0.8]
}
theme = [c4 e4 g4 b4].every(2, it.reverse)
answer = theme.transpose(+5st).thin(25%)
spread = [c5].euclid(hits: 4, steps: 8)
scene verse { bass: riff, lead: theme }
scene chorus { bass: riff.transpose(+5st), lead: answer }
scene outro { bass: ~, lead: spread }
launch verse
at bar 5 launch chorus
at bar 9 mute bass
at bar 11 unmute bass
at bar 13 launch outro
at bar 15 hush
launch verse
launch chorus @ next 4 bars
mute bass
unmute bass @ next bar
unsolo lead
launch outro @ next 2 bars
hush
hush

arrangements and samples

From sketch to finished piece.

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

song.nml
config { channels: stereo }
tempo 110bpm
kit drums = "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(azimuth: 0deg)
space += out.gain(-12db)
}
track hall { out = space.reverb(room: 0.8, damp: 0.4).gain(0.8) }
track beat { instrument = drums }
track hats { instrument = drums(gain: -8db) }
track bass { instrument = pluck(bright: 0.2) }
track lead { instrument = pluck(bright: 0.7) }
bassline = [c2 ~ c2 eb2 ~ g1 bb1 ~]
groove = [kick ~ ~ kick snare ~ kick ~ kick ~ ~ kick snare ~ ~ snare]
tune = [c4 e4 g4 b4].every(2, it.reverse)
scene intro { bass: bassline, beat: ~, hats: ~ }
scene verse { bass: bassline, beat: groove, hats: [hat hat hat hat hat hat hat hat], lead: ~ }
scene chorus { bass: bassline.transpose(+5st), beat: groove, hats: [hat hat hat hat hat hat hat hat], lead: tune }
scene outro { bass: bassline, beat: ~, hats: ~, lead: ~ }
arrangement song {
sections: [intro.over(2 bars) verse.over(4 bars) chorus.over(4 bars) verse.over(4 bars) outro.over(2 bars)]
}

the loop

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

# play sound and listen for code
$ niminal daemon examples/groove.nml --midi

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

# later: replay the whole session, sample for sample
$ niminal render --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 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.