# 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
}