profiles: ship an extended set of default profiles

This commit is contained in:
atagen 2026-05-24 19:20:29 +10:00
parent c8c221ba45
commit 28aa099e80
11 changed files with 564 additions and 1 deletions

11
PLAN.md
View file

@ -847,9 +847,18 @@ enabled = false
|---|---|
| `default` | Gentle transparent processing, sensible for daily use. |
| `night` | Aggressive: 20 LUFS, 4:1, fast release, narrow dynamic range. |
| `speech` | VoIP-focused; short attack, fast release, slight rumble cut. |
| `speech` | VoIP-focused; short attack, fast release, controlled dynamic range. |
| `transparent` | Limiter only. Compressor + AGC bypassed. Safety net only. |
| `bypass-all` | Routes everything directly to the real sink. The kill switch. |
| `spike-protection` | Minimal processing; high-threshold catch only. Untouched audio, hard guard against blasts. |
| `movie` | Wide-DR film: lifts dialogue, keeps action punchy but bounded. |
| `music` | Inter-track loudness leveling; routes music players *through* the bus. |
| `podcast` | Spoken-word playback: even narration loudness, smooth and unfatiguing. |
| `commute` | Listening in noise: heavy normalization + boost, kept loud. |
| `gaming` | Latency-first: games bypass, voice chat processed, notifications tamed per-app. |
| `party` | Loud room playback (anti-`night`): maximum loudness, dynamics sacrificed. |
| `broadcast-14` | Normalizes everything to 14 LUFS (streaming loudness) so sources match. |
| `quiet-hours` | More aggressive than `night`: very low ceiling, near-flat dynamics. |
The limiter section of `bypass-all` is irrelevant in practice (nothing
flows through `headroom-processed`), but its ceiling field is still

View file

@ -0,0 +1,24 @@
use headroom_core::profile::Profile;
use std::fs;
use std::path::Path;
#[test]
fn all_shipped_profiles_parse() {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../profiles");
let mut names = vec![];
for entry in fs::read_dir(&dir).unwrap() {
let path = entry.unwrap().path();
if path.extension().and_then(|e| e.to_str()) != Some("toml") {
continue;
}
let text = fs::read_to_string(&path).unwrap();
let p: Profile = toml::from_str(&text)
.unwrap_or_else(|e| panic!("failed to parse {}: {e}", path.display()));
let stem = path.file_stem().unwrap().to_str().unwrap();
assert_eq!(p.name, stem, "name/filename mismatch in {}", path.display());
names.push(p.name);
}
names.sort();
eprintln!("parsed profiles: {names:?}");
assert!(names.len() >= 14, "expected >=14 profiles, got {}", names.len());
}

View file

@ -0,0 +1,54 @@
name = "broadcast-14"
description = "Normalizes everything to -14 LUFS (streaming-platform loudness) so all sources match."
# -14 LUFS is the normalized target used by YouTube, Spotify, Tidal,
# Amazon, et al. Pinning everything here makes platform-normalized and
# non-normalized sources sit at the same loudness — no more reaching for
# the volume knob between an album and a loud web video. Slow, transparent.
[agc]
enabled = true
target_lufs = -14.0
attack_ms = 2500.0
release_ms = 1000.0
silence_threshold_lufs = -70.0
max_boost_db = 12.0
max_cut_db = 12.0
# Light RMS leveling — just enough glue to hold the target without
# audibly compressing program material.
[compressor]
enabled = true
detector = "rms"
threshold_db = -22.0
ratio = 2.0
knee_db = 8.0
attack_ms = 20.0
release_ms = 250.0
makeup_db = "auto"
rms_window_ms = 100.0
# Broadcast-style -1 dBTP true-peak ceiling (the EBU R128 / streaming
# delivery convention).
[limiter]
ceiling_dbtp = -1.0
lookahead_ms = 3.0
release_ms = 100.0
hold_ms = 5.0
oversample = 4
link = "stereo"
[limiter.soft]
max_psr_db = 16.0
static_ceiling_dbtp = -6.0
attack_ms = 6.0
release_ms = 200.0
[meters]
publish_hz = 20.0
[[rules]]
match = { process_binary = ["ardour", "reaper", "qpwgraph", "carla", "bitwig-studio"] }
route = "bypass"
[default_route]
route = "processed"

54
profiles/commute.toml Normal file
View file

