@audio/spectral-target
v1.1.0
Published
Target-curve library — per-content-type LTAS targets (speech/music/pink/voice-music) + deviation math for adaptive/match EQ
Readme
@audio/spectral-target
Target-curve library — per-content-type LTAS targets + the deviation math that turns (measured LTAS, target) into an EQ correction curve.
Curve data + pure math, no DSP kernel of its own. Companion of @audio/spectral-ltas
(returns linear RMS magnitude per FFT bin, length half+1, bin k ↔ k·fs/frameSize) — every
curve here renders onto that exact bin grid.
import target, { deviation, smooth } from '@audio/spectral-target'
let curve = target('speech', { fs: 44100, bins: 2049 }) // Float32Array dB per bin
let custom = target([[100, 0], [1000, -10], [8000, -25]], { fs, bins })
let correction = deviation(measuredLtas, curve, { fs }) // target − measured, smoothed, clamped
let smoothed = smooth(someDbCurve, { fs, oct: 1 / 3 })| export | role |
|---|---|
| target(name \| anchors, { fs=44100, bins=2049 }) | render 'speech' | 'music' | 'pink' | 'voice-music' or a custom [[freqHz, dB], …] table to a dB-per-bin curve |
| deviation(measuredLtas, targetDb, { fs=44100, smoothOct=1/3, clamp=12, band }) | target − measured over the target's band (band, or the curve's .band) → EQ correction curve, dB per bin |
| smooth(db, { fs=44100, oct=1/3 }) | fractional-octave smoothing of any per-bin dB curve |
Sources
'speech' — Byrne, Dillon, Tran et al. 1994, "An international comparison of long-term
average speech spectra," JASA 96(4):2108–2120. Table II,
p.2116, "Combined" column (male+female average across 17 samples / 12 languages, normalized
to 70 dB SPL overall) — the paper's own "universal LTASS." 1/3-octave, 100 Hz–10 kHz (the paper
also tabulates 63/80 Hz and 12.5/16 kHz; this module uses its stated 100 Hz–10 kHz range):
| Hz | 100 | 125 | 160 | 200 | 250 | 315 | 400 | 500 | 630 | 800 | 1000 | 1250 | 1600 | 2000 | 2500 | 3150 | 4000 | 5000 | 6300 | 8000 | 10000 | |---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| | dB | 54.4 | 57.7 | 56.8 | 60.2 | 60.3 | 59.0 | 62.1 | 62.1 | 60.5 | 56.8 | 53.7 | 53.0 | 52.0 | 48.7 | 48.1 | 46.8 | 45.6 | 44.5 | 44.3 | 43.4 | 43.4 |
'music' — Pestana, Ma, Reiss, Barbosa & Black 2013, "Spectral Characteristics of Popular
Commercial Recordings 1950–2010," AES
Convention 135, Paper 8960, p.5: mean LTAS of 772 UK/US number-one hits shows "a linearly
decaying distribution of around 5 dB per octave between 100 and 4000 Hz." (Quoted phrase
verified via Schedin, "Target Spectrums For Mastering,"
Luleå University of Technology bachelor's thesis, 2021, p.7, which cites the same page — the
primary AES paper wasn't directly reachable while sourcing this module; flagging the path
honestly rather than implying a direct read.) The paper also describes a low cut "around 60 Hz"
and a slope that gets "gradually steeper" above 4 kHz, but publishes no numeric rate for either,
so neither is modeled with an invented number — see Edge policies below for what happens instead.
'pink' — analytic, −10·log10(2) ≈ −3.0103 dB/octave (equal energy per octave). Definitional,
no citation needed: the neutral tonal-balance reference and the exact test anchor.
'voice-music' — audiojs-defined convention, not literature. Bin-wise max(speech, music)
− 6 dB: leans on whichever source curve is louder at each frequency (protects the speech
formant bands from being swamped by a music-shaped target, while still tracking music's wider
low/high extension), pulled down 6 dB so the blend doesn't read hotter than either source. Pragmatic choice for voice-over-music content; revisit if a cited source surfaces.
Edge policies
- Below the lowest anchor: hold its value (flat). Applies to bin 0 / DC — never returns
-Infinity. - Above the highest anchor: continue the last segment's slope, not flat.
- Interpolation: log-frequency linear between anchors (exact for
'pink', since a log-linear function sampled and re-interpolated log-linearly reproduces itself exactly at every bin — not an approximation). - Normalization: every curve
target()returns is mean-normalized to 0 dB over its own defined band =[lowest anchor, min(highest anchor, 0.45·fs)]. For'music'that band is[100, 4000]Hz — the paper's own measured range — sodeviation()guidance built from a rendered'music'curve is naturally quietest right at that band's edges; combine with'pink'or a custom table if you need sourced full-band guidance. deviation()'s band is the target's own: a curvetarget()returns carries its defined band as.band([100, 10000]for'speech',[100, 4000]for'music'), where its anchors make it known;{ band }overrides it, and a plain array (a reference's LTAS) gets[20 Hz, 0.45·fs]. Both curves are levelled by their mean over the band in log frequency, each octave weighing alike, and their difference is held at the band's edge bins beyond it, so the smoothing draws nothing from past it. Toward each edge the correction tapers to exactly 0 over a 0.5-octave raised-cosine ramp (no ultrasonic/DC boosts, no jump in the correction curve). 1.0 levelled by the mean of the bins over[20 Hz, 0.45·fs]for every target: the octave under fs/2 holds half the bins, and a preset is extrapolated past its anchors, so a voice with the speech target's shape under 10 kHz and falling away above read 12 dB over the target through the mids and was given treble to 16 kHz (@audio/chain0.3's EQ: clean speech's PESQ 4.64 → 4.27).
Manifest
No audio.js — this atom is not a processor and not a per-signal stat, it's curve data + pure
math (same category as @audio/spectral-pvoc). A balance stat (deviation vs a named target)
was considered but doesn't drop out cleanly: computing it from raw channels would mean either
depending on @audio/spectral-ltas (no @audio/spectral-* package depends on a sibling today)
or re-implementing Welch averaging here (duplicating it). Per the atom contract, a package
without a manifest is still an atom — consumers that already hold a measured LTAS call
deviation() directly.
Consumers
Mix Analyser's spectral-balance report, Speech/Music Enhancer's adaptive EQ, Auto-Chain's
Stage-2 EQ, and Match-by-Reference (where the "target" is a reference track's own measured LTAS
via @audio/spectral-ltas, passed straight into deviation() in place of a preset). MIT.
