Add readme files for each of the crates

This commit is contained in:
2026-03-25 16:46:09 +00:00
parent 496b6bdc71
commit c3cb7aa84b
3 changed files with 587 additions and 0 deletions

227
crates/synth-core/README.md Normal file
View File

@@ -0,0 +1,227 @@
# synth-core
A `no_std` DSP library providing analogue-modelling synthesizer building blocks. Designed to compile for any target — embedded bare-metal (Cortex-M), WebAssembly, and native — with no heap allocation.
## Overview
`synth-core` is the shared engine that powers both the RP2350 firmware (`synth-embedded`) and the browser visualiser (`synth-visualiser`). All modules operate on fixed-size blocks of `f32` samples using const-generic arrays, so every allocation happens on the stack at compile time.
```
VCO → Filter → VCA ← ADSR
↑ ↑
LFO LFO
```
## Modules
### `oscillator` — Voltage-Controlled Oscillator
Phase-accumulator oscillator with five waveforms.
```rust
use synth_core::{config::SR_48000, oscillator::{Vco, Waveform}, AudioProcessor};
let mut vco = Vco::new(SR_48000, 440.0, Waveform::Saw);
vco.waveform = Waveform::Square;
vco.freq_hz = 880.0;
let mut buf = [0.0f32; 256];
vco.process(&mut buf); // fills buf with one block of audio
```
**Waveforms:** `Sine`, `Saw`, `Square`, `Triangle`, `Pulse(f32)` (duty cycle 01).
### `filter` — State-Variable Filter
Two-integrator SVF with selectable mode and voltage-controlled cutoff.
```rust
use synth_core::{config::SR_48000, filter::{FilterMode, Svf}, AudioProcessor};
let mut filt = Svf::new(SR_48000, 2_000.0, 0.5, FilterMode::LowPass);
filt.cutoff_hz = 1_000.0;
filt.resonance = 0.8; // 0.0 = flat, 1.0 = self-oscillation
filt.process(&mut buf); // in-place
```
**Modes:** `LowPass`, `HighPass`, `BandPass`, `Notch`.
### `envelope` — ADSR Envelope Generator
Classic Attack / Decay / Sustain / Release envelope.
```rust
use synth_core::{config::SR_48000, envelope::Adsr, AudioProcessor};
let mut adsr = Adsr::new(SR_48000);
adsr.attack_s = 0.01;
adsr.decay_s = 0.2;
adsr.sustain = 0.7; // 0.01.0
adsr.release_s = 0.5;
adsr.gate_on(); // key press
let mut env = [0.0f32; 256];
adsr.process(&mut env); // outputs 0.01.0 amplitude envelope
adsr.gate_off(); // key release
```
### `vca` — Voltage-Controlled Amplifier
Multiplies audio by a gain value, optionally driven by an envelope.
```rust
use synth_core::{config::SR_48000, vca::Vca};
let mut vca = Vca::new(0.8); // fixed gain
vca.apply_envelope(&mut audio, &env); // per-sample multiply by envelope
```
### `lfo` — Low-Frequency Oscillator
Same waveform engine as the VCO, tuned to sub-audio rates.
```rust
use synth_core::{config::SR_48000, lfo::Lfo, oscillator::Waveform, AudioProcessor};
let mut lfo = Lfo::new(SR_48000);
lfo.rate_hz = 5.0;
lfo.depth = 0.5;
lfo.waveform = Waveform::Sine;
let mut cv = [0.0f32; 256];
lfo.process(&mut cv); // outputs ±depth CV
```
### `midi` — MIDI Byte-Stream Parser
Parses a raw MIDI byte stream (running status supported) into typed events.
```rust
use synth_core::midi::{MidiParser, MidiEvent};
let mut parser = MidiParser::new();
// Feed bytes from UART / USB
if let Some(event) = parser.push_byte(byte) {
match event {
MidiEvent::NoteOn { channel, note, velocity } => { /* ... */ }
MidiEvent::NoteOff { channel, note, velocity } => { /* ... */ }
MidiEvent::ControlChange { channel, controller, value } => { /* ... */ }
MidiEvent::PitchBend { channel, value } => { /* ... */ }
MidiEvent::ProgramChange { channel, program } => { /* ... */ }
_ => {}
}
}
```
### `patch` — Signal Routing / Patch Bay
Fixed-capacity cable graph connecting named outputs to inputs, no heap required.
```rust
use synth_core::patch::Patch;
let mut patch: Patch<16> = Patch::new(); // up to 16 cables
patch.connect("vco.out", "filter.in");
patch.connect("lfo.out", "filter.cv");
for cable in patch.cables() {
println!("{}{}", cable.src, cable.dst);
}
```
### `math` — Utility Functions
```rust
use synth_core::math::{midi_note_to_hz, db_to_linear, linear_to_db, lerp};
let freq = midi_note_to_hz(69); // 440.0 Hz (A4)
let gain = db_to_linear(-6.0); // ≈ 0.501
```
### `config` — Sample Rate
```rust
use synth_core::config::{SampleRate, SR_44100, SR_48000};
let period = SR_48000.period(); // 1.0 / 48000.0
```
## Traits
### `AudioProcessor<const BLOCK: usize>`
Implemented by every DSP module.
| Method | Description |
|--------|-------------|
| `process(&mut self, out: &mut [f32; BLOCK])` | Fill `out` with one block of samples |
| `reset(&mut self)` | Reset all internal state (phase, integrators, …) |
### `CVProcessor<const BLOCK: usize>`
Extends `AudioProcessor` for modules that accept a control-voltage input.
| Method | Description |
|--------|-------------|
| `set_cv(&mut self, cv: f32)` | Apply modulation (1 V/octave convention for pitch) |
## Complete Voice Example
```rust
use synth_core::{
config::SR_48000,
oscillator::{Vco, Waveform},
filter::{FilterMode, Svf},
envelope::Adsr,
vca::Vca,
AudioProcessor,
};
const BLOCK: usize = 256;
let sr = SR_48000;
let mut vco = Vco::new(sr, 440.0, Waveform::Saw);
let mut filt = Svf::new(sr, 2_000.0, 0.4, FilterMode::LowPass);
let mut adsr = Adsr::new(sr);
let mut vca = Vca::new(1.0);
adsr.gate_on();
let mut audio = [0.0f32; BLOCK];
let mut env = [0.0f32; BLOCK];
loop {
vco.process(&mut audio);
adsr.process(&mut env);
filt.process(&mut audio);
vca.apply_envelope(&mut audio, &env);
// send `audio` to output device
}
```
## Dependencies
| Crate | Purpose |
|-------|---------|
| `libm` | `no_std`-compatible transcendental math (`sin`, `pow`, …) |
| `micromath` | Fast numerical approximations |
| `num-traits` | Numeric trait abstractions |
| `heapless` | Fixed-size `Vec`/`ArrayVec` without allocation |
| `midi-types` | MIDI type definitions |
| `serde` | Optional serialization support |
## Building
```bash
# Native (for tests)
cargo test -p synth-core
# WebAssembly
cargo build -p synth-core --target wasm32-unknown-unknown
# Embedded (Cortex-M33, RP2350)
cargo build -p synth-core --target thumbv8m.main-none-eabihf
```