@ -0,0 +1,54 @@
name = "commute"
description = "Listening in noise (earbuds/transit): heavy normalization and boost, kept loud."
# The inverse of `night`. Big boost headroom and a responsive loop so
# quiet passages are dragged up over background noise. Small max_cut:
# we rarely want to pull things DOWN in a noisy environment.
[agc]
enabled = true
target_lufs = -16.0
attack_ms = 800.0
release_ms = 300.0
silence_threshold_lufs = -65.0
max_boost_db = 20.0
max_cut_db = 6.0
# Firm peak compression to flatten everything to a consistent, audible
# level. Higher ratio than `music`/`movie` — intelligibility over
# fidelity.
[compressor]
enabled = true
detector = "peak"
threshold_db = -22.0
ratio = 4.0
knee_db = 4.0
attack_ms = 5.0
release_ms = 80.0
makeup_db = "auto"
# Ceiling stays hot (-0.3) — unlike night, we are NOT trying to be
# quiet, we're trying to stay above traffic.
[limiter]
ceiling_dbtp = -0.3
lookahead_ms = 2.0
release_ms = 60.0
hold_ms = 5.0
oversample = 4
link = "stereo"
# Tight PSR cap so nothing ducks below the noise floor between hits.
[limiter.soft]
max_psr_db = 8.0
static_ceiling_dbtp = -8.0
attack_ms = 3.0
release_ms = 120.0
[meters]
publish_hz = 20.0
[[rules]]
match = { process_binary = ["ardour", "reaper", "qpwgraph", "carla", "bitwig-studio"] }
route = "bypass"
[default_route]
route = "processed"

75
profiles/gaming.toml Normal file
View file

@ -0,0 +1,75 @@
name = "gaming"
description = "Latency-first: games bypass the bus, voice chat is processed, notifications tamed per-app."
# DSP settings here govern the COMMS path (voice chat), which is the
# only thing routed through the bus. Speech-like leveling so teammates
# sit at a consistent, intelligible level.
[agc]
enabled = true
target_lufs = -16.0
attack_ms = 600.0
release_ms = 200.0
silence_threshold_lufs = -65.0
max_boost_db = 20.0
max_cut_db = 6.0
[compressor]
enabled = true
detector = "peak"
threshold_db = -22.0
ratio = 3.0
knee_db = 4.0
attack_ms = 3.0
release_ms = 50.0
makeup_db = "auto"
[limiter]
ceiling_dbtp = -0.3
lookahead_ms = 2.0
release_ms = 50.0
hold_ms = 3.0
oversample = 4
link = "stereo"
[limiter.soft]
max_psr_db = 10.0
static_ceiling_dbtp = -8.0
attack_ms = 2.0
release_ms = 100.0
[meters]
publish_hz = 20.0
# Voice chat goes THROUGH the processor.
[[rules]]
match = { process_binary = ["Discord", "discord", "element-desktop", "Slack", "zoom", "WEBRTC VoiceEngine", "mumble", "TeamSpeak"] }
route = "processed"
# Everything unmatched — crucially, the game itself — routes straight
# to the real sink for zero added latency.
[default_route]
route = "bypass"
# ----------------------------------------------------------------------
# Layer A does the work the bus can't: it tames Discord notification
# blasts and evens out chat volume via channelVolumes, with ZERO
# signal-path latency — so it can ride on top of the bypass-routed
# comms/game audio without touching the latency-critical path.
# ----------------------------------------------------------------------
[per_app]
enabled = true
default_enabled = false
[[per_app.rules]]
match = { process_binary = ["Discord", "discord", "element-desktop", "Slack", "zoom", "mumble", "TeamSpeak"] }
enabled = true
peak_threshold_db = -6.0
rms_target_db = -18.0
max_cut_db = 12.0
peak_attack_ms = 5.0
peak_release_ms = 400.0
# Never level the game or music — they own their mix.
[[per_app.rules]]
match = { process_binary = ["spotify", "mpv", "vlc"] }
enabled = false

65
profiles/movie.toml Normal file
View file

@ -0,0 +1,65 @@
name = "movie"
description = "Wide-dynamic-range film: lifts quiet dialogue, keeps action punchy but bounded."
# Gentle, slow AGC toward film reference loudness. Slow envelopes so it
# rides the program level across a scene without pumping during quiet
# stretches. Asymmetric limits: it's allowed to boost soft dialogue more
# than it cuts loud action.
[agc]
enabled = true
target_lufs = -20.0
attack_ms = 2500.0
release_ms = 1000.0
silence_threshold_lufs = -70.0
max_boost_db = 12.0
max_cut_db = 8.0
# RMS detector with a soft knee — lifts the dialogue floor smoothly
# without grabbing individual transients. Low-ish ratio: this is
# leveling, not crushing.
[compressor]
enabled = true
detector = "rms"
threshold_db = -28.0
ratio = 2.0
knee_db = 8.0
attack_ms = 15.0
release_ms = 250.0
makeup_db = "auto"
rms_window_ms = 50.0
[limiter]
ceiling_dbtp = -1.0
lookahead_ms = 3.0
release_ms = 120.0
hold_ms = 5.0
oversample = 4
link = "stereo"
# Generous PSR cap: explosions and stings still punch through, just
# bounded so they don't outright shock the listener.
[limiter.soft]
max_psr_db = 18.0
static_ceiling_dbtp = -6.0
attack_ms = 8.0
release_ms = 250.0
[meters]
publish_hz = 20.0
# Video players + browser video are routed THROUGH the processor here
# (default/night send media players to bypass). DAWs stay bypassed.
[[rules]]
match = { process_binary = ["ardour", "reaper", "qpwgraph", "carla", "bitwig-studio"] }
route = "bypass"
[[rules]]
match = { process_binary = ["mpv", "vlc", "firefox", "chromium", "google-chrome"] }
route = "processed"
[[rules]]
match = { media_role = ["Movie", "Video"] }
route = "processed"
[default_route]
route = "processed"

