# 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 "; 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 "; 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 "; 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 [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 }