~kris/dots

srice

ref: 178c9a25a484ead715392d50e21d03df486b8b41 srice/.config/.mksh/audiofile.sh -rw-r--r-- 11.7 KiB
178c9a25 — Kris Yotam conky: sync proper config from moirai (was the greenred variant) 2 months ago
                                                                                
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# audiofile -- audiophile workflow (PipeWire, Bluetooth, MPD, conversion)
# Sources: LukeSmithxyz, rgardam, hlissner, cdown/mpdmenu, bcardoso/msearch

# ============================================================================
# aliases
# ============================================================================

# PipeWire / wpctl
alias vol='wpctl get-volume @DEFAULT_AUDIO_SINK@'
alias vol+='wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+'
alias vol-='wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-'
alias mute='wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle'
alias micmute='wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle'
alias fix-audio='systemctl --user restart pipewire.service pipewire-pulse.service wireplumber.service'
alias sinks='wpctl status | head -40'
alias sources='wpctl status | grep -A 20 "Sources:"'
alias pw-info='pw-cli info all | grep node.name | sort'
alias pw-bt='pw-cli info all | grep -i bluez'

# Bluetooth
alias bt-on='bluetoothctl power on'
alias bt-off='bluetoothctl power off'
alias bt-scan='bluetoothctl scan on'
alias bt-devices='bluetoothctl devices'
alias bt-paired='bluetoothctl devices Paired'
alias bt-connected='bluetoothctl devices Connected'
alias bt-info='bluetoothctl info'

# AirPods specific
alias airpods-sniff-off='sudo hciconfig hci0 lp NONE'
alias airpods-check='hciconfig -a hci0 | grep "Link policy"'

# MPD / ncmpcpp
alias np='mpc current'
alias toggle='mpc toggle'
alias next='mpc next'
alias prev='mpc prev'
alias stop='mpc stop'
alias mpdup='mpc update'
alias mpdrand='mpc random'
alias mpdrepeat='mpc repeat'

# Audio conversion
alias flac2mp3='for f in *.flac; do ffmpeg -i "$f" -q:a 0 "${f%.flac}.mp3"; done'
alias flac2opus='for f in *.flac; do ffmpeg -i "$f" -c:a libopus -b:a 192k "${f%.flac}.opus"; done'
alias wav2flac='for f in *.wav; do ffmpeg -i "$f" -c:a flac "${f%.wav}.flac"; done'
alias alac2flac='for f in *.m4a; do ffmpeg -i "$f" -c:a flac "${f%.m4a}.flac"; done'

# Music download
alias ytmp3='yt-dlp -x --audio-format mp3 --audio-quality 0 --embed-thumbnail --add-metadata'
alias ytflac='yt-dlp -x --audio-format flac --embed-thumbnail --add-metadata'
alias ytopus='yt-dlp -x --audio-format opus --embed-thumbnail --add-metadata'
alias ytplaylist='yt-dlp -x --audio-format mp3 --audio-quality 0 --embed-thumbnail --add-metadata -o "%(playlist_title)s/%(playlist_index)s - %(title)s.%(ext)s"'
alias spdl='spotdl download'
alias spalbum='spotdl download --output "{album}/{track-number} - {title}"'

# Audio info
alias flacinfo='for f in *.flac; do echo "$f: $(metaflac --show-bps "$f")bit/$(metaflac --show-sample-rate "$f")Hz"; done'

# Beets
alias beet-dupes='beet duplicates'
alias beet-stats='beet stats'
alias beet-missing='beet missing'

# JACK (DAW)
alias jack-ports='jack_lsp -c'
alias jack-stop='killall jackd 2>/dev/null'

# Vinyl recording
alias vinyl-rec='rec -r 96000 -b 24 -c 2 "$(date +%Y%m%d_%H%M%S)_vinyl.wav"'

# ============================================================================
# functions
# ============================================================================

# fix AirPods audio dropouts by disabling BT sniff mode
# Source: rgardam/airpods-linux-audio-fix
airpods_fix() {
    echo "[*] Disabling Bluetooth sniff mode..."
    sudo hciconfig hci0 lp NONE
    echo "[*] Restarting audio stack..."
    systemctl --user restart pipewire.service pipewire-pulse.service wireplumber.service
    echo "[+] AirPods fix applied"
    echo "[*] Verify: hciconfig -a hci0 | grep 'Link policy'"
}

# fix sniff mode for a specific device MAC
airpods_fix_device() {
    mac="$1"
    [ -z "$mac" ] && { echo "Usage: airpods_fix_device <MAC>"; return 1; }
    sudo hcitool lp "$mac" NONE
    echo "[+] Sniff mode disabled for $mac"
}

# connect AirPods (pair + trust + connect)
airpods_connect() {
    mac="$1"
    [ -z "$mac" ] && { echo "Usage: airpods_connect <MAC>"; return 1; }
    bluetoothctl power on
    sleep 1
    bluetoothctl trust "$mac"
    bluetoothctl connect "$mac"
}

