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-flanger - flanger plugin for the @suaveplan/audio-fx-plugin-* family.
flanger 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
- [flanger](./src/flanger/flanger.md) — `FlangerPlugin` is the universal comb-filter flanger. A short LFO-modulated delay line (sub-10 ms) is summed with the d…
- [flanger](./src/flanger/flanger.md) — `FlangerPlugin` is the universal comb-filter flanger. A short LFO-modulated delay line (sub-10 ms) is summed with the d…
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options and preset payload of `FlangerPlugin`. These run on the public bou…
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options and preset payload of `FlangerPlugin`. These run on the public bou…
<!-- end:auto-docs -->
## Composition
```ts
import { PluginChain, DefaultBusRegistry } from "@suaveplan/audio-fx-plugin-base/chain";
import { /* Plugin */ } from "@suaveplan/audio-fx-plugin-flanger";
const plugin = new /* Plugin */();
const chain = new PluginChain({
buses: new DefaultBusRegistry(),
plugins: [plugin],
});
```
## Status
Replaces the `./flanger` subpath of the bundled `@suaveplan/audio-plugins-fx`.

View File

@@ -0,0 +1,50 @@
# `flanger.types`
## Purpose
TypeScript-only type definitions for `FlangerPlugin`'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 or storybook controls without pulling in the plugin's runtime code.
## Features
- `FlangerPluginOptions` — all fields optional so the default constructor (`new FlangerPlugin()`) is valid.
- `readonly` properties — emphasises that the options object is consumed once at construction and never mutated.
- Numeric ranges live in the Zod schema (single source of truth) rather than in TypeScript types.
## Usage: Basic Example
```ts
import type { FlangerPluginOptions } from "@suaveplan/audio-fx-plugin-flanger";
const opts: FlangerPluginOptions = { initialDepthMs: 2 };
```
## Usage: Advanced Example
```ts
import type { FlangerPluginOptions } from "@suaveplan/audio-fx-plugin-flanger";
const presets: Record<string, FlangerPluginOptions> = {
subtle: { initialDepthMs: 0.5, initialFeedback: 0.3, initialWet: 0.3 },
classic: { initialDepthMs: 2, initialFeedback: 0.5, initialWet: 0.5 },
jet: { initialDepthMs: 3, initialFeedback: 0.9, initialWet: 0.7 },
};
function load(name: keyof typeof presets): FlangerPluginOptions {
return presets[name]!;
}
```
## API Reference
- `FlangerPluginOptions`
- `readonly initialRateHz?: number`
- `readonly initialDepthMs?: number`
- `readonly initialFeedback?: number`
- `readonly initialWet?: number`
- `readonly initialBaseDelayMs?: number`
## Implementation Notes
The interface deliberately mirrors `ChorusPluginOptions` field-for-field except that `depth` is shorter-range (`0..5 ms` versus the chorus's `0..20 ms`) and `baseDelay` is much shorter (`0.1..10 ms` versus `5..30 ms`). This consistency lets a UI render both plugins from a shared component if it wants to, while the schema enforces the perceptually-distinct parameter ranges that define each effect.
All fields are `readonly` so structural-equality checks on saved options (e.g., undo/redo stacks) do not need defensive cloning. The constructor never mutates the passed object — it copies relevant values into the parameter list once and forgets the input.

View File

@@ -0,0 +1,69 @@
# `flanger`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`FlangerPlugin` is the universal comb-filter flanger. A short LFO-modulated delay line (sub-10 ms) is summed with the dry signal; the close tap spacing produces evenly-spaced comb peaks and notches in the frequency response. Feedback emphasises the comb peaks, producing the "metallic" or "jet-engine" timbre that defines the flange sound.
The plugin extends `AbstractAudioFxPlugin` so it runs identically across browser, server, offline, and worklet renderers. State is a per-channel circular `Float32Array` sized for the plugin's maximum delay; `reset()` zeros all buffers in place without reallocation.
## Features
- Five parameters: `rate` (Hz, 0.05..10), `depth` (ms, 0..5), `feedback` (0..0.95), `wet` (0..1), `baseDelay` (ms, 0.1..10).
- Sine LFO drives the delay tap around `baseDelay` with peak-to-peak excursion of `2 · depth`.
- Linear-interpolated fractional read keeps the modulation smooth at low rates and high depths.
- Feedback clamped at the parameter boundary (max 0.95) so the comb feedback loop stays bounded for any bounded input.
- `wet === 0` early-out: returns the dry path sample-for-sample with no allocation and no buffer writes — keeps the spec contract "fully-dry mix is transparent" verifiable by static inspection.
## Usage: Basic Example
```ts
import { FlangerPlugin } from "@suaveplan/audio-fx-plugin-flanger";
const flanger = new FlangerPlugin({
initialRateHz: 0.4,
initialDepthMs: 2,
initialFeedback: 0.6,
initialWet: 0.5,
});
chain.add(flanger);
```
## Usage: Advanced Example
```ts
import { FlangerPlugin } from "@suaveplan/audio-fx-plugin-flanger";
// "Through-zero" style deep flange: slow rate, very short base delay,
// near-max feedback for the classic resonant jet sweep.
const jet = new FlangerPlugin({
initialRateHz: 0.2,
initialDepthMs: 3,
initialFeedback: 0.9,
initialWet: 0.7,
initialBaseDelayMs: 0.5,
});
await jet.setup(
{ sampleRate: 48_000, channels: 2, blockSize: 512 },
ctx
);
```
## API Reference
- `FlangerPlugin` (class extends `AbstractAudioFxPlugin`)
- `new FlangerPlugin(options?: FlangerPluginOptions)` — throws `AUDIO_FX.INVALID_OPTIONS` on malformed options.
- Inherited parameter accessors for `rate`, `depth`, `feedback`, `wet`, `baseDelay`.
## Implementation Notes
The delay line is a circular `Float32Array` per channel sized for `MAX_DELAY_MS = 20 ms` plus a 4-sample safety margin (the fractional read indexes `i0` and `i0 + 1`). Twenty milliseconds is well over `baseDelay.max + 2·depth.max = 10 + 10 = 20`, with the extra margin so the wrap path is never hit on the hottest combination of parameters.
The LFO is mapped to the delay tap via `delay = base + (sin + 1)/2 · depth`, not `delay = base + sin · depth`. Mapping to the half-positive range `[0, depth]` means the flange always *adds* delay to the base — never *subtracts* — so the line never reads near `writeIndex - 0` (which would wrap aliased noise into the output). It also matches the standard convention used by most reference flanger plugins so the perceptual sweep direction is what producers expect.
Feedback at the comb-filter resonance is intentionally allowed to climb to 0.95 because the flanger character lives at the resonance edge. The test "max feedback over a long sine never produces non-finite samples" exercises this boundary against a one-second sine at `feedback = 0.95` and verifies every output sample is finite and amplitude-bounded. Allowing `1.0` would create a marginally stable system whose output diverges for any non-zero input; clamping at 0.95 leaves enough headroom that even a noise-floor leak stays bounded over arbitrarily long renders.
The `wet === 0` early-out short-circuits the entire DSP loop with a `Float32Array.set` copy per channel — both fast and bit-exact. This satisfies the spec's "wet=0 is bit-transparent" contract without relying on coincidence (`0 * read + dry === dry` would only hold for IEEE-clean values, not subnormals).

View File

@@ -0,0 +1,56 @@
# `flanger/schemas`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Zod schemas validating the construction-time options and preset payload of `FlangerPlugin`. These run on the public boundary between user code and the plugin: any malformed object hitting the constructor or `applyPreset` 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
- `FlangerPluginOptionsSchema` — validates all five optional construction fields with their documented ranges (`rate` 0.05..10 Hz, `depth` 0..5 ms, `feedback` 0..0.95, `wet` 0..1, `baseDelay` 0.1..10 ms).
- `FlangerPresetDataSchema` — empty strict object: the flanger has no plugin-specific preset payload, so any extra keys are rejected by `.strict()`.
- All numeric fields use `z.number().finite()` so `NaN` and `Infinity` cannot sneak through.
## Usage: Basic Example
```ts
import { FlangerPluginOptionsSchema } from "@suaveplan/audio-fx-plugin-flanger";
const parsed = FlangerPluginOptionsSchema.parse({ initialDepthMs: 1.5 });
```
## Usage: Advanced Example
```ts
import {
FlangerPluginOptionsSchema,
FlangerPresetDataSchema,
} from "@suaveplan/audio-fx-plugin-flanger";
// Pull keys + ranges from the schema for a generated form.
const fields = FlangerPluginOptionsSchema.keyof().options;
for (const field of fields) {
form.addRow(field);
}
// Strict preset validation at the IPC boundary.
function isFlangerPreset(blob: unknown): boolean {
return FlangerPresetDataSchema.safeParse(blob).success;
}
```
## API Reference
- `FlangerPluginOptionsSchema: z.ZodObject<{ initialRateHz?, initialDepthMs?, initialFeedback?, initialWet?, initialBaseDelayMs? }>`
- `FlangerPresetDataSchema: z.ZodObject<{}, "strict">`
## Implementation Notes
The maximum feedback is `0.95`, matching the chorus, phaser, and delay plugins so all four feedback-bearing effects share one mental model. The flanger lives close to that boundary by design — the resonant comb peaks are the effect's character — but the same hard cap keeps the loop strictly under unity gain so the output cannot diverge.
`FlangerPresetDataSchema` is `.strict()` so unknown fields are rejected, matching the sibling-plugin policy. The flanger has no plugin-specific preset payload today: every parameter (`rate`, `depth`, `feedback`, `wet`, `baseDelay`) travels through the universal parameter snapshot that `AbstractAudioFxPlugin` builds, and the empty `presetData` field marks that the plugin opts out of plugin-specific state.