Initial Commit
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
# `phaser`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`PhaserPlugin` is the universal all-pass phaser. A cascade of first-order all-pass sections (configurable from 2 to 12) has its break frequencies modulated together by a shared LFO. Mixing the cascade's output with the dry signal creates the moving notches that define the phaser sound. Feedback from the last stage back into the first stage strengthens the resonant peaks at the notch edges.
|
||||
|
||||
The plugin extends `AbstractAudioFxPlugin`, so it runs identically across browser / server / worklet renderers. State is per-channel arrays sized for the maximum stage count; `reset()` zeros them in place without reallocation.
|
||||
|
||||
## Features
|
||||
|
||||
- Six parameters: `rate` (Hz, 0.05..10), `depth` (0..1), `stages` (int, 2..12), `feedback` (0..0.95), `wet` (0..1), `center` (Hz, 100..5000).
|
||||
- LFO-modulated break frequency: `break = center · 2^(lfo · depth · 2)` so depth controls the modulation span in octaves around `center`.
|
||||
- `wet === 0` early-out: copies the dry path bit-exact, satisfying the spec's "fully-dry mix is transparent" contract.
|
||||
- Feedback clamped at 0.95 at the parameter boundary so the system stays bounded for any bounded input — proven by the test "max feedback over a long sine never produces non-finite samples".
|
||||
- Stages capped at 12 so a single instance can run from a thin "subtle two-stage phaser" all the way up to a deep "Bass Synth 12-stage" without re-allocating delay state.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { PhaserPlugin } from "@suaveplan/audio-fx-plugin-phaser";
|
||||
|
||||
const phaser = new PhaserPlugin({
|
||||
initialRateHz: 0.5,
|
||||
initialDepth: 0.7,
|
||||
initialWet: 0.5,
|
||||
});
|
||||
chain.add(phaser);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { PhaserPlugin } from "@suaveplan/audio-fx-plugin-phaser";
|
||||
|
||||
// Deep "jet flange" phaser: 10 stages, heavy feedback, slow modulation,
|
||||
// near-full wet. Designed for synth pads where you want the slow sweep
|
||||
// to dominate the timbre rather than sit behind it.
|
||||
const jet = new PhaserPlugin({
|
||||
initialRateHz: 0.25,
|
||||
initialDepth: 0.95,
|
||||
initialStages: 10,
|
||||
initialFeedback: 0.85,
|
||||
initialWet: 0.8,
|
||||
initialCenterHz: 600,
|
||||
});
|
||||
await jet.setup(
|
||||
{ sampleRate: 48_000, channels: 2, blockSize: 512 },
|
||||
ctx
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `PhaserPlugin` (class extends `AbstractAudioFxPlugin`)
|
||||
- `new PhaserPlugin(options?: PhaserPluginOptions)` — throws `AUDIO_FX.INVALID_OPTIONS` on malformed options.
|
||||
- Inherited parameter accessors for `rate`, `depth`, `stages`, `feedback`, `wet`, `center`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The first-order all-pass section is implemented as `y = a·x + xPrev − a·yPrev`, where the bilinear coefficient `a = (tan(πf/sr) − 1) / (tan(πf/sr) + 1)`. This form has unity magnitude response (all-pass) and phase response that swings 180° around `f`. Cascading N sections produces N evenly-spaced notches in the mixed dry+wet output; the LFO sweeps these notches across the spectrum to create the phasing motion.
|
||||
|
||||
`stages` is declared non-automatable because changing the count mid-block would either leave stale state in the unused stages or require gain compensation. The parameter accepts integer values 2..12; the inner loop iterates the rounded value each block, so users can still change stages between blocks without artefacts beyond the first-block transient.
|
||||
|
||||
The break-frequency clamp `[20, 0.45·sr]` is set well below Nyquist so the bilinear transform stays numerically stable even at the high-depth extreme. The `0.45·sr` upper bound matches the policy used in the state-variable filter plugin and was chosen so the all-pass coefficient stays in `(-1, 1)` across the parameter range; pushing closer to Nyquist would risk the coefficient approaching ±1 and amplifying float-precision noise.
|
||||
|
||||
Feedback runs through a per-channel `feedbackState` updated after each sample's cascade output. Clamping `feedback ≤ 0.95` keeps the loop strictly under unity gain so the output cannot diverge. 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.
|
||||
Reference in New Issue
Block a user