73
profiles/music.toml Normal file
View file

@ -0,0 +1,73 @@
name = "music"
description = "Inter-track loudness leveling for music players. Even volume, dynamics mostly preserved."
# Slow AGC to a music-friendly target so tracks and albums sit at a
# consistent loudness without per-track volume fiddling. Long envelopes
# keep it musical — it leans on program loudness, not transients.
[agc]
enabled = true
target_lufs = -16.0
attack_ms = 3000.0
release_ms = 1500.0
silence_threshold_lufs = -70.0
max_boost_db = 12.0
max_cut_db = 12.0
# Very light, soft-knee RMS leveling. Ratio is barely above 1:1 — this
# is glue, not compression. Most material passes near-untouched.
[compressor]
enabled = true
detector = "rms"
threshold_db = -22.0
ratio = 1.5
knee_db = 10.0
attack_ms = 20.0
release_ms = 300.0
makeup_db = "auto"
rms_window_ms = 50.0
[limiter]
ceiling_dbtp = -0.3
lookahead_ms = 2.0
release_ms = 100.0
hold_ms = 5.0
oversample = 4
link = "stereo"
# Wide PSR cap preserves musical dynamics; the soft tier only catches
# the rare track that's wildly hotter than its neighbours.
[limiter.soft]
max_psr_db = 16.0
static_ceiling_dbtp = -5.0
attack_ms = 6.0
release_ms = 250.0
[meters]
publish_hz = 20.0
# IMPORTANT: unlike default/night, this profile routes music players
# THROUGH the processor — otherwise it would have nothing to act on.
[[rules]]
match = { process_binary = [
"spotify",
"mpv",
"vlc",
"strawberry",
"rhythmbox",
"clementine",
"audacious",
"mpd",
] }
route = "processed"
# DAWs are never leveled — they're trusted to set their own gain.
[[rules]]
match = { process_binary = ["ardour", "reaper", "qpwgraph", "carla", "bitwig-studio"] }
route = "bypass"
[[rules]]
match = { media_role = ["Music"] }
route = "processed"
[default_route]
route = "processed"

52
profiles/party.toml Normal file
View file

@ -0,0 +1,52 @@
name = "party"
description = "Loud room playback (the anti-night): maximum perceived loudness, dynamics sacrificed."
# Push everything up to a hot, room-filling target. Symmetric limits so
# it both lifts quiet intros and reins in anything that lags behind.
[agc]
enabled = true
target_lufs = -12.0
attack_ms = 1500.0
release_ms = 600.0
silence_threshold_lufs = -70.0
max_boost_db = 12.0
max_cut_db = 12.0
# Heavy RMS compression with auto-makeup — this is the "make it LOUD
# and even, fidelity be damned" stage.
[compressor]
enabled = true
detector = "rms"
threshold_db = -24.0
ratio = 6.0
knee_db = 6.0
attack_ms = 10.0
release_ms = 150.0
makeup_db = "auto"
rms_window_ms = 60.0
# Ceiling right under 0 and NO soft tier — let the brickwall slam.
# Maximum loudness is the whole point here.
[limiter]
ceiling_dbtp = -0.1
lookahead_ms = 2.0
release_ms = 60.0
hold_ms = 3.0
oversample = 4
link = "stereo"
[meters]
publish_hz = 20.0
[[rules]]
match = { process_binary = ["ardour", "reaper", "qpwgraph", "carla", "bitwig-studio"] }
route = "bypass"
# Music players go through the processor so the whole room mix is loud
# and even.
[[rules]]
match = { process_binary = ["spotify", "mpv", "vlc", "strawberry", "rhythmbox", "audacious", "mpd"] }
route = "processed"
[default_route]
route = "processed"

59
profiles/podcast.toml Normal file
View file

