# 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)
1. **Install [Rust](https://rustup.rs/)** (stable toolchain).
2. **Clone** this repo and **`cd`** into it.
3. Run:
```bash
cargo run
```
**Platform setup** (Linux ALSA packages, Windows MSVC, macOS, WSL caveats): see **[docs/install.md](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 of `Seq` subdivide the parent's time span evenly. Children of `Par`
overlap. 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 `Graph` is itself a `Processor`, so complex instruments and
buses are single composable nodes.
- **Library first.** The core `trem` crate 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 `Command`s (play, pause, stop, set parameter), the audio thread sends back
`Notification`s (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
```bash
cargo run
```
Same as **Try it** above; full prerequisites on **[docs/install.md](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)
| `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
| `←` `→` | Move step cursor |
| `↑` `↓` | Move voice cursor |
| `h` `l` `k` `j` | Vim-style move |
| `e` | Enter edit mode |
### Pattern view — Edit mode
| `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
| `←` `→` | 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
| `↑` `↓` | 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
| `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
| `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
| `lim` | `Limiter` | Stereo brickwall limiter |
| `com` | `Compressor` | Stereo downward compressor |
### Filters & Modulators
| `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
| `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:
```rust
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);
```
## 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):
```rust
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`).
## Examples
Runnable examples live in `crates/trem/examples/`:
```bash
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 Processor
```
## Building the library only
```bash
cargo build -p trem
```
## Running tests
```bash
cargo test --workspace
```
## Benchmarks
```bash
cargo bench -p trem # core, DSP, and graph benchmarks
cargo bench -p trem-tui # spectrum analysis benchmarks
```
## Using as a library
```rust
use 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 channel
```
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) and [AGENTS.md](AGENTS.md).
## License
MIT