Swirl and comb a sound with a Phaser and a Flanger node (#341)
sfxr's "phaser" is a comb filter, the sound plus a copy delayed by up to 2.9 ms. That is a flanger, not the phaser a guitarist knows. Both effects come in as nodes of their own, so each name fits its sound and sfxr is covered in full. Both run on the FX worklet like the Bitcrusher, because a feedback loop in Web Audio costs a render quantum per round. - Phaser: first-order allpasses (Stages 2, 4, 6, 8, 12) around Frequency, moved by a sine LFO Depth octaves wide at Rate Hz, with Feedback and Mix. An Envelope can move Frequency and Mix. - Flanger: a delay line of Offset ms. Sweep moves it on in ms/s from the start of the voice, an LFO adds up to Depth ms. As in sfxr the delay is mirrored at 0, and it never goes past 20 ms. Feedback comes back exactly one delay later. An Envelope can move Offset and Mix. - Mix 0 leaves the sound as it is, 1 is half sound and half copy. The notches are deepest there and the peaks stay at the sound's level. sfxr adds both at full, so its sound comes out 6 dB louder. - The Flanger's processor gets the start of its voice and begins the sweep on that sample. stageNodes takes it as a new argument `at`. - Explosion gets a Flanger in one draw out of two, with sfxr's offset and sweep and no LFO. Offset is offset^2 * 2.89 ms, Sweep is ramp^2 * 125 ms/s, signed so the direction holds. - Randomize draws a Flanger's offset and sweep like sfxr. Its LFO and feedback and all of a Phaser are drawn within bounds of their own. Mix and Stages stay, as a Bitcrusher's Mix does. Mutate moves them too, and a 0 stays 0. - New instrument preset Phased Pad: saw, Envelope, low-pass, Phaser of six stages. The other 14 are unchanged. - The Bitcrusher's worklet setup is now shared with both nodes (workletNodes), its behaviour is unchanged. Tests: - Unit phaser (new): 8 green. Counter-check with clamping at 0 instead of mirroring and a flipped allpass coefficient: 3 red. - Unit soundfx, sound-library, chip-presets: 45 / 76 / 14 green. Counter-checks: start time not passed on, phaser frequency eased linearly, sweep sign dropped, Phaser left out of Mutate, each red. - Unit core, viewport, graph: 179 files, 2979 tests green. - e2e 34 (melektaus-atx): ALL PASSED, 70 checks (1 new), first run. Chrome offline: 1 kHz notch RMS 0.0000 with two stages, 0.5657 with four. Flanger echo after 48 samples, swept echo at 533/534. About 47 Phasers (12 stages) or 63 Flangers fill the audio thread. - Gate (melektaus-atx): CHECK EXIT 0, 257 files, 3991 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
29b604dc07
commit
01fd6e6464
13 files changed
+999
-48
No files matched your search
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* The Preset menu, Randomize and Mutate of the Instrument tab (#329): sfxr's generators worked out
|
||||
* into a Chip, an Envelope, a Filter and a Bitcrusher, and its randomizer and mutator on their numbers.
|
||||
* into a Chip, an Envelope, a Filter, a Bitcrusher and a Flanger (#341), and its randomizer and mutator
|
||||
* on their numbers.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ReplaceGraphCommand } from '../../src/core/commands/graphCommands';
|
||||
@@ -21,11 +22,13 @@ import type { InstrumentAsset } from '../../src/core/model/Asset';
|
||||
import type { FlatDef } from '../../src/core/properties/PropertySchema';
|
||||
import {
|
||||
CRUSH_ROWS,
|
||||
FLANGER_ROWS,
|
||||
type FxChipPlan,
|
||||
type FxEnvelopePlan,
|
||||
FILTER_ROWS,
|
||||
type FxPlan,
|
||||
fxChainOf,
|
||||
PHASER_ROWS,
|
||||
} from '../../src/core/soundfx/nodes';
|
||||
import { makeProject } from '../helpers';
|
||||
|
||||
@@ -44,6 +47,8 @@ const ROWS: Record<string, Record<string, FlatDef<'number'>>> = {
|
||||
InstrumentEnvelope: ENVELOPE_ROWS,
|
||||
SoundFxFilter: FILTER_ROWS,
|
||||
SoundFxBitcrusher: CRUSH_ROWS,
|
||||
SoundFxPhaser: PHASER_ROWS,
|
||||
SoundFxFlanger: FLANGER_ROWS,
|
||||
};
|
||||
|
||||
function withinRows(node: GraphNode, keys: string[]): void {
|
||||
@@ -61,13 +66,16 @@ describe('a preset (#329)', () => {
|
||||
...TONE_PRESETS.map((p) => p.value),
|
||||
...EFFECT_PRESETS.map((p) => p.value),
|
||||
]);
|
||||
expect(TONE_PRESETS).toHaveLength(14);
|
||||
expect(TONE_PRESETS).toHaveLength(15);
|
||||
});
|
||||
|
||||
it('of sfxr is a chain from every note through Chip, Envelope, Filter and Bitcrusher, ending by itself', () => {
|
||||
const sources = instrumentSourcesOf(makeProject().project);
|
||||
for (const { value } of EFFECT_PRESETS) {
|
||||
const graph = chipPresetGraph(value, seeded());
|
||||
// Only an explosion can have sfxr's phaser, as a Flanger after the Filter.
|
||||
const flanged = graph.nodes.some((n) => n.type === 'SoundFxFlanger');
|
||||
expect(flanged && value !== 'explosion', value).toBe(false);
|
||||
expect(
|
||||
graph.nodes.map((n) => n.type),
|
||||
value,
|
||||
@@ -76,6 +84,7 @@ describe('a preset (#329)', () => {
|
||||
'InstrumentChip',
|
||||
'InstrumentEnvelope',
|
||||
'SoundFxFilter',
|
||||
...(flanged ? ['SoundFxFlanger'] : []),
|
||||
'SoundFxBitcrusher',
|
||||
'InstrumentOutput',
|
||||
]);
|
||||
@@ -86,6 +95,7 @@ describe('a preset (#329)', () => {
|
||||
expect('chip' in chain && chain.stages.map((s) => s.kind), value).toEqual([
|
||||
'envelope',
|
||||
'pass',
|
||||
...(flanged ? ['flanger'] : []),
|
||||
'crush',
|
||||
]);
|
||||
const envelope = chain.stages[0]!;
|
||||
@@ -124,6 +134,31 @@ describe('a preset (#329)', () => {
|
||||
expect(byType(chipPresetGraph('explosion', seeded(3)), 'InstrumentChip').params.wave).toBe('noise');
|
||||
});
|
||||
|
||||
it("gives one explosion in two sfxr's phaser, as a Flanger with its offset and sweep (#341)", () => {
|
||||
// Near 1: sfxr's offset 0.5991 and ramp -0.2997, a delay of 1.04 ms falling 11.2 ms a second.
|
||||
expect(
|
||||
byType(
|
||||
chipPresetGraph('explosion', () => 0.999),
|
||||
'SoundFxFlanger',
|
||||
).params,
|
||||
).toEqual({
|
||||
offset: 1.04,
|
||||
sweep: -11.2,
|
||||
rate: 0,
|
||||
depth: 0,
|
||||
feedback: 0,
|
||||
mix: 1,
|
||||
});
|
||||
expect(chipPresetGraph('explosion', () => 0).nodes.some((n) => n.type === 'SoundFxFlanger')).toBe(false);
|
||||
const random = seeded(4);
|
||||
const flangers = Array.from({ length: 400 }, () => chipPresetGraph('explosion', random)).flatMap(
|
||||
(graph) => graph.nodes.filter((n) => n.type === 'SoundFxFlanger'),
|
||||
);
|
||||
expect(flangers.length).toBeGreaterThan(160);
|
||||
expect(flangers.length).toBeLessThan(240);
|
||||
for (const flanger of flangers) withinRows(flanger, Object.keys(FLANGER_ROWS));
|
||||
});
|
||||
|
||||
it('is drawn anew every time', () => {
|
||||
const random = seeded(7);
|
||||
const a = byType(chipPresetGraph('laser', random), 'InstrumentChip').params;
|
||||
@@ -202,6 +237,9 @@ describe('an instrument preset', () => {
|
||||
arpeggio: 12,
|
||||
repeat: 0.08,
|
||||
});
|
||||
// Phased Pad: a saw through a Phaser of six stages (#341).
|
||||
const pad = build(chipPresetGraph('phasedPad'), 60).layers[0]!;
|
||||
expect(fxChainOf(pad).stages.at(-1)).toMatchObject({ kind: 'phaser', stages: 6, frequency: 600, mix: 1 });
|
||||
});
|
||||
|
||||
it('Drum Kit: kick, snare and hi-hats on their General MIDI keys, each ending by itself', () => {
|
||||
@@ -287,6 +325,32 @@ describe('Randomize and Mutate (#329)', () => {
|
||||
expect(mutatedParams(graph, () => 0.1)).toEqual([]);
|
||||
});
|
||||
|
||||
it('draw and move the numbers of a Phaser and a Flanger too, but not their mix or stages (#341)', () => {
|
||||
const graph = chipPresetGraph('explosion', () => 0.999);
|
||||
const pad = chipPresetGraph('phasedPad');
|
||||
graph.nodes.push(byType(pad, 'SoundFxPhaser'));
|
||||
byType(graph, 'SoundFxFlanger').params.mix = 0.4;
|
||||
const changes = randomizedParams(graph, seeded(13));
|
||||
const keysOf = (type: string) =>
|
||||
changes.filter((c) => c.node === byType(graph, type).id).map((c) => c.key);
|
||||
expect(keysOf('SoundFxPhaser')).toEqual(['frequency', 'rate', 'depth', 'feedback']);
|
||||
expect(keysOf('SoundFxFlanger')).toEqual(['offset', 'sweep', 'rate', 'depth', 'feedback']);
|
||||
apply(graph, changes);
|
||||
withinRows(byType(graph, 'SoundFxPhaser'), Object.keys(PHASER_ROWS));
|
||||
withinRows(byType(graph, 'SoundFxFlanger'), Object.keys(FLANGER_ROWS));
|
||||
expect(byType(graph, 'SoundFxPhaser').params).toMatchObject({ stages: '6', mix: 1 });
|
||||
expect(byType(graph, 'SoundFxFlanger').params.mix).toBe(0.4);
|
||||
// Mutate leaves the Flanger's LFO off and moves its offset and sweep, and the Phaser's numbers.
|
||||
const fresh = chipPresetGraph('explosion', () => 0.999);
|
||||
fresh.nodes.push(byType(chipPresetGraph('phasedPad'), 'SoundFxPhaser'));
|
||||
const moved = (type: string) =>
|
||||
mutatedParams(fresh, () => 0.9)
|
||||
.filter((c) => c.node === byType(fresh, type).id)
|
||||
.map((c) => c.key);
|
||||
expect(moved('SoundFxFlanger')).toEqual(['offset', 'sweep']);
|
||||
expect(moved('SoundFxPhaser')).toEqual(['frequency', 'rate', 'depth', 'feedback']);
|
||||
});
|
||||
|
||||
it('find nothing to change in an instrument of Samples', () => {
|
||||
expect(hasChipNumbers(starterInstrumentGraph())).toBe(false);
|
||||
expect(randomizedParams(starterInstrumentGraph())).toEqual([]);
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/** The Phaser's and the Flanger's samples (#341), as their worklet makes them. */
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { Flanger, type FlangerNumbers, Phaser, type PhaserNumbers } from '../../src/core/soundfx/phaser';
|
||||
|
||||
const RATE = 48000;
|
||||
const one = (value: number) => new Float32Array([value]);
|
||||
|
||||
function flange(
|
||||
input: Float32Array,
|
||||
numbers: Partial<Record<keyof FlangerNumbers, number>>,
|
||||
flanger = new Flanger(RATE),
|
||||
) {
|
||||
const out = new Float32Array(input.length);
|
||||
const all = { offset: 0, sweep: 0, rate: 0, depth: 0, feedback: 0, mix: 1, ...numbers };
|
||||
flanger.render(
|
||||
[input],
|
||||
[out],
|
||||
Object.fromEntries(Object.entries(all).map(([k, v]) => [k, one(v)])) as FlangerNumbers,
|
||||
);
|
||||
return out;
|
||||
}
|
||||
|
||||
function phase(input: Float32Array, numbers: Partial<Record<keyof PhaserNumbers, number>>, stages = 4) {
|
||||
const out = new Float32Array(input.length);
|
||||
const all = { frequency: 1000, rate: 0, depth: 0, feedback: 0, mix: 1, ...numbers };
|
||||
new Phaser(RATE, stages).render(
|
||||
[input],
|
||||
[out],
|
||||
Object.fromEntries(Object.entries(all).map(([k, v]) => [k, one(v)])) as PhaserNumbers,
|
||||
);
|
||||
return out;
|
||||
}
|
||||
|
||||
const impulse = (length: number, at = 0) => {
|
||||
const samples = new Float32Array(length);
|
||||
samples[at] = 1;
|
||||
return samples;
|
||||
};
|
||||
|
||||
const sine = (hz: number, seconds: number) =>
|
||||
Float32Array.from({ length: Math.round(seconds * RATE) }, (_, i) =>
|
||||
Math.sin((2 * Math.PI * hz * i) / RATE),
|
||||
);
|
||||
|
||||
const rms = (samples: Float32Array, from = 0, to = samples.length) => {
|
||||
let sum = 0;
|
||||
for (let i = from; i < to; i++) sum += samples[i]! ** 2;
|
||||
return Math.sqrt(sum / (to - from));
|
||||
};
|
||||
|
||||
/** Where an impulse comes out again, after its own sample: the samples with anything in them. */
|
||||
const heard = (samples: Float32Array, from = 1) =>
|
||||
[...samples].flatMap((v, i) => (i >= from && Math.abs(v) > 1e-6 ? [[i, +v.toFixed(4)]] : []));
|
||||
|
||||
describe('the Flanger (#341)', () => {
|
||||
it('adds the sound offset ms late, half and half at mix 1, and leaves it be at mix 0', () => {
|
||||
const out = flange(impulse(200), { offset: 1 });
|
||||
expect(out[0]).toBeCloseTo(0.5);
|
||||
expect(heard(out)).toEqual([[48, 0.5]]);
|
||||
expect([...flange(impulse(200), { offset: 1, mix: 0 })]).toEqual([...impulse(200)]);
|
||||
});
|
||||
|
||||
it('feeds the copy back, every echo one offset after the last', () => {
|
||||
expect(heard(flange(impulse(200), { offset: 1, feedback: 0.5 }))).toEqual([
|
||||
[48, 0.5],
|
||||
[96, 0.25],
|
||||
[144, 0.125],
|
||||
[192, 0.0625],
|
||||
]);
|
||||
});
|
||||
|
||||
it('sweeps the delay on from the start of the sound, and mirrors it at 0 as sfxr does', () => {
|
||||
// 100 ms/s from 0: after 0.1 s the copy is 10 ms late and still moving, so an impulse then
|
||||
// comes back at j = 480 + 0.1 j, 533.3 samples on.
|
||||
const flanger = new Flanger(RATE);
|
||||
flange(new Float32Array(4800), { sweep: 100 }, flanger);
|
||||
const later = heard(flange(impulse(800), { sweep: 100 }, flanger));
|
||||
expect(later.map(([i]) => i)).toEqual([533, 534]);
|
||||
// Read 0.9 samples a sample while the line grows: the copy is drawn out, by 1/0.9 on average.
|
||||
const drawn = later.reduce((sum, [, v]) => sum + v!, 0);
|
||||
expect(drawn).toBeGreaterThan(0.5);
|
||||
expect(drawn).toBeLessThan(0.6);
|
||||
// From 1 ms down at 100 ms/s: 20 ms in, the delay has passed 0 and is 1 ms again, growing.
|
||||
const down = new Flanger(RATE);
|
||||
flange(new Float32Array(960), { offset: 1, sweep: -100 }, down);
|
||||
const back = flange(impulse(200), { offset: 1, sweep: -100 }, down);
|
||||
expect(back[0]).toBeCloseTo(0.5);
|
||||
expect(heard(back).map(([i]) => i)).toEqual([53, 54]);
|
||||
});
|
||||
|
||||
it('swings the delay up to depth ms with its LFO, and never past 20 ms', () => {
|
||||
// A quarter of a 10 Hz swing is 25 ms in: half way up from 1 ms towards 1 + 4 ms.
|
||||
const flanger = new Flanger(RATE);
|
||||
flange(new Float32Array(1200), { offset: 1, rate: 10, depth: 4 }, flanger);
|
||||
const out = flange(impulse(400), { offset: 1, rate: 10, depth: 4 }, flanger);
|
||||
const at = heard(out)[0]![0]!;
|
||||
expect(at).toBeGreaterThan(48 * 2.5);
|
||||
expect(at).toBeLessThan(48 * 3.6);
|
||||
const far = new Flanger(RATE);
|
||||
flange(new Float32Array(48000), { sweep: 125 }, far);
|
||||
expect(heard(flange(impulse(2000), { sweep: 125 }, far))).toEqual([[960, 0.5]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the Phaser (#341)', () => {
|
||||
it('cuts a notch at its frequency with two stages, where four make a peak', () => {
|
||||
const tone = sine(1000, 0.2);
|
||||
expect(rms(phase(tone, {}, 2), 4800)).toBeLessThan(0.01);
|
||||
expect(rms(phase(tone, {}, 4), 4800)).toBeCloseTo(Math.SQRT1_2, 2);
|
||||
// Far below the notch the two stages hardly turn the phase: the sound stays.
|
||||
expect(rms(phase(sine(50, 0.4), {}, 2), 4800)).toBeGreaterThan(0.69);
|
||||
});
|
||||
|
||||
it('leaves the sound be at mix 0', () => {
|
||||
const tone = sine(1000, 0.05);
|
||||
expect([...phase(tone, { mix: 0 }, 2)]).toEqual([...tone]);
|
||||
});
|
||||
|
||||
it('moves the notch with its LFO, depth octaves wide', () => {
|
||||
// At 1 Hz over 4 octaves the notch passes 1 kHz twice a second and is two octaves off between,
|
||||
// where two stages turn it by 2 * 2 * atan(1/4): cos(28°) of it is left, 0.62 of the RMS.
|
||||
const out = phase(sine(1000, 1), { rate: 1, depth: 4 }, 2);
|
||||
const windows = Array.from({ length: 40 }, (_, w) => rms(out, w * 1200, (w + 1) * 1200));
|
||||
expect(Math.min(...windows)).toBeLessThan(0.15);
|
||||
expect(Math.max(...windows)).toBeGreaterThan(0.58);
|
||||
expect(Math.max(...windows)).toBeLessThan(0.63);
|
||||
});
|
||||
|
||||
it('stays bounded at the most feedback there is', () => {
|
||||
const noise = Float32Array.from({ length: RATE }, (_, i) => Math.sin(i * 12.9898) % 1);
|
||||
const out = phase(noise, { feedback: 0.95, rate: 2, depth: 4 }, 12);
|
||||
expect(Math.max(...out.map(Math.abs))).toBeLessThan(20);
|
||||
expect(out.every(Number.isFinite)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -122,6 +122,8 @@ describe('the sound FX asset', () => {
|
||||
'SoundFxMix',
|
||||
'SoundFxDelay',
|
||||
'SoundFxBitcrusher',
|
||||
'SoundFxPhaser',
|
||||
'SoundFxFlanger',
|
||||
]);
|
||||
f.edit.addNode('SoundFxInput');
|
||||
f.edit.addNode('SoundFxOutput');
|
||||
@@ -133,6 +135,8 @@ describe('the sound FX asset', () => {
|
||||
'SoundFxMix',
|
||||
'SoundFxDelay',
|
||||
'SoundFxBitcrusher',
|
||||
'SoundFxPhaser',
|
||||
'SoundFxFlanger',
|
||||
]);
|
||||
|
||||
const behavior = makeBehavior(f, 'Logic');
|
||||
@@ -365,6 +369,36 @@ describe('a Bitcrusher in a sound FX graph (#322)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('a Phaser and a Flanger in a sound FX graph (#341)', () => {
|
||||
const at = (type: string, params?: Record<string, string | number>) =>
|
||||
fxChainOf(compileSoundFx(line([{ type, params }]).fx.graph).compiled!).stages;
|
||||
|
||||
it('play at their defaults and hold their numbers to their rows', () => {
|
||||
expect(at('SoundFxPhaser')).toEqual([
|
||||
{ kind: 'phaser', stages: 4, frequency: 800, rate: 0.5, depth: 2, feedback: 0.4, mix: 1 },
|
||||
]);
|
||||
expect(at('SoundFxPhaser', { stages: '12', frequency: 5, feedback: 2, depth: -1 })).toMatchObject([
|
||||
{ stages: 12, frequency: 20, feedback: 0.95, depth: 0 },
|
||||
]);
|
||||
expect(at('SoundFxPhaser', { stages: '5' })).toMatchObject([{ stages: 4 }]);
|
||||
expect(at('SoundFxFlanger')).toEqual([
|
||||
{ kind: 'flanger', offset: 1, sweep: 0, rate: 0.25, depth: 2, feedback: 0.5, mix: 1 },
|
||||
]);
|
||||
expect(at('SoundFxFlanger', { offset: 50, sweep: -500, depth: 99 })).toMatchObject([
|
||||
{ offset: 10, sweep: -125, depth: 10 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('are left out at a typed-in mix of 0, and kept for a mix from a parameter', () => {
|
||||
expect(at('SoundFxPhaser', { mix: 0 })).toEqual([]);
|
||||
expect(at('SoundFxFlanger', { mix: 0 })).toEqual([]);
|
||||
const f = withParam('SoundFxFlanger', 'mix', [{ key: 'wet', kind: 'number', default: 0 }]);
|
||||
const plan = soundFxBuildFor(f.fx).compiled!;
|
||||
expect(fxChainOf(plan).stages).toMatchObject([{ kind: 'flanger', mix: 0 }]);
|
||||
expect(fxChainOf(plan, { wet: 0.3 }).stages).toMatchObject([{ kind: 'flanger', mix: 0.3 }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the tail of a voice (#324)', () => {
|
||||
const echo = (time: number, feedback: number, mix: number): FxStage => ({
|
||||
kind: 'delay',
|
||||
|
||||
@@ -311,7 +311,14 @@ beforeEach(() => {
|
||||
const parameters = new Map(
|
||||
Object.entries(options.parameterData).map(([key, value]) => [key, param(value)] as const),
|
||||
);
|
||||
const kind = name === 'bit-crusher' ? 'crush' : name === 'chip-player' ? 'chip' : 'pitch';
|
||||
const kind =
|
||||
name === 'bit-crusher'
|
||||
? 'crush'
|
||||
: name === 'chip-player'
|
||||
? 'chip'
|
||||
: name === 'phaser' || name === 'flanger'
|
||||
? name
|
||||
: 'pitch';
|
||||
const node: FakeWorklet = wired(kind, {
|
||||
options,
|
||||
pitch: parameters.get('pitch') ?? param(0),
|
||||
@@ -562,6 +569,54 @@ describe('the sound library', () => {
|
||||
expect(node.parameters.get('mix')!.ramps).toEqual([]);
|
||||
});
|
||||
|
||||
it('sends a voice through a Phaser and a Flanger on the worklet, the sweep from its start (#341)', async () => {
|
||||
const library = new SoundLibrary();
|
||||
const amounts = (values: Record<string, number>) =>
|
||||
Object.fromEntries(Object.entries(values).map(([key, value]) => [key, { value }]));
|
||||
const plan: FxPlan = {
|
||||
pitch: [],
|
||||
stages: [
|
||||
{
|
||||
kind: 'phaser',
|
||||
stages: 6,
|
||||
...amounts({ rate: 0.5, depth: 2, feedback: 0.4, mix: 1 }),
|
||||
frequency: { value: 800, param: 'f' },
|
||||
} as FxStagePlan,
|
||||
{
|
||||
kind: 'flanger',
|
||||
...amounts({ sweep: -3, rate: 0, depth: 0, feedback: 0, mix: 1 }),
|
||||
offset: { value: 1.5, param: 'o' },
|
||||
} as FxStagePlan,
|
||||
],
|
||||
};
|
||||
const voice = await play(library, FILE, {}, { plan, params: {} });
|
||||
expect(wires).toEqual(['source > phaser', 'phaser > flanger', 'flanger > gain', 'gain > destination']);
|
||||
const [phaser, flanger] = worklets as [FakeWorklet, FakeWorklet];
|
||||
expect(phaser.options.parameterData).toEqual({
|
||||
frequency: 800,
|
||||
rate: 0.5,
|
||||
depth: 2,
|
||||
feedback: 0.4,
|
||||
mix: 1,
|
||||
});
|
||||
expect(phaser.options.processorOptions).toEqual({ stages: 6 });
|
||||
expect(flanger.options.parameterData).toEqual({
|
||||
offset: 1.5,
|
||||
sweep: -3,
|
||||
rate: 0,
|
||||
depth: 0,
|
||||
feedback: 0,
|
||||
mix: 1,
|
||||
});
|
||||
expect(flanger.options.processorOptions).toEqual({ startAt: library.stats().last!.at });
|
||||
expect(library.tune(voice, { f: 1600, o: 3 }, 0.5)).toBe(true);
|
||||
expect(phaser.parameters.get('frequency')!.ramps).toEqual([['exponential', 1600, 0.5]]);
|
||||
expect(flanger.parameters.get('offset')!.ramps).toEqual([['linear', 3, 0.5]]);
|
||||
library.stopVoice(voice);
|
||||
clockTo();
|
||||
expect([phaser.posted, flanger.posted]).toEqual([['stop'], ['stop']]);
|
||||
});
|
||||
|
||||
it('without the worklet a Bitcrusher lets the sound through as it is (#322)', async () => {
|
||||
loadModule = () => Promise.reject(new Error('no worklets here'));
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
Reference in new issue
Block a user