aboutsummaryrefslogtreecommitdiff
path: root/lib/project/src
diff options
context:
space:
mode:
Diffstat (limited to 'lib/project/src')
-rw-r--r--lib/project/src/producer/cli.py13
-rw-r--r--lib/project/src/producer/config.py3
-rw-r--r--lib/project/src/producer/pipeline.py26
3 files changed, 19 insertions, 23 deletions
diff --git a/lib/project/src/producer/cli.py b/lib/project/src/producer/cli.py
index 52cd1bd..4788fb5 100644
--- a/lib/project/src/producer/cli.py
+++ b/lib/project/src/producer/cli.py
@@ -13,9 +13,9 @@ import tomllib
from rich.console import Console
from . import __version__
-from .audio import ffmpeg_run, loudness, measure
+from .audio import ffmpeg_run
from .config import PROFILES, Settings, resolve, toml
-from .pipeline import check_target, process, targets_for
+from .pipeline import check_target, process, report_for, targets_for
from .progress import Display
from .setup import data_dir, ensure_ffmpeg
@@ -24,7 +24,7 @@ def parser():
app = argparse.ArgumentParser(prog="producer", description="Local speech cleanup and podcast mastering. Originals are never overwritten.")
app.add_argument("--version", action="version", version=__version__)
sub = app.add_subparsers(dest="command", required=True)
- for name in ("process", "preview", "config", "analyze"):
+ for name in ("process", "preview", "config"):
cmd = sub.add_parser(name)
cmd.add_argument("--config", type=Path, help="TOML configuration file")
cmd.add_argument("--profile", choices=PROFILES, default=None)
@@ -104,11 +104,6 @@ def main():
print(json.dumps(asdict(settings), indent=2) if args.json else toml(settings), end="\n" if args.json else "")
return
with Display() as display:
- if args.command == "analyze":
- stats = measure(args.inputs[0], display.update)
- stats["loudness"] = loudness(ensure_ffmpeg(), args.inputs[0], settings, stats["duration_seconds"], display.update)
- print(json.dumps(stats, indent=2, allow_nan=False))
- return
if args.command == "process":
if args.output and len(args.inputs) != 1:
raise ValueError("--output requires exactly one input")
@@ -156,7 +151,7 @@ def main():
output = output_dir / f"{source.stem}.{profile}.preview.wav"
if output.resolve() == original:
raise ValueError("Preview output would overwrite the original")
- targets = [output, output.with_suffix(".report.json")]
+ targets = [output, report_for(output)]
resolved = {target.resolve() for target in targets}
if len(resolved) < len(targets) or resolved & claimed:
raise ValueError("Preview outputs have duplicate names")
diff --git a/lib/project/src/producer/config.py b/lib/project/src/producer/config.py
index ef0cd49..b1d84cc 100644
--- a/lib/project/src/producer/config.py
+++ b/lib/project/src/producer/config.py
@@ -12,7 +12,6 @@ class Settings:
device: str = "auto"
denoise_strength: float = 0.85
fft_reduction_db: float = 8.0
- channel: str = "auto"
highpass: bool = True
highpass_hz: float = 70.0
lowpass: bool = False
@@ -97,7 +96,7 @@ def resolve(path: Path | None = None, profile: str | None = None,
raise ValueError(f"{key} must be between {RANGES[key][0]} and {RANGES[key][1]}")
for key, options in {
"denoiser": ("deepfilter", "fft", "none"), "device": ("auto", "cpu", "cuda"),
- "channel": ("auto", "left", "right", "mix"), "hum_hz": (0, 50, 60),
+ "hum_hz": (0, 50, 60),
"sample_rate": (44100, 48000), "mp3_bitrate": (128, 160, 192, 224, 256, 320),
}.items():
if merged[key] not in options:
diff --git a/lib/project/src/producer/pipeline.py b/lib/project/src/producer/pipeline.py
index 8fb5484..d7c2ced 100644
--- a/lib/project/src/producer/pipeline.py
+++ b/lib/project/src/producer/pipeline.py
@@ -31,9 +31,14 @@ def filters(settings: Settings) -> list[str]:
return result or ["anull"]
+def report_for(destination: Path) -> Path:
+ """Diagnostic reports live in a logs/ directory, out of the way of outputs."""
+ return destination.parent / "logs" / destination.with_suffix(".report.json").name
+
+
def targets_for(destination: Path, settings: Settings) -> list[Path]:
"""Every path a successful run publishes: master WAV, report, optional MP3."""
- targets = [destination, destination.with_suffix(".report.json")]
+ targets = [destination, report_for(destination)]
if settings.mp3:
targets.append(destination.with_suffix(".mp3"))
return targets
@@ -66,18 +71,17 @@ def process(source: Path, destination: Path, settings: Settings, progress,
raise ValueError(f"Input must be an existing WAV file: {source}")
if destination.suffix.lower() != ".wav":
raise ValueError("Master output must have a .wav extension")
- report_path = destination.with_suffix(".report.json")
+ report_path = report_for(destination)
for target in targets_for(destination, settings):
check_target(source, target, overwrite)
ffmpeg = ensure_ffmpeg()
original = measure(source, progress, "Analyzing original")
if original["sample_peak_dbfs"] < -100:
raise ValueError("Recording is silent or too quiet to master safely")
- channel = settings.channel
- if original["channels"] == 2 and channel == "auto":
- raise ValueError("Stereo input: select --channel left, right, or mix explicitly. Auto downmix could cancel your voice.")
- if original["channels"] == 1 and channel == "right":
- raise ValueError("Cannot select the right channel of a mono recording")
+ if original["channels"] == 2:
+ raise ValueError("Stereo input: downmix to mono first, for example "
+ "'ffmpeg -i input.wav -ac 1 mono.wav'. An automatic "
+ "downmix could cancel your voice.")
duration = original["duration_seconds"]
warnings = []
if original["near_full_scale_samples"]:
@@ -85,13 +89,11 @@ def process(source: Path, destination: Path, settings: Settings, progress,
if max(abs(x) for x in original["dc_offset"]) > 0.01:
warnings.append("Significant input DC offset detected; enable highpass filtering to remove it.")
destination.parent.mkdir(parents=True, exist_ok=True)
+ report_path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix=".producer-", dir=destination.parent) as temporary:
work = Path(temporary)
prepared, cleaned, shaped = [work / name for name in ("prepared.wav", "cleaned.wav", "shaped.wav")]
input_filters = []
- if original["channels"] == 2:
- input_filters.append({"left": "pan=mono|c0=c0", "right": "pan=mono|c0=c1",
- "mix": "pan=mono|c0=0.5*c0+0.5*c1"}[channel])
if settings.highpass:
input_filters.append(f"highpass=f={settings.highpass_hz}:p=2")
if settings.hum_hz:
@@ -110,8 +112,8 @@ def process(source: Path, destination: Path, settings: Settings, progress,
cleaned = prepared
clean_stats = measure(cleaned, progress, "Measuring cleaned voice")
if clean_stats["sample_peak_dbfs"] < -100:
- raise ValueError("The selected audio is silent or too quiet to master safely; "
- "check --channel, the mix, and cleanup settings")
+ raise ValueError("The audio is silent or too quiet to master safely; "
+ "check the denoise and cleanup settings")
gain = 0.0
if settings.leveling:
speech_db = clean_stats["speech_level_estimate_dbfs"]