Meter each track and the song, and limit the output at -1 dBFS (#359)
game-maker / gate (push) Successful in 3m43s
game-maker / suite (push) Failing after 25m11s

A song could clip at the output without anyone noticing: a track and a
sound item at +12 dB each add up to +24 dB. The Song tab now shows how
loud each track and the whole song are, and a fixed limiter behind the
master keeps every sample under -1 dBFS in the editor and in the game.

- Each track head has a stereo meter up its right edge. The head is
  190 px wide instead of 176.
- Out sits in the corner above the heads, with the loudest peak since
  Play in dB. In the tab header it made the row too wide for a 1280 px
  window, and the inspector lost its fields.
- The meters read AnalyserNodes beside the buses, one per side after a
  channel splitter, so a peak panned hard is not halved. They are tapped
  only while the song plays and its tab is shown.
- A clip lamp lights past 0 dBFS and stays through Stop. The next Play
  or a click on the meter puts it out.
- The meter of a track that mute or solo keeps from being heard is grey.
- Music always plays through a bus of its own, with or without a sound
  FX. That bus is what Out measures.
- The limiter is the one from #326 and needs the FX worklet. Until that
  is loaded the master goes straight out.
- It is 5 ms late. latency() counts that in, so the playhead and the
  alignment of a recorded take follow it.
- Unit tests build SoundLibrary without the limiter, as their wiring
  checks follow every node of a voice.
- Out shows "-inf" while nothing was loud. The UI font has no infinity
  sign, which the font test found.

Tests:
- Unit meter-view 8 new, sound-library 112 (2 new), 3 counter-tests
  each red
- e2e 65 (melektaus-atx): ALL PASSED, 55 checks (3 new). Out wrote
  +16.6 dB, behind the limiter the loudest sample was 0.8913
- Gate (melektaus-atx): 272 files, 4209 tests, one red in the font
  test. Fixed after it, then the font and meter tests ran green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-08 22:08:39 +02:00
1 parent dc666016db
commit 490fcba88f
12 files changed
+810 -137

No files matched your search

+160 -102
View File
@@ -156,6 +156,8 @@ const convolvers: { buffer: unknown }[] = [];
const compressors: Record<'threshold' | 'knee' | 'ratio' | 'attack' | 'release', FakeParam>[] = [];
/** The curve of every WaveShaper made, in order. */
const shapers: { curve: Float32Array | null }[] = [];
/** Every AnalyserNode made (#359), with the one sample it hands out among silence. */
const analysers: { fftSize: number; sample: number }[] = [];
let contextState = 'running';
/** Runs the audio clock up to `until`, ending each timer on the way in order, also ones set meanwhile. */
@@ -269,6 +271,18 @@ function fakeContext() {
return panner;
},
createChannelSplitter: () => wired('splitter', {}),
createAnalyser: () => {
const analyser = wired('analyser', {
fftSize: 2048,
sample: 0,
getFloatTimeDomainData: (into: Float32Array) => {
into.fill(0);
into[into.length >> 1] = analyser.sample;
},
});
analysers.push(analyser);
return analyser;
},
createConstantSource: () => {
const clock = wired('clock', {
offset: param(1),
@@ -328,6 +342,7 @@ beforeEach(() => {
disconnects.length = 0;
clocks.length = 0;
shapers.length = 0;
analysers.length = 0;
compressors.length = 0;
convolvers.length = 0;
panners.length = 0;
@@ -453,7 +468,7 @@ async function arrive(): Promise<void> {
describe('the sound library', () => {
it('fetches and decodes a file once, however often it is played', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await play(library);
await play(library);
expect(library.stats()).toMatchObject({ started: 2, decoded: 1, playing: 2 });
@@ -462,7 +477,7 @@ describe('the sound library', () => {
});
it('plays the stretch a sound is cut to, made from its file without fetching it again (#343)', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
// The file is 1200 samples at 800 Hz: 1.5 s. The cut keeps 0.5 s to 1 s.
const cut = `${FILE}#0.5~1~0.005~0.005~linear~linear`;
await play(library, cut);
@@ -478,6 +493,29 @@ describe('the sound library', () => {
expect(library.stats().playing).toBe(1);
});
it('sends the master out through a limiter at -1 dBFS once the worklet is there, late by it (#359)', async () => {
let load: () => void = () => {};
loadModule = () => new Promise((resolve) => (load = resolve));
const library = new SoundLibrary();
await play(library);
// Straight out until the worklet is there: nothing waits for it.
expect(wires).toEqual(expect.arrayContaining(['gain > destination', 'panner > gain']));
expect(library.latency()).toBe(0);
expect(library.guardPeaks()).toBeNull();
load();
await settle();
const limiter = worklets.find((w) => w.kind === 'limiter');
expect(Object.fromEntries([...limiter!.parameters].map(([name, p]) => [name, p.value]))).toEqual({
threshold: -1,
ceiling: -1,
release: 0.1,
autoGain: 0,
});
expect(wires.slice(-2)).toEqual(['gain > limiter', 'limiter > destination']);
expect(library.latency()).toBeCloseTo(0.005, 12);
expect(library.stats().playing).toBe(1);
});
it('loads ahead of the first play without opening the device, so that play starts at once', async () => {
const opened: string[] = [];
vi.stubGlobal(
@@ -498,7 +536,7 @@ describe('the sound library', () => {
}
},
);
const library = new SoundLibrary();
const library = new SoundLibrary(false);
library.preload([FILE, OTHER, '']);
await settle();
await arrive();
@@ -512,7 +550,7 @@ describe('the sound library', () => {
});
it('remembers what it last started, so a muted check can see it at all', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
library.play(FILE, 0.25, true);
await settle();
for (const resolve of decodes.splice(0)) resolve();
@@ -532,14 +570,14 @@ describe('the sound library', () => {
});
it('plays a dry voice straight into its volume, without loading the pitch worklet', async () => {
await play(new SoundLibrary());
await play(new SoundLibrary(false));
expect(wires).toEqual(['source > gain', ...INTO_MASTER]);
expect(sources[0]?.playbackRate.value).toBe(1);
expect(modules).toEqual([]);
});
it('sends a voice through its sound FX in order (#311)', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const fx: Chain = {
pitch: 12,
stages: [
@@ -556,7 +594,7 @@ describe('the sound library', () => {
});
it('plays a pitched way through the worklet at the length of the sound, its samples sent once (#317)', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await play(library);
await play(library, FILE, {}, { pitch: 7, stages: [] });
await play(library, FILE, {}, { pitch: -3.5, stages: [] });
@@ -579,7 +617,7 @@ describe('the sound library', () => {
});
it('sends a voice through a Bitcrusher on the worklet, which it loads first, and stops it with the voice (#322)', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const crush: Chain = { pitch: 0, stages: [{ kind: 'crush', bits: 4, rate: 8000, mix: 0.5 }] };
const voice = await play(library, FILE, {}, crush);
expect(modules).toHaveLength(1);
@@ -594,7 +632,7 @@ describe('the sound library', () => {
});
it('moves the numbers of a Bitcrusher while it plays, its rate in octaves (#322)', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const plan: FxPlan = {
pitch: [],
stages: [
@@ -615,7 +653,7 @@ describe('the sound library', () => {
});
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 library = new SoundLibrary(false);
const amounts = (values: Record<string, number>) =>
Object.fromEntries(Object.entries(values).map(([key, value]) => [key, { value }]));
const plan: FxPlan = {
@@ -665,7 +703,7 @@ describe('the sound library', () => {
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(() => {});
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await play(library, FILE, {}, { pitch: 0, stages: [{ kind: 'crush', bits: 4, rate: 8000, mix: 1 }] });
expect(worklets).toHaveLength(0);
expect(wires).toEqual(['source > gain', 'gain > gain', ...INTO_MASTER]);
@@ -675,7 +713,7 @@ describe('the sound library', () => {
it('a play that needs the worklet waits for it to load, as it would for its bytes', async () => {
let loaded = () => {};
loadModule = () => new Promise((resolve) => (loaded = resolve));
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await play(library);
await play(library, FILE, {}, { pitch: 7, stages: [] });
expect(library.stats()).toMatchObject({ started: 1 });
@@ -688,7 +726,7 @@ describe('the sound library', () => {
it('without the worklet a pitched way plays at its own pitch rather than not at all', async () => {
loadModule = () => Promise.reject(new Error('no worklets here'));
vi.spyOn(console, 'error').mockImplementation(() => {});
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const voice = await play(library, FILE, {}, { plan: open(), params: {} });
expect(worklets).toHaveLength(0);
expect(sources).toHaveLength(2);
@@ -698,7 +736,7 @@ describe('the sound library', () => {
});
it('mixes two sounds by their shares, a source per way, started and stopped together (#316)', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await play(
library,
FILE,
@@ -737,7 +775,7 @@ describe('the sound library', () => {
});
it('a pitched voice that ends by itself leaves the count when the worklet says so', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await play(library, FILE, {}, { pitch: 3, stages: [] });
worklets[0]!.port.onmessage!({ data: 'something else' });
expect(library.stats().playing).toBe(1);
@@ -746,7 +784,7 @@ describe('the sound library', () => {
});
it('gives two ways at the same pitch a source each, so each can glide on its own (#315)', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await play(
library,
FILE,
@@ -777,7 +815,7 @@ describe('the sound library', () => {
});
it('shapes both shares from one source carrying the mix, so a glide follows the curve', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const voice = await play(library, FILE, {}, both());
// The voice, the Mix, share of a, share of b: the shares hold nothing of their own.
const [, , shareA, shareB] = gains;
@@ -802,7 +840,7 @@ describe('the sound library', () => {
});
it('stops that source with the voice, cut or ended by itself', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await play(library, FILE, {}, both());
library.stop(FILE);
expect(clocks[0]!.stopped).toBe(false);
@@ -819,7 +857,7 @@ describe('the sound library', () => {
describe('a parameter changed while the voice plays (#315)', () => {
it('glides every knob it moves, a pitch in semitones, and leaves the rest alone', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const voice = await play(library, FILE, { owner: 'me' }, { plan: open(), params: {} });
// Made in the order: the voice, the Mix, share of a, the gain on b, share of b.
const [, , shareA, loud, shareB] = gains;
@@ -844,7 +882,7 @@ describe('the sound library', () => {
});
it('reaches the one voice it is given – a detached one too – and none that has ended', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const voices = [
await play(library, FILE, { owner: 'me' }, { plan: open(), params: {} }),
await play(library, FILE, { owner: 'me' }, { plan: open(), params: {} }),
@@ -864,7 +902,7 @@ describe('the sound library', () => {
});
it('starts a voice still waiting on its bytes with the values handed in meanwhile', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const voice = await play(
library,
FILE,
@@ -880,7 +918,7 @@ describe('the sound library', () => {
});
it('runs a timed change evenly on the audio clock: semitones and decibels in their own steps', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const voice = await play(library, FILE, { owner: 'me' }, { plan: open(), params: {} });
const [, , shareA, loud, shareB] = gains;
library.tune(voice, { depth: 12, loud: -6, blend: 0.5 }, 1.5);
@@ -899,7 +937,7 @@ describe('the sound library', () => {
});
it('runs a timed change along an easing as a curve, each number in its own steps', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const voice = await play(library, FILE, { owner: 'me' }, { plan: open(), params: {} });
const loud = gains[3]!;
const from = loud.value;
@@ -914,7 +952,7 @@ describe('the sound library', () => {
});
it('a step holds the value for the time and then jumps', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const voice = await play(library, FILE, { owner: 'me' }, { plan: open(), params: {} });
library.tune(voice, { depth: 12 }, 2, 'step');
expect(worklets[0]!.pitch.ramps).toEqual([['set', 12, 2]]);
@@ -922,7 +960,7 @@ describe('the sound library', () => {
});
it('starts a timed change to a voice still waiting on its bytes when the voice starts', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const voice = await play(library, FILE, { hold: true, owner: 'me' }, { plan: open(), params: {} });
library.tune(voice, { depth: -64 }, 1.28);
// A quick change after a timed one keeps its place behind it.
@@ -937,7 +975,7 @@ describe('the sound library', () => {
});
it('a stop while the bytes are in flight keeps the sound from starting afterwards', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await play(library, FILE, { hold: true });
library.stop(FILE);
for (const resolve of decodes.splice(0)) resolve();
@@ -946,7 +984,7 @@ describe('the sound library', () => {
});
it('but a stop before a play does not: an earlier silence is not a standing veto', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
// Exactly what ending a run does – and the next run has to be able to play that sound again,
// even though its bytes are still unread at that point.
library.stop('');
@@ -956,7 +994,7 @@ describe('the sound library', () => {
});
it('stopping everything silences every voice, and only the named file otherwise', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await play(library, FILE);
await play(library, OTHER);
library.stop(FILE);
@@ -969,14 +1007,14 @@ describe('the sound library', () => {
});
it('a voice that ends by itself leaves the count, without being stopped', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await play(library);
sources[0]?.onended?.();
expect(library.stats()).toMatchObject({ playing: 0, started: 1, stopped: 0 });
});
it('a file that cannot be loaded fails once and is never asked for again', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
vi.stubGlobal('fetch', () => Promise.resolve({ ok: false, status: 404 }));
vi.spyOn(console, 'error').mockImplementation(() => {});
expect(await library.load(FILE)).toBeNull();
@@ -999,7 +1037,7 @@ describe('the sound library', () => {
});
it('fades a stopped voice out instead of cutting it, then lets its nodes go', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await play(library);
library.stop(FILE);
const level = gains[0]!;
@@ -1015,7 +1053,7 @@ describe('the sound library', () => {
});
it('cuts at once where the clock stands still, so a paused run cannot sound in the next', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await play(library);
contextState = 'suspended';
library.stopAll();
@@ -1024,7 +1062,7 @@ describe('the sound library', () => {
});
it('wires an echo: the sound goes on, the line feeds back through the damping, each at its share of wet', async () => {
await play(new SoundLibrary(), FILE, {}, { pitch: 0, stages: [echo(0.4)] });
await play(new SoundLibrary(false), FILE, {}, { pitch: 0, stages: [echo(0.4)] });
expect(delays).toMatchObject([{ max: 2, delayTime: { value: 0.25 } }]);
expect(filters.map((f) => [f.type, f.frequency.value])).toEqual([['lowpass', 20000]]);
expect(filters[0]!.Q.value).toBeCloseTo(-3.0103, 3);
@@ -1048,7 +1086,7 @@ describe('the sound library', () => {
});
it('wires a ping-pong echo into the left only, crossing sides on every round', async () => {
await play(new SoundLibrary(), FILE, {}, { pitch: 0, stages: [echo(0.4, true)] });
await play(new SoundLibrary(false), FILE, {}, { pitch: 0, stages: [echo(0.4, true)] });
expect(wires).toEqual(
expect.arrayContaining(['gain > merger', 'merger > delay', 'filter > splitter', 'splitter > merger']),
);
@@ -1057,7 +1095,7 @@ describe('the sound library', () => {
});
it('rings on after its sound has ended, still reached, and goes when the echoes have faded', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const voice = await play(library, FILE, {}, { pitch: 0, stages: [echo()] });
expect(library.tail(voice)).toBe(0.25);
sources[0]!.onended!();
@@ -1074,7 +1112,7 @@ describe('the sound library', () => {
});
it('a dry voice ends with its sound, as before', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const voice = await play(library);
expect(library.tail(voice)).toBe(0);
sources[0]!.onended!();
@@ -1083,7 +1121,7 @@ describe('the sound library', () => {
});
it('counts a tail again when a change makes it longer', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const plan: FxPlan = {
pitch: [],
stages: [
@@ -1109,7 +1147,7 @@ describe('the sound library', () => {
});
it('counts an echo synced to beats by the tempo of the voice, never past the line (#332)', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const synced = (quarters: number): FxPlan => ({
pitch: [],
stages: [
@@ -1133,7 +1171,7 @@ describe('the sound library', () => {
});
it('lets the echoes ring when asked to: only the sound fades, the voice stays until they are gone', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await play(library, FILE, {}, { pitch: 0, stages: [echo()] });
library.stop(FILE, true);
const [level, fade] = gains;
@@ -1150,7 +1188,7 @@ describe('the sound library', () => {
});
it('a stop without letting ring cuts a ringing voice short, and asking again changes nothing', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await play(library, FILE, {}, { pitch: 0, stages: [echo()] });
library.stop(FILE, true);
library.stop(FILE, true);
@@ -1161,7 +1199,7 @@ describe('the sound library', () => {
});
it('a voice without echoes is simply faded out when asked to let ring', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await play(library);
library.stop(FILE, true);
expect(library.stats()).toMatchObject({ playing: 0, stopped: 1 });
@@ -1169,7 +1207,7 @@ describe('the sound library', () => {
});
it('stops one voice by its number, also one still waiting on its bytes', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const first = await play(library);
const second = await play(library);
library.stopVoice(first);
@@ -1183,7 +1221,7 @@ describe('the sound library', () => {
});
it('moves the numbers of an echo while it plays, the damping evenly in octaves', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const plan: FxPlan = {
pitch: [],
stages: [
@@ -1229,7 +1267,7 @@ describe('notes of an instrument (#331)', () => {
}
it('plays its layers side by side as one voice, each way its own file behind its own volume', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const voice = await note(library, [sample(FILE), sample(OTHER, { gate: false })]);
expect(voice).toBe(1);
expect(library.stats()).toMatchObject({ started: 1, playing: 1, decoded: 2 });
@@ -1241,7 +1279,7 @@ describe('notes of an instrument (#331)', () => {
});
it('a release fades out the ways that end with the note and lets a one-shot play on', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const voice = await note(library, [sample(FILE), sample(OTHER, { gate: false })]);
library.release(voice);
expect(gains[1]!.ramps).toEqual([['linear', 0, 0.01]]);
@@ -1255,7 +1293,7 @@ describe('notes of an instrument (#331)', () => {
});
it('a note with a duration is released that long after it starts, its fade set on the audio clock at once (#332)', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await note(library, [sample(FILE)], 0.5);
expect(gains[1]!.ramps).toEqual([
['set', 0.5, 0.5],
@@ -1269,7 +1307,7 @@ describe('notes of an instrument (#331)', () => {
});
it('a note planned ahead starts on that second of the audio clock and is released from there (#332)', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
library.playNote('instrument:i', [sample(FILE)], {}, 1, '', 0.5, { at: 2 });
await settle();
await arrive();
@@ -1286,7 +1324,7 @@ describe('notes of an instrument (#331)', () => {
});
it('starts a note with a Limiter as much earlier as it is late, its other layer held back to it (#326)', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const limit: FxStagePlan = {
kind: 'limit',
threshold: { value: -6 },
@@ -1311,7 +1349,7 @@ describe('notes of an instrument (#331)', () => {
});
it('tells a pitched note planned ahead when to start, the worklet having no start of its own (#332)', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const pitched = { ...sample(FILE), pitch: [{ value: 7 }] } as FxPlan;
library.playNote('instrument:i', [pitched], {}, 1, '', 0, { at: 1.25 });
await settle();
@@ -1320,7 +1358,7 @@ describe('notes of an instrument (#331)', () => {
});
it('a note planned to end later leaves its loop then, not now (#332)', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await note(library, [sample(FILE, { loop: true, gate: false })], 1);
expect(sources[0]!.loop).toBe(true);
clockTo(0.99);
@@ -1330,7 +1368,7 @@ describe('notes of an instrument (#331)', () => {
});
it('a note released before its bytes arrived still starts, and is released at once', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const voice = library.playNote(
'instrument:i',
[sample(FILE), sample(OTHER, { gate: false })],
@@ -1348,13 +1386,13 @@ describe('notes of an instrument (#331)', () => {
});
it('loops a way between its loop start and end', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await note(library, [sample(FILE, { loop: true, loopStart: 0.25, loopEnd: 1 })]);
expect(sources[0]).toMatchObject({ loop: true, loopStart: 0.25, loopEnd: 1 });
});
it('hands a pitched way its loop in samples of the sound', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const pitched = { ...sample(FILE, { loop: true, loopStart: 0.25, loopEnd: 0 }), pitch: [{ value: 3 }] };
await note(library, [pitched]);
// 800 samples a second, 1200 of them: from 200 to the end.
@@ -1366,7 +1404,7 @@ describe('notes of an instrument (#331)', () => {
});
it('a release lets a loop that does not end with the note leave its loop and play out', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const voice = await note(library, [
sample(FILE, { loop: true, loopEnd: 1, gate: false }),
sample(OTHER, { loop: true, loopEnd: 1 }),
@@ -1384,7 +1422,7 @@ describe('notes of an instrument (#331)', () => {
});
it('tells the pitch worklet to leave its loop on a release', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const pitched = { ...sample(FILE, { loop: true, loopEnd: 1, gate: false }), pitch: [{ value: 3 }] };
const voice = await note(library, [pitched]);
library.release(voice);
@@ -1392,7 +1430,7 @@ describe('notes of an instrument (#331)', () => {
});
it('plays nothing where no way has a file', () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
expect(library.playNote('instrument:i', [], {}, 1, '', 0)).toBe(0);
expect(library.playNote('instrument:i', [{ pitch: [], stages: [] }], {}, 1, '', 0)).toBe(0);
});
@@ -1425,7 +1463,7 @@ describe('notes with an Envelope (#335)', () => {
const driver = () => clocks[0]!;
it('starts its curve with the note: straight up in the attack, then easing towards the sustain', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await note(library, way(true));
expect(library.stats().last?.envelopes).toBe(1);
expect(driver().started).toBe(true);
@@ -1438,7 +1476,7 @@ describe('notes with an Envelope (#335)', () => {
});
it('a gated source plays on through the release, and the note ends with it', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const voice = await note(library, way(true));
library.release(voice);
// Released at 0, in the attack still: from 0, falling to silence in 0.3 s.
@@ -1461,7 +1499,7 @@ describe('notes with an Envelope (#335)', () => {
});
it('a note ending mid-decay releases from the level its curve has by then', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await note(library, way(true), 0.2);
const level = 0.5 + 0.5 * Math.exp((-0.1 * Math.log(1000)) / 0.2);
expect(driver().offset.ramps).toEqual([
@@ -1475,7 +1513,7 @@ describe('notes with an Envelope (#335)', () => {
});
it('a one-shot under an Envelope alone plays through, its curve held', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const voice = await note(library, way(false));
library.release(voice);
expect(driver().offset.glides).toEqual([0.5]);
@@ -1486,7 +1524,7 @@ describe('notes with an Envelope (#335)', () => {
});
it('Stop sound lets it fall away in its release with "let ring", and cuts it without', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const ringing = await note(library, way(false));
library.stopVoice(ringing, true);
expect(driver().offset.glides).toEqual([0.5, 0]);
@@ -1507,7 +1545,7 @@ describe('notes with an Envelope (#335)', () => {
});
it('one that only moves a cutoff ends nothing: the gated source fades with the note as before', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const sweep: FxStagePlan = {
kind: 'pass',
filter: 'lowpass',
@@ -1535,7 +1573,7 @@ describe('notes with an Envelope (#335)', () => {
});
it('ends once its decay is over where the sustain is 0, however long the note is held (#329)', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const plan: FxPlan = {
...way(true, false),
stages: [{ kind: 'envelope', envelope: { ...PLANNED, sustain: { value: 0 } } }],
@@ -1554,7 +1592,7 @@ describe('notes with an Envelope (#335)', () => {
});
it('moves a pitch on top of the one its way has, through the worklet', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const plan: FxPlan = {
...way(true, false),
pitch: [
@@ -1587,7 +1625,7 @@ describe('a Chip (#329)', () => {
const pitched: FxPlan = { pitch: [{ value: 3 }], stages: [], chip: CHIP };
it('plays a note without any file on a worklet of its own, its frequency, duty and pitch knobs', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const voice = library.playNote('instrument:i', [pitched], {}, 1, '', 0, { at: 0.5 });
await settle();
expect(modules).toHaveLength(1);
@@ -1602,7 +1640,7 @@ describe('a Chip (#329)', () => {
});
it('ends with its note, faded out, or by itself once it has slid below its lowest frequency', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const released = library.playNote('instrument:i', [pitched], {}, 1, '', 0);
await settle();
library.release(released);
@@ -1616,7 +1654,7 @@ describe('a Chip (#329)', () => {
it('is silent without the worklet, and ends at once', async () => {
loadModule = () => Promise.reject(new Error('no worklets here'));
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const voice = library.playNote('instrument:i', [pitched], {}, 1, '', 0);
await settle();
expect(worklets).toHaveLength(0);
@@ -1628,7 +1666,7 @@ describe('a Chip (#329)', () => {
describe('clips and tracks of a song (#334)', () => {
it('starts a clip at its second, as far into its sound as it says, round and round', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
// The sound is 1.5 s long: 2 s in is half a second into its second round.
library.playClip(FILE, { at: 2, offset: 2, duration: 3, volume: 0.5, bus: '' });
await settle();
@@ -1638,7 +1676,7 @@ describe('clips and tracks of a song (#334)', () => {
});
it('plays a clip turned up past unity as loud as +12 dB, and no louder', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
library.playClip(FILE, { at: 2, offset: 0, duration: 3, volume: 2, bus: '' });
await settle();
await arrive();
@@ -1650,7 +1688,7 @@ describe('clips and tracks of a song (#334)', () => {
});
it('fades a clip out at its end and lets it go, a source only once', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
library.playClip(FILE, { at: 2, offset: 0, duration: 3, volume: 1, bus: '' });
await settle();
await arrive();
@@ -1667,7 +1705,7 @@ describe('clips and tracks of a song (#334)', () => {
});
it('starts a pitched clip in its worklet as many samples in as the offset is', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const pitched = { pitch: [{ value: 5 }], stages: [] } as FxPlan;
library.playClip(
FILE,
@@ -1681,7 +1719,7 @@ describe('clips and tracks of a song (#334)', () => {
});
it("sends a song's voices through the bus of their track: its input, level, then its pan, then out", async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
library.setBus('song:s:t', 0.5, -0.25);
library.playNote(
'instrument:i',
@@ -1717,7 +1755,7 @@ describe('clips and tracks of a song (#334)', () => {
});
it('says how loud each stretch of a loaded sound is, and nothing before its bytes are there', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
expect(library.peaks(FILE)).toBeNull();
void library.load(FILE);
await settle();
@@ -1728,7 +1766,7 @@ describe('clips and tracks of a song (#334)', () => {
});
it('plays and draws a reversed sound from its end (#321)', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const reversed = `${FILE}#0~0~0~0~linear~linear~reverse`;
void library.load(FILE);
await settle();
@@ -1775,7 +1813,7 @@ describe('the patterns and songs a game plays (#336)', () => {
}
it('hears each playing through a master of its own, silent until it starts, its tracks into it', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const struck: ScheduledNote[] = [];
const id = library.playSequence(
'song:s',
@@ -1821,7 +1859,7 @@ describe('the patterns and songs a game plays (#336)', () => {
});
it('is gone once played to its end, its last voice gone too – asking after it does not ask itself', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
library.playSequence(
'pattern:p',
sequence(() => 77, { length: 10, buses: [] }),
@@ -1835,14 +1873,14 @@ describe('the patterns and songs a game plays (#336)', () => {
});
it('a playing without notes or clips is none', () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const empty = sequence(() => 1, { sequence: { notes: [], clips: [] } });
expect(library.playSequence('song:s', empty, '')).toBe(0);
expect(library.busLevels()).toEqual([]);
});
it('nothing that reads the clock wakes it while the toolbar holds it', () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
library.time();
let resumes = 0;
(library as unknown as { context: { resume: () => Promise<void> } }).context.resume = () => {
@@ -1907,7 +1945,7 @@ describe('a sound FX on a bus (#346)', () => {
}
it('sends all of a playing through one chain on its master, however many notes it plays', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const { id } = playing(library, { fx: voice(crush) });
await settle();
await arrive();
@@ -1921,7 +1959,7 @@ describe('a sound FX on a bus (#346)', () => {
});
it('shifts the pitch of a bus a frame late, and starts its notes as much earlier', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const { id, struck } = playing(library, { fx: voice(up) });
await settle();
await arrive();
@@ -1941,7 +1979,7 @@ describe('a sound FX on a bus (#346)', () => {
});
it('holds the other way of a Mix back as far as its Pitch makes one late, so the two stay together', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const doubled: FxPlan = { mix: { value: 0.5 }, a: up, b: { pitch: [], stages: [] }, stages: [] };
const { id } = playing(library, { fx: voice(doubled) });
await settle();
@@ -1950,7 +1988,7 @@ describe('a sound FX on a bus (#346)', () => {
});
it('starts each note as much earlier as the buses it is heard through are late, and no more', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const { id, struck } = playing(library, {
fx: voice(up),
buses: [
@@ -1979,7 +2017,7 @@ describe('a sound FX on a bus (#346)', () => {
});
it('hands values to the sound FX of a playing by its number, and nothing else by it', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const loud: FxPlan = {
pitch: [],
stages: [{ kind: 'gain', db: { value: 0, param: 'loud', min: -120, max: 24 } }],
@@ -1994,7 +2032,7 @@ describe('a sound FX on a bus (#346)', () => {
});
it('lets the chains of a playing go after its last voice, once what they hold has been heard', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
playing(library, { fx: voice(up) });
await settle();
await arrive();
@@ -2007,8 +2045,28 @@ describe('a sound FX on a bus (#346)', () => {
expect(library.busLevels()).toEqual([MASTER_LEVEL]);
});
it('meters a track after its pan, each side apart, and keeps the tap when it goes elsewhere (#359)', async () => {
const library = new SoundLibrary(false);
await library.setBuses([{ name: 'song:s:t', gain: 1, pan: 0 }]);
wires.length = 0;
expect(library.busPeaks(['song:s:t', 'song:s:gone'])).toEqual(new Map([['song:s:t', [0, 0]]]));
expect(wires).toEqual(['splitter > analyser', 'splitter > analyser', 'panner > splitter']);
analysers[0]!.sample = -1.5;
analysers[1]!.sample = 0.25;
expect(library.busPeaks(['song:s:t']).get('song:s:t')).toEqual([1.5, 0.25]);
expect(analysers).toHaveLength(2);
wires.length = 0;
await library.setBuses([{ name: 'song:s:t', gain: 1, pan: 0, into: 'song:s:u' }]);
expect(wires).toEqual(expect.arrayContaining(['panner > splitter']));
expect(library.metered()).toBe(1);
disconnects.length = 0;
library.unmeter();
expect(library.metered()).toBe(0);
expect(disconnects).toEqual(['panner', 'splitter']);
});
it('makes the chain of a track in the Song tab once, and again only for another sound FX', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const track = { name: 'song:s:t', gain: 1, pan: 0, fx: voice(crush) };
await library.setBuses([track]);
// As the Song tab hands them: a new voice of the same FX every time.
@@ -2028,7 +2086,7 @@ describe('a sound FX on a bus (#346)', () => {
});
it('lets the buses of the Song tab go after a stop, unless the next play takes them up first', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await library.setBuses([{ name: 'song:s:t', gain: 1, pan: 0, fx: voice(up) }]);
library.releaseBuses(['song:s:t']);
await library.setBuses([{ name: 'song:s:t', gain: 1, pan: 0, fx: voice(up) }]);
@@ -2051,7 +2109,7 @@ describe('the sends of the tracks of a song (#350)', () => {
library.busLevels().find((bus) => bus.name === name)?.sends;
it('taps each send where its mode says: after the pan, after the sound FX, or where it comes in', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await library.setBuses([
out,
{
@@ -2079,7 +2137,7 @@ describe('the sends of the tracks of a song (#350)', () => {
});
it('holds back the ways that pass a Pitch by, so all of a track is heard as late as its latest way', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await library.setBuses([
out,
{
@@ -2123,7 +2181,7 @@ describe('the sends of the tracks of a song (#350)', () => {
});
it('glides the level of a send that only changed its level, and makes the sends anew that go elsewhere', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const track = { name: 'song:s:t', gain: 1, pan: 0 };
await library.setBuses([out, { ...track, sends: [{ to: 'song:s:o', gain: 1, tap: 'post' as const }] }]);
const made = delays.length;
@@ -2146,7 +2204,7 @@ describe('the sends of the tracks of a song (#350)', () => {
});
it('sends the tracks of a game’s playing into one another and into its master', () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const id = library.playSequence(
'song:s',
{
@@ -2191,7 +2249,7 @@ describe('the master and the buses of the project (#325)', () => {
library.busLevels().map(({ name, gain, into }) => ({ name, gain, into }));
it('makes nothing before there is a context, then the master and every bus into the one it names', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await library.setMix(mix);
expect(wires).toEqual([]);
await play(library);
@@ -2204,7 +2262,7 @@ describe('the master and the buses of the project (#325)', () => {
});
it('plays a voice on the bus it is given, and a playing too', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await library.setMix(mix);
library.play(FILE, 1, false, '', DRY_VOICE, 'mix:b');
await settle();
@@ -2227,7 +2285,7 @@ describe('the master and the buses of the project (#325)', () => {
});
it('sends a bus on into another one when the mix says so, and lets one go whose asset is gone', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await library.setMix(mix);
await play(library);
await library.setMix([mix[0]!, mix[1]!, { ...mix[2]!, into: 'mix' }]);
@@ -2238,7 +2296,7 @@ describe('the master and the buses of the project (#325)', () => {
});
it('holds the volume a game sets over that of the asset until it is reset, gliding there over its time', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await library.setMix(mix);
await play(library);
library.setMixVolume('mix:b', 0.8, 1.5);
@@ -2254,7 +2312,7 @@ describe('the master and the buses of the project (#325)', () => {
});
it('changes the sound FX of a bus of the mix, and says so only for one that is there', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const plan: FxPlan = {
pitch: [],
stages: [{ kind: 'crush', bits: { value: 8, param: 'b' }, rate: { value: 8000 }, mix: { value: 1 } }],
@@ -2350,7 +2408,7 @@ describe('the stages of #318 to #328', () => {
});
it('holds back the other way of a Mix as far as a Compressor makes one late, on a voice and a bus (#326)', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const compress: FxStagePlan = {
kind: 'compress',
threshold: { value: -24 },
@@ -2407,7 +2465,7 @@ describe('the stages of #318 to #328', () => {
});
it('starts the LFO of a Vibrato with its voice and delays around its center, the voice that much earlier (#320)', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const vibrato: FxStagePlan = {
kind: 'vibrato',
shape: 'square',
@@ -2468,7 +2526,7 @@ describe('the stages of #318 to #328', () => {
});
it('loads the sound a Reverb takes as its impulse response with the voice, and rings as long (#328)', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const plan: FxPlan = {
pitch: [],
stages: [
@@ -2539,7 +2597,7 @@ describe('Wet (#354)', () => {
});
it('a wet from a parameter glides both shares while the voice plays', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const plan: FxPlan = {
pitch: [],
stages: [
@@ -2563,7 +2621,7 @@ describe('Wet (#354)', () => {
describe('what has ended is let go', () => {
it('closes the port of a pitched way once it ends or is stopped, so its voice can be collected', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await play(library, FILE, {}, { pitch: 3, stages: [] });
await play(library, FILE, {}, { pitch: 5, stages: [] });
const [ended, stopped] = worklets;
@@ -2577,7 +2635,7 @@ describe('what has ended is let go', () => {
});
it('takes a voice without echoes off its bus as soon as its sound has ended', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
await play(library);
const before = disconnects.length;
sources[0]!.onended!();
@@ -2586,7 +2644,7 @@ describe('what has ended is let go', () => {
});
it('cuts the oldest pitched voices past twelve, one already let go before one still held', async () => {
const library = new SoundLibrary();
const library = new SoundLibrary(false);
const oneShot: FxPlan = {
pitch: [{ value: 3 }],
stages: [],