From 9dfc102823e1fd73454a8590c37bfc8711acd5d8 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sun, 12 Oct 2025 18:57:55 +0200 Subject: [PATCH] fix: getting rnnoise model --- Bash/README_clean_audio.md | 104 ++++++++ Bash/clean_audio.sh | 390 +++++++++++++++++++++++++++++ Bash/get_rnnoise_model.sh | 190 ++++++++++++++ Bash/install_ffmpeg_with_arnndn.sh | 130 ++++++++++ test_input.wav | Bin 0 -> 176478 bytes test_input_clean.wav | Bin 0 -> 64078 bytes 6 files changed, 814 insertions(+) create mode 100644 Bash/README_clean_audio.md create mode 100755 Bash/clean_audio.sh create mode 100755 Bash/get_rnnoise_model.sh create mode 100755 Bash/install_ffmpeg_with_arnndn.sh create mode 100644 test_input.wav create mode 100644 test_input_clean.wav diff --git a/Bash/README_clean_audio.md b/Bash/README_clean_audio.md new file mode 100644 index 0000000..c0bc61f --- /dev/null +++ b/Bash/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/Bash/clean_audio.sh b/Bash/clean_audio.sh new file mode 100755 index 0000000..023b038 --- /dev/null +++ b/Bash/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/Bash/get_rnnoise_model.sh b/Bash/get_rnnoise_model.sh new file mode 100755 index 0000000..2ac1057 --- /dev/null +++ b/Bash/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/Bash/install_ffmpeg_with_arnndn.sh b/Bash/install_ffmpeg_with_arnndn.sh new file mode 100755 index 0000000..8c384c1 --- /dev/null +++ b/Bash/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 "$@" diff --git a/test_input.wav b/test_input.wav new file mode 100644 index 0000000000000000000000000000000000000000..debde9089007c13e0e32787caec1e5bc62040512 GIT binary patch literal 176478 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 i#=h