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