first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
# `pitch-shifter`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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).