#!/usr/bin/env bash # to_mp3.sh — Convert .m4a and .wav files to .mp3 # # Usage: # ./to_mp3.sh [options] # # Options: # --quality N VBR quality 0-9 (default: 2 ≈ 190kbps; lower = better) # --bitrate N CBR bitrate e.g. 192k (overrides --quality) # --replace Delete originals after successful conversion # --dry-run Show what would be converted without doing anything # # Examples: # ./to_mp3.sh recordings/ # ./to_mp3.sh interview.m4a # ./to_mp3.sh recordings/ --replace # ./to_mp3.sh recordings/ --bitrate 128k set -euo pipefail QUALITY=2 BITRATE="" REPLACE=false DRY_RUN=false TARGET="" # Parse arguments while [[ $# -gt 0 ]]; do case "$1" in --quality) QUALITY="$2"; shift 2 ;; --bitrate) BITRATE="$2"; shift 2 ;; --replace) REPLACE=true; shift ;; --dry-run) DRY_RUN=true; shift ;; -*) echo "Unknown option: $1" >&2; exit 1 ;; *) TARGET="$1"; shift ;; esac done if [[ -z "$TARGET" ]]; then echo "Usage: $(basename "$0") [--quality N] [--bitrate N] [--replace] [--dry-run]" exit 1 fi if ! command -v ffmpeg &>/dev/null; then echo "Error: ffmpeg not found." >&2 exit 1 fi # Collect files files=() if [[ -f "$TARGET" ]]; then files=("$TARGET") elif [[ -d "$TARGET" ]]; then while IFS= read -r -d '' f; do files+=("$f") done < <(find "$TARGET" -maxdepth 1 -type f \( -iname "*.m4a" -o -iname "*.wav" \) -print0 | sort -z) else echo "Error: '$TARGET' is not a file or directory." >&2 exit 1 fi if [[ ${#files[@]} -eq 0 ]]; then echo "No .m4a or .wav files found in: $TARGET" exit 0 fi # Build audio quality flags if [[ -n "$BITRATE" ]]; then audio_flags=(-b:a "$BITRATE") quality_desc="CBR ${BITRATE}" else audio_flags=(-q:a "$QUALITY") quality_desc="VBR quality ${QUALITY}" fi echo "Converting ${#files[@]} file(s) to MP3 (${quality_desc})" [[ "$REPLACE" == true ]] && echo " Originals will be deleted after conversion." [[ "$DRY_RUN" == true ]] && echo " Dry-run mode — no files will be written." echo "" converted=0 skipped=0 errors=0 for src in "${files[@]}"; do out="${src%.*}.mp3" if [[ -f "$out" && "$DRY_RUN" == false ]]; then echo " $(basename "$src"): output already exists, skipping" ((skipped++)) || true continue fi size_mb=$(( $(stat -f%z "$src" 2>/dev/null || stat -c%s "$src") / 1048576 )) printf " %-40s (%d MB)" "$(basename "$src")" "$size_mb" if [[ "$DRY_RUN" == true ]]; then echo " [dry-run] → $(basename "$out")" continue fi if ffmpeg -i "$src" -vn "${audio_flags[@]}" -y "$out" -loglevel error; then out_kb=$(( $(stat -f%z "$out" 2>/dev/null || stat -c%s "$out") / 1024 )) echo " → $(basename "$out") (${out_kb} KB)" ((converted++)) || true if [[ "$REPLACE" == true ]]; then rm "$src" fi else echo " ERROR" ((errors++)) || true [[ -f "$out" ]] && rm "$out" fi done echo "" echo "Done: ${converted} converted, ${skipped} skipped, ${errors} errors."