diff --git a/README_clean_audio.md b/README_clean_audio.md new file mode 100644 index 0000000..c0bc61f --- /dev/null +++ b/README_clean_audio.md @@ -0,0 +1,104 @@ +# clean_audio.sh — automatic speech cleaning (FFmpeg) + +This script batch‑cleans noisy speech recordings with ffmpeg using simple, reliable filters tuned for ASR (e.g., faster‑whisper). By default it REQUIRES RNNoise (arnndn) and will try to auto‑discover or download a model. You can opt‑in to fallback filters with `--allow-fallback`. + +## Install + +- Required: ffmpeg. Most distros: `sudo pacman -S ffmpeg` or `sudo apt install ffmpeg`. +- Recommended: ffmpeg with `arnndn` filter and an RNNoise model file (e.g., from Mozilla RNNoise community models). The script will auto-detect common model locations or download one via `Bash/get_rnnoise_model.sh`. You can pass a specific model with `-m /path/to/model.nn`. + +Make executable: + +```bash +chmod +x Bash/clean_audio.sh +``` + +## Quick start + +- Single file, default ASR preset (16k mono, denoise, high‑pass, limiter): +```bash +Bash/clean_audio.sh path/to/file.wav +``` +This produces `path/to/file_clean.wav`. + +- Whole folder, 4 parallel jobs, output to `cleaned/`: +```bash +Bash/clean_audio.sh path/to/folder -O cleaned -j 4 +``` + +- Use an RNNoise model explicitly (if your ffmpeg has arnndn): +```bash +Bash/clean_audio.sh input.wav -m models/rnnoise_model.nn +``` +If you omit `-m`, the script will look in common locations; if not found, it will attempt a download via `Bash/get_rnnoise_model.sh`. + +Advanced options and compatibility: +- The cleaner requires RNNoise by default. To allow non-ML fallback filters (afftdn), add `--allow-fallback`. +- The script uses advanced filter settings when available (e.g., afftdn with `md`). If your ffmpeg build lacks these options, it will error with guidance. Add `--no-advanced` (or `--compat`) to avoid such params. + +- Podcast preset (adds dynamics and loudness leveling): +```bash +Bash/clean_audio.sh input.wav --preset podcast +``` + +## Options + +```text +Usage: clean_audio.sh [options] + +Options: + -O, --out-dir DIR Output directory (default: alongside input file). + -e, --ext EXT Output extension/container: wav|flac (default: wav). + -m, --model PATH RNNoise model file for arnndn; falls back to afftdn if unavailable. + --no-ml Do not use arnndn even if model is provided; use afftdn. + --preset NAME asr (default) | podcast | aggressive + -j, --jobs N Parallel jobs for directory mode (default: 1). + -f, --force Overwrite outputs if they exist. + -q, --quiet Reduce ffmpeg logging noise. + --lowpass FREQ Optional low-pass cutoff (e.g., 8000). Disabled by default. + --suffix SUF Suffix for output basename (default: _clean). +``` + +## Designed for ASR (faster‑whisper) + +Default output format is mono, 16 kHz, PCM 16‑bit WAV—ideal for most Whisper/faster‑whisper pipelines. You can feed the cleaned files directly into your transcription step. + +If you prefer FLAC to save space without quality loss: +```bash +Bash/clean_audio.sh input.wav -e flac -O cleaned +``` + +## Presets + +- asr (default): light, ASR‑friendly cleanup; prevents clipping. +- podcast: adds gentle dynamics and approximate loudness normalization (single‑pass `loudnorm`). +- aggressive: heavier gate/dynamics; can suppress background more, but may slightly hurt ASR accuracy—use sparingly. + +## Tips + +- If you see artifacts from RNNoise, try without a model (uses `afftdn`), or add a low‑pass (e.g., `--lowpass 8000`). +- For extremely boomy bar recordings, raise high‑pass by editing `HIGHPASS` in the script or add `--lowpass`. +- If your ffmpeg lacks `arnndn`, you can install a newer build or keep the fallback (afftdn works fine for many cases). + - If your ffmpeg is missing features, you can use the helper: +```bash +chmod +x Bash/install_ffmpeg_with_arnndn.sh +Bash/install_ffmpeg_with_arnndn.sh +``` +It will suggest distro options or build FFmpeg from source with `--enable-librnnoise`. + + RNNoise model downloader helper: + ```bash + chmod +x Bash/get_rnnoise_model.sh + Bash/get_rnnoise_model.sh --yes + ``` + This saves a model into `Bash/models/` which the cleaner will auto-discover. + +## Troubleshooting + +- “arnndn not available”: Your ffmpeg wasn’t built with it. The script will use `afftdn` instead. +- Output sounds thin: lower the high‑pass (edit `HIGHPASS=80` in script to `60`) or remove low‑pass. +- Level too low/high: choose the `podcast` preset for auto leveling, or add your own `loudnorm` in post. + +## License + +This helper script is provided under the repository’s LICENSE. diff --git a/clean_audio.sh b/clean_audio.sh new file mode 100755 index 0000000..023b038 --- /dev/null +++ b/clean_audio.sh @@ -0,0 +1,390 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# 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) + +print_usage() { + cat < [options] + +Options: + -O, --out-dir DIR Output directory (default: alongside input file). + -e, --ext EXT Output extension/container: wav|flac (default: wav). + -m, --model PATH RNNoise model file for arnndn; required by default unless --allow-fallback. + --no-ml Do not use arnndn even if model is provided (requires --allow-fallback). + --preset NAME asr (default) | podcast | aggressive + -j, --jobs N Parallel jobs for directory mode (default: 1). + -f, --force Overwrite outputs if they exist (ffmpeg -y). + -q, --quiet Reduce ffmpeg logging noise. + --lowpass FREQ Optional low-pass cutoff (e.g., 8000). Disabled by default. + --suffix SUF Suffix for output basename (default: _clean). + -h, --help Show this help. + +Notes: + - Default sample rate is 16 kHz mono PCM 16-bit (good for most speech ASR models). + - If arnndn (RNNoise) is used, it usually outperforms afftdn for speech denoise. + - The 'podcast' preset adds gentle dynamics and loudness normalization (single-pass). +EOF +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { + echo "Error: Required command '$1' not found in PATH" >&2 + exit 1 + } +} + +# 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 + +# Try to auto-discover an RNNoise model if none provided +find_default_rn_model() { + local candidate="" + # Allow env variable override + if [[ -n "${RNNOISE_MODEL:-}" && -f "${RNNOISE_MODEL}" ]]; then + echo "${RNNOISE_MODEL}" + return 0 + fi + local dirs=( + "$SCRIPT_DIR/models" + "$SCRIPT_DIR/../models" + "/usr/share/rnnoise" + "/usr/local/share/rnnoise" + "/usr/share/ffmpeg/models" + "$HOME/.local/share/rnnoise" + ) + # Prefer '.rnnn' models (rnnoise-nu style) over legacy '.nn' + local exts=("rnnn" "nn" "model") + for d in "${dirs[@]}"; do + if [[ -d "$d" ]]; then + for ext in "${exts[@]}"; do + # Pick the first matching model file + for f in "$d"/*."$ext"; do + if [[ -f "$f" ]]; then + echo "$f" + return 0 + fi + done + done + fi + done + return 1 +} + +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 + +build_filters() { + local filters=() + # Remove low-frequency rumble typical for handheld/room noise + filters+=("highpass=f=${HIGHPASS}") + + # Denoise + if $use_arnndn; then + # arnndn with full mix keeps the model output; if no external model, rely on built-in + filters+=("aresample=48000") + filters+=("arnndn=m=${RN_MODEL}:mix=1.0") + else + # afftdn: FFT-based denoise, tune nf (noise floor) as needed + if $REQUIRE_ML; then + echo "Error: RNNoise required but not in use; aborting rather than falling back to afftdn. Use --allow-fallback to permit." >&2 + exit 11 + fi + if $NO_ADVANCED; then + filters+=("afftdn=nf=${AFFTDN_NF}") + else + if $afftdn_supports_md; then + filters+=("afftdn=nf=${AFFTDN_NF}:md=${AFFTDN_MD}") + else + echo "Error: Your ffmpeg's afftdn filter does not support 'md='." >&2 + echo "Hint: Install/upgrade ffmpeg to a build that supports afftdn md or rerun with --no-advanced." >&2 + echo " On Debian/Ubuntu you may need a newer ffmpeg from a PPA or build from source." >&2 + exit 8 + fi + fi + fi + + # Optional low-pass to shave hiss; keep disabled unless requested + if [[ -n "$LOWPASS" ]]; then + filters+=("lowpass=f=${LOWPASS}") + fi + + case "$PRESET" in + asr) + # ASR-friendly: avoid heavy gating/leveling, just prevent clipping + filters+=("alimiter=limit=0.94") + ;; + podcast) + # Gentle dynamic normalization and broadcast-ish loudness (single-pass) + # Note: single-pass loudnorm is approximate but OK for quick workflows + filters+=("dynaudnorm=f=500:g=5:p=0.1") + filters+=("loudnorm=i=-18:lra=9:tp=-2.0") + ;; + aggressive) + # Heavier clean-up; may harm ASR slightly but suppress background more + filters+=("agate=threshold=0.012:ratio=2.5:release=200") + filters+=("dynaudnorm=f=400:g=7:p=0.1") + filters+=("loudnorm=i=-18:lra=9:tp=-2.0") + ;; + *) ;; + esac + + # Resample and format at the end for ASR + filters+=("aresample=16000") + filters+=("aformat=channel_layouts=mono:sample_fmts=s16") + + local IFS=","; echo "${filters[*]}" +} + +make_out_path_for_file() { + local in_file="$1" + local base + base=$(basename -- "$in_file") + base="${base%.*}" + local out_base="${base}${SUFFIX}.${OUT_EXT}" + if [[ -n "$OUT_DIR" ]]; then + mkdir -p -- "$OUT_DIR" + echo "$OUT_DIR/$out_base" + else + local dir + dir=$(dirname -- "$in_file") + echo "$dir/$out_base" + fi +} + +process_one() { + local in_file="$1" + local out_file + out_file=$(make_out_path_for_file "$in_file") + + # Choose codec based on extension + local codec=( -c:a pcm_s16le ) + if [[ "$OUT_EXT" == "flac" ]]; then + codec=( -c:a flac ) + fi + + local af + af=$(build_filters) + + if [[ -f "$out_file" && $FORCE == false ]]; then + echo "Skip (exists): $out_file" + return 0 + fi + + echo "Cleaning: $in_file -> $out_file" + "$FFMPEG_BIN" "${FFMPEG_LOG[@]}" "${FFMPEG_OVERWRITE[@]}" -i "$in_file" -af "$af" "${codec[@]}" "$out_file" +} + +# 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 + +run_dir() { + local dir="$1" + # Common audio extensions (case-insensitive) + mapfile -d '' files < <(find "$dir" -type f \ + \( -iname "*.wav" -o -iname "*.mp3" -o -iname "*.m4a" -o -iname "*.aac" -o -iname "*.flac" \ + -o -iname "*.ogg" -o -iname "*.opus" -o -iname "*.wma" -o -iname "*.webm" \) -print0) + + if [[ ${#files[@]} -eq 0 ]]; then + echo "No audio files found in: $dir" + return 0 + fi + + local running=0 + for f in "${files[@]}"; do + if [[ "$JOBS" -le 1 || $supports_wait_n == false ]]; then + process_one "$f" + else + process_one "$f" & + ((running++)) + if (( running >= JOBS )); then + wait -n || true + ((running--)) + fi + fi + done + + # Wait for any remaining background jobs + if (( JOBS > 1 )) && $supports_wait_n; then + wait || 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 "$@" diff --git a/get_rnnoise_model.sh b/get_rnnoise_model.sh new file mode 100755 index 0000000..2ac1057 --- /dev/null +++ b/get_rnnoise_model.sh @@ -0,0 +1,190 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# get_rnnoise_model.sh — fetch an RNNoise model into a local models dir +# +# Prefers known-good rnnoise-nu models. You can override with: +# RN_URL, RN_TARGET_DIR, RN_TARGET_NAME +# +# Usage: +# Bash/get_rnnoise_model.sh # interactive download +# RN_TARGET_DIR=./models Bash/get_rnnoise_model.sh --yes + +ask_yes_no() { + read -r -p "$1 [y/N]: " ans || true + case "${ans:-}" in + y|Y|yes|YES) return 0;; + *) return 1;; + esac +} + +has_cmd() { command -v "$1" >/dev/null 2>&1; } + +YES=false +while [[ $# -gt 0 ]]; do + case "$1" in + -y|--yes) YES=true; shift;; + *) echo "Unknown option: $1" >&2; exit 2;; + esac +done + +RN_TARGET_DIR=${RN_TARGET_DIR:-"$(dirname "$0")/models"} +RN_TARGET_NAME=${RN_TARGET_NAME:-"rnnoise_model.rnnn"} + +mkdir -p "$RN_TARGET_DIR" +dest="$RN_TARGET_DIR/$RN_TARGET_NAME" + +if [[ -f "$dest" ]]; then + echo "Model already exists at: $dest" + exit 0 +fi + +if ! $YES; then + if ! ask_yes_no "Download RNNoise model to $dest?"; then + echo "Aborted." + exit 1 + fi +fi + +if ! has_cmd curl && ! has_cmd wget; then + echo "Error: Need curl or wget to download RNNoise model." >&2 + exit 3 +fi + +# Priority 1: explicit URL +if [[ -n "${RN_URL:-}" ]]; then + echo "Downloading RNNoise model from RN_URL: $RN_URL" >&2 + tmp=$(mktemp) + if has_cmd curl; then + curl -fsSL "$RN_URL" -o "$tmp" + else + wget -qO "$tmp" "$RN_URL" + fi + if [[ -s "$tmp" ]]; then + mv "$tmp" "$dest" + echo "Saved RNNoise model to: $dest" + exit 0 + fi + rm -f "$tmp" || true + echo "Warning: RN_URL download failed; continuing to fallback sources." >&2 +fi + +# Priority 2: rnnoise-nu known models (GregorR) +NU_URLS=( + "https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/sh.rnnn" + "https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/lq.rnnn" + "https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/mp.rnnn" + "https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/bd.rnnn" + "https://raw.githubusercontent.com/GregorR/rnnoise-nu/master/src/models/cb.rnnn" +) +for u in "${NU_URLS[@]}"; do + echo "Attempting to download RNNoise model from: $u" >&2 + tmp=$(mktemp) + if has_cmd curl; then + if curl -fsSL "$u" -o "$tmp"; then + if [[ -s "$tmp" ]]; then + mv "$tmp" "$dest" + echo "Saved RNNoise model to: $dest" >&2 + exit 0 + fi + fi + else + if wget -qO "$tmp" "$u"; then + if [[ -s "$tmp" ]]; then + mv "$tmp" "$dest" + echo "Saved RNNoise model to: $dest" >&2 + exit 0 + fi + fi + fi + rm -f "$tmp" || true +done + +# Priority 2b: arnndn-models fallback (richardpl) +RNNDN_URLS=( + "https://raw.githubusercontent.com/richardpl/arnndn-models/master/sh.rnnn" +) +for u in "${RNNDN_URLS[@]}"; do + echo "Attempting to download RNNoise model from: $u" >&2 + tmp=$(mktemp) + if has_cmd curl; then + if curl -fsSL "$u" -o "$tmp"; then + if [[ -s "$tmp" ]]; then + mv "$tmp" "$dest" + echo "Saved RNNoise model to: $dest" >&2 + exit 0 + fi + fi + else + if wget -qO "$tmp" "$u"; then + if [[ -s "$tmp" ]]; then + mv "$tmp" "$dest" + echo "Saved RNNoise model to: $dest" >&2 + exit 0 + fi + fi + fi + rm -f "$tmp" || true +done + +# Priority 3: repo archives (rnnoise-nu and arnndn-models) +ARCHIVES=( + "https://github.com/GregorR/rnnoise-nu/archive/refs/heads/master.zip" + "https://github.com/richardpl/arnndn-models/archive/refs/heads/master.zip" +) +for aurl in "${ARCHIVES[@]}"; do + echo "Attempting to download archive: $aurl" >&2 + tmpdir=$(mktemp -d) + archive="$tmpdir/models.zip" + set +e + if has_cmd curl; then + curl -fL "$aurl" -o "$archive" + else + wget -O "$archive" "$aurl" + fi + status=$? + set -e + if [[ $status -ne 0 ]]; then + rm -rf "$tmpdir" || true + continue + fi + if has_cmd bsdtar; then + bsdtar -xf "$archive" -C "$tmpdir" + elif has_cmd unzip; then + unzip -q "$archive" -d "$tmpdir" + else + echo "Warning: Need bsdtar or unzip to extract archive; skipping archive method." >&2 + rm -rf "$tmpdir" || true + continue + fi + mapfile -t nnfiles < <(bash -lc 'shopt -s globstar nullglob; for f in '"$tmpdir"'/**/*.rnnn '"$tmpdir"'/**/*.nn; do [[ -f "$f" ]] && echo "$f"; done') + if [[ ${#nnfiles[@]} -gt 0 ]]; then + cp -f "${nnfiles[0]}" "$dest" + echo "Saved RNNoise model to: $dest (from archive)" >&2 + rm -rf "$tmpdir" || true + exit 0 + fi + rm -rf "$tmpdir" || true +done + +# Priority 4: Arch-based AUR packages and search only .nn/.rnnn +if has_cmd yay; then + echo "Attempting to install AUR packages that may include RNNoise models..." >&2 + set +e + yay -S --noconfirm denoiseit-git 2>/dev/null + yay -S --noconfirm speech-denoiser-git 2>/dev/null + set -e + mapfile -t found < <(bash -lc 'shopt -s globstar nullglob; for f in /usr/share/**/*.nn /usr/share/**/*.rnnn /usr/local/share/**/*.nn /usr/local/share/**/*.rnnn; do [[ -f "$f" ]] && echo "$f"; done' 2>/dev/null || true) + if [[ ${#found[@]} -gt 0 ]]; then + echo "Found candidate models:" >&2 + printf ' %s\n' "${found[@]}" >&2 + cp -f "${found[0]}" "$dest" + echo "Copied model to: $dest" >&2 + exit 0 + fi +fi + +echo "Error: Could not obtain an RNNoise model automatically." >&2 +echo "Hint: Set RN_URL to a reachable model URL, or place a model file at: $dest" >&2 +exit 5 diff --git a/install_ffmpeg_with_arnndn.sh b/install_ffmpeg_with_arnndn.sh new file mode 100755 index 0000000..8c384c1 --- /dev/null +++ b/install_ffmpeg_with_arnndn.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# install_ffmpeg_with_arnndn.sh — helper to install/upgrade FFmpeg with arnndn and full audio filters +# +# Tries distro packages first; if not suitable, offers to build from source. +# This script prints commands and asks for confirmation before building. + +print_info() { + echo "[info] $*" +} + +ask_yes_no() { + read -r -p "$1 [y/N]: " ans || true + case "${ans:-}" in + y|Y|yes|YES) return 0;; + *) return 1;; + esac +} + +has_cmd() { command -v "$1" >/dev/null 2>&1; } + +detect_distro() { + if [[ -f /etc/os-release ]]; then + . /etc/os-release + echo "${ID:-unknown}" + else + echo "unknown" + fi +} + +main() { + local distro + distro=$(detect_distro) + print_info "Detected distro: $distro" + + if has_cmd ffmpeg && ffmpeg -hide_banner -filters | grep -q " arnndn "; then + print_info "Your ffmpeg already supports arnndn." + else + case "$distro" in + ubuntu|debian) + print_info "On Ubuntu/Debian, the official repo may lack newer filters. Consider a PPA or build from source." + echo "Options:" + echo " - ppa: sudo add-apt-repository ppa:savoury1/ffmpeg6 && sudo apt update && sudo apt install ffmpeg" + echo " - source build (recommended for latest): run this script to build from source" + ;; + arch|manjaro|endeavouros) + print_info "On Arch-based distros, ffmpeg is recent. Try: sudo pacman -Syu ffmpeg" + ;; + fedora) + print_info "On Fedora, try: sudo dnf install ffmpeg" + ;; + *) + print_info "Distro not recognized; will offer source build." + ;; + esac + fi + + if ask_yes_no "Build FFmpeg from source with rnnoise/arnndn support now?"; then + echo "This will clone FFmpeg and build locally under ./ffmpeg-build. Continue?" + if ! ask_yes_no "Proceed"; then + exit 0 + fi + set -x + mkdir -p ffmpeg-build && cd ffmpeg-build + # Prepare repository + if [[ -d FFmpeg ]]; then + if [[ -d FFmpeg/.git ]]; then + if ask_yes_no "An existing FFmpeg source directory was found. Reuse and update it?"; then + set +e + git -C FFmpeg fetch --all --tags --prune + git -C FFmpeg pull --rebase --ff-only || true + set -e + else + if ask_yes_no "Delete existing FFmpeg directory and re-clone?"; then + rm -rf FFmpeg + else + echo "Keeping existing FFmpeg directory as-is." + fi + fi + else + if ask_yes_no "Non-git 'FFmpeg' directory exists. Delete and re-clone?"; then + rm -rf FFmpeg + else + echo "Cannot proceed with a non-git FFmpeg directory present. Aborting." + exit 4 + fi + fi + fi + # Dependencies + if [[ "$distro" == "ubuntu" || "$distro" == "debian" ]]; then + sudo apt update + sudo apt install -y git build-essential yasm nasm pkg-config libx264-dev libx265-dev libvpx-dev libopus-dev libfdk-aac-dev libmp3lame-dev libvorbis-dev libass-dev libfreetype6-dev libgnutls28-dev libaom-dev libdav1d-dev libxvidcore-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev libxcb-shape0-dev libdrm-dev libvulkan-dev libva-dev libvdpau-dev librtmp-dev libunistring-dev libgnutls28-dev libchromaprint-dev libbluray-dev librubberband-dev libspeex-dev libsoxr-dev libvmaf-dev libzimg-dev libsvtav1-dev libtheora-dev libwebp-dev libopenal-dev libjack-jackd2-dev libpulse-dev librnnoise-dev + elif [[ "$distro" == "arch" || "$distro" == "manjaro" || "$distro" == "endeavouros" ]]; then + sudo pacman -Syu --needed base-devel yasm nasm pkgconf rnnoise + elif [[ "$distro" == "fedora" ]]; then + sudo dnf install -y git make gcc yasm nasm pkgconf-pkg-config rnnoise-devel libX11-devel libXext-devel libXfixes-devel libXv-devel libXrandr-devel libXi-devel libXtst-devel libXinerama-devel freetype-devel fontconfig-devel libass-devel libvpx-devel libaom-devel libdav1d-devel zimg-devel rubberband-devel soxr-devel libvorbis-devel opus-devel lame-devel + else + echo "Note: please ensure rnnoise development headers are installed (pkg-config rnnoise)." >&2 + fi + if [[ ! -d FFmpeg/.git ]]; then + git clone https://github.com/FFmpeg/FFmpeg.git --depth=1 + fi + cd FFmpeg + RN_FLAG="" + # Some FFmpeg versions auto-detect rnnoise without a flag; include the flag only if supported + if ./configure --help | grep -q "librnnoise"; then + RN_FLAG="--enable-librnnoise" + else + echo "[info] configure has no --enable-librnnoise; relying on auto-detection via pkg-config (rnnoise)." >&2 + fi + + ./configure \ + --enable-gpl --enable-nonfree \ + --enable-libx264 --enable-libx265 --enable-libvpx --enable-libopus --enable-libmp3lame \ + --enable-libvorbis --enable-libass --enable-fontconfig --enable-libfreetype \ + --enable-librubberband --enable-libsoxr --enable-libzimg --enable-libvmaf \ + --enable-libdav1d --enable-libaom --enable-libsvtav1 \ + ${RN_FLAG} \ + --enable-ffplay --enable-ffprobe + make -j"$(nproc)" + echo "Build complete. You can run ./ffmpeg-build/FFmpeg/ffmpeg from this folder or 'sudo make install' to install system-wide." + set +x + else + echo "Skipped building from source." + fi +} + +main "$@"