math-sonify 1.4.0

Real-time procedural audio from mathematical dynamical systems (Lorenz, Rossler, Double Pendulum, and more)
# Contributing to math-sonify


Thank you for your interest in contributing. This document covers the most common extension points.

---

## Code structure overview


```
src/
├── main.rs              # Entry point: thread setup, sim loop, build_system()
├── config.rs            # Config struct (TOML-backed) — one sub-struct per system
├── patches.rs           # Named presets (PRESETS constant)
├── audio.rs             # Audio thread, DSP engine, shared type aliases
├── ui.rs                # egui UI, AppState
├── arrangement.rs       # Scene/arrangement interpolation
├── presets.rs           # Preset application helpers
├── lib.rs               # core library (shared by app and plugin)
├── systems/             # Dynamical systems
│   ├── mod.rs           # DynamicalSystem trait, rk4/rk45 helpers, re-exports
│   ├── lorenz.rs
│   └── ...              # One file per system
├── sonification/        # State → AudioParams mappers
│   ├── mod.rs           # AudioParams, SonifMode, Sonification trait
│   ├── direct.rs
│   └── ...              # One file per mode
└── synth/               # DSP building blocks
    ├── mod.rs
    ├── oscillator.rs
    └── ...              # One file per module
```

---

## How to add a new dynamical system


### 1. Implement `DynamicalSystem`


Create `src/systems/my_system.rs`:

```rust
use crate::systems::{DynamicalSystem, rk4};

pub struct MySystem {
    state: Vec<f64>,
    // system parameters
    pub param_a: f64,
}

impl MySystem {
    pub fn new(param_a: f64) -> Self {
        Self { state: vec![1.0, 0.0, 0.0], param_a }
    }

    fn deriv(state: &[f64], param_a: f64) -> Vec<f64> {
        // dx/dt = ...
        vec![/* ... */]
    }
}

impl DynamicalSystem for MySystem {
    fn state(&self) -> &[f64] { &self.state }
    fn dimension(&self) -> usize { 3 }
    fn name(&self) -> &str { "my_system" }

    fn step(&mut self, dt: f64) {
        let a = self.param_a;
        rk4(&mut self.state, dt, |s| Self::deriv(s, a));
    }

    fn deriv_at(&self, state: &[f64]) -> Vec<f64> {
        Self::deriv(state, self.param_a)
    }
}
```

Use `rk4` for dissipative systems and `leapfrog`/`yoshida4` for Hamiltonian ones. Override `speed()` if the granular sonification mode should respond to trajectory velocity. Override `energy_error()` for conservative systems that should display a conservation error.

### 2. Register in `systems/mod.rs`


```rust
pub mod my_system;
pub use my_system::MySystem;
```

### 3. Add a Config struct in `config.rs`


```rust
#[derive(Debug, Clone, Deserialize, Serialize)]

#[serde(default)]

pub struct MySystemConfig {
    pub param_a: f64,
}
impl Default for MySystemConfig {
    fn default() -> Self { Self { param_a: 1.0 } }
}
```

Add the field to the top-level `Config` struct and its `Default` impl, following the pattern of every existing system (e.g. `lorenz: LorenzConfig`).

### 4. Add to `build_system()` in `main.rs`


Find the `build_system` function and add a match arm:

```rust
"my_system" => Box::new(MySystem::new(config.my_system.param_a)),
```

### 5. Add a preset in `patches.rs`


```rust
Preset {
    name: "My Preset Name",
    description: "One evocative sentence about what this sounds like.",
    color: Color32::from_rgb(100, 200, 80),
    category: "Experimental",
},
```

Presets are display-only metadata. Patch application logic (mapping a preset name to concrete Config values) lives in `presets.rs` — add a corresponding arm there if you want the preset to change parameters when selected.

---

## How to add a new sonification mode


### 1. Create `src/sonification/my_mode.rs`


```rust
use crate::sonification::{AudioParams, Sonification};
use crate::config::SonificationConfig;

pub struct MyMapping { /* internal state */ }

impl MyMapping {
    pub fn new() -> Self { Self {} }
}

impl Sonification for MyMapping {
    fn map(&mut self, state: &[f64], speed: f64, config: &SonificationConfig) -> AudioParams {
        let mut p = AudioParams::default();
        p.mode = crate::sonification::SonifMode::MyMode;
        // populate p.freqs, p.amps, etc. from state
        p
    }
}
```

### 2. Add variant to `SonifMode` in `sonification/mod.rs`


```rust
pub enum SonifMode { Direct, Orbital, Granular, Spectral, FM, Vocal, Waveguide, MyMode }
```

Update the `Display` impl and the `From<&str>` impl in `config.rs`.

### 3. Register in `sonification/mod.rs`


```rust
pub mod my_mode;
pub use my_mode::MyMapping;
```

### 4. Wire into the sim thread in `main.rs`


In `sim_thread`, find where `DirectMapping`, `OrbitalResonance`, etc. are constructed and add your mode. Also add a rendering branch in `audio.rs` where the audio thread switches on `params.mode`.

---

## How to add a new synth module


Create `src/synth/my_module.rs` with a plain struct that takes `sample_rate: f32` in its constructor and exposes a `process(&mut self, input: f32) -> f32` (or stereo equivalent).

Register in `synth/mod.rs`:

```rust
pub mod my_module;
pub use my_module::MyModule;
```

Instantiate in `LayerSynth` inside `audio.rs` and call it in the audio callback. Expose control parameters through `AudioParams` fields (add them to the struct and its `Default` impl in `sonification/mod.rs`).

---

## Build instructions


### Standalone application


```sh
cargo build --release
```

The binary is at `target/release/math-sonify[.exe]`. Copy alongside `config.toml` for distribution.

### VST3 / CLAP plugin


```sh
cargo build --release -p math-sonify-plugin
```

The compiled `.dll` / `.so` / `.dylib` is the plugin binary. Use [cargo-nih-plug](https://github.com/robbert-vdh/nih-plug) or nih-plug's own bundler for proper VST3/CLAP bundle layout:

```sh
cargo xtask bundle math_sonify --release
```

### Development build


```sh
cargo build          # opt-level 1 for faster compile
cargo run            # runs the standalone app
```

### Lints and tests


```sh
cargo clippy --all-targets
cargo test
```

---

## Conventions


- Keep each dynamical system in its own file; do not add system logic to `main.rs`.
- `AudioParams` is `Clone` and crosses a bounded crossbeam channel from sim to audio — keep it `Send` and allocation-free (no heap fields).
- UI mutations go through `AppState` under a `parking_lot::Mutex`; use `try_lock` in hot paths.
- Prefer `f64` for simulation state and `f32` for audio.