- C 99.6%
- Scheme 0.4%
motif
A programmable music composition engine built in Sigil. Describe tracks using a DSL, render them to WAV files with a native C render loop.
Building
Requires a local checkout of sigil-dsp at ../sigil-dsp.
sigil deps install --redirects dev-redirects.sgl
sigil build --redirects dev-redirects.sgl
Rendering tracks
# List available tracks
./build/dev/bin/motif render
# Render a track to WAV
./build/dev/bin/motif render light-ambient -o light-ambient.wav
# Render with a shorter duration for testing
./build/dev/bin/motif render techno -d 30 -o test.wav
Example tracks
Five demo tracks in examples/ showcase different genres and motif features:
| Track | Duration | Tempo | Key | Style |
|---|---|---|---|---|
light-ambient |
3 min | 70 BPM | C major | Warm pad, bell melody, gentle kick+hihat pulse |
dark-ambient |
4 min | 60 BPM | C minor | Sub-bass drone, dark pad, sparse bells |
dnb |
3 min | 174 BPM | F minor | Syncopated breakbeat, reese bass, pad stabs |
deep-house |
4 min | 122 BPM | A minor | Four-on-floor, sub-bass, warm pad, pluck melody |
techno |
3 min | 135 BPM | D minor | Driving kick, 16th hihats, acid bass filter sweep |
Each track demonstrates:
- Song structure with sections (intro, build, drop, breakdown, outro)
- Filter cutoff and volume automation across sections
- Layer muting per section for arrangement
- Multiple synth presets and drum synthesis
Architecture
DSP layer
Built on sigil-dsp (SoundPipe bindings): oscillators, filters, envelopes, reverb, delay, noise generators, dynamics.
Composition DSL
- Tracks contain layers, each with a synth preset and pattern
- Chord progressions drive melodic layers
- Step sequencer drives drum layers with grid patterns
- Sections define which layers are active and automate parameters
- Synth presets: warm-pad, dark-pad, bright-pad, bell, sub-bass, reese-bass, acid-bass, pluck
Native C render loop
The per-sample DSP loop runs entirely in C for performance. Sigil builds a "render plan" (DSP objects + parameters), the C code processes it and writes WAV directly. A 3-minute track renders in under 90 seconds.
Synth Presets
Curated, named voice presets for the seven cinder-cantata design-doc §7 voice families. Each preset bundles a target voice, the underlying synth (or drum) graph, and a short description.
(import (motif presets))
(synth-presets) ;; => (kick snare hi-hat bass lead pad arp)
(let ((p (synth-preset 'kick)))
(preset-name p) ;; => kick
(preset-kind p) ;; => drum
(preset-synth p) ;; => "kick" (drum-track name)
(preset-description p)) ;; => "Deep sub sine with fast ..."
(let* ((p (synth-preset 'pad))
(pad-pattern (make-sustained-chord 'c3 'minor 32)))
(layer name: "pad"
synth: (preset-synth p) ;; 'warm-pad
pattern: pad-pattern
volume: (preset-suggested-volume p)))
| Preset | Kind | Underlying voice | Sonic target |
|---|---|---|---|
| kick | drum | "kick" |
Deep sub + fast pitch-sweep click |
| snare | drum | "snare" |
200Hz sine body + highpassed noise |
| hi-hat | drum | "hat" |
Highpassed noise, 40ms decay |
| bass | melodic | 'reese-bass |
Detuned saws through a Moog low-pass |
| lead | melodic | 'bell |
FM bell, 1.5s decay |
| pad | melodic | 'warm-pad |
Detuned saws, Moog LP, long attack |
| arp | melodic | 'karplus-strong |
Plucked string (feedback-delay physical model) |
Audition all seven presets back-to-back (one 4-beat window each):
motif render presets-demo -o /tmp/motif-presets.ogg
Tuning a preset
v0.4 makes each melodic preset a parameter bundle rather than a hard-coded
graph. preset-params returns the defaults the preset was tuned against;
you can override individual knobs and thread the result back into
layer synth: via preset-voice-spec:
(import (motif presets) (motif synth))
(let* ((p (synth-preset 'pad))
(tuned (alist-set (preset-params p) 'cutoff 1200.0))
(voice (preset-voice-spec p tuned)))
(layer name: "pad"
synth: voice ;; carries kind + params
pattern: my-pad-prog
volume: (preset-suggested-volume p)))
The layer API still accepts a bare kind symbol (synth: 'warm-pad) — that
continues to work unchanged and uses the preset's defaults.
The full parameter surface per kind is whatever (synth-default-params kind)
returns; (synth-kinds) lists every registered kind. Under the hood every
kind is a thin wrapper over (make-synth-graph kind params), so (motif synth)
graph builders are now composable data rather than fixed code.
See folio topics/motif-cantata-synth-presets for the voice-family
rationale and folio topics/motif-synth-parameterized-construction for
the parameter-bundle design.
Karplus-Strong arp
The arp preset routes through a real Karplus-Strong plucked string
(feedback delay line with 2-tap averaging lowpass, excited by a noise
burst on the gate's rising edge). The KS node ships in sigil-dsp
(sigil dsp osc) as make-ks freq decay excite-gain and is wired into
the motif graph DSL as g:ks. See folio topics/sigil-dsp-karplus-strong
for the primitive's parameters and decay math.
motif render arp-ks-demo -o /tmp/arp-demo.ogg
Streaming render
Alongside the one-shot render-track-to-ogg, motif exposes a chunked
streaming API that pulls N samples at a time, preserving all render
state between calls. Every chunk also emits a deterministic event
stream — one entry per drum onset or melodic chord change, tagged
with a sample-offset within the chunk, a voice name, a note frequency,
and a duration in samples. Downstream consumers (e.g. a game loop
that wants visuals coupled to sample-accurate audio) can feed the PCM
bytevectors to an audio device at their own cadence while reading the
events synchronously.
(import (motif render))
(let* ((stream (open-render-stream trk))
(result (render-stream-chunk! stream 4096)))
(car result) ;; => bytevector, 4096 stereo float32 frames
;; (length = 4096 * 2 * 4 bytes, host endian)
(cadr result)) ;; => ((offset voice-name note duration-samples) ...)
;; offset in [0, 4096)
;; Full-track convenience:
(render-track-streaming trk 4096) ;; => pcm bytevector
(render-track-streaming-with-events trk 4096) ;; => (pcm absolute-events)
At any chunk size the resulting PCM is byte-for-byte identical to a
one-shot render of the same track — verified at chunk sizes
{total, 4096, 1024, 1}. The one-shot render-track-to-ogg has been
reimplemented on top of render-track-streaming; existing callers see
no change.
Streams are not thread-safe: each <render-stream> is single-owner.
Call close-render-stream! when done (the finalizer also releases the
C-side state if close is skipped).
See folio topics/motif-streaming-render-architecture for the state
machine design, event generation, and chunk-size tradeoffs.
Modules
| Module | Purpose |
|---|---|
(motif main) |
CLI dispatch |
(motif track) |
Track and layer data structures |
(motif theory) |
Note/MIDI/frequency conversion, scales, chords |
(motif pattern) |
Chord progressions and note patterns |
(motif synth) |
Synth preset definitions |
(motif presets) |
Curated named voices for cinder-cantata and friends |
(motif drums) |
Drum synthesis (kick, snare, hihat, clap) |
(motif sequencer) |
Step sequencer with grid patterns |
(motif song) |
Song sections and automation |
(motif render) |
Render pipeline orchestration |
(motif render plan) |
Build native render plans from track data |
(motif render native) |
C render loop and WAV writer bindings |
(motif render stream) |
Streaming / chunked render API |
(motif wav) |
Sigil-side WAV writer (legacy) |