233 lines
6.8 KiB
Markdown
233 lines
6.8 KiB
Markdown
# 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 |
|