# connect all paired BT devices and switch audio
bt_connect_all() {
    bluetoothctl power on
    while [ -z "$(bluetoothctl show | grep 'Powered: yes')" ]; do sleep 1; done
    bluetoothctl devices Paired | awk '{print $2}' | while read -r dev; do
        bluetoothctl connect "$dev" 2>/dev/null
    done
    btsink=$(pactl list short sinks | grep bluez | awk '{print $1}' | head -1)
    [ -n "$btsink" ] && pactl set-default-sink "$btsink"
}

# BT battery percentage
bt_battery() {
    devices=$(bluetoothctl devices Connected | awk '{print $2}')
    for dev in $devices; do
        name=$(bluetoothctl info "$dev" | grep "Name:" | sed 's/.*Name: //')
        batt=$(bluetoothctl info "$dev" | grep "Battery Percentage" | awk -F'[()]' '{print $2}')
        [ -n "$batt" ] && echo "$name: ${batt}%"
    done
}

# auto-reconnect loop for BT headphones
bt_autoreconnect() {
    mac="$1"
    [ -z "$mac" ] && { echo "Usage: bt_autoreconnect <MAC>"; return 1; }
    echo "[*] Auto-reconnecting $mac (Ctrl+C to stop)..."
    while true; do
        connected=$(bluetoothctl info "$mac" 2>/dev/null | grep "Connected: yes")
        if [ -z "$connected" ]; then
            bluetoothctl connect "$mac" >/dev/null 2>&1
        fi
        sleep 5
    done
}

# switch audio output via dmenu/rofi
audioswitch() {
    options=$(pactl -f json list sinks 2>/dev/null | jq -r '.[] | .description')
    if command -v dmenu >/dev/null; then
        selection=$(echo "$options" | dmenu -i -l 5 -c -p "Output:")
    elif command -v rofi >/dev/null; then
        selection=$(echo "$options" | rofi -dmenu -i -p "Output:")
    else
        echo "No menu available"; return 1
    fi
    [ -z "$selection" ] && return
    sink=$(pactl -f json list sinks | jq -r --arg d "$selection" \
        '.[] | select(.description == $d) | .name')
    [ -n "$sink" ] && pactl set-default-sink "$sink" && \
        notify-send "Audio: $selection" 2>/dev/null
}

# cycle to next audio sink
audionext() {
    sinks=$(pactl list short sinks | awk '{print $2}')
    current=$(pactl get-default-sink)
    next=$(echo "$sinks" | grep -A1 "^${current}$" | tail -1)
    [ "$next" = "$current" ] && next=$(echo "$sinks" | head -1)
    pactl set-default-sink "$next"
    pactl list short sink-inputs | awk '{print $1}' | while read -r stream; do
        pactl move-sink-input "$stream" "$next"
    done
    desc=$(pactl -f json list sinks | jq -r --arg n "$next" '.[] | select(.name == $n) | .description')
    notify-send "Audio: $desc" 2>/dev/null
    echo "Switched to: $desc"
}

# hi-res FLAC to CD quality (16bit/44.1kHz)
flac_downconvert() {
    for f in "$@"; do
        bps=$(metaflac --show-bps "$f" 2>/dev/null)
        rate=$(metaflac --show-sample-rate "$f" 2>/dev/null)
        if [ "$bps" = "16" ] && [ "$rate" = "44100" ]; then
            echo "Already CD quality: $f"; continue
        fi
        out="${f%.flac} [16-44].flac"
        ffmpeg -i "$f" -y -vn \
            -af "aresample=resampler=soxr:dither_method=triangular" \
            -ar 44100 -sample_fmt s16 -c:a flac "$out"
        echo "Converted: $out"
    done
}

# batch convert to MP3 320k
to_mp3() {
    for f in "$@"; do
        ffmpeg -i "$f" -ab 320k "${f%.*}.mp3"
    done
}

# DSD to PCM FLAC
dsd2pcm() {
    for f in "$@"; do
        ffmpeg -i "$f" -af "lowpass=f=20000" -ar 176400 -sample_fmt s32 \
            -c:a flac "${f%.*}.flac"
    done
}

# audio file info (bit depth, sample rate, codec, duration)
audio_info() {
    for f in "$@"; do
        echo "=== $(basename "$f") ==="
        ffprobe -hide_banner -show_entries stream=codec_name,sample_rate,bits_per_raw_sample,channels,duration \
            -of default=noprint_wrappers=1 "$f" 2>/dev/null
        echo ""
    done
}

# batch EBU R128 loudness normalization
loudnorm_batch() {
    for f in "$@"; do
        out="${f%.*}_normalized.${f##*.}"
        ffmpeg -i "$f" -af loudnorm=I=-14:LRA=11:TP=-1 "$out"
        echo "Normalized: $out"
    done
}

