Files
beets-setup/scripts/convert/convert-wav

496 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
set -u
set -o pipefail
program="$(basename "$0")"
dry_run=0
keep_originals=0
keep_foreign_metadata=1
float_to_flac24=0
delete_float_originals=0
usage() {
cat <<EOF
Usage:
$program [options] <directory>
Recursively convert WAV files to FLAC.
Normal integer PCM WAV files:
- Converted with the reference flac encoder.
- Encoded to a temporary directory first.
- Verified with flac --verify.
- Moved into place only after successful encoding.
- Original WAV is deleted after successful placement unless --keep is set.
32-bit float WAV files, WAV format type 3:
- Skipped by default, because FLAC does not store floating-point samples.
- Use --float-to-flac24 to explicitly convert them to 24-bit integer FLAC.
- Float originals are kept by default even when --keep is not set.
- Use --delete-float-originals to delete float WAVs after successful 24-bit FLAC conversion.
Options:
-n, --dry, --dry-run
Show what would happen without writing, moving, converting, or deleting files.
-k, --keep
Keep original WAV files after successful conversion.
--float-to-flac24
Convert 32-bit float WAV files to 24-bit integer FLAC.
This is not mathematically lossless for arbitrary float WAVs.
--delete-float-originals
When used with --float-to-flac24, delete the original float WAV after
successful conversion. Without this option, float WAV originals are kept.
--no-foreign-metadata
Do not ask flac to preserve non-audio WAV chunks.
-h, --help
Show this help text.
Behavior:
- Finds *.wav case-insensitively.
- Skips a WAV if a same-stem .flac already exists in the same directory.
- Prunes its own temporary directories.
EOF
}
die() {
echo "ERROR: $*" >&2
exit 1
}
log() {
echo "$*"
}
args=()
while [[ $# -gt 0 ]]; do
case "$1" in
-n|--dry|--dry-run)
dry_run=1
shift
;;
-k|--keep)
keep_originals=1
shift
;;
--float-to-flac24)
float_to_flac24=1
shift
;;
--delete-float-originals)
delete_float_originals=1
shift
;;
--no-foreign-metadata)
keep_foreign_metadata=0
shift
;;
-h|--help)
usage
exit 0
;;
--)
shift
while [[ $# -gt 0 ]]; do
args+=("$1")
shift
done
;;
-*)
die "Unknown option: $1"
;;
*)
args+=("$1")
shift
;;
esac
done
[[ "${#args[@]}" -eq 1 ]] || {
usage >&2
exit 2
}
root="${args[0]}"
while [[ "$root" != "/" && "$root" == */ ]]; do
root="${root%/}"
done
[[ -e "$root" ]] || die "Path does not exist: $root"
[[ -d "$root" ]] || die "Path is not a directory: $root"
if [[ "$dry_run" -eq 0 ]]; then
command -v flac >/dev/null 2>&1 || die "'flac' command not found. Install the reference FLAC encoder first."
if [[ "$float_to_flac24" -eq 1 ]]; then
if ! command -v sox >/dev/null 2>&1 && ! command -v ffmpeg >/dev/null 2>&1; then
die "--float-to-flac24 requires either 'sox' or 'ffmpeg'."
fi
fi
fi
lowercase() {
printf '%s' "$1" | tr '[:upper:]' '[:lower:]'
}
same_stem_flac_exists() {
local dir="$1"
local stem="$2"
local candidate name base ext ext_lower
while IFS= read -r -d '' candidate; do
name="${candidate##*/}"
base="${name%.*}"
ext="${name##*.}"
ext_lower="$(lowercase "$ext")"
if [[ "$base" == "$stem" && "$ext_lower" == "flac" ]]; then
return 0
fi
done < <(find "$dir" -maxdepth 1 -type f -print0)
return 1
}
wav_format_tag() {
local path="$1"
if ! command -v python3 >/dev/null 2>&1; then
printf 'unknown\n'
return 0
fi
python3 - "$path" <<'PY'
import struct
import sys
path = sys.argv[1]
try:
with open(path, "rb") as f:
header = f.read(12)
if len(header) < 12:
print("unknown")
raise SystemExit(0)
riff = header[0:4]
wave = header[8:12]
if riff not in (b"RIFF", b"RF64") or wave != b"WAVE":
print("unknown")
raise SystemExit(0)
while True:
chunk_header = f.read(8)
if len(chunk_header) < 8:
print("unknown")
raise SystemExit(0)
chunk_id, chunk_size = struct.unpack("<4sI", chunk_header)
if chunk_id == b"fmt ":
data = f.read(chunk_size)
if len(data) < 2:
print("unknown")
else:
tag = struct.unpack("<H", data[:2])[0]
print(tag)
raise SystemExit(0)
# RIFF chunks are word-aligned.
skip = chunk_size + (chunk_size % 2)
f.seek(skip, 1)
except Exception:
print("unknown")
PY
}
cleanup_tmpdir() {
local tmpdir="${1:-}"
if [[ -n "$tmpdir" && -d "$tmpdir" ]]; then
rm -rf -- "$tmpdir"
fi
}
convert_integer_wav_with_flac() {
local wav="$1"
local tmp_flac="$2"
local log_file="$3"
local flac_args=(
--silent
--best
--verify
--preserve-modtime
)
if [[ "$keep_foreign_metadata" -eq 1 ]]; then
flac_args+=(--keep-foreign-metadata-if-present)
fi
flac "${flac_args[@]}" -o "$tmp_flac" -- "$wav" >"$log_file" 2>&1
}
convert_float_wav_to_flac24() {
local wav="$1"
local tmp_flac="$2"
local log_file="$3"
if command -v sox >/dev/null 2>&1; then
sox "$wav" -t flac -e signed-integer -b 24 -C 8 "$tmp_flac" >"$log_file" 2>&1
return $?
fi
ffmpeg \
-hide_banner \
-nostdin \
-loglevel error \
-y \
-i "$wav" \
-map 0:a:0 \
-map_metadata 0 \
-vn \
-c:a flac \
-compression_level 12 \
-sample_fmt s32 \
-bits_per_raw_sample 24 \
"$tmp_flac" >"$log_file" 2>&1
}
converted=0
converted_float=0
skipped_existing=0
unsupported_float=0
failed=0
would_convert=0
would_convert_float=0
would_skip_existing=0
would_skip_float=0
log "Root: $root"
if [[ "$dry_run" -eq 1 ]]; then
log "Mode: dry run"
elif [[ "$keep_originals" -eq 1 ]]; then
log "Mode: convert and keep originals"
else
log "Mode: convert and delete integer WAV originals after successful conversion"
fi
if [[ "$float_to_flac24" -eq 1 ]]; then
log "Float WAV mode: convert to 24-bit FLAC"
if [[ "$delete_float_originals" -eq 1 ]]; then
log "Float WAV deletion: enabled"
else
log "Float WAV deletion: disabled; originals will be kept"
fi
else
log "Float WAV mode: skip"
fi
log
while IFS= read -r -d '' wav; do
dir="${wav%/*}"
filename="${wav##*/}"
stem="${filename%.*}"
target="$dir/$stem.flac"
if same_stem_flac_exists "$dir" "$stem"; then
log "SKIP: FLAC already exists for: $wav"
if [[ "$dry_run" -eq 1 ]]; then
((would_skip_existing++))
else
((skipped_existing++))
fi
continue
fi
tag="$(wav_format_tag "$wav")"
is_float_wav=0
if [[ "$tag" == "3" ]]; then
is_float_wav=1
fi
if [[ "$is_float_wav" -eq 1 && "$float_to_flac24" -eq 0 ]]; then
log "SKIP FLOAT: $wav"
log " REASON: WAV format type 3 is IEEE float; reference flac cannot encode it directly."
if [[ "$dry_run" -eq 1 ]]; then
((would_skip_float++))
else
((unsupported_float++))
fi
continue
fi
if [[ "$dry_run" -eq 1 ]]; then
if [[ "$is_float_wav" -eq 1 ]]; then
log "WOULD CONVERT FLOAT TO 24-BIT FLAC: $wav"
log " TO: $target"
if [[ "$delete_float_originals" -eq 1 && "$keep_originals" -eq 0 ]]; then
log " ACTION: would delete original float WAV after successful conversion"
else
log " ACTION: would keep original float WAV"
fi
((would_convert_float++))
else
log "WOULD CONVERT: $wav"
log " TO: $target"
if [[ "$keep_originals" -eq 1 ]]; then
log " ACTION: would keep original WAV"
else
log " ACTION: would delete original WAV after successful conversion"
fi
((would_convert++))
fi
continue
fi
tmpdir="$(mktemp -d "$dir/.wav2flac.tmp.XXXXXXXXXX")" || {
log "FAIL: Could not create temporary directory in: $dir" >&2
((failed++))
continue
}
tmp_flac="$tmpdir/$stem.flac"
encode_log="$tmpdir/encode.log"
if [[ "$is_float_wav" -eq 1 ]]; then
log "CONVERT FLOAT TO 24-BIT FLAC: $wav"
if ! convert_float_wav_to_flac24 "$wav" "$tmp_flac" "$encode_log"; then
log "FAIL: Float WAV conversion failed for: $wav" >&2
cat "$encode_log" >&2
cleanup_tmpdir "$tmpdir"
((failed++))
continue
fi
if ! flac --silent --test "$tmp_flac" >"$tmpdir/flac-test.log" 2>&1; then
log "FAIL: Created FLAC did not pass flac --test: $tmp_flac" >&2
cat "$tmpdir/flac-test.log" >&2
cleanup_tmpdir "$tmpdir"
((failed++))
continue
fi
else
log "CONVERT: $wav"
if ! convert_integer_wav_with_flac "$wav" "$tmp_flac" "$encode_log"; then
if grep -qi 'unsupported format type 3' "$encode_log"; then
log "SKIP FLOAT: $wav"
log " REASON: WAV format type 3 is IEEE float; reference flac cannot encode it directly."
cleanup_tmpdir "$tmpdir"
((unsupported_float++))
continue
fi
log "FAIL: Conversion failed for: $wav" >&2
cat "$encode_log" >&2
cleanup_tmpdir "$tmpdir"
((failed++))
continue
fi
fi
if [[ ! -s "$tmp_flac" ]]; then
log "FAIL: Encoded FLAC is missing or empty: $tmp_flac" >&2
cleanup_tmpdir "$tmpdir"
((failed++))
continue
fi
if same_stem_flac_exists "$dir" "$stem"; then
log "SKIP: FLAC appeared during conversion for: $wav" >&2
cleanup_tmpdir "$tmpdir"
((skipped_existing++))
continue
fi
if ! mv -- "$tmp_flac" "$target"; then
log "FAIL: Could not move temp FLAC into place for: $wav" >&2
cleanup_tmpdir "$tmpdir"
((failed++))
continue
fi
cleanup_tmpdir "$tmpdir"
should_delete_original=1
if [[ "$keep_originals" -eq 1 ]]; then
should_delete_original=0
fi
if [[ "$is_float_wav" -eq 1 && "$delete_float_originals" -eq 0 ]]; then
should_delete_original=0
fi
if [[ "$should_delete_original" -eq 1 ]]; then
if rm -- "$wav"; then
log "DONE: $target"
log "DELETED: $wav"
else
log "WARN: FLAC was created, but failed to delete original WAV: $wav" >&2
((failed++))
continue
fi
else
log "DONE: $target"
log "KEPT: $wav"
fi
if [[ "$is_float_wav" -eq 1 ]]; then
((converted_float++))
else
((converted++))
fi
done < <(
find "$root" \
-type d -name '.wav2flac.tmp.*' -prune -o \
-type f -iname '*.wav' -print0
)
log
log "Summary:"
if [[ "$dry_run" -eq 1 ]]; then
log " Would convert integer WAVs: $would_convert"
log " Would convert float WAVs: $would_convert_float"
log " Would skip existing FLACs: $would_skip_existing"
log " Would skip unsupported floats: $would_skip_float"
else
log " Converted integer WAVs: $converted"
log " Converted float WAVs to FLAC24: $converted_float"
log " Skipped existing FLACs: $skipped_existing"
log " Skipped unsupported float WAVs: $unsupported_float"
log " Failed: $failed"
fi
if [[ "$dry_run" -eq 0 && "$failed" -gt 0 ]]; then
exit 1
fi
exit 0