testsAndMisc/linux_configuration/scripts/single_use/misc/testsAndMisc-bash/clean_audio.sh
Krzysztof kuhy Rudnicki 76e73830f6 shell: split clean_audio, enforce_vbox_hosts and migrate_hosts_guard
clean_audio.sh          419 -> 230 + 204
  enforce_vbox_hosts.sh   443 -> 220 + 236
  migrate_hosts_guard.sh  444 -> 167 + 162 + 141

migrate_hosts_guard needed two libraries: with all ten work functions moved
the single lib was 295 lines, so migrate and rollback were separated.

The extractor's brace matcher required index > start and so silently skipped
one-line `warn() { ...; }` definitions. Fixed, and regression-checked against
the committed fresh-install/main.sh split byte-for-byte.

The harness earned its keep on a fourth file: adding `export` to libre_translate
to satisfy SC2034 changed two function bodies, and verify_shell_split.sh caught
what lint had accepted. Those exports were reverted, and libre_translate.sh is
parked - what remains after its clean seams both assigns and reads the same
globals, so getting it under the cap needs parse_args restructured rather than
relocated, which is a refactor rather than a verbatim move.

That cost is now written down: a lib must contain the readers of every global
it assigns. The precedent at install_pacman_wrapper.sh:29 is about
definitions-only libs, not about assignments as such, and the pre-commit hook
runs shellcheck with no -x, so a source= directive will not make it follow the
link. This is the shell analogue of the monkeypatch-target trap.

Verified: all 8, 13 and 19 functions verbatim; all four libraries source with
no side effects; shellcheck passes standalone on every file with no
suppression; bash -n passes on all seven. None of the three was executed -
they drive ffmpeg over real media, mutate VirtualBox guest disks, and rewrite
systemd units.

Over-cap files 29 -> 26.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 14:27:57 +02:00

