trem
A mathematical music engine in Rust.
trem is a library-first DAW built on exact arithmetic, xenharmonic pitch
systems, recursive temporal trees, and typed audio graphs. The terminal UI is a
first-class citizen. The name is an anagram of term and also a nod to
tremolo: repeating, pulsing motion in sound.
Try it (install & run)
- Install Rust (stable toolchain).
- Clone this repo and
cdinto it. - Run:
Platform setup (Linux ALSA packages, Windows MSVC, macOS, WSL caveats): see docs/install.md.
Principles
- Exact where possible. Time is rational (integer numerator/denominator pairs). Pitch degree is an integer index into an arbitrary scale. Floating-point only appears at the DSP boundary.
- Few assumptions. No 12-TET default, no 4/4 default, no fixed grid resolution. Tuning, meter, and subdivision are all parameters.
- Composition is a tree. Patterns are recursive
Tree<Event>structures. Children ofSeqsubdivide the parent's time span evenly. Children ofParoverlap. Triplets, quintuplets, nested polyrhythms — just tree shapes. - Sound is a graph. Audio processing is a DAG of typed processor nodes.
Each processor declares its own inputs, outputs, and parameters. Graphs nest
recursively — a
Graphis itself aProcessor, so complex instruments and buses are single composable nodes. - Library first. The core
tremcrate has zero I/O dependencies. It compiles to WASM. It renders offline to sample buffers. The TUI and audio driver are separate crates that depend on it.
Architecture
┌─────────────────────────────────────────────────────────┐
│ trem (core library, no I/O) │
│ │
│ math::Rational ──▶ pitch::Scale ──▶ event::NoteEvent │
│ │ │ │
│ ▼ ▼ │
│ time::Span ──▶ tree::Tree ──▶ render ──▶ TimedEvent │
│ │ │
│ grid::Grid ──────────────────────┘ │ │
│ ▼ │
│ graph::Graph ◀── dsp::* ◀── euclidean process() │
│ │ registry │
│ ▼ │
│ output_buffer() ──▶ [f32] │
└────────────┬────────────────────────────────────────────┘
│
┌───────┴───────┐
▼ ▼
┌─────────┐ ┌───────────┐
│trem-cpal│ │ trem-tui │
│ │ │ │
│ cpal │◀──│ ratatui │
│ stream │cmd│ crossterm │
│ │ │ │
└─────────┘ └───────────┘
trem — Core library. Rational arithmetic, pitch/scale systems, temporal
trees, audio processing graphs, DSP primitives (oscillators, envelopes,
filters, dynamics, effects), Euclidean rhythm generation, grid sequencer,
processor registry, and offline rendering. No runtime dependencies beyond
bitflags and num-rational.
trem-cpal — Real-time audio backend. Drives a Graph from a cpal output
stream. Communicates with the UI via a lock-free ring buffer (rtrb): the UI
sends Commands (play, pause, stop, set parameter), the audio thread sends back
Notifications (beat position, meter levels).
trem-tui — Terminal interface. Pattern sequencer with per-step note entry, audio graph viewer with inline parameter editing, transport bar, spectrum-first bottom pane (in Graph view: side-by-side instrument bus vs master previews), waveform scope, a sidebar (cursor / project / keys / contextual hints, with PROC stats for this process — CPU % and RSS — at the bottom), and contextual key hints. Built on ratatui + crossterm.
Quick start
Same as Try it above; full prerequisites on docs/install.md.
The default graph and pattern live in src/demo/ (levels.rs for gains/FX, graph.rs for routing, pattern.rs for the grid). src/main.rs is thin I/O glue.
This launches the demo project: a ~146 BPM loop (32-step pattern) with a dense
pentatonic arp on the lead (triangle-heavy dual osc, light wavetable, warm filter),
short delay only on the lead for a fluttery echo, bass, a louder snare through
dst distortion (foldback / crisp transient), and hats —
routed through a nested bus architecture:
Lead > ────────┐
├── Inst Bus > ──┐
Bass > ────────┘ │
├── Main Bus > ── [output]
Kick > ────┐ │
Snare >(dst)──┼── Drum Bus > ──────┘
Hat > ─────┘
Every node marked > is a nested graph you can Enter to inspect and edit.
Press Space to play/pause. Press Tab to switch views. The bottom strip defaults
to the spectrum. Bins use per-bin peak decay ((\tau \approx 18) ms, App::spectrum_fall_ms) and adaptive level:
a decaying global peak + silence-aware reference so quiet buffers don’t normalize to full height;
each column uses the max of its FFT bins. In Graph
view the strip splits into IN and OUT for whichever node is highlighted — summed
inputs vs that node’s outputs (including inside nested graphs). In Pattern view the
spectrum shows the master output (waveform/spectrum use the same scope buffer). Press ` to toggle waveform vs spectrum.
The sidebar PROC section (bottom of the info column) reports this process only (trem CPU % and RSS), not whole-machine totals. The transport bar shows beat position with a φ-weighted phase glyph for a slightly less grid-locked readout.
Keybindings
Global (all views)
| Key | Action |
|---|---|
Space |
Play / pause |
Tab |
Cycle SEQ ↔ GRAPH |
? |
Full keymap overlay |
+ / - |
BPM up / down |
[ / ] |
Octave down / up |
` |
Toggle bottom: waveform ↔ spectrum |
Ctrl-S / Ctrl-O |
Save / load project (project.trem.json in cwd) |
Ctrl-C / Ctrl-Q |
Quit |
Pattern view — Navigate mode
| Key | Action |
|---|---|
← → |
Move step cursor |
↑ ↓ |
Move voice cursor |
h l k j |
Vim-style move |
e |
Enter edit mode |
Pattern view — Edit mode
| Key | Action |
|---|---|
z–m |
Enter note (chromatic keyboard layout) |
0–9 |
Enter note by degree |
Del / BS |
Delete note |
w / q |
Velocity up / down |
f |
Euclidean fill (cycle hit count) |
r |
Randomize voice |
t |
Reverse voice |
, / . |
Shift voice left / right |
Esc |
Back to navigate |
Graph view — Navigate mode
| Key | Action |
|---|---|
← → |
Follow connections |
↑ ↓ |
Move within layer |
Enter |
Dive into nested graph |
Esc |
Back up one level (nested graph only) |
e |
Enter edit mode |
Graph view — Edit mode
| Key | Action |
|---|---|
↑ ↓ |
Select parameter |
← → |
Adjust value |
+ / - |
Fine adjust |
Esc |
Back to navigate |
DSP library
All processors implement the Processor trait and declare their parameters via
ParamDescriptor, enabling automatic UI generation for any frontend.
Sources
| Tag | Processor | Description |
|---|---|---|
osc |
Oscillator |
PolyBLEP oscillator (sine, saw, square, triangle) |
noi |
Noise |
White noise (deterministic LCG) |
wav |
Wavetable |
Wavetable oscillator with shape crossfade |
kick |
KickSynth |
Sine with pitch sweep + amplitude envelope |
snr |
SnareSynth |
Sine body + bandpass-filtered noise burst |
hat |
HatSynth |
Highpass-filtered noise with short envelope |
syn |
analog_voice |
Composite synth graph (2 osc, filter, env, gain) |
ldv |
lead_voice |
Lead stack: saw + tri, wavetable air, modulated LP, ADSR |
Effects & EQ
| Tag | Processor | Description |
|---|---|---|
dly |
StereoDelay |
Stereo delay with feedback and dry/wet mix |
dst |
Distortion |
Mono waveshaper: tanh / hard / fold / soft / diode + mix |
vrb |
PlateReverb |
Schroeder plate reverb (4 combs + 2 allpasses) |
peq |
ParametricEq |
3-band stereo parametric EQ |
geq |
GraphicEq |
7-band mono graphic EQ |
Dynamics
| Tag | Processor | Description |
|---|---|---|
lim |
Limiter |
Stereo brickwall limiter |
com |
Compressor |
Stereo downward compressor |
Filters & Modulators
| Tag | Processor | Description |
|---|---|---|
lpf |
BiquadFilter |
Low-pass biquad (2nd-order IIR) |
hpf |
BiquadFilter |
High-pass biquad |
bpf |
BiquadFilter |
Band-pass biquad |
env |
Adsr |
Attack-decay-sustain-release envelope |
lfo |
Lfo |
Low-frequency oscillator (sine, tri, saw, square) |
Mixing & Utility
| Tag | Processor | Description |
|---|---|---|
vol |
StereoGain |
Stereo pass-through gain |
gain |
MonoGain |
Simple mono gain |
pan |
StereoPan |
Stereo panning (equal-power) |
mix |
StereoMixer |
N-input stereo summing bus |
xfade |
MonoCrossfade |
Mono crossfade between two inputs |
Processor registry
The Registry maps short tags to factory functions, so processors can be
instantiated at runtime without compile-time coupling:
use Registry;
let reg = standard;
let delay = reg.create.unwrap;
println!;
Nested graphs
A Graph implements Processor, so any graph can be a node inside another
graph. The demo project uses this to build self-contained instrument channels
(synth + level/pan in one node) and mix buses (mixer + dynamics + gain):
use ;
use dsp;
let mut ch = labeled;
let osc = ch.add_node;
let gain = ch.add_node;
ch.connect;
ch.set_output;
// Expose internal params to the parent graph
let g = ch.add_group;
ch.expose_param_in_group;
// Now `ch` acts as a single stereo-output Processor
assert_eq!;
In the TUI, press Enter on any nested graph node to dive in and edit its
internal parameters. Press Esc to return to the parent level. A breadcrumb
trail shows your current position (e.g. Graph > Lead > Oscillator).
Examples
Runnable examples live in crates/trem/examples/:
Building the library only
Running tests
Benchmarks
Using as a library
use ;
use Graph;
use Tuning;
use NoteEvent;
use Rational;
// Build a simple synth graph
let mut graph = new;
let osc = graph.add_node;
let env = graph.add_node;
let gain = graph.add_node;
graph.connect;
graph.connect;
// Render offline
let scale = edo12.to_scale;
let tree = seq;
let audio = render_pattern;
// audio[0] = left channel, audio[1] = right channel
Contributing
See CONTRIBUTING.md and AGENTS.md.
License
MIT