Mix stems at specified dB levels into a single OGG file.
(
ffmpeg: str,
mix_name: str,
stem_levels: dict[str, float],
stems_dir: Path,
output: Path,
)
| 331 | |
| 332 | |
| 333 | def create_mix( |
| 334 | ffmpeg: str, |
| 335 | mix_name: str, |
| 336 | stem_levels: dict[str, float], |
| 337 | stems_dir: Path, |
| 338 | output: Path, |
| 339 | ) -> None: |
| 340 | """Mix stems at specified dB levels into a single OGG file.""" |
| 341 | inputs: list[str] = [] |
| 342 | filter_parts: list[str] = [] |
| 343 | |
| 344 | for i, (stem, db_level) in enumerate(stem_levels.items()): |
| 345 | stem_path = stems_dir / f"{stem}.ogg" |
| 346 | if not stem_path.exists(): |
| 347 | print(f" WARNING: Stem {stem_path} not found, skipping mix {mix_name}") |
| 348 | return |
| 349 | inputs.extend(["-i", str(stem_path)]) |
| 350 | # Convert dB to linear gain |
| 351 | import math |
| 352 | |
| 353 | gain = math.pow(10.0, db_level / 20.0) |
| 354 | filter_parts.append(f"[{i}:a]volume={gain:.4f}[s{i}]") |
| 355 | |
| 356 | n = len(stem_levels) |
| 357 | if n == 1: |
| 358 | # Single stem, just copy with volume |
| 359 | stem_name, db_level = next(iter(stem_levels.items())) |
| 360 | stem_path = stems_dir / f"{stem_name}.ogg" |
| 361 | import math |
| 362 | |
| 363 | gain = math.pow(10.0, db_level / 20.0) |
| 364 | run_ffmpeg( |
| 365 | ffmpeg, |
| 366 | [ |
| 367 | "-i", |
| 368 | str(stem_path), |
| 369 | "-af", |
| 370 | f"volume={gain:.4f}", |
| 371 | "-c:a", |
| 372 | "libvorbis", |
| 373 | "-b:a", |
| 374 | OGG_BITRATE, |
| 375 | str(output), |
| 376 | ], |
| 377 | f"Creating {mix_name}", |
| 378 | ) |
| 379 | else: |
| 380 | # Multi-stem mix |
| 381 | mixer_inputs = "".join(f"[s{i}]" for i in range(n)) |
| 382 | filter_complex = ( |
| 383 | ";".join(filter_parts) |
| 384 | + f";{mixer_inputs}amix=inputs={n}:duration=longest[out]" |
| 385 | ) |
| 386 | run_ffmpeg( |
| 387 | ffmpeg, |
| 388 | [ |
| 389 | *inputs, |
| 390 | "-filter_complex", |