first commit
This commit is contained in:
@@ -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).
|
||||
Reference in New Issue
Block a user