math-sonify is a real-time generative audio engine that runs mathematical dynamical systems — differential equations, maps, and coupled oscillators — and routes every variable of their evolving state directly into audio synthesis parameters. The Lorenz attractor is actually integrating at 120 Hz; the Kuramoto coupling constant is live; the Three-Body gravitational problem advances at each control frame. The result is not a preset synthesiser with math-themed names: the mathematics is the music, and every parameter change propagates to sound within 8 ms.
- 53 dynamical systems — Lorenz, Rossler, Double Pendulum, Kuramoto, Three-Body, Hyperchaos (Chen-Li), WINDMI, Finance, all Sprott cases, Tinkerbell map, and more (full list below).
- 9 sonification modes — Direct, Orbital, Granular, Spectral, FM, AM, Vocal, Waveguide, Resonator.
- 20 musical scales — Pentatonic through Microtonal, EDO-19/24/31, Harmonic Series, Just Intonation.
- MIDI export — trajectory-to-MIDI conversion; outputs Standard MIDI Files (SMF) importable into any DAW.
- Preset gallery — 16+ named presets with mood tags, complexity ratings, favorites, and a discovery mode that surfaces less-played entries.
- Collaborative session mode — real-time multi-user parameter control via a WebSocket server with per-participant colour highlights, conflict resolution, and full session replay log.
- Audio-driven ODE morphing — reverse the sonification pipeline: incoming microphone audio extracts features (RMS, spectral centroid, flux, 8-band energy) and maps them to ODE parameters in real time. Can run simultaneously with the forward synthesis path (dual mode).
- Lyapunov exponent tracker — real-time estimation of the maximal Lyapunov exponent; displayed in the MATH VIEW tab.
- FFT spectral overlay — live FFT spectrum superimposed on the phase portrait and the WAVEFORM tab.
- Scene arranger — 8-scene timeline with smooth parameter morphs; AUTO generator builds full arrangements from a mood pool.
- VST3 / CLAP plugin — load inside Ableton, FL Studio, Logic Pro, Reaper, and any other NIH-plug-compatible DAW.
- Headless render —
--headless --duration 60 --output clip.wavwith no display required. - Live config reload — edit
config.tomlwhile the engine runs; changes take effect without restart.
Download math-sonify.exe from the latest release and double-click it. Audio starts immediately on the system default output device.
Requires Rust 1.75+ and a working audio output device.
git clone https://github.com/Mattbusel/math-sonify
cd math-sonify
cargo run --releasecargo run --release -- --headless --duration 60 --output clip.wavODE Solver (120 Hz, sim thread)
|
| 53 dynamical systems -- Lorenz, Rossler, Duffing, Kuramoto, Three-Body,
| Hyperchaos (4D), Finance, WINDMI, Liu, Genesio-Tesi, Shimizu-Morioka, ...
| RK4 integration per configured dt
|
v
Parameter Morphing (arrangement layer)
|
| Scene arranger linearly interpolates all numeric config fields
| between named snapshots; string fields switch at midpoint
|
v
Sonification Mapper (sim thread, 120 Hz)
|
| DirectMapping -- state quantized to musical scale -> oscillator freqs
| OrbitalResonance -- angular velocity + Lyapunov exponent drive pitch
| GranularMapping -- trajectory speed -> grain density and pitch
| SpectralMapping -- state -> 32-partial additive envelope
| FmMapping -- attractor drives carrier/modulator ratio and index
| AmMapping -- amplitude modulation driven by state variables
| VocalMapping -- state interpolates between vowel formant positions
| Waveguide -- Karplus-Strong string with chaotic modulation
| Resonator -- modal resonator bank driven by attractor
|
v [crossbeam bounded channel, try_recv in audio callback]
v
Audio Synthesis (audio thread, 44100 / 48000 Hz)
|
| Per-layer DSP:
| Oscillator(s) [PolyBLEP anti-aliased] --> ADSR --> Waveshaper --> Bitcrusher
|
| Master bus (shared across up to 4 layers):
| 3-Band EQ --> LP BiquadFilter --> Stereo DelayLine --> Chorus
| --> FDN Reverb (8-channel, modulated) --> Lookahead Limiter
|
v
DAW (VST3 / CLAP plugin) or Desktop (standalone cpal output)
Thread safety: the sim thread and audio thread communicate through a bounded crossbeam-channel of capacity 16. The audio callback calls try_recv and renders silence on a miss, so it is never blocked. The UI thread reads shared state through parking_lot::Mutex on the control rate.
| System | Dim | Type | Notes |
|---|---|---|---|
| Lorenz | 3 | chaos | Classic butterfly attractor; chaos onset near rho=24.74 |
| Rossler | 3 | chaos | Spiral attractor; period-doubling as c increases |
| Double Pendulum | 4 | chaos | Lagrangian mechanics (theta1, theta2, p1, p2); leapfrog integrator |
| Geodesic Torus | 4 | quasi-periodic | Ergodic irrational winding on a flat torus |
| Kuramoto | N | sync | N coupled oscillators; synchronization at critical K |
| Three-Body | 12 | chaos | Newtonian gravity, 3 point masses in 2D; figure-8 ICs |
| Duffing | 2 | chaos | Driven nonlinear oscillator; period-doubling cascade |
| Van der Pol | 2 | limit cycle | Self-sustaining limit cycle; relaxation oscillations |
| Halvorsen | 3 | chaos | Dense cyclic-symmetry spiral attractor |
| Aizawa | 3 | chaos | Six-parameter torus-like attractor |
| Chua | 3 | chaos | Piecewise-linear double-scroll circuit |
| Hindmarsh-Rose | 3 | chaos | Neuron firing model; bursting and spiking |
| Lorenz-96 | N | chaos | Weather model; spatiotemporal chaos at F > 8 |
| Mackey-Glass | DDE | chaos | Delay differential equation; history-dependent |
| Nose-Hoover | 3 | chaos | Thermostatted Hamiltonian; conservative chaos |
| Coupled Map Lattice | N | chaos | Logistic map on a 1D lattice with diffusive coupling |
| Henon Map | 2 | chaos | Discrete map; fractal strange attractor (dim ~1.26) |
| Custom ODE | 3-4 | user | User-defined equations via text input |
| Fractional Lorenz | 3 | chaos | Lorenz with derivative order alpha in (0.5, 1.0] |
| Logistic Map | 1 | chaos | Period-doubling route to chaos; bifurcation diagram classic |
| Standard Map | 2 | chaos | Area-preserving Chirikov map; KAM tori to global chaos |
| Arnold Cat | 2 | chaos | Ergodic linear torus map; hyperbolic fixed point |
| Stochastic Lorenz | 3 | chaos | Lorenz with additive Wiener noise per axis |
| Delayed Map | 1 | chaos | Logistic map with discrete delay tau |
| Oregonator | 3 | oscillation | Belousov-Zhabotinsky chemical reaction oscillator |
| Mathieu | 2 | parametric | Parametric resonance; stability tongues in a/q space |
| Kuramoto-Driven | N | sync | Kuramoto + external sinusoidal drive on first oscillator |
| Thomas | 3 | chaos | Conservative symmetric attractor; b~0.208 chaos boundary |
| Lorenz-84 | 3 | chaos | Low-order atmospheric circulation model |
| Dadras | 3 | chaos | Five-parameter attractor with rich bifurcation structure |
| Rucklidge | 3 | chaos | Double-scroll from a convection model |
| Chen | 3 | chaos | Lorenz-family; denser scroll than standard Lorenz |
| Burke-Shaw | 3 | chaos | Two-scroll; sigma/rho parameterization |
| Rabinovich-Fabrikant | 3 | chaos | Plasma wave instability model |
| Rikitake | 3 | chaos | Two coupled dynamos; geomagnetic reversal model |
| Bouali | 3 | chaos | Slow-manifold attractor; a/s parameterization |
| Newton-Leipnik | 3 | chaos | Two coupled rigid bodies; two coexisting attractors |
| Sprott B | 3 | chaos | Minimal 5-term polynomial system |
| Sprott C | 3 | chaos | Minimal polynomial; single quadratic term |
| Sprott D (Case I) | 3 | chaos | y^2 instability with -1.1z dissipation |
| Sprott E | 3 | chaos | Minimal chaos from a yz product |
| Sprott F | 3 | chaos | Slow-spiral; x^2 drives z |
| Sprott G | 3 | chaos | Linear + quadratic; minimal form |
| Sprott H | 3 | chaos | Single xz product nonlinearity |
| Sprott K | 3 | chaos | xy product; one of Sprott's simplest forms |
| Sprott L | 3 | chaos | Bounded strange attractor; yz coupling |
| Shimizu-Morioka | 3 | chaos | Two-scroll; x^2-driven z destabilizes y |
| Genesio-Tesi | 3 | chaos | Jerk circuit: one x^2 term is all the chaos needed |
| Liu | 3 | chaos | Single-band scroll; y^2 and xz/xy cross-coupling |
| WINDMI | 3 | chaos | Ionospheric substorm model; exponential nonlinearity |
| Finance | 3 | chaos | Macroeconomic chaos: interest rate, investment, price |
| Hyperchaos (Chen-Li) | 4 | hyperchaos | Two positive Lyapunov exponents; richer than ordinary chaos |
| Tinkerbell | 2 | chaos | Complex-plane map; orbit traps and fractal basins |
| Mode | How math maps to audio |
|---|---|
| Direct | State variables quantized to configured scale -> oscillator frequencies. Amplitude tracks normalized magnitude. |
| Orbital | State interpreted as polar coordinates. Angular velocity drives pitch; Lyapunov exponent modulates inharmonicity. |
| Granular | Trajectory speed controls grain spawn rate (0-50 grains/sec). Position in state space sets grain frequency. |
| Spectral | 32 additive partials. Each partial amplitude derived from a normalized component of the state vector. |
| FM | Two-operator FM synthesis. Carrier tracks first state variable; modulator ratio and index driven by remaining variables. |
| AM | Amplitude modulation. Carrier frequency from state; AM depth and rate driven by trajectory speed. |
| Vocal | State coordinates mapped to vowel formant positions (F1/F2). Trajectory wanders through /a/ /e/ /i/ /o/ /u/. |
| Waveguide | Karplus-Strong string model. Tension and damping modulated by the attractor in real time. |
| Resonator | Modal resonator bank. Attractor state excites a set of tuned resonant modes. |
The composition engine (ComposerEngine) turns the running attractor into a structured musical piece in real time, without any pre-written score.
| Form | Description |
|---|---|
| ABA | Statement (A), contrast (B), varied return (A'). Section boundaries follow basin changes. |
| Theme & Variations | Theme derived from the initial attractor basin; each variation alters a different synthesis parameter. |
| Rondo | Refrain (A) alternates with episodes driven by bifurcation events. |
| Through-Composed | Linear succession of sections with no repeats; topology-driven. |
| Stochastic | Section boundaries placed by a pseudo-random walk seeded from the attractor's own bit-mixing. |
| Component | Role |
|---|---|
MotifGenerator |
Slides a window over recent pitches; finds the most-repeated sub-sequence as the current motif. |
HarmonicProgression |
Maps phase-space regions to chord progressions: Classical (I–IV–V–I), Jazz Turnaround (ii7–V7–Imaj7–VI7), Modal (scale-derived triads), Jazz Extended (9th/11th/13th tensions added proportional to chaos level). |
RhythmicQuantizer |
Snaps the continuous ODE output to a rhythmic grid. Supports 4/4, 3/4, 5/4, 6/8, and 7/8 time signatures with configurable sub-division. |
CompositionExporter |
Builds a 3-track MIDI SMF (melody, harmony, bass) and writes it to disk via the existing MIDI infrastructure. |
use math_sonify::composer::{ComposerEngine, MusicalForm, ProgressionStyle, TimeSig};
let mut engine = ComposerEngine::new(
MusicalForm::Aba,
ProgressionStyle::JazzTurnaround,
TimeSig::FOUR_FOUR,
120.0, // BPM
120.0, // control rate Hz
8, // section length in bars
);
// Tick the engine each sim step:
let frame = engine.tick(&state, melody_pitch, velocity, chaos_level, lyapunov);
// frame.chord, frame.motif, frame.section_idx, frame.bar_position, …
// Export when done:
engine.export_midi("composition.mid", 120.0).ok();The fractal analyzer characterises the geometric and dynamical structure of the attractor in real time. All metrics are displayed in the Math View tab.
| Algorithm | Struct | What it computes |
|---|---|---|
| Box-counting | BoxCounting |
D₀ (Minkowski–Bouligand dimension). 2-D projection; slope of log N(ε) vs log(1/ε). Lorenz ≈ 2.05, Hénon ≈ 1.26. |
| Correlation dimension | CorrelationDimension |
D₂ (Grassberger–Procaccia). Counts point pairs within distance r; slope of log C(r) vs log r. |
| Full Lyapunov spectrum | LyapunovSpectrum |
All N exponents via QR/Gram–Schmidt on the tangent bundle. Kaplan–Yorke dimension D_KY = j + Σλᵢ/ |
pub struct AttractorCharacterization {
pub fractal_dim: f64, // Box-counting D₀
pub correlation_dim: f64, // Grassberger-Procaccia D₂
pub lyapunov_spectrum: Vec<f64>, // Full spectrum λ₁ ≥ λ₂ ≥ ... ≥ λₙ
pub kaplan_yorke_dim: f64, // D_KY from the spectrum
pub kolmogorov_entropy: f64, // hKS = Σ positive λᵢ
pub phase_space_volume_contraction: f64, // Σ all λᵢ (< 0 for dissipative)
pub attractor_type: AttractorType, // FixedPoint / LimitCycle / StrangeAttractor / Hyperchaos
pub sample_size: usize,
pub last_updated_ticks: u64,
}AttractorType is derived automatically from the spectrum sign pattern:
| Type | Criterion |
|---|---|
| Fixed Point | All λᵢ < 0 |
| Limit Cycle | Exactly one zero exponent, rest negative |
| Quasi-Periodic (T²) | Two zero exponents |
| Strange Attractor | Exactly one positive exponent |
| Hyperchaos | Two or more positive exponents |
use math_sonify::fractal::FractalAnalyzer;
let mut analyzer = FractalAnalyzer::new(3); // 3-D system
// Called periodically in the sim thread:
let ch = analyzer.analyze(&trajectory, &lorenz_deriv, dt, current_tick);
println!("{}", analyzer.lyapunov_spectrum().summary());
// λ = [+0.9053, -0.0001, -14.572] D_KY=2.062 hKS=0.9053 div=-13.667OscillatorNetwork places up to 16 coupled oscillators on an arbitrary graph topology, with each oscillator mapped to a separate audio voice.
| Topology | Description |
|---|---|
Ring |
Each node connected to its two nearest neighbours (circular). |
StarGraph |
Central hub connected to all leaves; leaves only connect to the hub. |
SmallWorld(p) |
Watts–Strogatz: start from a ring and rewire each edge with probability p. |
RandomErdos(p) |
Each pair connected independently with probability p. |
FullyConnected |
All-to-all coupling; mean-field limit of the Kuramoto model. |
Kuramoto network — phase oscillators on an arbitrary graph:
dθᵢ/dt = ωᵢ + Σⱼ Kᵢⱼ sin(θⱼ − θᵢ)
Each oscillator's phase maps directly to an audio frequency. The order parameter r ∈ [0, 1] measures synchronisation.
Stuart–Landau network — complex-amplitude oscillators (normal form of the Hopf bifurcation):
dAᵢ/dt = (μᵢ + iωᵢ − |Aᵢ|²)·Aᵢ + Σⱼ Kᵢⱼ·Aⱼ
With diffusive coupling and heterogeneous μ, the network exhibits:
- Amplitude death (μ < 0 and coupling pushes all voices to zero).
- Oscillation revival (coupling restores oscillations suppressed by individual μ < 0).
Each tick, OscillatorNetwork::state() returns a NetworkState:
pub struct NetworkState {
pub n: usize,
pub frequencies: Vec<f64>, // per-voice audio frequency (Hz)
pub amplitudes: Vec<f64>, // per-voice amplitude [0, 1]
pub phases: Vec<f64>, // oscillator phase (radians)
pub order_parameter: f64, // Kuramoto r or mean amplitude
pub amplitude_death: bool, // true if network collapsed to zero
pub active_voices: usize, // number of non-silent voices
}NetworkState::sorted_voices() returns (frequency, amplitude) pairs sorted by amplitude, ready for polyphonic voice assignment.
use math_sonify::network::{OscillatorNetwork, NetworkTopology};
// 8 Kuramoto oscillators in a small-world graph.
let mut net = OscillatorNetwork::kuramoto(
8,
&NetworkTopology::SmallWorld { rewire_prob: 0.15 },
2.0, // coupling K
0.4, // natural frequency spread
220.0, // base audio frequency (Hz)
440.0, // frequency range (Hz)
42, // seed
);
// Sim loop (120 Hz):
net.step(1.0 / 120.0);
let st = net.state();
// Route st.frequencies[i] and st.amplitudes[i] to audio voice i.
println!("r = {:.3} voices = {}", st.order_parameter, st.active_voices);| Scale | Intervals (semitones) |
|---|---|
| Pentatonic | 0, 2, 4, 7, 9 |
| Natural Minor (Aeolian) | 0, 2, 3, 5, 7, 8, 10 |
| Harmonic Minor | 0, 2, 3, 5, 7, 8, 11 |
| Dorian | 0, 2, 3, 5, 7, 9, 10 |
| Phrygian | 0, 1, 3, 5, 7, 8, 10 |
| Lydian | 0, 2, 4, 6, 7, 9, 11 |
| Mixolydian | 0, 2, 4, 5, 7, 9, 10 |
| Locrian | 0, 1, 3, 5, 6, 8, 10 |
| Whole Tone | 0, 2, 4, 6, 8, 10 |
| Blues | 0, 3, 5, 6, 7, 10 |
| Hirajoshi | 0, 2, 3, 7, 8 |
| Hungarian Minor | 0, 2, 3, 6, 7, 8, 11 |
| Octatonic (dim.) | 0, 2, 3, 5, 6, 8, 9, 11 |
| Chromatic | all 12 semitones |
| Just Intonation | pure-ratio tuning |
| Microtonal | 24 equal divisions per octave |
| EDO-19 | 19 equal divisions of the octave |
| EDO-31 | 31 equal divisions of the octave |
| EDO-24 | 24-TET (quarter-tones) |
| Harmonic Series | 16 partials of a fundamental |
math-sonify can export attractor trajectories to Standard MIDI Files (SMF format 0) that import cleanly into Ableton Live, FL Studio, Logic Pro, Reaper, and any other DAW.
| Attractor coordinate | MIDI parameter |
|---|---|
| X | Note pitch -- quantised to the selected scale |
| Y | Velocity (64-127) |
| Z | Note duration (16th note to whole note, exponentially scaled) |
| Simulation speed | BPM written into the file tempo event |
- Open the MIXER tab.
- Click Record MIDI to start capturing. The status bar shows the frame count.
- Click Stop + Export to choose a filename and write the
.midfile.
use math_sonify_plugin::midi_export::{MidiExporter, SCALE_PENTATONIC_C4};
// trajectory is a Vec<(f64, f64, f64)> collected from the ODE solver
let exporter = MidiExporter::new(); // 480 ticks per quarter note
let track = exporter.trajectory_to_track(
"Lorenz Take 1",
&trajectory,
SCALE_PENTATONIC_C4,
120.0, // BPM
);
exporter.export_to_file(&[track], "lorenz_take1.mid")?;Multiple tracks can be passed to export_smf / export_to_file; they are merged into the single track required by SMF format 0, with each track's notes placed on a different MIDI channel (0-15) for DAW separation.
cargo run --release -- --headless --duration 30 --output take.wav --export-midi take.midmath-sonify ships with 16 named presets organised in a browsable in-memory catalogue. Each preset carries:
- System -- which dynamical system it uses.
- Mood tags --
atmospheric,rhythmic,experimental,meditative,melodic,percussive,drone,eerie,evolving,hypnotic,minimalist,complex,electronic,energetic. - BPM range -- the tempo window in which the preset sounds best.
- Complexity -- 1 (minimal) to 5 (dense).
The SYNTH tab has a Presets panel with:
- A scrollable list filtered by mood or system.
- A search box for partial name/description match.
- A heart icon to toggle favorites.
- A Discover button that picks a random preset weighted toward entries you have played least.
use math_sonify_plugin::preset_gallery::PresetGallery;
let mut gallery = PresetGallery::with_builtin_presets();
// Filter by mood
let drones = gallery.by_mood("drone");
// Search
let results = gallery.search("butterfly");
// Random discovery (weighted by inverse play count)
if let Some(preset) = gallery.random_discovery() {
println!("Try: {} ({})", preset.name, preset.system);
gallery.record_play(&preset.name.clone());
}
// Favorites
gallery.toggle_favorite("Lorenz Ambience");
let favs = gallery.favorites();| Name | System | Moods | Complexity |
|---|---|---|---|
| Lorenz Ambience | Lorenz | atmospheric, meditative, melodic | 2 |
| Pendulum Rhythm | Double Pendulum | rhythmic, percussive, energetic | 3 |
| Torus Drone | Geodesic Torus | atmospheric, meditative, drone | 2 |
| Kuramoto Sync | Kuramoto | experimental, evolving, hypnotic | 3 |
| Three-Body Jazz | Three-Body | melodic, rhythmic, complex | 4 |
| Rossler Drift | Rossler | atmospheric, melodic, meditative | 2 |
| FM Chaos | Lorenz | experimental, electronic, energetic | 4 |
| Pendulum Meditation | Double Pendulum | meditative, atmospheric, drone | 2 |
| Thomas Labyrinth | Thomas | atmospheric, experimental, eerie | 3 |
| Neural Burst | Hindmarsh-Rose | rhythmic, percussive, experimental | 4 |
| Chemical Wave | Oregonator | atmospheric, evolving, hypnotic | 3 |
| Sprott Minimal | Sprott E | experimental, electronic, minimalist | 2 |
| Substorm Pulse | WINDMI | rhythmic, atmospheric, electronic | 3 |
| Market Collapse | Finance | experimental, eerie, complex | 4 |
| Hyperdimensional | Hyperchaos | experimental, complex, electronic | 5 |
| Magyar Trance | Dadras | meditative, melodic, atmospheric | 3 |
math-sonify includes two complementary collaboration features: a low-level protocol (collaboration.rs) for sharing attractor state between performers, and a full-featured Collaborative Session server (collab.rs) for real-time multi-user parameter editing.
The session server is a raw-TCP WebSocket-style server that accepts JSON connections from any number of participants. Each participant can claim ownership of specific ODE parameters, edit them in real time, and all changes propagate immediately to every other connected client.
Key types:
| Type | Role |
|---|---|
CollabServer |
TCP listener; spawns a thread per client |
SessionEvent |
Events emitted to the simulation thread (ParamChanged, ClientJoined, ClientLeft) |
SharedSynthState |
ODE parameters + sonification mode + scale, wrapped in Arc<RwLock<>> for lock-free reads |
ParticipantCursor |
Each participant has a unique colour highlight on the parameter they are currently editing |
SessionLog |
Full ordered history of every parameter change for post-session replay |
Wire protocol (newline-delimited JSON):
// Client -> Server
{ "claim": ["rho", "sigma"] }
{ "set": { "rho": 28.5 } }
{ "release": ["rho"] }
// Server -> Client
{ "welcome": { "client_id": 3 } }
{ "update": { "rho": 28.5, "owner": 3 } }
{ "error": "parameter 'rho' is owned by client 1" }
{ "peer_joined": { "client_id": 4, "total": 2 } }
{ "peer_left": { "client_id": 4, "total": 1 } }Conflict resolution: last-write-wins per parameter. A participant must first claim a parameter; attempts to set an unclaimed or foreign-owned parameter are rejected with an error message.
Starting the server:
use math_sonify_plugin::collab::{CollabServer, SessionEvent};
use crossbeam_channel::unbounded;
let (tx, rx) = unbounded::<SessionEvent>();
let server = CollabServer::new("127.0.0.1:9001", tx).unwrap();
server.run_background();
// In the simulation thread:
for event in rx.try_iter() {
match event {
SessionEvent::ParamChanged { name, value, .. } => { /* apply to ODE */ }
_ => {}
}
}Connect any WebSocket client (browser, websocat, Python websockets) to ws://127.0.0.1:9001 to join the session.
math-sonify also includes a JSON-based collaborative performance protocol that lets multiple performers share attractor state in real time. Any transport layer (WebSocket, UDP, OSC) can carry the messages; the module itself only handles serialisation and session logic.
- Session -- a named room (e.g.
"concert-2026-03-22") that holds up to 8 performers. - Performer -- identified by a unique string ID, carries live
(x, y, z)attractor coordinates, BPM, volume, and an RGB colour. - Messages -- typed JSON objects:
JoinSession,LeaveSession,StateUpdate,ParameterSync,ChatMessage,KickOff.
use math_sonify_plugin::collaboration::{CollaborationClient, PerformerState};
// Create local performer
let performer = PerformerState::new("alice-01", "Alice");
let mut client = CollaborationClient::new(performer);
// Join
let join_msg = client.join_message("my-session");
let json = CollaborationClient::serialize_message(&join_msg);
// ... send json over your WebSocket / UDP socket ...
// Each sim tick: push current attractor state
let update = client.push_xyz(lorenz_x, lorenz_y, lorenz_z);
let json = CollaborationClient::serialize_message(&update);
// ... send json ...
// Receive a message
let incoming_json = r#"{"type":"KickOff","session_id":"my-session","bpm":128.0}"#;
let msg = CollaborationClient::deserialize_message(incoming_json).unwrap();use math_sonify_plugin::collaboration::CollaborationSession;
let mut session = CollaborationSession::new("my-session");
// On receive JoinSession
session.join(performer_state)?;
// On receive StateUpdate
session.update_state(performer_state);
// Broadcast mean-attractor coordinates to new joiners
let sync_msg = session.broadcast_message();{ "type": "JoinSession", "session_id": "room1", "performer": { ... } }
{ "type": "LeaveSession", "performer_id": "alice-01" }
{ "type": "StateUpdate", "performer": { "x": 1.2, "y": -3.4, "z": 0.8, ... } }
{ "type": "ParameterSync", "params": { "rho": 28.0, "sigma": 10.0 } }
{ "type": "ChatMessage", "performer_id": "alice-01", "text": "raising sigma now" }
{ "type": "KickOff", "session_id": "room1", "bpm": 128.0 }math-sonify ships with ~40 named presets organised into four moods:
- Atmospheric -- Midnight Approach, Breathing Galaxy, Aurora Borealis, Deep Hypnosis, Cathedral Organ, Substorm, and more.
- Rhythmic -- Frozen Machinery, The Phase Transition, Clockwork Insect, Industrial Heartbeat, Velocity Band, and more.
- Experimental -- Neon Labyrinth, Dissociation, Jerk Circuit, Invisible Hand, Hyperchaos Engine, and more.
- Melodic -- Glass Harp, Electric Kelp, The Butterfly's Aria, Solar Wind, and more.
The AUTO arrangement generator picks 6 presets from a mood pool, scatters system parameters into varied dynamical regimes, randomises synthesis settings, and builds an 8-scene timeline with morphs as the main musical event.
math-sonify outputs 32-bit IEEE float stereo PCM at the system default sample rate (44100 or 48000 Hz).
| Export method | Details |
|---|---|
Clip save (S) |
Last 60 seconds -> 32-bit float WAV in clips/ |
| Loop export | Current loop region -> WAV |
| MIDI export | Trajectory -> SMF .mid importable into any DAW |
| Headless render | --headless --duration 60 --output clip.wav -- no display required |
Download math-sonify.exe from the latest release and run it. No dependencies, no install.
Requires Rust 1.75+ and a working audio output device.
git clone https://github.com/Mattbusel/math-sonify
cd math-sonify
cargo run --releasecargo build --release --libCopy the output to your DAW plugin folder:
| Platform | File | Destination |
|---|---|---|
| Windows | math_sonify_plugin.dll |
C:\Program Files\Common Files\VST3\ |
| Linux | libmath_sonify_plugin.so |
~/.vst3/ |
| macOS | libmath_sonify_plugin.dylib |
~/Library/Audio/Plug-Ins/VST3/ |
After copying, trigger a plugin rescan in your DAW (Options > Plug-in Manager in Ableton; Plug-in Database > Rescan in FL Studio).
- Run
cargo build --release --lib. - Locate the output file in
target/release/. - Copy to the system VST3 folder for your platform (table above).
- Open your DAW and trigger a plugin rescan.
- Search for "math-sonify" in the plugin browser.
- The plugin exposes all system parameters as automatable VST3 parameters.
- MIDI output from the plugin can be routed to any instrument track.
Five top-level tabs:
- SYNTH -- system selector, parameter sliders, sonification mode, scale, effects chain, randomize, preset browser.
- MIXER -- per-layer volume/pan/ADSR, master effects (EQ, delay, chorus, reverb), VU meters, WAV export, MIDI record/export.
- ARRANGE -- scene timeline, morph time controls, AUTO arrangement generator with mood selection.
- MATH VIEW -- live phase portrait (XY/XZ/YZ/3D), bifurcation diagram, custom ODE text input, state readout.
- WAVEFORM -- oscilloscope and spectrum analyzer.
Performance mode (F) switches to fullscreen phase portrait only.
| Key | Action |
|---|---|
F |
Toggle fullscreen performance mode |
Space |
Pause / resume simulation |
R |
Reset attractor to default initial condition |
S |
Save clip (last 60 seconds as WAV) |
Ctrl+S |
Save current configuration to config.toml |
1 -- 7 |
Switch sonification mode |
< / > |
Previous / next dynamical system |
Up / Down |
Increase / decrease simulation speed by 10% |
E |
Toggle Evolve (autonomous parameter wandering) |
A |
Toggle AUTO arrangement playback |
P |
Play / stop scene arranger |
M |
Toggle MIDI record |
Escape |
Exit fullscreen |
The application reads config.toml from the current working directory at startup. The file is watched with notify; edits take effect without restarting.
[system]
name = "lorenz" # see full system list above
dt = 0.001 # ODE integration time step (clamped 0.0001..0.1)
speed = 1.0 # simulation speed multiplier (0..100)
[lorenz]
sigma = 10.0
rho = 28.0
beta = 2.6667
[rossler]
a = 0.2
b = 0.2
c = 5.7
[hyperchaos]
a = 35.0
b = 3.0
c = 28.0
d = -7.0 # must be negative
[windmi]
a = 0.9
b = 2.5
[finance]
a = 3.0
b = 0.1
c = 1.0
[kuramoto]
n_oscillators = 8
coupling = 1.5
[duffing]
delta = 0.3
alpha = -1.0
beta = 1.0
gamma = 0.5
omega = 1.2
[van_der_pol]
mu = 2.0
[audio]
sample_rate = 44100
buffer_size = 512
reverb_wet = 0.4
delay_ms = 300.0
delay_feedback = 0.3
master_volume = 0.7
bit_depth = 16.0 # 1..32 (32 = bypass bitcrusher)
rate_crush = 0.0 # 0..1 (0 = bypass)
chorus_mix = 0.0
chorus_rate = 0.5 # Hz
chorus_depth = 3.0 # ms
waveshaper_drive = 1.0
waveshaper_mix = 0.0
[sonification]
mode = "direct"
# Modes: direct | orbital | granular | spectral | fm | am | vocal | waveguide | resonator
scale = "pentatonic"
# Scales: pentatonic | natural_minor | harmonic_minor | dorian | phrygian | lydian
# | mixolydian | locrian | whole_tone | blues | hirajoshi | hungarian_minor
# | octatonic | chromatic | just_intonation | microtonal | edo19 | edo31 | edo24
# | harmonic_series
base_frequency = 220.0
octave_range = 3.0
transpose_semitones = 0.0
chord_mode = "none"
# Chord modes: none | major | minor | power | sus2 | octave | dom7 | open_fifth | cluster
portamento_ms = 80.0
voice_levels = [1.0, 0.8, 0.6, 0.4]
voice_shapes = ["sine", "sine", "sine", "sine"]
# Shapes: sine | saw | square | triangle | noise
[viz]
trail_length = 800
projection = "xy" # xy | xz | yz | 3d
glow = true
theme = "neon" # neon | amber | ice | monoIn addition to the classic forward pipeline (ODE state → audio), math-sonify can reverse the flow: use incoming microphone audio to continuously modify ODE parameters in real time.
Microphone / line-in (cpal default input)
|
v per-frame (configurable hop size, default 512 samples)
AudioInputAnalyzer
| computes:
| RMS → Lorenz σ (louder = more chaos, σ ∈ [5, 30])
| Centroid → Lorenz ρ (brighter spectrum = higher ρ, ρ ∈ [15, 60])
| Flux → system switch trigger (transients trigger attractor changes)
| 8-band energy → Lorenz β (mid-band energy → β ∈ [1.5, 4.0])
v
AudioFeatures { rms, centroid, flux, bands: [f32; 8] }
|
v
AudioOdeBridge (delta suppression: only emits patch when change exceeds threshold)
|
v
OdePatch → simulation thread (applies sigma/rho/beta overrides)
DualMode lets both pipelines run simultaneously:
| Mode | Description |
|---|---|
ForwardOnly |
Classic: ODE state drives audio synthesis (default) |
ReverseOnly |
Microphone input drives ODE parameters only |
Both |
Both paths active simultaneously — environment modulates the attractor which modulates the sound which feeds back into the environment |
use math_sonify_plugin::audio_driven::{AudioOdeBridge, BridgeConfig, DualMode, DualModeKind};
use crossbeam_channel::unbounded;
// Build the reverse pipeline
let dual = DualMode::new(BridgeConfig::default());
let (mut analyzer, patch_rx, _stop) = dual.build_reverse_pipeline();
// Feed audio samples from cpal input callback:
// analyzer.feed(sample); // called per sample in the cpal callback
// In the simulation thread:
for patch in patch_rx.try_iter() {
if let Some(sigma) = patch.sigma { ode_params.sigma = sigma; }
if let Some(rho) = patch.rho { ode_params.rho = rho; }
if patch.trigger_system_switch { /* switch to next attractor */ }
}| Field | Default | Description |
|---|---|---|
sigma_min / sigma_max |
5.0 / 30.0 | Lorenz σ range |
rho_min / rho_max |
15.0 / 60.0 | Lorenz ρ range |
beta_min / beta_max |
1.5 / 4.0 | Lorenz β range |
flux_switch_threshold |
0.6 | Flux above this triggers a system switch |
fft_size |
1024 | FFT frame size (power of two) |
hop_size |
512 | Samples between analysis frames |
A dynamical system is a set of differential equations dx/dt = f(x) or a map x_{n+1} = f(x_n). The long-term behaviour of trajectories in phase space determines the system's character:
- Fixed point — all trajectories converge to a single point (stable equilibrium).
- Limit cycle — trajectories converge to a closed loop (periodic oscillation).
- Quasi-periodic — trajectories wind around a torus; the ratio of frequencies is irrational.
- Chaotic attractor (strange attractor) — trajectories are bounded but never repeat; nearby trajectories diverge exponentially (sensitive dependence on initial conditions).
The maximal Lyapunov exponent λ₁ quantifies the average rate of exponential divergence of nearby trajectories:
||δx(t)|| ≈ e^{λ₁ t} ||δx(0)||
- λ₁ < 0: stable fixed point or limit cycle.
- λ₁ = 0: quasi-periodic or at a bifurcation boundary.
- λ₁ > 0: chaotic. The Lorenz system at standard parameters has λ₁ ≈ 0.906.
math-sonify estimates λ₁ using a standard rescaling algorithm: a shadow trajectory is integrated alongside the main one, the separation is measured every N steps, its logarithm is accumulated, and the separation is rescaled. This runs at LYAP_INTERVAL_TICKS (every 2 seconds of sim time).
src/hindmarsh_rose.rs provides a clean typed API for the Hindmarsh-Rose
bursting neuron model (HindmarshRoseConfig, HindmarshRoseState,
HindmarshRoseNeuron). The neuron implements the classic three-variable system:
dx/dt = y - a·x³ + b·x² - z + I_ext
dy/dt = c - d·x² - y
dz/dt = r · (s·(x - x_rest) - z)
Classic chaotic-bursting parameters: a=1, b=3, c=1, d=5, r=0.001, s=4, x_rest=-1.6, i_ext=1.5.
# Render hindmarsh-rose to WAV
math-sonify --headless --attractor hindmarsh-rose --duration 5 --output hr.wav
# Run spectral analysis on the trajectory
math-sonify --headless --attractor hindmarsh-rose --spectrumsrc/spectrum_analyzer.rs provides SpectralAnalyzer which computes the DFT
of an arbitrary sample sequence and returns dominant frequencies.
- Cooley-Tukey radix-2 FFT for power-of-2 lengths; O(N²) DFT fallback.
- Hann windowing to reduce spectral leakage.
DftResult { frequencies, magnitudes, dominant_freq, spectral_centroid }.SpectralAnalyzer::dominant_frequencies(result, top_k)— sorted by magnitude.
The FFT module (spectrum.rs) computes a 1024-point Hann-windowed FFT of the synthesised audio output and displays:
- Magnitude spectrum (dB scale, linear frequency axis).
- Spectral centroid (brightness indicator).
- Fundamental frequency estimate via parabolic interpolation on the magnitude peak.
The audio-driven morphing module uses the same FFT on the input signal to extract audio features.
The Lorenz system (σ=10, β=8/3, ρ) undergoes the following transitions as ρ increases:
| ρ range | Behaviour |
|---|---|
| ρ < 1 | All trajectories converge to origin |
| 1 < ρ < 13.93 | Two stable fixed points (C+ and C−) |
| 13.93 < ρ < 24.06 | Unstable limit cycles; trajectories still attracted to C± |
| ρ > 24.74 | Strange attractor (chaos onset) — the classic butterfly |
# Run all unit and integration tests (~1650 tests, no display required)
cargo test --lib --tests
# Release binary
cargo build --release --bin math-sonify
# Release plugin
cargo build --release --lib
# Documentation
cargo doc --no-deps --openThe test suite covers: ODE solver accuracy (attractor bounds, energy conservation, synchronization thresholds), scale quantization, polyphony, config parsing and clamping, scene arranger timeline consistency, oscillator amplitude bounds, ADSR envelope behavior, all-presets load/validate, lerp_config correctness for every system, bifurcation parameter sweeps, MIDI frame recording and SMF export, preset gallery filtering and discovery, and collaboration session/client message round-trips.
No audio / device not found
- math-sonify uses
cpal::default_host().default_output_device(). Ensure a device is selected in OS audio settings. - Windows exclusive mode: close any application holding the device exclusively.
- Linux ALSA:
sudo apt install libasound2-dev, add user toaudiogroup. - Sample rate mismatch: set
sample_rate = 48000inconfig.toml.
High CPU usage
- Increase
buffer_sizeto 1024 or 2048. - Disable Evolve mode when not in use.
- For Three-Body and Lorenz-96, reduce
system.speed.
Distorted audio
- Lower
audio.master_volume. - Set
waveshaper_drive = 1.0andwaveshaper_mix = 0.0.
Phase portrait blank
- Wait 2-3 seconds for the trail to build after startup or after pressing
R.
Config not loading
- math-sonify looks for
config.tomlin the current working directory.
VST3/CLAP not appearing
- Copy to the correct system folder and trigger a plugin rescan in your DAW.
- The plugin requires
cargo build --release --lib, not--bin.
MIDI export produces empty file
- Start recording before the session (click Record MIDI in the MIXER tab), then export.
- Headless: pass
--export-midi output.midon the command line.
The bifurcation sweeper runs a dynamical system across a continuous range of a single parameter, records the steady-state attractor at each step, and exports the results in two formats.
| Output | Description |
|---|---|
| SVG diagram | Attractor z-coordinate vs parameter value — classic bifurcation plot rendered as a dark-background SVG. |
| Sweep WAV | Each parameter step rendered as a short audio clip, concatenated into a single mono WAV file that audibly sweeps through the parameter range. |
Trigger from the UI with the Bifurcation Sweep button in the Bifurc tab (tab 7), or call from Rust:
use math_sonify::bifurcation::{BifurcationConfig, BifurcationSweeper};
let config = BifurcationConfig {
parameter_name: "rho".into(),
range_start: 0.5,
range_end: 30.0,
steps: 200,
duration_per_step_ms: 300,
};
let result = BifurcationSweeper::sweep(&config, &base_cfg, Path::new("recordings"))?;
println!("WAV written to: {}", result.audio_path.display());Files are written to the recordings/ directory (created automatically).
Linear interpolation between any two named presets. All numeric fields are blended continuously; string fields (system name, mode, scale, chord mode) switch at t = 0.5.
| Type | Purpose |
|---|---|
PresetInterpolator |
Single shot — interpolate between two configs at any t in [0, 1]. |
PresetMorphSchedule |
A sequence of (preset_name, duration_ms) pairs forming a morph timeline. |
MorphTimeline |
Stateful player for a PresetMorphSchedule; call .tick() each frame. |
MorphState |
Current position (0–1) between source and target with completion check. |
use math_sonify::preset_interpolation::{interpolate, PresetMorphSchedule, MorphTimeline};
// Single interpolation at t = 0.5
let mid = interpolate(&load_preset("Lorenz Ambience"), &load_preset("FM Chaos"), 0.5);
// Full morph timeline
let sched = PresetMorphSchedule::from_pairs(&[
("Lorenz Ambience", 8_000),
("FM Chaos", 6_000),
("Thomas Labyrinth", 10_000),
]);
let mut timeline = MorphTimeline::new(sched);
loop {
let cfg = timeline.tick(); // call each UI frame
engine.apply_config(cfg);
if timeline.is_finished() { break; }
}The Morph control in the ARRANGE tab exposes source/target preset selectors and a duration slider.
Enables real-time parameter synchronization between multiple running instances over UDP multicast.
Enable with --features osc (adds the rosc optional dependency).
| Path | Arguments | Action |
|---|---|---|
/mathsonify/param/{name} |
f32 value |
Set a named parameter on all peers |
/mathsonify/preset/{name} |
(none) | Switch preset across all instances |
/mathsonify/sync/beat |
f32 timestamp |
Beat sync for tempo alignment |
| Type | Role |
|---|---|
OscSyncServer |
Listens on UDP port 9001; joins multicast group 239.0.0.1. |
OscSyncClient |
Broadcasts messages to the same multicast group. |
CollaborativeSession |
Tracks connected peers by IP; applies last-writer-wins conflict resolution with monotonic timestamps. Peer count shown in status bar. |
use math_sonify::osc_sync::{OscSyncServer, OscSyncClient, CollaborativeSession};
let server = OscSyncServer::new()?;
let client = OscSyncClient::new()?;
let session = CollaborativeSession::new();
// Broadcast a parameter change:
client.send_param("reverb_wet", 0.6)?;
// Receive and apply incoming changes:
if let Some((addr, msg)) = server.try_recv() {
session.apply(addr, msg);
}
println!("{} peer(s) connected", session.peer_count());Press R to start/stop recording. Files are saved to recordings/YYYYMMDD_HHMMSS.wav (epoch timestamp). A pulsing red REC indicator appears in the status bar while active.
| Option | Values | Default |
|---|---|---|
| Bit depth | 16-bit int, 32-bit float | 32-bit float |
| Sample rate | 44.1 kHz, 48 kHz | matches audio engine |
| Type | Purpose |
|---|---|
AudioRecorder |
Appends live stereo interleaved f32 samples to a WAV file. |
SegmentRecorder |
Fixed-duration clip (default 60 s), auto-named by system + preset. |
use math_sonify::recorder::{AudioRecorder, RecordingDepth, RecordingSampleRate, SegmentRecorder};
// Open a rolling recorder
let mut rec = AudioRecorder::start(
Path::new("recordings"),
RecordingDepth::Bits32,
RecordingSampleRate::Hz44100,
)?;
rec.push_samples(&stereo_f32_samples)?;
let path = rec.stop()?;
// Auto-named segment (60 s clip)
let mut seg = SegmentRecorder::start(
Path::new("recordings"),
"lorenz",
"Lorenz Ambience",
60,
RecordingDepth::Bits32,
RecordingSampleRate::Hz44100,
)?;
let done = seg.push_samples(&samples)?; // returns true when clip is full
if done { seg.finish()?; }- Fork and create a feature branch.
- Run
cargo fmt --allandcargo clippy --all-targets --all-features -- -D warnings. - Add tests for new public API (unit tests in the module, integration tests in
tests/integration.rs). - Open a pull request. CI (fmt, clippy, test, doc, release build) must pass.
Code style: no unsafe without comment, no .unwrap() in src/ outside tests, audio thread must be real-time safe (no heap allocation, no blocking I/O).
A standalone module providing idiomatic config/state types for the Rössler spiral strange attractor.
| Type | Description |
|---|---|
RosslerConfig { a, b, c } |
Classic params: a=0.2, b=0.2, c=5.7 (default) |
RosslerState { x, y, z } |
Current phase-space position |
RosslerAttractor |
RK4 integrator with step(dt), state(), derivatives() |
dx/dt = -y - z
dy/dt = x + a·y
dz/dt = b + z·(x - c)
With a=0.2, b=0.2, c=5.7 the attractor is bounded (|x|, |y| < 30) and exhibits near-periodic chaos.
use math_sonify_plugin::rossler::{RosslerAttractor, RosslerConfig};
let mut attractor = RosslerAttractor::new(RosslerConfig::default());
for _ in 0..1000 {
attractor.step(0.01);
}
let s = attractor.state();
println!("x={:.3} y={:.3} z={:.3}", s.x, s.y, s.z);A standalone module for the Van der Pol self-sustaining limit-cycle oscillator.
| Type | Description |
|---|---|
VanDerPolConfig { mu } |
Nonlinearity parameter; mu=1.0 is classic |
VanDerPolState { x, y } |
Displacement and velocity |
VanDerPolOscillator |
RK4 integrator with step(dt), state(), derivatives() |
dx/dt = y
dy/dt = μ·(1 − x²)·y − x
For μ > 0 the system converges to a stable limit cycle. Larger μ gives increasingly relaxation-oscillator-like behaviour with sharp transitions.
use math_sonify_plugin::vanderpol::{VanDerPolConfig, VanDerPolOscillator};
let mut osc = VanDerPolOscillator::new(VanDerPolConfig { mu: 2.0 });
for _ in 0..2000 {
osc.step(0.01);
}
let s = osc.state();
println!("x={:.3} y={:.3}", s.x, s.y);DX7-style frequency modulation synthesis is now available as a PhysicalSynth mode.
| Type | Description |
|---|---|
FmConfig { carrier_ratio, modulator_ratio, modulation_index } |
Classic DX7-style parameters |
AdsrEnvelope { attack_samples, decay_samples, sustain_level, release_samples } |
Sample-accurate ADSR |
FmSynth |
FM synthesizer implementing PhysicalSynth |
PhysicalMode::Fm |
New factory variant |
| State dimension | FM parameter |
|---|---|
state[0] |
Carrier frequency (log-mapped over freq_min..freq_max) |
state[1] |
Modulation index (0→4) |
state[2] |
ADSR re-trigger threshold |
use math_sonify_plugin::synthesis::{build_physical_synth, FmConfig, FmSynth, PhysicalMode};
let mut synth = build_physical_synth(PhysicalMode::Fm, 80.0, 1200.0, 44100.0);
let state = [1.0f64, 0.5, 0.0];
let sample = synth.next_sample(&state, 44100.0);Smoothly interpolates between two attractor states and sequences multiple attractors with S-curve crossfades.
use math_sonify_plugin::blend::{AttractorBlend, AttractorState, BlendConfig, MultiAttractorSequencer, SequenceEntry};
// Linear blend at 50%
let a = AttractorState::new(1.0, 2.0, 3.0);
let b = AttractorState::new(10.0, 20.0, 30.0);
let cfg = BlendConfig::new(0.5);
let mid = AttractorBlend::interpolate(a, b, &cfg);
// S-curve crossfade between two trajectories
let a_traj: Vec<AttractorState> = vec![AttractorState::new(0.0, 0.0, 0.0); 100];
let b_traj: Vec<AttractorState> = vec![AttractorState::new(5.0, 5.0, 5.0); 100];
let blended = AttractorBlend::smooth_transition(&a_traj, &b_traj, 50);
// Alpha schedule (smoothstep 0→1)
let schedule = AttractorBlend::morph(64);
// Sequence two attractors with crossfade
let entries = vec![
SequenceEntry { attractor_name: "lorenz".to_string(), duration_samples: 200, crossfade_samples: 40 },
SequenceEntry { attractor_name: "rossler".to_string(), duration_samples: 200, crossfade_samples: 40 },
];
let trajectory = MultiAttractorSequencer::render(&entries, 0.01);CLI: --blend lorenz:rossler — renders a blended trajectory and prints state count.
Key types:
AttractorState { x, y, z }— phase-space pointBlendConfig { alpha }— blend weight (0 = A, 1 = B)AttractorBlend::smooth_transition— smoothstep S-curve crossfadeAttractorBlend::morph— alpha schedule from 0 to 1MultiAttractorSequencer::render— full multi-segment sequencer
Maps attractor state values to musical pitches in a chosen scale/mode.
use math_sonify_plugin::scale_mapper::{MusicalScale, ScaleMapper, ScaleMode, midi_to_freq};
use math_sonify_plugin::blend::AttractorState;
// Create a D major scale
let scale = MusicalScale::new(62, ScaleMode::Major);
println!("{:?}", scale.pitch_class_set()); // [0, 2, 4, 5, 7, 9, 11]
// Quantize a value to the nearest note
let midi = scale.quantize(0.3, 2); // across 2 octaves
// Get a triad
let chord = scale.chord(-0.5); // [root, third, fifth]
// Full attractor → MIDI pipeline
let mapper = ScaleMapper::new(scale, 2);
let state = AttractorState::new(2.5, -0.8, 14.0);
let pitch = mapper.map_state(&state);
println!("MIDI {} ({:.1} Hz), degree {}, chord {:?}",
pitch.midi_note, pitch.freq_hz, pitch.scale_degree, pitch.chord);
// Hz conversion
let freq = midi_to_freq(69); // 440.0CLI: --scale major:60 — maps a sample Lorenz trajectory to the C major scale and prints MIDI notes.
Supported modes: Major, Minor, Pentatonic, Dorian, Phrygian, Lydian, WholeTone, Chromatic.
Key types:
MusicalScale { root_midi, mode }— scale definitionMusicalScale::pitch_class_set()— semitone intervals above rootMusicalScale::quantize(value, octaves)— maps [-1,1] → nearest MIDI noteMusicalScale::chord(value)— returns triad[root, third, fifth]ScaleMapper::map_state(state)→MappedPitch { midi_note, freq_hz, scale_degree, chord }
MIT. See LICENSE.
Built with Rust, cpal, egui, nih-plug, crossbeam, parking_lot, hound, rayon, tracing, serde, serde_json, midly.