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,31 @@
# @suaveplan/audio-fx-plugin-pitch-shifter - pitch-shifter plugin for the @suaveplan/audio-fx-plugin-* family.
pitch-shifter plugin for the @suaveplan/audio-fx-plugin-* family.
Member of the `@suaveplan/audio-fx-plugin-*` family. Universal (browser / server / offline / worklet identical). Depends on `@suaveplan/audio-fx-plugin-base` for the `AudioFxPlugin` contract; declares no other `@suaveplan/audio-fx-plugin-*` deps.
<!-- begin:auto-docs -->
## Documentation tree
- [pitch-shifter](./src/pitch-shifter/pitch-shifter.md) — `PitchShifterPlugin` is the universal granular pitch shifter. It writes incoming audio into a per-channel circular ring…
- [pitch-shifter](./src/pitch-shifter/pitch-shifter.md) — `PitchShifterPlugin` is the universal granular pitch shifter. It writes incoming audio into a per-channel circular ring…
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options and preset payload of `PitchShifterPlugin`. These run on the publi…
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options and preset payload of `PitchShifterPlugin`. These run on the publi…
<!-- end:auto-docs -->
## Composition
```ts
import { PluginChain, DefaultBusRegistry } from "@suaveplan/audio-fx-plugin-base/chain";
import { /* Plugin */ } from "@suaveplan/audio-fx-plugin-pitch-shifter";
const plugin = new /* Plugin */();
const chain = new PluginChain({
buses: new DefaultBusRegistry(),
plugins: [plugin],
});
```
## Status
Replaces the `./pitch-shifter` subpath of the bundled `@suaveplan/audio-plugins-fx`.

View File

@@ -0,0 +1,49 @@
# `pitch-shifter.types`
## Purpose
TypeScript-only type definitions for `PitchShifterPlugin`'s construction options. Co-located with the implementation so the public type surface stays aligned with the Zod validator in `schemas/schemas.ts`. Consumers can import the type for typed form state, dependency-injection wiring, or storybook controls without pulling in the plugin runtime.
## Features
- `PitchShifterPluginOptions` — both fields optional so the default constructor (`new PitchShifterPlugin()`) is valid.
- `readonly` properties — emphasises that the options object is consumed once at construction.
- `windowSize` accepts one of `512 | 1024 | 2048 | 4096`; the validation lives in the Zod schema rather than in the TypeScript type to avoid forcing a literal-union cast at every call site.
## Usage: Basic Example
```ts
import type { PitchShifterPluginOptions } from "@suaveplan/audio-fx-plugin-pitch-shifter";
const opts: PitchShifterPluginOptions = { initialSemitones: 5 };
```
## Usage: Advanced Example
```ts
import type { PitchShifterPluginOptions } from "@suaveplan/audio-fx-plugin-pitch-shifter";
const presets: Record<string, PitchShifterPluginOptions> = {
fast: { windowSize: 512 },
balanced: { windowSize: 2048 },
fidelity: { windowSize: 4096 },
};
function pickWindow(latencyBudgetSamples: number): PitchShifterPluginOptions {
if (latencyBudgetSamples >= 2048) return presets.fidelity!;
if (latencyBudgetSamples >= 1024) return presets.balanced!;
return presets.fast!;
}
```
## API Reference
- `PitchShifterPluginOptions`
- `readonly initialSemitones?: number` — initial pitch offset in semitones (-24..24).
- `readonly windowSize?: number` — analysis/synthesis window, one of 512/1024/2048/4096 (default 2048).
## Implementation Notes
The window size is constrained to a small set of powers of two. The implementation does not technically require a power of two (the ring buffer indexes by modulo, not bitmask), but constraining the choice makes the plugin behaviourally portable: a future FFT-based revision (phase vocoder) would benefit from power-of-two window sizes for efficient FFT routines, and locking the choice here means a swap is non-breaking. The four sizes cover the spectrum from "low latency, low fidelity" (512 = ~10 ms at 48 kHz) to "high fidelity, high latency" (4096 = ~85 ms).
`initialSemitones` is bounded at ±24 (two octaves). Beyond this, the granular algorithm's wrap rate exceeds the perceptual fusion frequency and the output starts to sound like granular synthesis rather than a clean shift. Callers wanting extreme transposition should chain multiple instances or wait for the phase-vocoder variant.

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).

View File

@@ -0,0 +1,57 @@
# `pitch-shifter/schemas`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Zod schemas validating the construction-time options and preset payload of `PitchShifterPlugin`. These run on the public boundary so any malformed object is rejected with `AUDIO_FX.INVALID_OPTIONS` / `AUDIO_FX.INVALID_PRESET` before it can corrupt runtime state.
Separating schemas from the plugin module lets `index.ts` re-export them so callers can introspect the option surface (for forms, RPC validation, or storybook controls) without importing the audio implementation.
## Features
- `PitchShifterPluginOptionsSchema` — validates `initialSemitones ∈ [-24, 24]` and `windowSize ∈ {512, 1024, 2048, 4096}`.
- `PitchShifterPresetDataSchema` — empty strict object; the plugin has no plugin-specific preset payload, so any extra keys are rejected.
- `windowSize` uses `z.number().int().refine(...)` rather than `z.enum([512, 1024, 2048, 4096])` because Zod's enum validator only accepts string keys; the refinement gives clear error messages on the numeric option.
## Usage: Basic Example
```ts
import { PitchShifterPluginOptionsSchema } from "@suaveplan/audio-fx-plugin-pitch-shifter";
const parsed = PitchShifterPluginOptionsSchema.parse({
initialSemitones: 7,
windowSize: 2048,
});
```
## Usage: Advanced Example
```ts
import {
PitchShifterPluginOptionsSchema,
PitchShifterPresetDataSchema,
} from "@suaveplan/audio-fx-plugin-pitch-shifter";
// Driver for an editor form: pull the keys from the schema.
const fields = PitchShifterPluginOptionsSchema.keyof().options;
for (const field of fields) form.addRow(field);
// Strict preset validation at the IPC boundary.
function isPitchShifterPreset(blob: unknown): boolean {
return PitchShifterPresetDataSchema.safeParse(blob).success;
}
```
## API Reference
- `PitchShifterPluginOptionsSchema: z.ZodObject<{ initialSemitones?, windowSize? }>`
- `PitchShifterPresetDataSchema: z.ZodObject<{}, "strict">`
## Implementation Notes
The `windowSize` constraint is enforced by `z.refine` rather than `z.enum` because Zod's `z.enum` requires string keys. Using `z.literal(512).or(z.literal(1024))…` would work too but produces awkward type unions; the refinement keeps the inferred type as `number` while still rejecting non-power-of-two values at the boundary. The error message names the four legal values so a UI driving this schema can produce a useful dropdown.
`PitchShifterPresetDataSchema` is `.strict()` so unknown fields are rejected, matching the sibling-plugin policy. The plugin has no plugin-specific preset payload today: the `semitones` parameter travels through the universal parameter snapshot that `AbstractAudioFxPlugin` builds, and the empty `presetData` field marks that the plugin opts out of plugin-specific state.