@ -0,0 +1,59 @@
name = "podcast"
description = "Spoken-word playback (podcasts/audiobooks): even narration loudness, smooth and unfatiguing."
# Distinct from `speech`, which is tuned for realtime duplex VoIP with
# very fast envelopes. This is one-way narration: a moderately paced
# AGC that holds a steady, consistent loudness so volume doesn't drift
# between segments, ad reads, and guests.
[agc]
enabled = true
target_lufs = -16.0
attack_ms = 1000.0
release_ms = 600.0
silence_threshold_lufs = -65.0
max_boost_db = 18.0
max_cut_db = 6.0
# RMS leveling with a slower release than `speech` — smooth, even
# narration rather than the snappy duplex feel of a call.
[compressor]
enabled = true
detector = "rms"
threshold_db = -24.0
ratio = 3.0
knee_db = 6.0
attack_ms = 15.0
release_ms = 200.0
makeup_db = "auto"
rms_window_ms = 120.0
[limiter]
ceiling_dbtp = -0.5
lookahead_ms = 2.0
release_ms = 60.0
hold_ms = 4.0
oversample = 4
link = "stereo"
# Moderate PSR cap: tames plosives and laughter spikes without
# squashing the life out of speech.
[limiter.soft]
max_psr_db = 10.0
static_ceiling_dbtp = -8.0
attack_ms = 3.0
release_ms = 150.0
[meters]
publish_hz = 20.0
[[rules]]
match = { process_binary = ["ardour", "reaper", "qpwgraph", "carla", "bitwig-studio"] }
route = "bypass"
# Podcast/audiobook playback typically rides browser or media players.
[[rules]]
match = { process_binary = ["mpv", "vlc", "firefox", "chromium", "google-chrome"] }
route = "processed"
[default_route]
route = "processed"

54
profiles/quiet-hours.toml Normal file
View file

@ -0,0 +1,54 @@
name = "quiet-hours"
description = "More aggressive than night: very low ceiling and narrow dynamics for sleeping households."
# For when someone's asleep in the next room and you still want to watch
# or listen. Pulls everything down to a low target with a fast loop so
# nothing escapes upward.
[agc]
enabled = true
target_lufs = -24.0
attack_ms = 800.0
release_ms = 350.0
silence_threshold_lufs = -70.0
max_boost_db = 20.0
max_cut_db = 6.0
# Hard, fast peak compression — clamp transients hard so a sudden sound
# effect can't carry into the next room.
[compressor]
enabled = true
detector = "peak"
threshold_db = -30.0
ratio = 6.0
knee_db = 8.0
attack_ms = 4.0
release_ms = 50.0
makeup_db = "auto"
# Drastically reduced ceiling — the defining knob of this profile. Even
# a true over lands well below a normal listening peak.
[limiter]
ceiling_dbtp = -6.0
lookahead_ms = 3.0
release_ms = 60.0
hold_ms = 5.0
oversample = 4
link = "stereo"
# Extremely tight PSR cap: ~5 dB crest. Transients are almost entirely
# flattened so nothing spikes through the quiet.
[limiter.soft]
max_psr_db = 5.0
static_ceiling_dbtp = -14.0
attack_ms = 2.0
release_ms = 120.0
[meters]
publish_hz = 20.0
[[rules]]
match = { process_binary = ["ardour", "reaper", "qpwgraph", "carla", "bitwig-studio"] }
route = "bypass"
[default_route]
route = "processed"

View file

@ -0,0 +1,44 @@
name = "spike-protection"
description = "Minimal processing until a high threshold. Untouched audio, hard guard against sudden blasts."
# No loudness leveling at all — this profile does not try to make
# anything louder or more consistent. It only catches sudden, jarring
# level jumps (autoplay ads, notification stingers, a video mastered
# 12 dB hotter than the last).
[agc]
enabled = false
# A fast, high-threshold peak "catch". Below -6 dBFS the signal passes
# 1:1 (untouched); only genuine spikes above the threshold get clamped,
# and hard (10:1). makeup_db is pinned to 0 — we never raise the floor,
# so quiet/normal material is bit-for-bit what it was.
[compressor]
enabled = true
detector = "peak"
threshold_db = -6.0
ratio = 10.0
knee_db = 2.0
attack_ms = 1.0
release_ms = 120.0
makeup_db = 0.0
# Brickwall safety net a hair below 0 dBTP. No [limiter.soft]: we do
# not reshape musical transients, only stop a true over.
[limiter]
ceiling_dbtp = -0.5
lookahead_ms = 2.0
release_ms = 80.0
hold_ms = 5.0
oversample = 4
link = "stereo"
[meters]
publish_hz = 20.0
# Production/creative tools route around the processor regardless.
[[rules]]
match = { process_binary = ["ardour", "reaper", "qpwgraph", "carla", "bitwig-studio"] }
route = "bypass"
[default_route]
route = "processed"