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-microphone - microphone plugin for the @suaveplan/audio-fx-plugin-* family.
microphone 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
- [microphone](./src/microphone/microphone.md) — `MicrophoneSimulationPlugin` is the universal microphone-simulation source plugin. It synthesises a microphone-coloured…
- [microphone](./src/microphone/microphone.md) — `MicrophoneSimulationPlugin` is the universal microphone-simulation source plugin. It synthesises a microphone-coloured…
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options, the eight control groups, the full configuration tree, and the pr…
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options, the eight control groups, the full configuration tree, and the pr…
<!-- end:auto-docs -->
## Composition
```ts
import { PluginChain, DefaultBusRegistry } from "@suaveplan/audio-fx-plugin-base/chain";
import { /* Plugin */ } from "@suaveplan/audio-fx-plugin-microphone";
const plugin = new /* Plugin */();
const chain = new PluginChain({
buses: new DefaultBusRegistry(),
plugins: [plugin],
});
```
## Status
Replaces the `./microphone` subpath of the bundled `@suaveplan/audio-plugins-fx`.

View File

@@ -0,0 +1,70 @@
# `microphone.types`
## Purpose
TypeScript-only type definitions for `MicrophoneSimulationPlugin`'s control groups, configuration tree, preset payload, and UI section descriptor. Co-located with the implementation so the public type surface stays aligned with the Zod validator in `schemas/schemas.ts`. The control-group split (FrequencyControls, ResonanceControls, …) mirrors the main-branch `microphone.types.ts` so a UI built against either codebase can render the other.
## Features
- Eight `*Controls` interfaces, one per microphone signal stage.
- `MicrophoneConfig` aggregates them under named sections; consumers can pass `Partial<MicrophoneConfig>` to merge with current state.
- `MicrophoneSimulationPluginOptions` is a thin shell carrying the partial config.
- `MicrophonePresetData` carries the full live config so a preset restores every section.
- `MicrophoneUIControl` / `MicrophoneUISection` describe the UI without prescribing a renderer.
## Usage: Basic Example
```ts
import type {
MicrophoneConfig,
MicrophoneSimulationPluginOptions,
} from "@suaveplan/audio-fx-plugin-microphone";
const opts: MicrophoneSimulationPluginOptions = {
config: { noise: { level: 0.1, color: "pink" } },
};
```
## Usage: Advanced Example
```ts
import type {
MicrophoneConfig,
MicrophoneUISection,
} from "@suaveplan/audio-fx-plugin-microphone";
function indexControlsById(
sections: ReadonlyArray<MicrophoneUISection>
): Record<string, MicrophoneUISection["controls"][number]> {
const out: Record<string, MicrophoneUISection["controls"][number]> = {};
for (const section of sections) {
for (const control of section.controls) {
out[control.id] = control;
}
}
return out;
}
function presetFrom(config: MicrophoneConfig): MicrophoneConfig {
return { ...config };
}
```
## API Reference
- `FrequencyControls`, `ResonanceControls`, `DistortionControls`, `NoiseControls`, `DynamicsControls`, `GainControls`, `CharacterControls`, `WowAndFlutterControls` — one `readonly`-fielded interface per signal stage.
- `MicrophoneConfig` — the eight control groups composed into one object.
- `MicrophoneSimulationPluginOptions` — `{ config?: Partial<MicrophoneConfig> }`.
- `MicrophonePresetData` — `{ config: MicrophoneConfig }`.
- `MicrophoneUIControl` — `{ id, label, min, max, step, unit, group }`.
- `MicrophoneUISection` — `{ id, label, controls: ReadonlyArray<MicrophoneUIControl> }`.
## Implementation Notes
The eight `*Controls` interfaces are kept distinct (not collapsed into one flat record) because:
1. UI code can iterate the sections rather than the flat field list, producing a grouped form for free.
2. `Partial<MicrophoneConfig>` provides per-section partials; consumers can pass `setMicrophoneConfig({ noise: { level: 0.7 } })` without restating other fields.
3. The Zod schema for each section is reusable in RPC contracts that target one section at a time.
`MicrophoneUIControl.group` is typed as `keyof MicrophoneConfig` (a string literal union over the eight section names) rather than a freeform string so the type system enforces consistency: adding a section requires updating the interface name, the schema, and the UI descriptor in lockstep, all caught at compile time.

View File