231 lines
5.7 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# shellcheck source=lib/clean_audio_filters.sh
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/clean_audio_filters.sh"
# clean_audio.sh — Fully automatic audio cleaner for speech (ASR-friendly)
#
# - Default preset is tuned for ASR (faster-whisper):
# mono, 16 kHz, high-pass filter, denoise (RNNoise arnndn by default if model found/provided; else afftdn),
# peak limiting to -0.5 dBFS. No aggressive gating/compression by default.
# - Optional "podcast" preset adds gentle dynamics and loudness leveling.
# - Accepts single files or directories (recursively).
# - Optional parallel processing.
#
# Dependencies: ffmpeg (arnndn filter recommended for best results)
# Optional: an RNNoise model file for arnndn (auto-discovered if present; otherwise falls back to afftdn)
#
# Usage examples:
# Bash/clean_audio.sh input.wav # -> input_clean.wav (same folder)
# Bash/clean_audio.sh input.wav -O out_dir # -> out_dir/input_clean.wav
# Bash/clean_audio.sh input_dir -O cleaned/ -j 4 # -> processes all audio files in dir
# Bash/clean_audio.sh input.wav -m models/rn.nn # -> use RNNoise model
# Bash/clean_audio.sh input.wav --preset podcast # -> add dynamics leveler
#
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)
# Defaults
OUT_DIR=""
OUT_EXT="wav"
RN_MODEL=""
NO_ML=false
REQUIRE_ML=true # default: require RNNoise; install/guide if missing; fail fast if unavailable
PRESET="asr"
JOBS=1
FORCE=false
QUIET=false
LOWPASS=""
SUFFIX="_clean"
HIGHPASS="80"
AFFTDN_NF="-25" # noise floor in dB for afftdn
AFFTDN_MD="8" # mode for afftdn (higher can be more aggressive); requires builds that support 'md'
NO_ADVANCED=false # when true, avoid advanced options that some ffmpeg builds lack
# Parse args
if [[ $# -lt 1 ]]; then
print_usage
exit 1
fi
INPUT_PATH="$1"
shift || true
while [[ $# -gt 0 ]]; do
case "$1" in
-O | --out-dir)
OUT_DIR="$2"
shift 2
;;
-e | --ext)
OUT_EXT="$2"
shift 2
;;
-m | --model)
RN_MODEL="$2"
shift 2
;;
--no-ml)
NO_ML=true
shift
;;
--preset)
PRESET="$2"
shift 2
;;
-j | --jobs)
JOBS="$2"
shift 2
;;
-f | --force)
FORCE=true
shift
;;
-q | --quiet)
QUIET=true
shift
;;
--lowpass)
LOWPASS="$2"
shift 2
;;
--suffix)
SUFFIX="$2"
shift 2
;;
--no-advanced | --compat)
NO_ADVANCED=true
shift
;;
--allow-fallback)
REQUIRE_ML=false
shift
;;
-h | --help)
print_usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
print_usage
exit 1
;;
esac
done
require_cmd ffmpeg
# Resolve FFmpeg binary (env override -> local build -> system)
FFMPEG_BIN=${FFMPEG_BIN:-}
if [[ -z ${FFMPEG_BIN} ]]; then
if [[ -x "$SCRIPT_DIR/ffmpeg-build/FFmpeg/ffmpeg" ]]; then
FFMPEG_BIN="$SCRIPT_DIR/ffmpeg-build/FFmpeg/ffmpeg"
else
FFMPEG_BIN="ffmpeg"
fi
fi
if ! command -v "$FFMPEG_BIN" > /dev/null 2>&1 && [[ ! -x $FFMPEG_BIN ]]; then
echo "Error: FFmpeg binary not found: $FFMPEG_BIN" >&2
exit 1
fi
if ! $QUIET; then
echo "Using FFmpeg binary: $FFMPEG_BIN" >&2
fi
FFMPEG_LOG=(-hide_banner)
if $QUIET; then
FFMPEG_LOG+=(-loglevel error)
else
FFMPEG_LOG+=(-loglevel info)
fi
FFMPEG_OVERWRITE=(-n)
if $FORCE; then
FFMPEG_OVERWRITE=(-y)
fi
arnndn_available=false
if "$FFMPEG_BIN" -hide_banner -h filter=arnndn > /dev/null 2>&1; then
arnndn_available=true
else
if "$FFMPEG_BIN" -hide_banner -filters 2> /dev/null | grep -Eq '(^|[[:space:]])arnndn([[:space:]]|$)'; then
arnndn_available=true
fi
fi
if ! $QUIET; then
echo "arnndn_available=$arnndn_available" >&2
fi
# Check if afftdn supports 'md' option
afftdn_supports_md=false
if "$FFMPEG_BIN" -hide_banner -h filter=afftdn 2> /dev/null | grep -q " md="; then
afftdn_supports_md=true
fi
use_arnndn=false
if [[ $NO_ML == false ]]; then
if [[ $arnndn_available == false ]]; then
if $REQUIRE_ML; then
echo "Error: FFmpeg 'arnndn' filter not available. Please install/upgrade FFmpeg with librnnoise (see Bash/install_ffmpeg_with_arnndn.sh)." >&2
exit 9
fi
else
# arnndn available; require an external model
if [[ -n $RN_MODEL && -f $RN_MODEL ]]; then
:
else
if model_path=$(find_default_rn_model); then
RN_MODEL="$model_path"
else
if [[ -x "$SCRIPT_DIR/get_rnnoise_model.sh" ]]; then
RN_TARGET_DIR="$SCRIPT_DIR/models" RN_TARGET_NAME="rnnoise_model.rnnn" "$SCRIPT_DIR/get_rnnoise_model.sh" --yes || true
if model_path=$(find_default_rn_model); then
RN_MODEL="$model_path"
fi
fi
fi
fi
if [[ -z $RN_MODEL ]]; then
echo "Error: RNNoise model required but not found. Automatic download failed." >&2
echo "Hint: Set RN_URL to a reachable model URL and run Bash/get_rnnoise_model.sh, or supply -m /path/to/model.nn." >&2
exit 10
fi
use_arnndn=true
echo "Using RNNoise external model: $RN_MODEL" >&2
fi
fi
# Concurrency helpers (bash >= 5 supports wait -n; fallback to sequential if not)
supports_wait_n=false
if [[ -n ${BASH_VERSINFO:-} && ${BASH_VERSINFO[0]} -ge 5 ]]; then
supports_wait_n=true
fi
main() {
# Sanity checks and notices
if [[ -n $RN_MODEL && $use_arnndn == false && $NO_ML == false ]]; then
echo "Note: arnndn filter not available in your ffmpeg or model missing — using afftdn." >&2
fi
if [[ -f $INPUT_PATH ]]; then
process_one "$INPUT_PATH"
elif [[ -d $INPUT_PATH ]]; then
run_dir "$INPUT_PATH"
else
echo "Error: Input path not found: $INPUT_PATH" >&2
exit 1
fi
}
main "$@"