Meter each track and the song, and limit the output at -1 dBFS (#359)
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:
1 parent
dc666016db
commit
490fcba88f
12 files changed
+810
-137
No files matched your search
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* The level meters of the Song tab (#359): the dB scale, how a level and its peak rise and fall, and
|
||||
* the clip lamp that stays lit.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
gainToDb,
|
||||
hushMeter,
|
||||
METER_FALL,
|
||||
meterClipped,
|
||||
meterShare,
|
||||
meterZone,
|
||||
PEAK_HOLD,
|
||||
peakText,
|
||||
QUIET_METER,
|
||||
QUIET_SIDE,
|
||||
stepMeter,
|
||||
stepSide,
|
||||
} from '../../src/editor/song/meterView';
|
||||
|
||||
describe('meter scale', () => {
|
||||
it('runs from -48 to +6 dBFS, full scale at 0 dB', () => {
|
||||
expect(gainToDb(1)).toBe(0);
|
||||
expect(gainToDb(0.5)).toBeCloseTo(-6.02, 2);
|
||||
expect(gainToDb(0)).toBe(-Infinity);
|
||||
expect(meterShare(-48)).toBe(0);
|
||||
expect(meterShare(-Infinity)).toBe(0);
|
||||
expect(meterShare(6)).toBe(1);
|
||||
expect(meterShare(0)).toBeCloseTo(48 / 54, 6);
|
||||
});
|
||||
|
||||
it('is green, yellow from -6 dB and red past 0 dB', () => {
|
||||
expect(meterZone(-6.1)).toBe('ok');
|
||||
expect(meterZone(-6)).toBe('warm');
|
||||
expect(meterZone(0)).toBe('warm');
|
||||
expect(meterZone(0.1)).toBe('hot');
|
||||
});
|
||||
|
||||
it('writes a peak to a tenth of a dB', () => {
|
||||
expect(peakText(-Infinity)).toBe('-inf');
|
||||
expect(peakText(-3.24)).toBe('-3.2');
|
||||
expect(peakText(1.46)).toBe('+1.5');
|
||||
expect(peakText(-0.01)).toBe('0.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('a side of a meter', () => {
|
||||
it('jumps up at once and falls back slowly', () => {
|
||||
const up = stepSide(QUIET_SIDE, 1, 0, 0.016);
|
||||
expect(up.level).toBe(0);
|
||||
const later = stepSide(up, 0, 0.5, 0.5);
|
||||
expect(later.level).toBeCloseTo(-METER_FALL * 0.5, 6);
|
||||
const louder = stepSide(later, 0.5, 0.516, 0.016);
|
||||
expect(louder.level).toBeCloseTo(-6.02, 2);
|
||||
});
|
||||
|
||||
it('holds its peak, then lets it fall', () => {
|
||||
const hit = stepSide(QUIET_SIDE, 1, 10, 0.016);
|
||||
const held = stepSide(hit, 0.01, 10 + PEAK_HOLD - 0.1, 0.016);
|
||||
expect(held.peak).toBe(0);
|
||||
const falling = stepSide(held, 0.01, 10 + PEAK_HOLD + 0.1, 0.2);
|
||||
expect(falling.peak).toBeCloseTo(-METER_FALL * 0.2, 6);
|
||||
const gone = stepSide(falling, 0, 20, 3);
|
||||
expect(gone.peak).toBe(-Infinity);
|
||||
expect(gone.level).toBe(-Infinity);
|
||||
});
|
||||
|
||||
it('lights the clip lamp past full scale and keeps it lit', () => {
|
||||
expect(stepSide(QUIET_SIDE, 1, 0, 0).clipped).toBe(false);
|
||||
const clipped = stepSide(QUIET_SIDE, 1.01, 0, 0);
|
||||
expect(clipped.clipped).toBe(true);
|
||||
expect(stepSide(clipped, 0, 30, 30).clipped).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps the two sides apart: a sound panned hard clips one side only', () => {
|
||||
const meter = stepMeter(QUIET_METER, [2, 0], 0, 0);
|
||||
expect(meter[0].clipped).toBe(true);
|
||||
expect(meter[1].clipped).toBe(false);
|
||||
expect(meter[0].level).toBeCloseTo(6.02, 2);
|
||||
expect(meter[1].level).toBe(-Infinity);
|
||||
expect(meterClipped(meter)).toBe(true);
|
||||
});
|
||||
|
||||
it('goes dark when nothing plays, its clip lamp still lit', () => {
|
||||
const hushed = hushMeter(stepMeter(QUIET_METER, [2, 0.5], 0, 0));
|
||||
expect(hushed.map((side) => [side.level, side.peak])).toEqual([
|
||||
[-Infinity, -Infinity],
|
||||
[-Infinity, -Infinity],
|
||||
]);
|
||||
expect(hushed.map((side) => side.clipped)).toEqual([true, false]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user