@@ -0,0 +1,89 @@
# `microphone`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`MicrophoneSimulationPlugin` is the universal microphone-simulation source plugin. It synthesises a microphone-coloured signal from a deterministic noise generator (selectable white or pink) passed through a one-pole low-pass filter, an input/output gain stage, optional `tanh`-soft-clip distortion, a "warmth + presence" tonal stage, and wow/flutter amplitude modulation. The plugin is a source (`io.inputs[0].count === 0`, `io.outputs[0].count === 1`).
The configuration is grouped into eight control families (frequency, resonance, distortion, noise, dynamics, gain, character, wowFlutter); each family is defined as a structured interface so editors can render the UI by iterating `getUISections()`. The plugin extends `AbstractAudioFxPlugin` and runs identically across browser / server / worklet renderers.
## Features
- Eight control groups (`MicrophoneConfig`): `frequency`, `resonance`, `distortion`, `noise`, `dynamics`, `gain`, `character`, `wowFlutter`. Each is a typed `*Controls` interface with a Zod schema and a UI section descriptor.
- One automatable parameter: `outputGain` (0..4) for top-of-chain level.
- `getUISections()` returns a `ReadonlyArray<MicrophoneUISection>` with stable `id`s, min/max/step/unit/group metadata, so editors can render the full surface without hard-coding any of it.
- Deterministic noise generator (xorshift32 RNG); `reset()` re-seeds so two reset-then-process runs produce byte-identical output — useful for regression testing.
- `createMicrophonePlugin(options)` factory mirrors the main-branch surface for migration parity.
- Pink-noise mode uses Paul Kellet's three-pole approximation, the same one the main-branch package ships, so the spectral colour matches.
## Usage: Basic Example
```ts
import { MicrophoneSimulationPlugin } from "@suaveplan/audio-fx-plugin-microphone";
const mic = new MicrophoneSimulationPlugin();
chain.add(mic);
```
## Usage: Advanced Example
```ts
import {
MicrophoneSimulationPlugin,
createMicrophonePlugin,
} from "@suaveplan/audio-fx-plugin-microphone";
const lofi = createMicrophonePlugin({
config: {
frequency: { cutoffHz: 3_500, tiltDb: 3 },
noise: { level: 0.2, color: "pink" },
distortion: { drive: 0.6, mix: 0.4 },
wowFlutter: {
wowRateHz: 1.2,
wowDepth: 0.3,
flutterRateHz: 10,
flutterDepth: 0.15,
},
},
});
await lofi.setup(
{ sampleRate: 48_000, channels: 1, blockSize: 512 },
ctx
);
// Drive a UI form from the section descriptor.
for (const section of lofi.getUISections()) {
form.addSection(section.id, section.label);
for (const control of section.controls) {
form.addControl(control);
}
}
```
## API Reference
- `MicrophoneSimulationPlugin` (class extends `AbstractAudioFxPlugin`)
- `new MicrophoneSimulationPlugin(options?: MicrophoneSimulationPluginOptions)` — throws `AUDIO_FX.INVALID_OPTIONS` on malformed options.
- `getMicrophoneConfig(): MicrophoneConfig`
- `setMicrophoneConfig(partial: Partial<MicrophoneConfig>): void` — merge-updates the live config; missing keys stay at their current values.
- `getUISections(): ReadonlyArray<MicrophoneUISection>`
- Inherited accessor for `outputGain`.
- `createMicrophonePlugin(options?)` — factory equivalent.
- Types: `FrequencyControls`, `ResonanceControls`, `DistortionControls`, `NoiseControls`, `DynamicsControls`, `GainControls`, `CharacterControls`, `WowAndFlutterControls`, `MicrophoneConfig`, `MicrophoneUIControl`, `MicrophoneUISection`.
## Implementation Notes
The noise generator is xorshift32 seeded with a fixed constant (`0x12345678`). Determinism matters for testing: the "reset re-seeds" test asserts that two runs after `reset()` produce byte-identical output. A `Math.random()`-based generator would fail that test on every invocation because the RNG state would advance between runs. The xorshift32 output is mapped to `[-1, 1]` via the standard `(u32 / 0xffffffff) * 2 - 1` formula.
Pink-noise mode uses Paul Kellet's three-pole approximation rather than a true 1/f filter because the latter requires a much higher-order filter to be accurate and would add per-sample CPU cost out of proportion with the perceptual improvement. The three-pole shape is the same as the main-branch implementation, so a side-by-side spectral comparison passes within a couple of dB.
The filter is a one-pole low-pass (`y = y_prev + α · (x - y_prev)`, with `α = ω / (ω + 1)`), not a biquad. The single-pole shape is what consumer microphones colour the signal with — biquad-class shaping is the territory of the `FilterPlugin` and `EQPlugin` siblings, which a caller can chain downstream. Cutoff is clamped to `[20, 0.45·sr]` so the bilinear transform stays stable across the full range.
Wow and flutter use two independent sine LFOs modulating the output gain. The wow rate is around 0.6 Hz by default (slow tape drift); flutter is around 8 Hz (fast tape wobble). Both depths are scaled by small constants (0.1 and 0.05 respectively) so the user-facing range `[0, 1]` translates to ±10% and ±5% amplitude variation — perceptually-sensible defaults rather than mathematically-extreme ones.
`MicrophoneConfig` is consumed as a tree of `Partial<*Controls>` so callers can update only the field they care about. `setMicrophoneConfig` merges with the current state per-section, so a `setMicrophoneConfig({ noise: { level: 0.7 } })` call does **not** require respecifying `color` — the previous value is preserved.
The plugin declares only one parameter (`outputGain`); every other control lives on the `MicrophoneConfig` tree because they are configuration, not real-time automation. If a future revision wants to automate a specific control (e.g., automate `cutoffHz`), promoting it to a parameter is a one-line change that does not break the section descriptor (which would still list it under `frequency`).

