# @euterpe/audio-engine-web

The browser front-end for the Euterpe real-time audio engine. The DSP is pure Rust
(the [`audio-engine`](../audio-engine) Cargo workspace: `dsp-core` primitives →
`dsp-graph` mixer → `dsp-wasm` binding) compiled to WebAssembly and run **on the
audio thread** inside an `AudioWorklet`. This package is the main-thread control
surface plus the worklet processor and wasm artifact.

## Architecture

```
 main thread                          audio thread (AudioWorkletGlobalScope)
 ┌─────────────────┐   postMessage    ┌──────────────────────────────────────┐
 │ AudioEngine     │ ───commands────▶ │ euterpe-engine processor             │
 │  (this package) │                  │   wasm_bindgen.WasmEngine (Rust/WASM) │
 │                 │ ◀──meter events── │   process(L,R) every 128-frame quantum│
 └─────────────────┘                  └──────────────────────────────────────┘
        │ compiles wasm, posts the WebAssembly.Module via processorOptions
```

The audio thread never blocks on the main thread: all control is one-way
fire-and-forget messages, and loudness/peak metering streams back as periodic
events. Track and insert indices are assigned deterministically on the main thread
(the Rust engine creates them sequentially) so callers get them synchronously.

## Build

```bash
pnpm --filter @euterpe/audio-engine-web build:worklet   # wasm-pack + assemble bundle
```

This runs `wasm-pack build --target no-modules` on the `dsp-wasm` crate, concatenates
the wasm-bindgen glue ahead of the worklet processor, and writes:

- `wasm/euterpe-engine-processor.js` — the classic script for `audioWorklet.addModule`.
- `wasm/dsp_engine_bg.wasm` — the engine module the main thread fetches + compiles.

Serve both as static assets and point `AudioEngine` at their URLs.

## Usage

```ts
import { AudioEngine, Waveform } from '@euterpe/audio-engine-web';

const engine = new AudioEngine({
  processorUrl: new URL('/audio/euterpe-engine-processor.js', location.origin),
  wasmUrl: new URL('/audio/dsp_engine_bg.wasm', location.origin),
  onMeter: (m) => updateMeters(m.lufs, m.peakL, m.peakR),
});
await engine.init(); // from a user gesture

const lead = engine.addSynthTrack('Lead');
engine.configureSynth(lead, { waveform: Waveform.Saw, cutoffHz: 3000 });
engine.addReverb(lead, { room: 0.7, mix: 0.25 });
engine.noteOn(lead, 69, 1.0); // A4
```

## Tests

`src/__tests__/wasm-engine.spec.ts` drives the **compiled wasm** (the exact artifact
the worklet loads) in Node — no browser — instantiating it via the same
`initSync({ module })` path and asserting real audio out, transport advance, a live
EQ-band edit across the WASM boundary, and finite LUFS. It is gated on the artifact
existing (build it first), skipping loudly otherwise.
