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
Nodeimplementations. Each node declares its own inputs, outputs, and parameters. Graphs nest recursively — aGraphis itself aNode, so complex instruments and buses are single composable nodes. - Library first. The core
tremcrate keeps required dependencies minimal. It compiles to WASM with default features.trem-mio(importtrem_mio) holds planar WAV/FLAC I/O (hound,flacenc,claxon); browserwasm32hosts should use in-memory APIs intrem_mio::audio(from_bytes,write_planar_to_vec,open_memory). Offline rendering produces sample buffers when paired withtrem-dsp(or your ownNodeimplementations). 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 ◀── trem_dsp::standard ◀── euclidean process() │
│ │ registry │
│ ▼ │
│ output_buffer() ──▶ [f32] │
└────────────┬────────────────────────────────────────────┘
│
┌───────┴───────┐
▼ ▼
┌─────────┐ ┌───────────┐
│trem-rta │ │ trem-tui │
│ │ │ │
│ cpal │◀──│ ratatui │
│ stream │cmd│ crossterm │
│ │ │ │
└─────────┘ └───────────┘
trem — Core library. Rational arithmetic, pitch/scale systems, temporal
trees, audio processing graphs, Euclidean rhythm generation, grid sequencer,
Registry types, and offline rendering. Stays lean (bitflags, num-*).
trem-mio — WAV/FLAC file I/O today; room for images and other media. Depends on trem only for signal. Not the Tokio mio crate.
trem-dsp — Stock Node implementations for the graph (oscillators, envelopes,
filters, dynamics, effects, drum synths, nested voices) plus
register_standard / standard_registry. Crate trem-dsp, import as
trem_dsp. The interfaces module re-exports trem graph/event types for
custom node authors; standard holds the built-in implementations.
trem-rta — Real-time playback host. 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.
Rung clips (trem::rung) — JSON note clips (class × beat time, voice,
velocity, meta) for sharing between tools and building transforms. Lives in crate
trem behind features rung / midi (SMF import via midly). See
flow/prop/piano-roll-editor-model.md.
cargo run # terminal synth / pattern demo (default)
cargo run -- rung import tune.mid # MIDI → tune.rung.json (`clip` = alias for `rung`)
cargo run -- rung import tune.mid -o - # same, print JSON to stdout
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 crates/trem-bin/src/demo/ (levels.rs for gains/FX, graph.rs for routing, pattern.rs for the grid). crates/trem-bin/src/main.rs is thin CLI + 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 built-in nodes implement Node and declare parameters via
ParamDescriptor, enabling automatic UI generation for any frontend.
Design reference: docs/graph-architecture.md — terminology, Graph::from_chain / chain / pipeline, nesting.
| Tag | Kind | 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 | Kind | 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 | Kind | Description |
|---|---|---|
lim |
Limiter |
Stereo brickwall limiter |
com |
Compressor |
Stereo downward compressor |
| Tag | Kind | 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 | Kind | 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 factories, so Node instances can be
created at runtime without compile-time coupling to concrete types:
use trem::registry::Registry;
use trem_dsp::standard_registry;
let reg = standard_registry();
let delay = reg.create("dly").unwrap();
println!("{}: {} in, {} out", delay.info().name, delay.info().sig.inputs, delay.info().sig.outputs);A Graph implements Node, 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, ParamGroup, GroupHint};
use trem_dsp::standard as 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 node
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:
cargo run -p trem-mio --example save_planar_wav # planar stereo WAV (`trem_mio::audio`)
cargo run -p trem-dsp --example offline_render # render a pattern to samples (core + stock DSP)
cargo run -p trem-dsp --example custom_processor # custom Node + stock oscillatorcargo build -p tremcargo test --workspacecargo bench -p trem # core and graph benchmarks
cargo bench -p trem-dsp # DSP / graph node benchmarks
cargo bench -p trem-tui # spectrum analysis benchmarksuse trem_dsp::{Adsr, Gain, Oscillator, 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. Proposals and work lifecycle: flow/README.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
