first commit
This commit is contained in:
31
docs/media/audio-fx/audio-fx-plugin-chorus/README.md
Normal file
31
docs/media/audio-fx/audio-fx-plugin-chorus/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# @suaveplan/audio-fx-plugin-chorus - chorus plugin for the @suaveplan/audio-fx-plugin-* family.
|
||||
|
||||
chorus 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
|
||||
|
||||
- [chorus](./src/chorus/chorus.md) — `ChorusPlugin` is the universal modulated-delay chorus. Each channel reads from its own delay line at an LFO-modulated…
|
||||
- [chorus](./src/chorus/chorus.md) — `ChorusPlugin` is the universal modulated-delay chorus. Each channel reads from its own delay line at an LFO-modulated…
|
||||
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options and preset payload of `ChorusPlugin`. These run on the public boun…
|
||||
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options and preset payload of `ChorusPlugin`. These run on the public boun…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Composition
|
||||
|
||||
```ts
|
||||
import { PluginChain, DefaultBusRegistry } from "@suaveplan/audio-fx-plugin-base/chain";
|
||||
import { /* Plugin */ } from "@suaveplan/audio-fx-plugin-chorus";
|
||||
|
||||
const plugin = new /* Plugin */();
|
||||
const chain = new PluginChain({
|
||||
buses: new DefaultBusRegistry(),
|
||||
plugins: [plugin],
|
||||
});
|
||||
```
|
||||
|
||||
## Status
|
||||
|
||||
Replaces the `./chorus` subpath of the bundled `@suaveplan/audio-plugins-fx`.
|
||||
@@ -0,0 +1,50 @@
|
||||
# `chorus.types`
|
||||
|
||||
## Purpose
|
||||
|
||||
TypeScript-only type definitions for `ChorusPlugin`'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's runtime code.
|
||||
|
||||
## Features
|
||||
|
||||
- `ChorusPluginOptions` — all fields optional so the default constructor (`new ChorusPlugin()`) is valid.
|
||||
- `readonly` properties — emphasises that the options object is consumed once at construction and never mutated.
|
||||
- Numeric ranges are encoded in the Zod schema (single source of truth) rather than in TypeScript types.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { ChorusPluginOptions } from "@suaveplan/audio-fx-plugin-chorus";
|
||||
|
||||
const opts: ChorusPluginOptions = { initialRateHz: 0.5 };
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type { ChorusPluginOptions } from "@suaveplan/audio-fx-plugin-chorus";
|
||||
|
||||
const presets: Record<string, ChorusPluginOptions> = {
|
||||
subtle: { initialDepthMs: 3, initialWet: 0.2, initialRateHz: 0.4 },
|
||||
lush: { initialDepthMs: 14, initialWet: 0.6, initialRateHz: 1.2 },
|
||||
seasick: { initialDepthMs: 18, initialWet: 0.8, initialRateHz: 2.5 },
|
||||
};
|
||||
|
||||
function load(name: keyof typeof presets): ChorusPluginOptions {
|
||||
return presets[name]!;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `ChorusPluginOptions`
|
||||
- `readonly initialRateHz?: number`
|
||||
- `readonly initialDepthMs?: number`
|
||||
- `readonly initialFeedback?: number`
|
||||
- `readonly initialWet?: number`
|
||||
- `readonly initialBaseDelayMs?: number`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The options interface mirrors the parameter declarations exactly so a future "save these options" round-trip can be implemented by just copying field-for-field from the plugin's parameter snapshot. There is no separate "options vs. parameters" split here — every option corresponds to one parameter, named `initial<X>` to make it clear that the value is consumed once at construction and then handed to the universal parameter system.
|
||||
|
||||
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 — but typing the surface as `readonly` lets the type system enforce that intent at every call site.
|
||||
@@ -0,0 +1,70 @@
|
||||
# `chorus`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`ChorusPlugin` is the universal modulated-delay chorus. Each channel reads from its own delay line at an LFO-modulated tap point, centred on `baseDelay`. The LFO is phase-offset per channel so a stereo signal smears in two different directions, producing the characteristic chorus width without relying on platform-specific spatial APIs. The plugin extends `AbstractAudioFxPlugin`, so it runs identically across browser, server, offline, and worklet renderers.
|
||||
|
||||
Feedback is clamped at the parameter boundary (`0..0.95`) so the output cannot diverge for bounded input, and `wet === 0` is bit-transparent to the dry signal (early-out copy) — both contracts are exercised in the test suite.
|
||||
|
||||
## Features
|
||||
|
||||
- Five parameters: `rate` (Hz, 0.05..10), `depth` (ms, 0..20), `feedback` (0..0.95), `wet` (0..1), `baseDelay` (ms, 5..30).
|
||||
- Per-channel delay buffer sized for the spec's max delay (60 ms) plus a fractional-read safety margin.
|
||||
- Sine LFO with channel-indexed phase offset (`2π / channels`) so two-channel input gets the classic L/R width.
|
||||
- Linear-interpolated fractional read so the modulation is smooth at low rates and high depths.
|
||||
- `wet === 0` early-out: returns the dry path sample-for-sample with no allocation and no buffer writes — saves CPU when the plugin is fully bypassed by automation.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ChorusPlugin } from "@suaveplan/audio-fx-plugin-chorus";
|
||||
|
||||
const chorus = new ChorusPlugin({
|
||||
initialRateHz: 0.5,
|
||||
initialDepthMs: 5,
|
||||
initialWet: 0.4,
|
||||
});
|
||||
chain.add(chorus);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ChorusPlugin } from "@suaveplan/audio-fx-plugin-chorus";
|
||||
|
||||
// "Lush" chorus: faster rate, deeper modulation, moderate feedback.
|
||||
// Useful as the front of a pad-synth chain to thicken the sound.
|
||||
const lush = new ChorusPlugin({
|
||||
initialRateHz: 1.2,
|
||||
initialDepthMs: 14,
|
||||
initialFeedback: 0.35,
|
||||
initialWet: 0.6,
|
||||
initialBaseDelayMs: 18,
|
||||
});
|
||||
await lush.setup(
|
||||
{ sampleRate: 48_000, channels: 2, blockSize: 512 },
|
||||
ctx,
|
||||
);
|
||||
|
||||
// Crossfade to dry on a long note-off via parameter automation.
|
||||
lush.setParameterAt("wet", 0.6, 0);
|
||||
lush.setParameterAt("wet", 0, 0.8);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `ChorusPlugin` (class extends `AbstractAudioFxPlugin`)
|
||||
- `new ChorusPlugin(options?: ChorusPluginOptions)` — 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 = 60 ms` plus a 4-sample safety margin (the fractional read indexes `i0` and `i0 + 1`). Sixty milliseconds is well over `baseDelay.max + depth.max = 30 + 20 = 50`, so a typical real-time use case never wraps inside a single LFO period; the modulus is there for correctness when the buffer is much smaller than the modulation window (e.g., very low sample rates).
|
||||
|
||||
The LFO is a plain `Math.sin(phase)` evaluated per sample. Phase advances by `phaseInc = 2π · rate / sr` per sample and the running phase is kept in `this.lfoPhase % 2π` so a long-running session never accumulates floating-point error past about `1e-15` per cycle. Phase-offset per channel is `2π / channels` — for stereo that's `π`, which gives the classic "left goes high while right goes low" L/R smear; for mono it collapses to a single voice with no decorrelation, which is the expected behaviour.
|
||||
|
||||
Feedback is clamped at the parameter boundary (`max: 0.95`) rather than at runtime so the bounded-output property is provable by static inspection of the parameter declaration. A higher feedback would push the system close to self-oscillation; the test "max feedback over a long sine never produces non-finite samples" exercises the boundary and verifies the result stays finite and within a comfortable amplitude budget. The `wet === 0` early-out short-circuits the entire DSP loop with a `Float32Array.set` copy per channel — both fast and bit-exact, satisfying the spec's "wet=0 is bit-transparent" contract without relying on coincidence (`0 * lfo + dry === dry` would only hold for IEEE-clean values, not subnormals).
|
||||
@@ -0,0 +1,57 @@
|
||||
# `chorus/schemas`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Zod schemas validating the construction-time options and preset payload of `ChorusPlugin`. 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, editors, RPC validation) without importing the audio implementation.
|
||||
|
||||
## Features
|
||||
|
||||
- `ChorusPluginOptionsSchema` — validates all five optional construction fields with their documented ranges (`rate` 0.05..10 Hz, `depth` 0..20 ms, `feedback` 0..0.95, `wet` 0..1, `baseDelay` 5..30 ms).
|
||||
- `ChorusPresetDataSchema` — empty strict object: the chorus has no plugin-specific preset payload, so any extra keys are rejected by `.strict()`.
|
||||
- Both schemas use `z.number().finite()` so `NaN` and `Infinity` cannot sneak through.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ChorusPluginOptionsSchema } from "@suaveplan/audio-fx-plugin-chorus";
|
||||
|
||||
const parsed = ChorusPluginOptionsSchema.parse({ initialWet: 0.4 });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
ChorusPluginOptionsSchema,
|
||||
ChorusPresetDataSchema,
|
||||
} from "@suaveplan/audio-fx-plugin-chorus";
|
||||
|
||||
// Build a form from the schema's shape keys.
|
||||
const fields = ChorusPluginOptionsSchema.keyof().options;
|
||||
for (const field of fields) {
|
||||
form.addRow(field);
|
||||
}
|
||||
|
||||
// Reject foreign preset payloads at the IPC boundary before they
|
||||
// reach the plugin.
|
||||
function validateChorusPreset(blob: unknown): boolean {
|
||||
return ChorusPresetDataSchema.safeParse(blob).success;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `ChorusPluginOptionsSchema: z.ZodObject<{ initialRateHz?, initialDepthMs?, initialFeedback?, initialWet?, initialBaseDelayMs? }>`
|
||||
- `ChorusPresetDataSchema: z.ZodObject<{}, "strict">`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The maximum feedback is `0.95`, not `1.0`. Allowing feedback at unity 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 test "max feedback over a long sine never produces non-finite samples" exercises this boundary.
|
||||
|
||||
`ChorusPresetDataSchema` is `.strict()` for the same reason as the other plugins: unknown fields almost always indicate a payload from a sibling plugin or a forwards-incompatible schema. Silent rejection at the boundary is much less harmful than partial application, which can leave the plugin in a half-restored state.
|
||||
Reference in New Issue
Block a user