import { clamp } from '../../ui'; /** The scale of a level meter (#359) in dBFS: what is quieter shows nothing, past 0 it clips. */ const METER_MIN_DB = -48; const METER_MAX_DB = 6; /** From here on a meter is yellow; past 0 dBFS it is red. */ export const METER_WARM_DB = -6; /** Seconds the loudest peak stays put before it falls. */ export const PEAK_HOLD = 1.5; /** dB a second a level and a peak fall. */ export const METER_FALL = 24; /** One side of a meter: what it shows now, the loudest peak and when it came, and whether it clipped. */ export interface MeterSide { level: number; peak: number; peakAt: number; clipped: boolean; } export const QUIET_SIDE: MeterSide = { level: -Infinity, peak: -Infinity, peakAt: 0, clipped: false }; export function gainToDb(gain: number): number { return gain > 0 ? 20 * Math.log10(gain) : -Infinity; } /** How far up the meter a level is, 0 to 1. */ export function meterShare(db: number): number { return clamp((db - METER_MIN_DB) / (METER_MAX_DB - METER_MIN_DB), 0, 1); } export type MeterZone = 'ok' | 'warm' | 'hot'; export function meterZone(db: number): MeterZone { return db > 0 ? 'hot' : db >= METER_WARM_DB ? 'warm' : 'ok'; } /** * A side `dt` seconds on, at `now`, with `gain` the loudest sample since: the level jumps up to it * and falls back slowly, the peak holds `PEAK_HOLD` seconds and then falls the same way. A sample * past full scale lights the clip lamp, and it stays lit until it is cleared. */ export function stepSide(side: MeterSide, gain: number, now: number, dt: number): MeterSide { const db = gainToDb(gain); const fall = (from: number) => { const to = from - METER_FALL * Math.max(0, dt); return to < METER_MIN_DB ? -Infinity : to; }; const level = Math.max(db, fall(side.level)); const fresh = db >= side.peak; const held = now - side.peakAt < PEAK_HOLD; return { level, peak: fresh ? db : held ? side.peak : Math.max(db, fall(side.peak)), peakAt: fresh ? now : side.peakAt, clipped: side.clipped || gain > 1, }; } /** Both sides of a meter, left and right. */ export type StereoMeter = readonly [MeterSide, MeterSide]; export const QUIET_METER: StereoMeter = [QUIET_SIDE, QUIET_SIDE]; export function stepMeter( meter: StereoMeter, gains: readonly [number, number], now: number, dt: number, ): StereoMeter { return [stepSide(meter[0], gains[0], now, dt), stepSide(meter[1], gains[1], now, dt)]; } /** What it shows goes; only the clip lamp stays as it was. */ export function hushMeter(meter: StereoMeter): StereoMeter { return [ { ...QUIET_SIDE, clipped: meter[0].clipped }, { ...QUIET_SIDE, clipped: meter[1].clipped }, ]; } export function meterClipped(meter: StereoMeter): boolean { return meter[0].clipped || meter[1].clipped; } /** "-3.2", "+1.5", "-inf": a peak as the Out meter writes it, in letters the UI font has. */ export function peakText(db: number): string { if (!Number.isFinite(db)) return '-inf'; const shown = Math.round(db * 10) / 10; return `${shown > 0 ? '+' : ''}${shown.toFixed(1)}`; }