# play a directory via MPD
mpd_play_dir() {
    dir="${1:-.}"
    mpc clear
    mpc ls "$dir" | mpc add
    mpc shuffle
    mpc play
}

# MPD album art notification
mpd_notify() {
    musicdir="$HOME/music"
    while true; do
        mpc idle player >/dev/null
        song=$(mpc current -f '%file%')
        [ -z "$song" ] && continue
        artist=$(mpc current -f '%artist%')
        title=$(mpc current -f '%title%')
        album=$(mpc current -f '%album%')
        songdir="$musicdir/$(dirname "$song")"
        cover=""
        for img in "$songdir"/cover.jpg "$songdir"/cover.png "$songdir"/folder.jpg; do
            [ -f "$img" ] && cover="$img" && break
        done
        if [ -z "$cover" ]; then
            cover="/tmp/mpd_cover.jpg"
            ffmpeg -y -i "$musicdir/$song" -an -vcodec copy "$cover" 2>/dev/null
        fi
        notify-send -i "${cover:-audio-x-generic}" "$title" "$artist -- $album" 2>/dev/null
    done
}

# extract embedded album art
extract_cover() {
    for f in "$@"; do
        dir=$(dirname "$f")
        [ -f "$dir/cover.jpg" ] && continue
        ffmpeg -i "$f" -an -vcodec copy "$dir/cover.jpg" 2>/dev/null && \
            echo "Extracted: $dir/cover.jpg"
    done
}

# import each file individually (avoids auto-grouping)
beet_cherry() {
    dir="${1:-.}"
    find "$dir" -maxdepth 1 -type f \( -name '*.mp3' -o -name '*.flac' -o -name '*.m4a' \) | while read -r f; do
        beet import "$f"
    done
}

# find tracks on disk not in beets DB
beet_orphans() {
    musicdir="${1:-$HOME/music}"
    beetsdir="${2:-$musicdir}"
    echo "[*] Comparing disk vs beets database..."
    beet list --format '$path' | sort > /tmp/beet_imported.txt
    find "$musicdir" -type f \( -iname '*.mp3' -o -iname '*.flac' -o -iname '*.ogg' -o -iname '*.m4a' \) | sort > /tmp/beet_disk.txt
    echo "Orphaned files (on disk, not in beets):"
    comm -23 /tmp/beet_disk.txt /tmp/beet_imported.txt
}

# sanitize music filenames
sanitize_music() {
    dir="${1:-.}"
    find "$dir" -type f \( -name "*.mp3" -o -name "*.flac" -o -name "*.opus" \) | while read -r f; do
        d=$(dirname "$f"); b=$(basename "$f")
        clean=$(echo "$b" | sed 's/[<>:"/\\|?*]//g; s/  */ /g; s/^ //; s/ $//')
        [ "$b" != "$clean" ] && mv -v "$f" "$d/$clean"
    done
}

# find duplicate audio files by checksum
find_audio_dupes() {
    dir="${1:-.}"
    find "$dir" -type f \( -name "*.mp3" -o -name "*.flac" -o -name "*.m4a" -o -name "*.opus" \) \
        -exec md5sum {} + | sort | uniq -w32 -d --all-repeated=separate
}

# split a full-side recording into tracks by silence
vinyl_split() {
    input="$1"; threshold="${2:--40}"; min_silence="${3:-2}"
    [ -z "$input" ] && { echo "Usage: vinyl_split <recording.wav> [threshold-dB] [min-silence-sec]"; return 1; }
    echo "[*] Detecting silence points..."
    ffmpeg -i "$input" -af "silencedetect=noise=${threshold}dB:d=${min_silence}" \
        -f null - 2>&1 | grep "silence_end" | awk '{print $5}' | sed 's/|//' > /tmp/vinyl_splits.txt
    prev=0; track=1
    while read -r end; do
        start=$(echo "$end - 0.5" | bc)
        if [ "$(echo "$start > $prev" | bc -l)" = "1" ]; then
            ffmpeg -i "$input" -ss "$prev" -to "$start" -c copy "track_$(printf '%02d' $track).flac"
            track=$((track + 1))
        fi
        prev="$end"
    done < /tmp/vinyl_splits.txt
    ffmpeg -i "$input" -ss "$prev" -c copy "track_$(printf '%02d' $track).flac"
    echo "[+] Split into $track tracks"
}

# verify realtime audio setup
check_rt() {
    echo "=== Realtime Audio Check ==="
    echo "User groups: $(groups)"
    echo "rtprio: $(ulimit -r 2>/dev/null || echo 'N/A')"
    echo "memlock: $(ulimit -l 2>/dev/null || echo 'N/A')"
    if grep -q "^@audio" /etc/security/limits.conf 2>/dev/null; then
        echo "Audio group limits: configured"
    else
        echo "WARNING: No audio group limits"
    fi
    if lsmod | grep -q snd_hrtimer 2>/dev/null; then
        echo "snd_hrtimer: loaded"
    else
        echo "snd_hrtimer: not loaded"
    fi
}