View File

@@ -0,0 +1,68 @@
# `microphone/schemas`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Zod schemas validating the construction-time options, the eight control groups, the full configuration tree, and the preset payload of `MicrophoneSimulationPlugin`. These run on the public boundary so any malformed object 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
- Eight per-section schemas: `FrequencyControlsSchema`, `ResonanceControlsSchema`, `DistortionControlsSchema`, `NoiseControlsSchema`, `DynamicsControlsSchema`, `GainControlsSchema`, `CharacterControlsSchema`, `WowAndFlutterControlsSchema`. Each enforces its documented range with `z.number().finite()` to block `NaN`/`Infinity`.
- `MicrophoneConfigSchema` composes the eight sections into one object.
- `MicrophoneSimulationPluginOptionsSchema` wraps `MicrophoneConfigSchema.partial()` so callers can pass any subset of fields.
- `MicrophonePresetDataSchema` is `.strict()` and demands the full `MicrophoneConfig` payload.
## Usage: Basic Example
```ts
import { MicrophoneConfigSchema } from "@suaveplan/audio-fx-plugin-microphone";
const parsed = MicrophoneConfigSchema.partial().parse({
noise: { level: 0.05, color: "white" },
});
```
## Usage: Advanced Example
```ts
import {
MicrophoneConfigSchema,
NoiseControlsSchema,
} from "@suaveplan/audio-fx-plugin-microphone";
// Validate a section in isolation (useful for an RPC that updates only one
// part of the mic state).
function setNoise(plugin: MicrophoneSimulationPlugin, raw: unknown) {
const noise = NoiseControlsSchema.parse(raw);
plugin.setMicrophoneConfig({ noise });
}
const partial = MicrophoneConfigSchema.partial().parse({
frequency: { cutoffHz: 4_000, tiltDb: 0 },
});
```
## API Reference
- `FrequencyControlsSchema: z.ZodObject<{ cutoffHz, tiltDb }>`
- `ResonanceControlsSchema: z.ZodObject<{ q }>`
- `DistortionControlsSchema: z.ZodObject<{ drive, mix }>`
- `NoiseControlsSchema: z.ZodObject<{ level, color }>`
- `DynamicsControlsSchema: z.ZodObject<{ thresholdDb, ratio }>`
- `GainControlsSchema: z.ZodObject<{ inputDb, outputDb }>`
- `CharacterControlsSchema: z.ZodObject<{ warmth, presence }>`
- `WowAndFlutterControlsSchema: z.ZodObject<{ wowRateHz, wowDepth, flutterRateHz, flutterDepth }>`
- `MicrophoneConfigSchema: z.ZodObject<…>`
- `MicrophoneSimulationPluginOptionsSchema: z.ZodObject<{ config? }>`
- `MicrophonePresetDataSchema: z.ZodObject<{ config }, "strict">`
## Implementation Notes
The per-section schemas are exported as named values rather than inlined into `MicrophoneConfigSchema` so they can be reused by callers that want to validate a single section at a time — for example, an RPC endpoint that updates only the noise floor. The combined `MicrophoneConfigSchema` is built by composing the section schemas, so adding a field to a section is a single-place change.
`MicrophonePresetDataSchema` demands the full `MicrophoneConfig` (not a partial). Presets restore the entire microphone state in one shot; allowing a partial would leave the restored plugin in an indeterminate state where some sections came from the preset and others from whatever happened to be loaded. The `.strict()` modifier additionally rejects unknown fields so a forwards-incompatible preset payload is caught at the boundary rather than silently dropping unknown keys.