Add readme files for each of the crates
This commit is contained in:
227
crates/synth-core/README.md
Normal file
227
crates/synth-core/README.md
Normal 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 0–1).
|
||||||
|
|
||||||
|
### `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.0–1.0
|
||||||
|
adsr.release_s = 0.5;
|
||||||
|
|
||||||
|
adsr.gate_on(); // key press
|
||||||
|
let mut env = [0.0f32; 256];
|
||||||
|
adsr.process(&mut env); // outputs 0.0–1.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
|
||||||
|
```
|
||||||
128
crates/synth-embedded/README.md
Normal file
128
crates/synth-embedded/README.md
Normal 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 (0–8 s) |
|
||||||
|
| CC 73 | Attack time (0–4 s) |
|
||||||
|
| CC 74 | Filter cutoff (80 Hz–18 kHz) |
|
||||||
|
| CC 75 | Decay time (0–4 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.
|
||||||
232
crates/synth-visualiser/README.md
Normal file
232
crates/synth-visualiser/README.md
Normal 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 0–255) 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 |
|
||||||
Reference in New Issue
Block a user