Skip to content

alkemdev/trem

Repository files navigation

trem

A mathematical music engine in Rust.

trem logo showing TERM rearranged to TREM

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.

Try it (install & run)

  1. Install Rust (stable toolchain).
  2. Clone this repo and cd into it.
  3. Run the demo TUI:
cargo 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 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 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-rungRung 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.

Quick start

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.mid

Re-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.

Keybindings

Global (all views)

Key Action
Space Play / pause
Tab Cycle SEQGRAPH
? 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
Enter Fullscreen piano roll for the selected voice column only; Esc writes that column back and closes
e Enter grid edit mode (paint notes)

Pattern roll (from SEQ navigate)

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

Pattern view — Edit mode

Key Action
zm Enter note (chromatic keyboard layout)
09 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

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 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):

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/:

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

cargo build -p trem

Running tests

cargo test --workspace

Benchmarks

cargo bench -p trem          # core, DSP, and graph benchmarks
cargo bench -p trem-tui      # spectrum analysis benchmarks

Using as a library

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 and AGENTS.md.

Name

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.

License

MIT

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors