From 28aa099e80f036c8e3292711173a88f0f67face2 Mon Sep 17 00:00:00 2001 From: atagen Date: Sun, 24 May 2026 19:20:29 +1000 Subject: [PATCH] profiles: ship an extended set of default profiles --- PLAN.md | 11 ++- .../headroom-core/tests/shipped_profiles.rs | 24 ++++++ profiles/broadcast-14.toml | 54 +++++++++++++ profiles/commute.toml | 54 +++++++++++++ profiles/gaming.toml | 75 +++++++++++++++++++ profiles/movie.toml | 65 ++++++++++++++++ profiles/music.toml | 73 ++++++++++++++++++ profiles/party.toml | 52 +++++++++++++ profiles/podcast.toml | 59 +++++++++++++++ profiles/quiet-hours.toml | 54 +++++++++++++ profiles/spike-protection.toml | 44 +++++++++++ 11 files changed, 564 insertions(+), 1 deletion(-) create mode 100644 crates/headroom-core/tests/shipped_profiles.rs create mode 100644 profiles/broadcast-14.toml create mode 100644 profiles/commute.toml create mode 100644 profiles/gaming.toml create mode 100644 profiles/movie.toml create mode 100644 profiles/music.toml create mode 100644 profiles/party.toml create mode 100644 profiles/podcast.toml create mode 100644 profiles/quiet-hours.toml create mode 100644 profiles/spike-protection.toml diff --git a/PLAN.md b/PLAN.md index b0c0cec..efe60e2 100644 --- a/PLAN.md +++ b/PLAN.md @@ -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 diff --git a/crates/headroom-core/tests/shipped_profiles.rs b/crates/headroom-core/tests/shipped_profiles.rs new file mode 100644 index 0000000..d7cf04e --- /dev/null +++ b/crates/headroom-core/tests/shipped_profiles.rs @@ -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()); +} diff --git a/profiles/broadcast-14.toml b/profiles/broadcast-14.toml new file mode 100644 index 0000000..671f858 --- /dev/null +++ b/profiles/broadcast-14.toml @@ -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" diff --git a/profiles/commute.toml b/profiles/commute.toml new file mode 100644 index 0000000..adb5fc7 --- /dev/null +++ b/profiles/commute.toml @@ -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" diff --git a/profiles/gaming.toml b/profiles/gaming.toml new file mode 100644 index 0000000..104f878 --- /dev/null +++ b/profiles/gaming.toml @@ -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 diff --git a/profiles/movie.toml b/profiles/movie.toml new file mode 100644 index 0000000..304aca5 --- /dev/null +++ b/profiles/movie.toml @@ -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" diff --git a/profiles/music.toml b/profiles/music.toml new file mode 100644 index 0000000..e046662 --- /dev/null +++ b/profiles/music.toml @@ -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" diff --git a/profiles/party.toml b/profiles/party.toml new file mode 100644 index 0000000..f383bb6 --- /dev/null +++ b/profiles/party.toml @@ -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" diff --git a/profiles/podcast.toml b/profiles/podcast.toml new file mode 100644 index 0000000..3ffb17b --- /dev/null +++ b/profiles/podcast.toml @@ -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" diff --git a/profiles/quiet-hours.toml b/profiles/quiet-hours.toml new file mode 100644 index 0000000..ca1e59f --- /dev/null +++ b/profiles/quiet-hours.toml @@ -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" diff --git a/profiles/spike-protection.toml b/profiles/spike-protection.toml new file mode 100644 index 0000000..beb9845 --- /dev/null +++ b/profiles/spike-protection.toml @@ -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"