View File

@@ -0,0 +1,128 @@
# synth-embedded
Real-time audio synthesizer firmware for the **Raspberry Pi Pico 2** (RP2350), built on the [Embassy](https://embassy.dev) async runtime. Outputs 48 kHz stereo I2S audio to a PCM5102A DAC and accepts MIDI input over UART.
## Hardware
| GPIO | Signal | Connected to |
|------|--------|-------------|
| 9 | BCK (bit clock) | PCM5102A BCK |
| 10 | LRCK (word select) | PCM5102A LRCK |
| 11 | DATA | PCM5102A DIN |
| 1 | UART0 RX | MIDI IN (optocoupler) |
Any PCM5102A-compatible I2S DAC (e.g., the CJMCU-5102 module) works. Standard MIDI current-loop interface: 5 mA optocoupler on GPIO 1, no UART TX required.
## Architecture
Two Embassy tasks run concurrently on core 0:
```
┌──────────────┐ Mutex<SynthParams> ┌──────────────┐
│ midi_task │ ──────────────────────────▶│ audio_task │
│ (UART RX) │ NoteOn/Off, CC, PB, PC │ (PIO I2S) │
└──────────────┘ └──────────────┘
│ │
GPIO 1 GPIO 9/10/11
31 250 baud 48 kHz I2S
```
### `audio_task`
1. Initialises the `PioI2sOut` driver (PIO0 state machine 0, 24-bit, 48 kHz).
2. Creates the DSP chain: **VCO → SVF → VCA ← ADSR**.
3. Runs a double-buffered DMA loop — while one 256-frame block transfers to the PIO FIFO, the next block is rendered on the CPU.
**Block size:** 256 frames × 2 channels × 4 bytes = 2 KB per buffer, 5.33 ms per block. At 150 MHz the Cortex-M33 has roughly 10× headroom for the DSP render.
**Sample format:** 24-bit signed PCM, left-justified in bits 31..8 of a `u32` word, interleaved L/R: `[L0, R0, L1, R1, …]`.
### `midi_task`
Reads raw bytes from UART0 at 31 250 baud (standard MIDI) and feeds them to `synth_core::MidiParser`. Decoded events update the shared `PARAMS` mutex:
| Event | Action |
|-------|--------|
| Note On | Set note, velocity, gate = true |
| Note Off | Clear gate if note matches |
| CC 1 (mod wheel) | Filter resonance |
| CC 7 | Master volume |
| CC 72 | Release time (08 s) |
| CC 73 | Attack time (04 s) |
| CC 74 | Filter cutoff (80 Hz18 kHz) |
| CC 75 | Decay time (04 s) |
| Pitch Bend | ±2 semitones |
| Program Change | Waveform (0=Sine 1=Saw 2=Square 3=Triangle 4=Pulse) |
## Building
### Prerequisites
```bash
# Rust target for RP2350 Cortex-M33
rustup target add thumbv8m.main-none-eabihf
# probe-rs for flashing
cargo install probe-rs-tools
```
### Build
```bash
cargo build -p synth-embedded --release
```
### Flash
Connect a debug probe (Raspberry Pi Debug Probe, J-Link, etc.) and run:
```bash
cargo run -p synth-embedded --release
# or equivalently:
probe-rs run --chip RP2350 target/thumbv8m.main-none-eabihf/release/synth-embedded
```
The `runner` in `.cargo/config.toml` calls `probe-rs run` automatically.
### Debug logging
`defmt` logs are streamed over RTT. View them with:
```bash
probe-rs attach --chip RP2350 --log-format '{t} {L} {s}'
```
The `DEFMT_LOG` environment variable (set to `debug` by default in `.cargo/config.toml`) controls verbosity.
## Linker Setup
The crate uses a custom `memory.x` to describe the RP2350's 2 MB flash and 520 KB SRAM. `build.rs` copies it to the build output directory so `cortex-m-rt`'s `link.x` can find it via `INCLUDE memory.x`.
`.cargo/config.toml` passes two linker scripts:
- `-Tdefmt.x` — places the defmt log sections
- `-Tlink.x` — cortex-m-rt startup, vector table, and `INCLUDE memory.x`
## Dependencies
| Crate | Purpose |
|-------|---------|
| `synth-core` | DSP modules (VCO, SVF, ADSR, VCA, MIDI parser) |
| `embassy-executor` | Async task executor (Cortex-M) |
| `embassy-rp` | RP2350 HAL + PIO I2S driver |
| `embassy-sync` | `Mutex` for shared parameter state |
| `embassy-time` | Timekeeping |
| `cortex-m` / `cortex-m-rt` | ARM Cortex-M runtime |
| `defmt` + `defmt-rtt` | Structured logging over RTT |
| `panic-probe` | Panic messages sent to debug probe |
| `fixed` | Fixed-point clock divisor math |
| `libm` | `no_std` math (`powf` for pitch bend) |
## Customisation
**Change the oscillator waveform default** — edit `SynthParams::new()` in `src/params.rs`.
**Change block size** — edit `BLOCK_FRAMES` in `src/audio.rs`. Larger blocks reduce CPU wake-up overhead; smaller blocks reduce latency.
**Add a second voice** — instantiate a second `(Vco, Adsr, Svf, Vca)` tuple in `audio_task`, mix both outputs before the DMA write.
**Change GPIO pins** — update the constants in `src/main.rs` and re-wire accordingly.

View File

@@ -0,0 +1,232 @@
# synth-visualiser
A browser-based synthesizer front-end compiled to WebAssembly. Provides real-time oscilloscope and spectrum analysis via the Web Audio API, a drag-and-drop patch-bay for signal routing, and a parameter model that can be posted to an `AudioWorkletNode`.
## Features
- **Oscilloscope** — time-domain waveform display using `AnalyserNode.getFloatTimeDomainData()`
- **Spectrum analyser** — frequency-magnitude bar chart using `AnalyserNode.getByteFrequencyData()`
- **Patch bay** — canvas-based jack-and-cable UI for routing signal connections
- **Parameter model** — `SynthParams` struct that serialises to JSON for worklet messaging
## Building
### Prerequisites
```bash
cargo install wasm-pack
```
### Build
```bash
wasm-pack build crates/synth-visualiser --target web --out-dir ../../www/pkg
```
This produces:
```
www/pkg/
synth_visualiser.wasm # compiled WASM module
synth_visualiser.js # JS glue / bindings
synth_visualiser.d.ts # TypeScript type declarations
package.json
```
### Serve
```bash
# Any static HTTP server works; WASM requires a server (not file://)
npx serve www/
# or
python3 -m http.server --directory www/
```
## API
All public types are exported via `wasm-bindgen` and available as ES module exports.
### `AudioEngine`
Wraps an `AudioContext`, `AnalyserNode`, and `GainNode`.
```js
import { AudioEngine } from './pkg/synth_visualiser.js';
const engine = new AudioEngine();
// AudioContext is created suspended; resume after a user gesture:
engine.audio_context().resume();
const analyser = engine.analyser_node();
const sr = engine.sample_rate(); // 44100
```
The signal chain is: `GainNode → AnalyserNode → AudioContext.destination`.
FFT size: 2048. Smoothing time constant: 0.8.
### `OscilloscopeView`
Renders the time-domain waveform onto a `<canvas>`.
```js
import { OscilloscopeView } from './pkg/synth_visualiser.js';
const scope = new OscilloscopeView('scope-canvas', analyser);
function frame() {
scope.draw();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
```
`draw()` fetches 1024 float samples from the analyser and draws a cyan line on a dark background.
### `SpectrumView`
Renders the frequency-magnitude bar chart onto a `<canvas>`.
```js
import { SpectrumView } from './pkg/synth_visualiser.js';
const spectrum = new SpectrumView('spectrum-canvas', analyser);
function frame() {
spectrum.draw();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
```
`draw()` fetches 1024 frequency bins (byte magnitudes 0255) and draws colour-coded bars, graduating from cyan at low frequencies to magenta at high frequencies.
### `PatchBay`
Interactive cable-routing UI on a `<canvas>`.
```js
import { PatchBay } from './pkg/synth_visualiser.js';
const patchbay = new PatchBay('patchbay-canvas');
// Register jacks: (module_id, jack_id, x, y, is_output)
patchbay.register_jack('vco', 'out', 60, 80, true);
patchbay.register_jack('filter', 'in', 200, 80, false);
patchbay.register_jack('lfo', 'out', 60, 200, true);
patchbay.register_jack('filter', 'cv', 200, 200, false);
// Forward pointer events from the canvas element
const canvas = document.getElementById('patchbay-canvas');
canvas.addEventListener('pointerdown', e => patchbay.on_pointer_down(e.offsetX, e.offsetY));
canvas.addEventListener('pointermove', e => patchbay.on_pointer_move(e.offsetX, e.offsetY));
canvas.addEventListener('pointerup', e => patchbay.on_pointer_up(e.offsetX, e.offsetY));
function frame() {
patchbay.draw();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
```
- Output jacks are drawn in **cyan**, input jacks in **pink**.
- Drag from an output jack to an input jack to connect a cable.
- Cables are drawn as quadratic Bézier curves.
- Only output → input connections are valid; the drag is reversed automatically if needed.
### `SynthParams`
Parameter object that mirrors the embedded firmware's `SynthParams`.
```js
import { SynthParams } from './pkg/synth_visualiser.js';
const params = new SynthParams();
params.osc_freq = 440.0;
params.osc_wave = 1; // 0=Sine 1=Saw 2=Square 3=Triangle
params.filter_cutoff = 2000.0;
params.filter_res = 0.5;
params.env_attack = 0.01;
params.env_decay = 0.2;
params.env_sustain = 0.7;
params.env_release = 0.5;
params.lfo_rate = 5.0;
params.lfo_depth = 0.3;
params.master_gain = 0.8;
const json = params.to_json(); // → JSON string for AudioWorklet postMessage
```
## Complete Example
```html
<!DOCTYPE html>
<html>
<body>
<canvas id="scope" width="600" height="200"></canvas>
<canvas id="spectrum" width="600" height="200"></canvas>
<canvas id="patchbay" width="600" height="400"></canvas>
<script type="module">
import init, {
AudioEngine, OscilloscopeView, SpectrumView, PatchBay
} from './pkg/synth_visualiser.js';
await init();
const engine = new AudioEngine();
const analyser = engine.analyser_node();
const scope = new OscilloscopeView('scope', analyser);
const spectrum = new SpectrumView('spectrum', analyser);
const patchbay = new PatchBay('patchbay');
patchbay.register_jack('vco', 'out', 50, 80, true);
patchbay.register_jack('filter', 'in', 200, 80, false);
const pb = document.getElementById('patchbay');
pb.addEventListener('pointerdown', e => patchbay.on_pointer_down(e.offsetX, e.offsetY));
pb.addEventListener('pointermove', e => patchbay.on_pointer_move(e.offsetX, e.offsetY));
pb.addEventListener('pointerup', e => patchbay.on_pointer_up(e.offsetX, e.offsetY));
document.addEventListener('click', () => engine.audio_context().resume(), { once: true });
(function frame() {
scope.draw();
spectrum.draw();
patchbay.draw();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
```
## Project Structure
```
src/
lib.rs — wasm-bindgen entry, feature flags
engine.rs — AudioEngine (AudioContext + AnalyserNode + GainNode)
oscilloscope.rs — OscilloscopeView
spectrum.rs — SpectrumView
patchbay.rs — PatchBay (jacks, cables, hit-testing, drag)
params.rs — SynthParams (mirrors embedded firmware params)
```
## Dependencies
| Crate | Purpose |
|-------|---------|
| `wasm-bindgen` | Rust ↔ JavaScript bindings |
| `wasm-bindgen-futures` | `async/await` and `Promise` interop |
| `js-sys` | Raw JavaScript API access |
| `web-sys` | WebAPI bindings (AudioContext, Canvas 2D, Pointer events) |
| `serde` | Derive macros for JSON serialization |
| `console_error_panic_hook` | Forward Rust panics to the browser console |
| `synth-core` | Shared DSP types (e.g., `Waveform`) |
## Features
| Feature | Default | Description |
|---------|---------|-------------|
| `console-panic` | ✓ | Install `console_error_panic_hook` for readable panic messages |