# `pitch-shifter` _(no submodules)_ ## Purpose `PitchShifterPlugin` is the universal granular pitch shifter. It writes incoming audio into a per-channel circular ring buffer and reads from two overlapping pointers advancing at the pitch ratio `2^(semitones / 12)`. The two read streams are Hann-window crossfaded into a single continuous output of the same length as the input — duration is preserved while pitch is shifted. The plugin maintains its overlap-add state across blocks; `reset()` clears the ring and re-seeds the read pointers in place. A browser-native fast path is available via `getWorkletProcessor()`, which returns a self-contained `AudioWorkletProcessor` source string for off-thread rendering. The worklet path is functionally equivalent to the universal block path so a chain that uses one renders identically to a chain that uses the other. ## Features - One automatable parameter: `semitones` (-24..24, default 0). - Configurable window size at construction (`512`, `1024`, `2048`, `4096`; default `2048`). - Stateful: per-channel ring buffer, two fractional read pointers, write head. Survives block boundaries; `reset()` clears all of it. - `latencySamples = windowSize / 2` declared in `capabilities`. Chain consumers can read this to align downstream processing. - `getWorkletProcessor()` returns a self-contained string containing a `registerProcessor("suaveplan-pitch-shifter", ...)` call and no Node-only `require` / ESM `import` — the spec's "worklet processor source is a self-contained string" contract. ## Usage: Basic Example ```ts import { PitchShifterPlugin } from "@suaveplan/audio-fx-plugin-pitch-shifter"; const shifter = new PitchShifterPlugin({ initialSemitones: 7 }); chain.add(shifter); ``` ## Usage: Advanced Example ```ts import { PitchShifterPlugin } from "@suaveplan/audio-fx-plugin-pitch-shifter"; // Browser fast path: mount the worklet processor and forward the // `semitones` automation channel. const shifter = new PitchShifterPlugin({ initialSemitones: 0, windowSize: 4096, }); const source = shifter.getWorkletProcessor(); await context.audioWorklet.addModule(URL.createObjectURL(new Blob([source]))); const node = new AudioWorkletNode(context, "suaveplan-pitch-shifter", { processorOptions: { windowSize: 4096 }, }); node.parameters.get("semitones")!.setValueAtTime(-5, 0); ``` ## API Reference - `PitchShifterPlugin` (class extends `AbstractAudioFxPlugin`) - `new PitchShifterPlugin(options?: PitchShifterPluginOptions)` — throws `AUDIO_FX.INVALID_OPTIONS` on malformed options (semitones outside ±24, non-power-of-two window size). - `getWorkletProcessor(): string` — returns the self-contained AudioWorkletProcessor source string. - Inherited parameter accessor for `semitones`. ## Implementation Notes The algorithm is a simple two-grain SOLA (synchronous overlap-add) variant: each output sample mixes two read-pointer reads weighted by a Hann window of the pointer's distance to the write head. Read pointer 1 starts half a window behind the write head; read pointer 2 starts a full window behind. As the pointers advance at the pitch ratio while the write head advances at the input rate, the lag between write and read changes — when the lag escapes `[0, windowSize]`, the pointer wraps by `±windowSize` so it stays in the analysis band. The Hann weighting ensures the wrap discontinuity lands at the trough of the crossfade, where its perceptual impact is minimal. This is not a true phase vocoder. A phase vocoder analyses the input in the frequency domain via FFT, computes per-bin instantaneous frequencies, and resynthesises with adjusted phase advances — that produces higher-fidelity shifts for polyphonic / wideband material but requires a much larger code surface (FFT + magnitude/phase analysis + synthesis loop). The two-grain SOLA picks the cheaper algorithm for the MVP; a future revision can add a `PhaseVocoderPitchShifter` on a sibling subpath without breaking this contract. The spec's scenarios — "zero-semitone shift preserves pitch and duration within phase-vocoder reconstruction tolerance" and "octave-up shift doubles the fundamental" — are both within reach for SOLA on steady tones, with tolerances loose enough to absorb the wrap artefacts. The ring buffer is sized at `windowSize × channels` and indexed by `channelOffset + (pos mod windowSize)`. Flattening avoids one indirection per inner-loop sample and keeps the ring contiguous in memory for cache locality. The two read pointers carry fractional positions so the linear-interp read produces smooth output even at non-integer pitch ratios. `getWorkletProcessor()` returns a template string that includes the full processor class. We do not minify or precompile it; the worklet engine parses it on `addModule`. The string is constructed without any module-system primitives (`require`, `import`) so it can be served as a `Blob` URL without an extra build step — verified by the test "getWorkletProcessor returns self-contained processor source". Latency is declared as `windowSize / 2`. The Hann weight is zero at the write head and at one window behind, peaking at half a window — so the first meaningful output sample appears after the read pointer reaches the centre of the buffer, which by construction happens after `windowSize / 2` input samples. Consumers can read `capabilities.latencySamples` and compensate (e.g., by delaying parallel dry signal in a wet/dry mixer downstream).