feat: add loud-only extraction mode with agate gating
This commit is contained in:
1 parent
46f8998f21
commit
e341aae658
2 files changed
+36
-13
No files matched your search
@@ -14,7 +14,7 @@ else
|
|||||||
PIP_INSTALL = $(PIP) install
|
PIP_INSTALL = $(PIP) install
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: default install install-cpu install-rocm install-cuda extract ast sync verify clean help
|
.PHONY: default install install-cpu install-rocm install-cuda extract extract-loud ast sync verify clean help
|
||||||
|
|
||||||
default: ast
|
default: ast
|
||||||
|
|
||||||
@@ -25,6 +25,7 @@ help:
|
|||||||
@echo " make install-rocm # force ROCm torch (RX 7900 XTX gfx1100)"
|
@echo " make install-rocm # force ROCm torch (RX 7900 XTX gfx1100)"
|
||||||
@echo " make install-cuda # force CUDA torch"
|
@echo " make install-cuda # force CUDA torch"
|
||||||
@echo " make extract # run extract.py (uses config.toml if present)"
|
@echo " make extract # run extract.py (uses config.toml if present)"
|
||||||
|
@echo " make extract-loud # extract with loud-only gating (keep explosions/transients)"
|
||||||
@echo " make ast # run sync_audio_test.py (audio_sync_test, PyQtGraph GUI)"
|
@echo " make ast # run sync_audio_test.py (audio_sync_test, PyQtGraph GUI)"
|
||||||
@echo " make sync LAG=0.042 # run apply_sync.py to mux synced output (LAG in seconds)"
|
@echo " make sync LAG=0.042 # run apply_sync.py to mux synced output (LAG in seconds)"
|
||||||
@echo " make verify LAG=0.042 # verify sync: video + dual waveform player"
|
@echo " make verify LAG=0.042 # verify sync: video + dual waveform player"
|
||||||
@@ -82,6 +83,9 @@ install-cuda:
|
|||||||
extract:
|
extract:
|
||||||
@if [ -x $(PY) ]; then $(PY) extract.py; else python3 extract.py; fi
|
@if [ -x $(PY) ]; then $(PY) extract.py; else python3 extract.py; fi
|
||||||
|
|
||||||
|
extract-loud:
|
||||||
|
@if [ -x $(PY) ]; then $(PY) extract.py loud; else python3 extract.py loud; fi
|
||||||
|
|
||||||
sync:
|
sync:
|
||||||
@if [ -x $(PY) ]; then PYTHONPATH=. $(PY) apply_sync.py --config $(CONFIG) --lag $(LAG); else PYTHONPATH=. python3 apply_sync.py --config $(CONFIG) --lag $(LAG); fi
|
@if [ -x $(PY) ]; then PYTHONPATH=. $(PY) apply_sync.py --config $(CONFIG) --lag $(LAG); else PYTHONPATH=. python3 apply_sync.py --config $(CONFIG) --lag $(LAG); fi
|
||||||
|
|
||||||
|
|||||||
+31
-12
@@ -5,6 +5,10 @@ Extracts the first 1.5 minutes of a video into:
|
|||||||
- an audio-only file (no video)
|
- an audio-only file (no video)
|
||||||
Output goes into an 'extracted' folder next to this script.
|
Output goes into an 'extracted' folder next to this script.
|
||||||
Requires: ffmpeg installed and on PATH.
|
Requires: ffmpeg installed and on PATH.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python extract.py # normal extraction
|
||||||
|
python extract.py loud # loud-only mode: gate out quiet audio, keep only transients
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -13,6 +17,11 @@ import sys
|
|||||||
import shutil
|
import shutil
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
# ─── Loud mode (pass "loud" as arg or env) ───
|
||||||
|
LOUD_MODE = "loud" in sys.argv
|
||||||
|
if LOUD_MODE:
|
||||||
|
sys.argv.remove("loud")
|
||||||
|
|
||||||
# ─── Input / config (config.toml is base config, fallback to hardcoded) ───
|
# ─── Input / config (config.toml is base config, fallback to hardcoded) ───
|
||||||
CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.toml")
|
CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.toml")
|
||||||
_cfg_inputs = None
|
_cfg_inputs = None
|
||||||
@@ -307,12 +316,14 @@ def _parse_loudnorm_json(stderr: str) -> dict | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def extract_audio_loudnorm(input_file: str, duration: int, start_sec: float,
|
def extract_audio_loudnorm(input_file: str, duration: int, start_sec: float,
|
||||||
out_path: str, stem: str, stream_idx: int | None = None) -> None:
|
out_path: str, stem: str, stream_idx: int | None = None,
|
||||||
|
loud_gate: bool = False) -> None:
|
||||||
"""Two-pass EBU R128 loudnorm extraction: measure then apply."""
|
"""Two-pass EBU R128 loudnorm extraction: measure then apply."""
|
||||||
ss_args = ["-ss", str(start_sec)] if start_sec and start_sec != 0 else []
|
ss_args = ["-ss", str(start_sec)] if start_sec and start_sec != 0 else []
|
||||||
map_args = ["-map", f"0:a:{stream_idx}"] if stream_idx is not None else []
|
map_args = ["-map", f"0:a:{stream_idx}"] if stream_idx is not None else []
|
||||||
base = ["ffmpeg", "-y", *ss_args, "-i", input_file, "-t", str(duration), "-vn", *map_args]
|
base = ["ffmpeg", "-y", *ss_args, "-i", input_file, "-t", str(duration), "-vn", *map_args]
|
||||||
measure_af = f"loudnorm=I={TARGET_LUFS}:TP={TARGET_TP}:LRA={TARGET_LRA}:print_format=json"
|
measure_af = f"loudnorm=I={TARGET_LUFS}:TP={TARGET_TP}:LRA={TARGET_LRA}:print_format=json"
|
||||||
|
gate_filter = ",agate=threshold=0.02:ratio=20:attack=5:release=50" if loud_gate else ""
|
||||||
|
|
||||||
# ─── Pass 1: measure ───
|
# ─── Pass 1: measure ───
|
||||||
print(f"[loudness] {stem}: pass 1/2 measuring…")
|
print(f"[loudness] {stem}: pass 1/2 measuring…")
|
||||||
@@ -320,6 +331,9 @@ def extract_audio_loudnorm(input_file: str, duration: int, start_sec: float,
|
|||||||
r = subprocess.run(cmd_measure, capture_output=True, text=True)
|
r = subprocess.run(cmd_measure, capture_output=True, text=True)
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
print(f"[loudness] {stem}: pass 1 failed ({r.stderr.strip()[-80:]}), falling back to simple extract", file=sys.stderr)
|
print(f"[loudness] {stem}: pass 1 failed ({r.stderr.strip()[-80:]}), falling back to simple extract", file=sys.stderr)
|
||||||
|
if loud_gate:
|
||||||
|
cmd_fallback = base + ["-af", f"agate=threshold=0.02:ratio=20:attack=5:release=50", *AUDIO_CODEC, out_path]
|
||||||
|
else:
|
||||||
cmd_fallback = base + [*AUDIO_CODEC, out_path]
|
cmd_fallback = base + [*AUDIO_CODEC, out_path]
|
||||||
r2 = subprocess.run(cmd_fallback, capture_output=True, text=True)
|
r2 = subprocess.run(cmd_fallback, capture_output=True, text=True)
|
||||||
if r2.returncode != 0:
|
if r2.returncode != 0:
|
||||||
@@ -329,6 +343,9 @@ def extract_audio_loudnorm(input_file: str, duration: int, start_sec: float,
|
|||||||
measured = _parse_loudnorm_json(r.stderr)
|
measured = _parse_loudnorm_json(r.stderr)
|
||||||
if measured is None:
|
if measured is None:
|
||||||
print(f"[loudness] {stem}: could not parse measurement, falling back to simple extract", file=sys.stderr)
|
print(f"[loudness] {stem}: could not parse measurement, falling back to simple extract", file=sys.stderr)
|
||||||
|
if loud_gate:
|
||||||
|
cmd_fallback = base + ["-af", f"agate=threshold=0.02:ratio=20:attack=5:release=50", *AUDIO_CODEC, out_path]
|
||||||
|
else:
|
||||||
cmd_fallback = base + [*AUDIO_CODEC, out_path]
|
cmd_fallback = base + [*AUDIO_CODEC, out_path]
|
||||||
r2 = subprocess.run(cmd_fallback, capture_output=True, text=True)
|
r2 = subprocess.run(cmd_fallback, capture_output=True, text=True)
|
||||||
if r2.returncode != 0:
|
if r2.returncode != 0:
|
||||||
@@ -346,7 +363,7 @@ def extract_audio_loudnorm(input_file: str, duration: int, start_sec: float,
|
|||||||
print(f"[loudness] {stem}: pass 2/2 applying loudnorm → {TARGET_LUFS} LUFS…")
|
print(f"[loudness] {stem}: pass 2/2 applying loudnorm → {TARGET_LUFS} LUFS…")
|
||||||
apply_af = (f"loudnorm=I={TARGET_LUFS}:TP={TARGET_TP}:LRA={TARGET_LRA}"
|
apply_af = (f"loudnorm=I={TARGET_LUFS}:TP={TARGET_TP}:LRA={TARGET_LRA}"
|
||||||
f":measured_I={mi}:measured_TP={mtp}:measured_LRA={mlra}"
|
f":measured_I={mi}:measured_TP={mtp}:measured_LRA={mlra}"
|
||||||
f":measured_thresh={mthresh}:offset={offset}:linear=true")
|
f":measured_thresh={mthresh}:offset={offset}:linear=true{gate_filter}")
|
||||||
cmd_apply = base + ["-af", apply_af, *AUDIO_CODEC, out_path]
|
cmd_apply = base + ["-af", apply_af, *AUDIO_CODEC, out_path]
|
||||||
r = subprocess.run(cmd_apply, capture_output=True, text=True)
|
r = subprocess.run(cmd_apply, capture_output=True, text=True)
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
@@ -470,19 +487,21 @@ for _file_idx, INPUT_FILE in enumerate(INPUT_FILES):
|
|||||||
print(f"[audio: {stem}] skipped (no stream selected)")
|
print(f"[audio: {stem}] skipped (no stream selected)")
|
||||||
elif NORMALIZE:
|
elif NORMALIZE:
|
||||||
print(f"[audio: {stem}] extracting stream {audio_stream} with two-pass loudnorm…")
|
print(f"[audio: {stem}] extracting stream {audio_stream} with two-pass loudnorm…")
|
||||||
extract_audio_loudnorm(INPUT_FILE, DURATION, START_SEC, audio_out, stem, stream_idx=audio_stream)
|
extract_audio_loudnorm(INPUT_FILE, DURATION, START_SEC, audio_out, stem,
|
||||||
|
stream_idx=audio_stream, loud_gate=LOUD_MODE)
|
||||||
else:
|
else:
|
||||||
map_args = ["-map", f"0:a:{audio_stream}"]
|
map_args = ["-map", f"0:a:{audio_stream}"]
|
||||||
|
if LOUD_MODE:
|
||||||
cmd_audio = [
|
cmd_audio = [
|
||||||
"ffmpeg",
|
"ffmpeg", "-y", *ss_args, "-i", INPUT_FILE, "-t", str(DURATION),
|
||||||
"-y",
|
"-vn", *map_args, "-af",
|
||||||
*ss_args,
|
"agate=threshold=0.02:ratio=20:attack=5:release=50",
|
||||||
"-i", INPUT_FILE,
|
*AUDIO_CODEC, audio_out
|
||||||
"-t", str(DURATION),
|
]
|
||||||
"-vn", # no video
|
else:
|
||||||
*map_args, # selected audio stream
|
cmd_audio = [
|
||||||
*AUDIO_CODEC, # pcm_f32le 48kHz mono, CPU (no GPU equiv.)
|
"ffmpeg", "-y", *ss_args, "-i", INPUT_FILE, "-t", str(DURATION),
|
||||||
audio_out
|
"-vn", *map_args, *AUDIO_CODEC, audio_out
|
||||||
]
|
]
|
||||||
print(f"[audio: {stem}] running ffmpeg (no normalization)…")
|
print(f"[audio: {stem}] running ffmpeg (no normalization)…")
|
||||||
r = subprocess.run(cmd_audio, capture_output=True, text=True)
|
r = subprocess.run(cmd_audio, capture_output=True, text=True)
|
||||||
|
|||||||
Reference in new issue
Block a user