Show a Limiter node as a device that holds what it took away (#397)
A Limiter only said what it does in rows of numbers. How much it took away, and when, could not be seen at all, and a short sound is over before any needle could show it. Now the node is a device of its own size, and its screen catches the reduction from the moment a sound starts and holds it once the sound ends. - LimiterPanel draws a 720x280 plate of brushed brick. The threshold knob is large, ceiling and release are small beside it, auto-gain is a lever. A LIMIT lamp lights while it takes something away. - The screen hangs the reduction down from the top, 0 to 24 dB by the square root like the Compressor's meter. The level coming in runs along the bottom, orange above the limit. Time runs from the start of the sound over 1, 2, 4 or 8 s, and a longer sound rolls. - ReductionCapture in limiterView holds the capture after 0.4 s of silence, without the silence, until the next sound. It writes the most taken away and when, and NOW or LASTED on the right. - The Limiter is tapped now, like the Compressor. The face reads the loudest sample since the last frame on both sides. - SpectrumTaps.levels takes a lateBy. The side in front is read 5 ms earlier, so a peak going in is met by what it became. - limiterReduction leaves the auto-gain out and solves Wet's blend for the limited part. Less than 0.2 dB counts as none. - A capture is also held when the node leaves the picture. Changing a knob or the lever leaves it as it is. - Hook __gm.limiter.panel(id) for the sound FX and instrument graphs. Decided myself: - The capture stays held until the next sound, not for a fixed time. - The screen has no wet knob, as the Compressor's has none. - The Limiter's two taps cost two gains of 1 per voice. The analysers only exist while the node is in the picture. Measured on stein2: the seeded blip at -4.4 dBFS against -12 dB showed 7.56 dB. Three hits at 0, -8 and -2 dBFS showed 11.9 dB at the first. In e2e 34 on melektaus-atx, -10.44 dBFS in against -20 dB showed 9.56 dB, the difference to the hundredth. Tests: - Unit: limiter-view 13 tests (new), spectrum 6 (1 new), soundfx 67 with the Limiter's taps. - Counter-test: without Wet's share in the formula the blend test fails. - e2e 34: ALL PASSED, 138 checks (1 new). - Gate: CHECK EXIT 0, 290 files, 4445 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
80b25488ce
commit
28909f0adf
13 files changed
+1106
-21
No files matched your search
@@ -636,8 +636,13 @@ export class SoundLibrary implements AudioSink {
|
||||
return this.spectra.read(id);
|
||||
}
|
||||
|
||||
nodeLevels(id: string, seconds: number): Levels | null {
|
||||
return this.spectra.levels(id, seconds);
|
||||
nodeLevels(id: string, seconds: number, lateBy = 0): Levels | null {
|
||||
return this.spectra.levels(id, seconds, lateBy);
|
||||
}
|
||||
|
||||
/** The rate the page's audio runs at, once there is any; null before. */
|
||||
sampleRate(): number | null {
|
||||
return this.context?.sampleRate ?? null;
|
||||
}
|
||||
|
||||
/** The node listened to, the taps playing, and how many of them feed an analyser (test hook). */
|
||||
|
||||
+11
-10
@@ -106,23 +106,24 @@ export class SpectrumTaps {
|
||||
}
|
||||
|
||||
/**
|
||||
* The loudest sample on both sides of the node `id` over the last `seconds` (#378). Null before
|
||||
* anything has played through it, or while it is not listened to.
|
||||
* The loudest sample on both sides of the node `id` over the last `seconds` (#378), the side in
|
||||
* front `lateBy` seconds earlier (#397): what came in then is what goes out of a node as late as
|
||||
* that now. Null before anything has played through it, or while it is not listened to.
|
||||
*/
|
||||
levels(id: string, seconds: number): Levels | null {
|
||||
levels(id: string, seconds: number, lateBy = 0): Levels | null {
|
||||
const sides = this.listened.get(id);
|
||||
if (!sides) return null;
|
||||
const count = Math.max(
|
||||
1,
|
||||
Math.min(SPECTRUM_SIZE, Math.round(seconds * sides.in.analyser.context.sampleRate)),
|
||||
);
|
||||
const peakOf = ({ analyser, wave }: Analysis) => {
|
||||
const rate = sides.in.analyser.context.sampleRate;
|
||||
const late = Math.max(0, Math.min(SPECTRUM_SIZE - 1, Math.round(lateBy * rate)));
|
||||
const count = Math.max(1, Math.min(SPECTRUM_SIZE - late, Math.round(seconds * rate)));
|
||||
const peakOf = ({ analyser, wave }: Analysis, back: number) => {
|
||||
analyser.getFloatTimeDomainData(wave);
|
||||
let peak = 0;
|
||||
for (let i = wave.length - count; i < wave.length; i++) peak = Math.max(peak, Math.abs(wave[i]!));
|
||||
for (let i = wave.length - back - count; i < wave.length - back; i++)
|
||||
peak = Math.max(peak, Math.abs(wave[i]!));
|
||||
return peak > 0 ? 20 * Math.log10(peak) : -Infinity;
|
||||
};
|
||||
return { in: peakOf(sides.in), out: peakOf(sides.out) };
|
||||
return { in: peakOf(sides.in, late), out: peakOf(sides.out, 0) };
|
||||
}
|
||||
|
||||
/** The nodes listened to, the taps playing and how many of them feed an analyser (test hook). */
|
||||
|
||||
Reference in new issue
Block a user