Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
# @suaveplan/audio-fx-plugin-flanger - flanger plugin for the @suaveplan/audio-fx-plugin-* family.
flanger 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
- [flanger](./src/flanger/flanger.md) — `FlangerPlugin` is the universal comb-filter flanger. A short LFO-modulated delay line (sub-10 ms) is summed with the d…
- [flanger](./src/flanger/flanger.md) — `FlangerPlugin` is the universal comb-filter flanger. A short LFO-modulated delay line (sub-10 ms) is summed with the d…
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options and preset payload of `FlangerPlugin`. These run on the public bou…
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options and preset payload of `FlangerPlugin`. These run on the public bou…
<!-- end:auto-docs -->
## Composition
```ts
import { PluginChain, DefaultBusRegistry } from "@suaveplan/audio-fx-plugin-base/chain";
import { /* Plugin */ } from "@suaveplan/audio-fx-plugin-flanger";
const plugin = new /* Plugin */();
const chain = new PluginChain({
buses: new DefaultBusRegistry(),
plugins: [plugin],
});
```
## Status
Replaces the `./flanger` subpath of the bundled `@suaveplan/audio-plugins-fx`.

View File

@@ -0,0 +1,47 @@
{
"name": "@suaveplan/audio-fx-plugin-flanger",
"version": "0.1.0",
"type": "module",
"sideEffects": false,
"description": "flanger plugin for the @suaveplan/audio-fx-plugin-* family.",
"license": "MIT",
"author": "Suaveplan Team",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["dist", "README.md"],
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
},
"scripts": {
"build": "vite build",
"lint": "biome check --max-diagnostics 500 src/",
"typecheck": "tsc --noEmit -p tsconfig.lib.json",
"test": "vitest run --passWithNoTests",
"test:coverage": "vitest run --coverage --passWithNoTests",
"docs:check": "bun run ../../../../scripts/docs-check.ts ."
},
"dependencies": {
"@suaveplan/audio-fx-plugin-base": "workspace:^",
"@suaveplan/error": "workspace:^",
"@suaveplan/types": "workspace:^",
"tslib": "catalog:build",
"zod": "catalog:forms"
},
"devDependencies": {
"@suaveplan/audio-core": "workspace:^",
"@suaveplan/testing": "workspace:^",
"@suaveplan/typescript-config": "workspace:^",
"@vitest/coverage-istanbul": "catalog:testing",
"typescript": "catalog:",
"vite": "catalog:build",
"vite-plugin-dts": "catalog:build",
"vitest": "catalog:testing"
}
}

View File

