Files
adminandClaude Opus 5.5 d349251a01
game-maker / gate (push) Successful in 4m0s
game-maker / suite (push) Failing after 13m51s
Put a sound in a room, a hall or a cave with a Reverb node (#328)
A sound FX had no space around it. The Reverb node folds the sound
with an impulse response: one it makes itself, or a sound asset as the
answer of a real space. It works on a voice and on a bus.

- Type: Room, Hall, Cave and Plate have fixed numbers. Custom shows
  Decay (up to 10 s), Damping and Pre-delay. Sound shows the pick of a
  sound asset and Pre-delay. The rows are derived ports, the sound pick
  an idle param, so they only appear for their type.
- The made impulse response is seeded noise per side, 60 dB down at
  Decay, through a low-pass closing from 20 kHz toward 500 Hz as far
  as Damping says. It is made once per rate, decay and damping, the
  last 16 are kept.
- A sound as impulse response: the sound FX compilers now get the
  project, so a node can turn a sound asset into its file. The build
  key holds that file, so a cut or swapped sound builds again. The
  instrument compiler does the same for a Reverb inside an instrument.
- The library loads that file with the voice, a sequence or before a
  bus builds its chain. A missing file leaves the reverb silent.
- Mix blends dry and reverb and is the only number that moves while
  the sound plays, as the issue said. The voice rings for pre-delay
  plus decay, or as long as its sound.
- The open point from #325 about sends on project buses is #351, not
  part of this.

Tests:
- Unit reverb (new) 4, soundfx 57 (4 new), sound-library 104 (2 new),
  green.
- Counter-checks, each turns one test red: the impulse file not loaded
  with the voice, the tail without the length of the sound.
- e2e 34 (melektaus-atx): ALL PASSED, 81 checks (1 new). Chrome
  offline: silent during the pre-delay, 35.9 dB fall over 0.6 s of a
  1 s decay (36 expected), the dry half at 0.25 for mix 0.5. A sound
  with one sample at 0.2 s gives the impulse back 0.25 s later and
  nowhere else.
- Gate (melektaus-atx) over the end of this series #318 to #328:
  CHECK EXIT 0, 267 files, 4134 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 21:27:22 +02:00

54 lines
2.2 KiB
TypeScript

/** A Reverb (#328): the impulse response it makes for itself. */
import { describe, expect, it } from 'vitest';
import { REVERB_MAX_DECAY, REVERB_PRESETS, reverbImpulse } from '../../src/core/soundfx/reverb';
const RATE = 48000;
/** The RMS of a stretch of samples, in dB. */
function db(data: Float32Array, from: number, to: number): number {
let sum = 0;
for (let i = from; i < to; i++) sum += data[i]! ** 2;
return 10 * Math.log10(sum / (to - from));
}
/** The share of a stretch's energy that is above a few kHz: what is left after a two-tap average. */
function highShare(data: Float32Array, from: number, to: number): number {
let all = 0;
let high = 0;
for (let i = from + 1; i < to; i++) {
all += data[i]! ** 2;
high += ((data[i]! - data[i - 1]!) / 2) ** 2;
}
return high / all;
}
describe('the impulse response of a Reverb', () => {
it('lasts its decay and falls by 60 dB over it, two channels of their own noise', () => {
const [left, right] = reverbImpulse(RATE, 2, 0);
expect(left!.length).toBe(2 * RATE);
expect(left).not.toEqual(right);
// A tenth of the way at the start and at the end: 54 dB apart, 6 dB a tenth.
expect(db(left!, 0, RATE / 5) - db(left!, (9 * RATE) / 5, 2 * RATE)).toBeCloseTo(54, 0);
});
it('is the same every time, and never longer than 10 s', () => {
expect(reverbImpulse(RATE, 1, 0.5)).toEqual(reverbImpulse(RATE, 1, 0.5));
expect(reverbImpulse(RATE, 60, 0)[0]!.length).toBe(REVERB_MAX_DECAY * RATE);
});
it('loses its highs faster with damping', () => {
const [open] = reverbImpulse(RATE, 1, 0);
const [damped] = reverbImpulse(RATE, 1, 1);
const late = [0.8 * RATE, RATE] as const;
expect(highShare(damped!, ...late)).toBeLessThan(highShare(open!, ...late) / 4);
// At the start both are open.
expect(highShare(damped!, 0, RATE / 20)).toBeCloseTo(highShare(open!, 0, RATE / 20), 1);
});
it('has four spaces from a small room to a cave', () => {
expect(Object.keys(REVERB_PRESETS)).toEqual(['room', 'hall', 'cave', 'plate']);
expect(REVERB_PRESETS.room.decay).toBeLessThan(REVERB_PRESETS.hall.decay);
expect(REVERB_PRESETS.hall.decay).toBeLessThan(REVERB_PRESETS.cave.decay);
});
});