aboutsummaryrefslogtreecommitdiff
path: root/scripts/normalize-notify-sounds.sh
blob: 7dffbbc853571892ef66c793ea6546b8085b72b8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#!/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