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.
- Install Rust (stable toolchain).
- Clone this repo and
cdinto it. - Run the demo TUI:
cargo runPlatform setup (Linux ALSA packages, Windows MSVC, macOS, WSL caveats): see docs/install.md.
- 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.
┌─────────────────────────────────────────────────────────┐
│ 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.
trem-rung — Rung clip interchange: JSON note clips (class × beat time, voice,
velocity, meta) for sharing between tools and building transforms. Optional
MIDI import (midly). Crate trem-rung, import as trem_rung. See
crates/trem-rung/README.md and prop/piano-roll-editor-model.md.
cargo run # terminal synth / pattern demo (default)
cargo run -- rung import tune.mid # MIDI → tune.rung.json
cargo run -- rung edit tune.rung.json # piano-roll + synth preview (TTY + audio)Bundled examples: assets/ includes Well-Tempered Clavier MIDI — John
Sankey (full Book I + partial Book II via jsbach.net)
plus Mutopia fragments — and a tiny generated WAV under assets/samples/.
See assets/midi/wtc/README.md. Example import (Book I, pair 1 — prelude+fugue in one file):
cargo run -- rung import assets/midi/wtc/sankey/bwv846.midRe-download sources: python3 scripts/fetch_wtc_example_midis.py
Full prerequisites and import details: 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.
| 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 |
| Key | Action |
|---|---|
← → |
Move step cursor |
↑ ↓ |
Move voice cursor |
h l k j |
Vim-style move |
Enter |
Fullscreen piano roll for the selected voice column only; Esc writes that column back and closes |
e |
Enter grid edit mode (paint notes) |
Full input story & v2 roadmap: docs/modes/pattern-roll.md. Shared mode principles: docs/modes/principles.md.
| Key | Action |
|---|---|
Esc |
Validate, write this voice column back to the step grid (others unchanged), close |
Space |
Play / pause (preview = full pattern: other columns from snapshot at open, this column from roll; includes swing) |
s |
Re-sync preview audio only |
| (playhead) | Red ▼ on the time ruler + tinted column; tracks global transport beat, wrapped to pattern length (grid rows as beats) |
| (see roll footer) | Move/zoom, edit note time/pitch/velocity/voice — same family as rung edit |
| 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 / Enter |
Back to navigate |
| Key | Action |
|---|---|
← → |
Follow connections |
↑ ↓ |
Move within layer |
Enter |
Dive into nested graph |
Esc |
Back up one level (nested graph only) |
e |
Enter edit mode |
| Key | Action |
|---|---|
↑ ↓ |
Select parameter |
← → |
Adjust value |
+ / - |
Fine adjust |
Esc |
Back to navigate |
All processors implement the Processor trait and declare their parameters via
ParamDescriptor, enabling automatic UI generation for any frontend.
| 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 |
| 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 |
| Tag | Processor | Description |
|---|---|---|
lim |
Limiter |
Stereo brickwall limiter |
com |
Compressor |
Stereo downward compressor |
| 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) |
| 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 |
The Registry maps short tags to factory functions, so processors can be
instantiated at runtime without compile-time coupling:
use trem::registry::Registry;
let reg = Registry::standard();
let delay = reg.create("dly").unwrap();
println!("{}: {} in, {} out", delay.info().name, delay.info().sig.inputs, delay.info().sig.outputs);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 trem::graph::{Graph, Processor, ParamGroup, GroupHint};
use trem::dsp;
let mut ch = Graph::labeled(512, "lead");
let osc = ch.add_node(Box::new(dsp::Oscillator::new(dsp::Waveform::Saw)));
let gain = ch.add_node(Box::new(dsp::Gain::new(0.5)));
ch.connect(osc, 0, gain, 0);
ch.set_output(gain, 2);
// Expose internal params to the parent graph
let g = ch.add_group(ParamGroup { id: 0, name: "Channel", hint: GroupHint::Level });
ch.expose_param_in_group(gain, 0, "Level", g);
// Now `ch` acts as a single stereo-output Processor
assert_eq!(ch.info().sig.outputs, 2);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).
Runnable examples live in crates/trem/examples/:
cargo run -p trem --example offline_render # render a pattern to samples
cargo run -p trem --example euclidean_rhythm # generate and print euclidean patterns
cargo run -p trem --example xenharmonic # explore tuning systems
cargo run -p trem --example custom_processor # implement your own Processorcargo build -p tremcargo test --workspacecargo bench -p trem # core, DSP, and graph benchmarks
cargo bench -p trem-tui # spectrum analysis benchmarksuse trem::dsp::{Oscillator, Adsr, Gain, Waveform};
use trem::graph::Graph;
use trem::pitch::Tuning;
use trem::event::NoteEvent;
use trem::math::Rational;
// Build a simple synth graph
let mut graph = Graph::new(512);
let osc = graph.add_node(Box::new(Oscillator::new(Waveform::Saw)));
let env = graph.add_node(Box::new(Adsr::new(0.01, 0.1, 0.3, 0.2)));
let gain = graph.add_node(Box::new(Gain::new(0.5)));
graph.connect(osc, 0, env, 0);
graph.connect(env, 0, gain, 0);
// Render offline
let scale = Tuning::edo12().to_scale();
let tree = trem::tree::Tree::seq(vec![
trem::tree::Tree::leaf(NoteEvent::simple(0)),
trem::tree::Tree::rest(),
trem::tree::Tree::leaf(NoteEvent::simple(4)),
trem::tree::Tree::rest(),
]);
let audio = trem::render::render_pattern(
&tree, Rational::integer(4), 120.0, 44100.0,
&scale, 440.0, &mut graph, gain,
);
// audio[0] = left channel, audio[1] = right channelSee CONTRIBUTING.md and AGENTS.md.
trem is an anagram of term and a nod to tremolo.
The logo's TERM -> TREM swap matches the size-4 FFT bit-reversal permutation
([0,1,2,3] -> [0,2,1,3]).
Also: 02-13 is the author's birthday.
MIT