@@ -0,0 +1,50 @@
# `flanger.types`
## Purpose
TypeScript-only type definitions for `FlangerPlugin`'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 or storybook controls without pulling in the plugin's runtime code.
## Features
- `FlangerPluginOptions` — all fields optional so the default constructor (`new FlangerPlugin()`) is valid.
- `readonly` properties — emphasises that the options object is consumed once at construction and never mutated.
- Numeric ranges live in the Zod schema (single source of truth) rather than in TypeScript types.
## Usage: Basic Example
```ts
import type { FlangerPluginOptions } from "@suaveplan/audio-fx-plugin-flanger";
const opts: FlangerPluginOptions = { initialDepthMs: 2 };
```
## Usage: Advanced Example
```ts
import type { FlangerPluginOptions } from "@suaveplan/audio-fx-plugin-flanger";
const presets: Record<string, FlangerPluginOptions> = {
subtle: { initialDepthMs: 0.5, initialFeedback: 0.3, initialWet: 0.3 },
classic: { initialDepthMs: 2, initialFeedback: 0.5, initialWet: 0.5 },
jet: { initialDepthMs: 3, initialFeedback: 0.9, initialWet: 0.7 },
};
function load(name: keyof typeof presets): FlangerPluginOptions {
return presets[name]!;
}
```
## API Reference
- `FlangerPluginOptions`
- `readonly initialRateHz?: number`
- `readonly initialDepthMs?: number`
- `readonly initialFeedback?: number`
- `readonly initialWet?: number`
- `readonly initialBaseDelayMs?: number`
## Implementation Notes
The interface deliberately mirrors `ChorusPluginOptions` field-for-field except that `depth` is shorter-range (`0..5 ms` versus the chorus's `0..20 ms`) and `baseDelay` is much shorter (`0.1..10 ms` versus `5..30 ms`). This consistency lets a UI render both plugins from a shared component if it wants to, while the schema enforces the perceptually-distinct parameter ranges that define each effect.
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.

View File

@@ -0,0 +1,8 @@
/** Construction options for `FlangerPlugin`. */
export interface FlangerPluginOptions {
readonly initialRateHz?: number;
readonly initialDepthMs?: number;
readonly initialFeedback?: number;
readonly initialWet?: number;
readonly initialBaseDelayMs?: number;
}

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

View File

@@ -0,0 +1,184 @@
import type { BlockContext, ProcessorConfig } from "@suaveplan/audio-core";
import type {
ChainContext,
ChainHandle,
} from "@suaveplan/audio-fx-plugin-base/base";
import { defaultAmbientState } from "@suaveplan/audio-fx-plugin-base/chain";
import {
AUDIO_FX_CODES,
type AudioFxError,
} from "@suaveplan/audio-fx-plugin-base/error-codes";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { FlangerPlugin } from "./flanger.js";
const SAMPLE_RATE = 48_000;
function blockCtx(): BlockContext {
return {
blockOffsetSamples: 0,
sampleRate: SAMPLE_RATE,
channels: 1,
opts: {},
};
}
function buffers(
size: number,
channels = 1
): { input: Float32Array[]; output: Float32Array[] } {
const input: Float32Array[] = [];
const output: Float32Array[] = [];
for (let c = 0; c < channels; c++) {
input.push(new Float32Array(size));
output.push(new Float32Array(size));
}
return { input, output };
}
function newConfig(blockSize: number, channels = 1): ProcessorConfig {
return { sampleRate: SAMPLE_RATE, channels, blockSize };
}
function newCtx(channels = 1): ChainContext {
const chain: ChainHandle = {
id: "flanger-test",
name: "flanger-test",
channels,
sampleRate: SAMPLE_RATE,
blockSize: 256,
slotCount: 1,
};
return {
chain,
position: { index: 0, total: 1 },
predecessors: [],
successors: [],
ambient: defaultAmbientState(),
buses: { get: () => undefined, list: () => [] },
};
}
describe("FlangerPlugin: contract surface", () => {
it("declares the AudioFxPlugin contract", { timeout: 5_000 }, () => {
const f = new FlangerPlugin();
expect(f.metadata.id).toBe("flanger");
expect(f.metadata.category).toBe("modulation");
expect(f.parameters.map((p) => p.id)).toEqual([
"rate",
"depth",
"feedback",
"wet",
"baseDelay",
]);
});
it("rejects malformed options with INVALID_OPTIONS", {
timeout: 5_000,
}, () => {
try {
new FlangerPlugin({ initialFeedback: 1.5 });
} catch (error) {
expect((error as AudioFxError).code).toBe(
AUDIO_FX_CODES.INVALID_OPTIONS
);
return;
}
throw new Error("expected INVALID_OPTIONS");
});
});
describe("FlangerPlugin: wet=0 is bit-transparent to the dry signal", () => {
it("output equals input sample-for-sample at wet=0", {
timeout: 5_000,
}, async () => {
const f = new FlangerPlugin({ initialWet: 0 });
await f.setup(newConfig(1024, 1), newCtx(1));
const { input, output } = buffers(1024, 1);
for (let i = 0; i < 1024; i++) {
input[0]![i] =
0.5 * Math.sin((2 * Math.PI * 1_000 * i) / SAMPLE_RATE);
}
f.process(input, output, blockCtx());
for (let i = 0; i < 1024; i++) {
expect(output[0]![i]).toBe(input[0]![i]!);
}
});
});
describe("FlangerPlugin: bounded output for bounded input", () => {
it("max feedback over a long sine never produces non-finite samples", {
timeout: 5_000,
}, async () => {
const f = new FlangerPlugin({
initialWet: 1,
initialFeedback: 0.95,
initialDepthMs: 3,
initialRateHz: 1,
});
await f.setup(newConfig(SAMPLE_RATE, 1), newCtx(1));
const { input, output } = buffers(SAMPLE_RATE, 1);
for (let i = 0; i < SAMPLE_RATE; i++) {
input[0]![i] =
0.5 * Math.sin((2 * Math.PI * 500 * i) / SAMPLE_RATE);
}
f.process(input, output, blockCtx());
for (let i = 0; i < SAMPLE_RATE; i++) {
expect(Number.isFinite(output[0]![i]!)).toBe(true);
expect(Math.abs(output[0]![i]!)).toBeLessThan(100);
}
});
});
describe("FlangerPlugin: comb-filter character", () => {
it("wet output differs from dry input on a steady sine", {
timeout: 5_000,
}, async () => {
const f = new FlangerPlugin({
initialWet: 1,
initialFeedback: 0.5,
initialDepthMs: 2,
initialRateHz: 0.5,
initialBaseDelayMs: 2,
});
await f.setup(newConfig(2048, 1), newCtx(1));
const { input, output } = buffers(2048, 1);
for (let i = 0; i < 2048; i++) {
input[0]![i] =
0.5 * Math.sin((2 * Math.PI * 440 * i) / SAMPLE_RATE);
}
f.process(input, output, blockCtx());
let diff = 0;
for (let i = 1024; i < 2048; i++) {
diff += Math.abs(output[0]![i]! - input[0]![i]!);
}
expect(diff).toBeGreaterThan(1);
});
});
describe("FlangerPlugin: reset clears state", () => {
it("after a wet block + reset, empty input produces empty output", {
timeout: 5_000,
}, async () => {
const f = new FlangerPlugin({ initialWet: 1 });
await f.setup(newConfig(256, 1), newCtx(1));
const { input, output } = buffers(256, 1);
for (let i = 0; i < 256; i++) input[0]![i] = 1;
f.process(input, output, blockCtx());
f.reset();
const { input: in2, output: out2 } = buffers(256, 1);
f.process(in2, out2, blockCtx());
for (let i = 0; i < 256; i++) {
expect(out2[0]![i]).toBe(0);
}
});
});
describe("FlangerPlugin: preset round-trip", () => {
it("getPreset / applyPreset round-trips rate", { timeout: 5_000 }, () => {
const a = new FlangerPlugin({ initialRateHz: 2 });
const snap = a.getPreset();
const b = new FlangerPlugin();
b.applyPreset(snap);
expect(b.getParameter("rate")).toBe(2);
});
});

View File

@@ -0,0 +1,178 @@
import type { BlockContext, ProcessorConfig } from "@suaveplan/audio-core";
import type { ChainContext } from "@suaveplan/audio-fx-plugin-base/base";
import { AbstractAudioFxPlugin } from "@suaveplan/audio-fx-plugin-base/base";
import {
AUDIO_FX_CODES,
AudioFxError,
} from "@suaveplan/audio-fx-plugin-base/error-codes";
import type { FlangerPluginOptions } from "../flanger.types.js";
import {
FlangerPluginOptionsSchema,
FlangerPresetDataSchema,
} from "../schemas/index.js";
const MAX_DELAY_MS = 20;
const TWO_PI = 2 * Math.PI;
/**
* Comb-filter flanger. A very short LFO-modulated delay line is added
* back into the dry signal; the close spacing produces evenly-spaced
* comb peaks/notches in the frequency response. Feedback emphasises
* the peaks for the "metallic" flange sound. Feedback is clamped at
* 0.95 so the output cannot diverge; `wet === 0` is bit-transparent to
* the dry path (no flange voice is mixed in).
*/
export class FlangerPlugin extends AbstractAudioFxPlugin {
private buffers: Float32Array[] = [];
private writeIndex = 0;
private bufferSize = 0;
private lfoPhase = 0;
public constructor(options: FlangerPluginOptions = {}) {
const parsed = FlangerPluginOptionsSchema.safeParse(options);
if (!parsed.success) {
throw new AudioFxError(
AUDIO_FX_CODES.INVALID_OPTIONS,
"audio-fx.flanger-invalid-options",
{ context: { issues: parsed.error.issues } }
);
}
const opts = parsed.data;
super({
metadata: {
id: "flanger",
name: "Flanger",
version: "1.0.0",
category: "modulation",
},
schema: FlangerPresetDataSchema,
io: {
inputs: [{ count: "preserve" }],
outputs: [{ count: "preserve" }],
},
parameters: [
{
id: "rate",
name: "Rate",
value: opts.initialRateHz ?? 0.5,
min: 0.05,
max: 10,
default: 0.5,
unit: "hz",
automatable: true,
},
{
id: "depth",
name: "Depth",
value: opts.initialDepthMs ?? 1,
min: 0,
max: 5,
default: 1,
unit: "ms",
automatable: true,
},
{
id: "feedback",
name: "Feedback",
value: opts.initialFeedback ?? 0.5,
min: 0,
max: 0.95,
default: 0.5,
unit: "ratio",
automatable: true,
},
{
id: "wet",
name: "Wet",
value: opts.initialWet ?? 0.5,
min: 0,
max: 1,
default: 0.5,
unit: "percent",
automatable: true,
},
{
id: "baseDelay",
name: "Base Delay",
value: opts.initialBaseDelayMs ?? 1.5,
min: 0.1,
max: 10,
default: 1.5,
unit: "ms",
automatable: true,
},
],
supportedSchemaVersion: 1,
});
}
protected override setupState(
config: ProcessorConfig,
_chain: ChainContext
): void {
const samples =
Math.ceil((MAX_DELAY_MS / 1000) * config.sampleRate) + 4;
this.bufferSize = samples;
this.buffers = [];
for (let c = 0; c < config.channels; c++) {
this.buffers.push(new Float32Array(samples));
}
this.writeIndex = 0;
this.lfoPhase = 0;
}
protected override resetState(): void {
for (const b of this.buffers) b.fill(0);
this.writeIndex = 0;
this.lfoPhase = 0;
}
protected processBlock(
input: Float32Array[],
output: Float32Array[],
ctx: BlockContext
): void {
const wet = this.parameterValue("wet");
const blockSize = input[0]!.length;
if (wet === 0) {
for (let ch = 0; ch < input.length; ch++) {
output[ch]!.set(input[ch]!);
}
return;
}
const rate = this.parameterValue("rate");
const depthMs = this.parameterValue("depth");
const feedback = this.parameterValue("feedback");
const baseDelayMs = this.parameterValue("baseDelay");
const sr = ctx.sampleRate;
const baseDelaySamples = (baseDelayMs / 1000) * sr;
const depthSamples = (depthMs / 1000) * sr;
const phaseInc = (TWO_PI * rate) / sr;
const channels = input.length;
for (let i = 0; i < blockSize; i++) {
const lfo = Math.sin(this.lfoPhase + phaseInc * i);
const delaySamples =
baseDelaySamples + (lfo + 1) * 0.5 * depthSamples;
for (let ch = 0; ch < channels; ch++) {
const buf = this.buffers[ch]!;
const read = this.readFractional(buf, delaySamples);
const dry = input[ch]![i]!;
buf[this.writeIndex] = dry + read * feedback;
output[ch]![i] = dry * (1 - wet) + read * wet;
}
this.writeIndex = (this.writeIndex + 1) % this.bufferSize;
}
this.lfoPhase = (this.lfoPhase + phaseInc * blockSize) % TWO_PI;
}
private readFractional(buf: Float32Array, delaySamples: number): number {
const read = this.writeIndex - delaySamples;
const wrapped =
((read % this.bufferSize) + this.bufferSize) % this.bufferSize;
const i0 = Math.floor(wrapped);
const i1 = (i0 + 1) % this.bufferSize;
const frac = wrapped - i0;
return buf[i0]! * (1 - frac) + buf[i1]! * frac;
}
}

View File

@@ -0,0 +1 @@
export { FlangerPlugin } from "./flanger.js";

View File

@@ -0,0 +1,6 @@
export { FlangerPlugin } from "./flanger/index.js";
export type { FlangerPluginOptions } from "./flanger.types.js";
export {
FlangerPluginOptionsSchema,
FlangerPresetDataSchema,
} from "./schemas/index.js";

View File

@@ -0,0 +1,4 @@
export {
FlangerPluginOptionsSchema,
FlangerPresetDataSchema,
} from "./schemas.js";

View File

@@ -0,0 +1,56 @@
# `flanger/schemas`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Zod schemas validating the construction-time options and preset payload of `FlangerPlugin`. 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, RPC validation, or storybook controls without importing the audio implementation.
## Features
- `FlangerPluginOptionsSchema` — validates all five optional construction fields with their documented ranges (`rate` 0.05..10 Hz, `depth` 0..5 ms, `feedback` 0..0.95, `wet` 0..1, `baseDelay` 0.1..10 ms).
- `FlangerPresetDataSchema` — empty strict object: the flanger has no plugin-specific preset payload, so any extra keys are rejected by `.strict()`.
- All numeric fields use `z.number().finite()` so `NaN` and `Infinity` cannot sneak through.
## Usage: Basic Example
```ts
import { FlangerPluginOptionsSchema } from "@suaveplan/audio-fx-plugin-flanger";
const parsed = FlangerPluginOptionsSchema.parse({ initialDepthMs: 1.5 });
```
## Usage: Advanced Example
```ts
import {
FlangerPluginOptionsSchema,
FlangerPresetDataSchema,
} from "@suaveplan/audio-fx-plugin-flanger";
// Pull keys + ranges from the schema for a generated form.
const fields = FlangerPluginOptionsSchema.keyof().options;
for (const field of fields) {
form.addRow(field);
}
// Strict preset validation at the IPC boundary.
function isFlangerPreset(blob: unknown): boolean {
return FlangerPresetDataSchema.safeParse(blob).success;
}
```
## API Reference
- `FlangerPluginOptionsSchema: z.ZodObject<{ initialRateHz?, initialDepthMs?, initialFeedback?, initialWet?, initialBaseDelayMs? }>`
- `FlangerPresetDataSchema: z.ZodObject<{}, "strict">`
## Implementation Notes
The maximum feedback is `0.95`, matching the chorus, phaser, and delay plugins so all four feedback-bearing effects share one mental model. The flanger lives close to that boundary by design — the resonant comb peaks are the effect's character — but the same hard cap keeps the loop strictly under unity gain so the output cannot diverge.
`FlangerPresetDataSchema` is `.strict()` so unknown fields are rejected, matching the sibling-plugin policy. The flanger has no plugin-specific preset payload today: every parameter (`rate`, `depth`, `feedback`, `wet`, `baseDelay`) travels through the universal parameter snapshot that `AbstractAudioFxPlugin` builds, and the empty `presetData` field marks that the plugin opts out of plugin-specific state.

View File

@@ -0,0 +1,11 @@
import z from "zod";
export const FlangerPluginOptionsSchema = z.object({
initialRateHz: z.number().finite().min(0.05).max(10).optional(),
initialDepthMs: z.number().finite().min(0).max(5).optional(),
initialFeedback: z.number().finite().min(0).max(0.95).optional(),
initialWet: z.number().finite().min(0).max(1).optional(),
initialBaseDelayMs: z.number().finite().min(0.1).max(10).optional(),
});
export const FlangerPresetDataSchema = z.object({}).strict();

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "./dist",
"lib": ["ES2022", "DOM"],
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules"]
}

View File

@@ -0,0 +1,18 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"lib": ["ES2022", "DOM"],
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": [
"src/**/*.test.ts",
"src/**/*.bench.ts",
"tests/**",
"dist",
"node_modules"
]
}

View File

@@ -0,0 +1,38 @@
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
root: __dirname,
cacheDir: "../../../node_modules/.vite/packages/audio-fx-plugin-flanger",
plugins: [
dts({
entryRoot: "src",
tsconfigPath: "./tsconfig.lib.json",
insertTypesEntry: true,
}),
],
build: {
outDir: "./dist",
emptyOutDir: true,
reportCompressedSize: true,
sourcemap: true,
lib: {
entry: { index: "src/index.ts" },
formats: ["es"],
},
rollupOptions: {
treeshake: true,
output: {
preserveModules: true,
preserveModulesRoot: "src",
entryFileNames: "[name].js",
},
external: [
/^@suaveplan\//,
/^@opentelemetry\//,
"tslib",
"zod",
],
},
},
});

View File

@@ -0,0 +1,3 @@
import { createTestConfig } from "@suaveplan/testing/config";
export default createTestConfig({ testTimeout: 30_000 });