#!/bin/bash # SPDX-License-Identifier: GPL-3.0-or-later # Normalize notify sound files to a uniform RMS loudness so every notification # plays at the same perceived level. Re-encodes each file in place (ogg -> ogg). # Run once after adding or changing a sound in the notify set. # # Each file is measured with ffmpeg's volumedetect, then shifted by a constant # gain so its mean (RMS) volume lands on TARGET_DB. Peaks sit near 0 dB already, # so the spread was all in the RMS; leveling the RMS makes them match by ear. # # Usage: # normalize-notify-sounds.sh [SOUND_DIR] # # Environment: # TARGET_DB Target mean (RMS) loudness in dB. Default -31 (gentle + even). # Lower is quieter. The notify script's NOTIFY_VOLUME knob tunes # the master level at playback time without re-encoding. set -euo pipefail SOUND_DIR="${1:-$HOME/.local/share/sounds/notify}" TARGET_DB="${TARGET_DB:--31}" command -v ffmpeg >/dev/null || { echo "ffmpeg not found" >&2; exit 1; } command -v ffprobe >/dev/null || { echo "ffprobe not found" >&2; exit 1; } shopt -s nullglob files=("$SOUND_DIR"/*.ogg) (( ${#files[@]} )) || { echo "No .ogg files in $SOUND_DIR" >&2; exit 1; } # Clean up the in-flight temp file on any exit (a failed encode under set -e # aborts mid-loop, so the trap is what stops the temp from leaking). tmp="" cleanup() { [ -n "${tmp:-}" ] && rm -f "$tmp"; return 0; } trap cleanup EXIT for f in "${files[@]}"; do mean=$(ffmpeg -hide_banner -nostats -i "$f" -af volumedetect -f null /dev/null 2>&1 \ | grep -oP 'mean_volume: \K[-0-9.]+' || true) if [ -z "$mean" ]; then echo "skip (could not measure): $f" >&2 continue fi gain=$(awk -v t="$TARGET_DB" -v m="$mean" 'BEGIN { printf "%.1f", t - m }') # Resolve the real target (SOUND_DIR is often the stow-symlinked ~/.local # copy) and stage the temp beside it, so the mv is atomic on the same # filesystem and replacing the real file leaves the stow symlink pointing # at it. A truncate-in-place (cat > "$f") would have corrupted the tracked # file if the encode produced a short or empty output. target=$(readlink -f "$f") tmp=$(mktemp --suffix=.ogg --tmpdir="$(dirname "$target")") ffmpeg -hide_banner -loglevel error -y -i "$f" \ -af "volume=${gain}dB" -c:a libvorbis -q:a 6 "$tmp" if [ ! -s "$tmp" ]; then echo "skip (empty re-encode): $f" >&2 rm -f "$tmp"; tmp="" continue fi mv -f "$tmp" "$target" tmp="" printf "%-14s mean %7s dB gain %+6s dB -> target %s dB\n" \ "$(basename "$f")" "$mean" "$gain" "$TARGET_DB" done