blob: ab48bdbfbd41f4856f26a0d56d0ffea9d5def276 (
plain) (
blame)
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
|
#!/usr/bin/env bash
set -euo pipefail
OUTPUT="voice"
FORMAT="mp3"
BITRATE="128k"
SOURCE=""
GAIN="6dB"
NORMALIZE=1
PROBE=0
usage() {
cat <<EOF
Usage: $0 -o name [-f mp3|wav] [-b bitrate] [-s source] [-g gain] [-n] [-p]
Records the currently active audio output (whatever is playing through the
default sink). Auto-detects the correct monitor source.
Options:
-o NAME Output basename (without extension)
-f FORMAT mp3 (default) or wav
-b BITRATE MP3 bitrate (default 128k)
-s SOURCE Override monitor source (default: auto-detect from default sink)
-g GAIN Pre-normalization gain (default 8dB; set 0dB to disable)
-n No normalization (raw capture)
-p Probe mode: 5s test recording, print levels, exit
-h Help
Notes:
Pre-gain default (8dB) is tuned for an attenuated chain (e.g. Bluetooth sink
at ~0.35 + app-level stream attenuation). Run -p after any sink or headphone
change; chain attenuation is device-specific.
Examples:
$0 -o lecture
$0 -o music -f wav -g 6dB
$0 -p # check levels before committing to a long recording
EOF
exit 0
}
while getopts "o:f:b:s:g:nph" opt; do
case "$opt" in
o) OUTPUT="$OPTARG" ;;
f) FORMAT="$OPTARG" ;;
b) BITRATE="$OPTARG" ;;
s) SOURCE="$OPTARG" ;;
g) GAIN="$OPTARG" ;;
n) NORMALIZE=0 ;;
p) PROBE=1 ;;
h) usage ;;
*) usage ;;
esac
done
# Auto-detect monitor of the default sink (what the user is actually hearing)
if [ -z "$SOURCE" ]; then
DEFAULT_SINK=$(pactl get-default-sink)
SOURCE="${DEFAULT_SINK}.monitor"
fi
# Verify the source exists before invoking ffmpeg
if ! pactl list sources short | awk '{print $2}' | grep -Fxq "$SOURCE"; then
echo "Error: monitor source not found: $SOURCE" >&2
echo "Available sources:" >&2
pactl list sources short | awk '{print " " $2}' >&2
exit 1
fi
# Build audio filter chain
FILTER=""
if [ "$NORMALIZE" -eq 1 ]; then
FILTER="volume=${GAIN},loudnorm=I=-16:TP=-1.5:LRA=11"
elif [ "$GAIN" != "0dB" ]; then
FILTER="volume=${GAIN}"
fi
FORMAT=$(echo "$FORMAT" | tr '[:upper:]' '[:lower:]')
# Probe mode: short capture, report levels, exit
if [ "$PROBE" -eq 1 ]; then
echo "Probing $SOURCE for 5 seconds..."
PROBE_OUT=$(ffmpeg -hide_banner -nostats -loglevel info \
-f pulse -i "$SOURCE" -t 5 -af "volumedetect" -f null - 2>&1) || true
LEVELS=$(echo "$PROBE_OUT" | grep -E "mean_volume|max_volume" || true)
if [ -z "$LEVELS" ]; then
echo "No level data — source emitted silence or ffmpeg failed."
echo
echo "ffmpeg output (last 20 lines):"
echo "$PROBE_OUT" | tail -20
else
echo "$LEVELS"
echo
echo "Current gain: -g ${GAIN}"
echo
echo "Reference:"
echo " mean_volume near -16 dB good for voice"
echo " mean_volume below -25 dB increase -g or raise sink volume"
echo " max_volume above -1 dB reduce -g (risk of clipping)"
fi
exit 0
fi
# Assemble ffmpeg invocation
FILE="${OUTPUT}.${FORMAT}"
ARGS=(-f pulse -i "$SOURCE" -ac 2)
[ -n "$FILTER" ] && ARGS+=(-af "$FILTER")
case "$FORMAT" in
mp3) ARGS+=(-c:a libmp3lame -b:a "$BITRATE") ;;
wav) : ;;
*)
echo "Error: unsupported format '$FORMAT' (use mp3 or wav)" >&2
exit 1
;;
esac
echo "Recording: $SOURCE"
echo "Filter: ${FILTER:-(none)}"
echo "Output: $FILE"
echo "Press Ctrl+C to stop."
echo
ffmpeg "${ARGS[@]}" "$FILE"
|