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>
54 lines
2.2 KiB
TypeScript
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);
|
|
});
|
|
});
|