Audio Fx
src.xil_pipeline.audio_fx
ffmpeg-backed audio treatments for dialogue stems.
The rest of the mixing pipeline uses pydub, which offers only single-pole high/low-pass filters, gain and fades. Treatments that need compression, saturation, parametric EQ or delay are built here instead, as ffmpeg filter graphs. ffmpeg is already a hard requirement (pydub shells out to it for decode/encode), so this adds no new dependency.
Audio is round-tripped through ffmpeg as raw PCM over pipes rather than temp files: there is no lossy intermediate re-encode, and no filesystem churn on drvfs-mounted checkouts where temp-file I/O is dramatically slower than a pipe.
Every treatment is length-preserving. Some ffmpeg filters (notably
aecho) extend their output, and the pipeline derives cue positions from
unfiltered MP3 header durations — a treatment that changed a stem's length
would silently desync the rendered mix from the label/dry-run timeline.
:func:run_ffmpeg_filter therefore trims or pads output back to the input
length by default.
Failures degrade rather than abort. A DAW export runs ffmpeg once per treated
stem across a whole episode; killing a multi-minute render because one
invocation failed is worse than emitting an untreated stem plus a warning. Set
XIL_STRICT_FX in the environment to raise :class:AudioFxError instead.
Module Attributes
TREATMENTS: Registry of named treatments, keyed by treatment name.
STRICT_ENV_VAR: Environment variable that switches failures from
warn-and-passthrough to raising :class:AudioFxError.
FILM
module-attribute
FILM = Treatment(name='film', summary='Warm, reflective 1990s indie film print — rolled-off top, recessed presence, tape-style saturation and an audible pink-noise grain.', graph='[0:a]highpass=f=90:poles=2,lowpass=f=5500:poles=2,lowshelf=f=180:g=3,equalizer=f=350:w=1.2:t=q:g=2,equalizer=f=2800:w=1.6:t=q:g=-6,highshelf=f=7000:g=-4,acompressor=threshold=-24dB:ratio=3.5:attack=12:release=280:makeup=2:knee=6,volume=12dB,asoftclip=type=tanh:param=1:oversample=4,volume=-12dB,volume=8.4dB[v];anoisesrc=color=pink:amplitude=0.018:sample_rate={rate},aformat=channel_layouts={layout}[n];[v][n]amix=inputs=2:duration=first:normalize=0[out]')
SPEAKERPHONE
module-attribute
SPEAKERPHONE = Treatment(name='speakerphone', summary='Narrow-band speakerphone — steep 350 Hz/3.4 kHz skirts, hard AGC, odd-harmonic crunch and a short tabletop slap.', graph='[0:a]highpass=f=350:poles=2,highpass=f=350:poles=2,lowpass=f=3400:poles=2,lowpass=f=3400:poles=2,equalizer=f=700:w=1.0:t=q:g=-4,equalizer=f=1800:w=1.1:t=q:g=5,acompressor=threshold=-24dB:ratio=6:attack=5:release=120:makeup=3:knee=2,volume=8dB,asoftclip=type=atan:param=1:oversample=4,volume=-8dB,aecho=0.9:0.9:55:0.22,volume=13.6dB[out]')
PHONE
module-attribute
PHONE = Treatment(name='phone', summary='Mobile call — steep 300 Hz/3.4 kHz skirts, earpiece presence lift, hard AGC and genuine GSM codec grit, sitting just under the room.', graph='[0:a]highpass=f=300:poles=2,highpass=f=300:poles=2,lowpass=f=3400:poles=2,lowpass=f=3400:poles=2,equalizer=f=500:w=1.0:t=q:g=-4,equalizer=f=1700:w=1.2:t=q:g=6,acompressor=threshold=-22dB:ratio=8:attack=3:release=90:makeup=3:knee=2,volume=6dB,asoftclip=type=atan:param=1:oversample=4,volume=-6dB,volume=10dB[out]', codec='libgsm', container='gsm', codec_rate=8000)
TREATMENTS
module-attribute
AudioFxError
Bases: RuntimeError
Raised when an ffmpeg treatment fails and strict mode is enabled.
Treatment
dataclass
A named ffmpeg filter graph applied to dialogue stems.
Attributes:
-
name(str) –Registry key, as written in a cast config
filterfield. -
graph(str) –ffmpeg
-filter_complexgraph producing a[out]pad. May contain{rate}and{layout}placeholders, substituted with the input segment's sample rate and channel layout. -
summary(str) –One-line description of the sound, for docs and logs.
-
codec(str | None) –Optional ffmpeg encoder to round-trip the filtered audio through, for treatments whose character comes from real codec artifacts rather than from EQ alone.
Noneskips the round-trip entirely, which is the behaviour every filter-only treatment relies on. -
container(str | None) –Muxer for that round-trip. Codec and container are not freely interchangeable —
libgsmneeds the rawgsmformat and errors out inside a WAV container. -
codec_rate(int | None) –Sample rate to encode at. This is where a telephone treatment gets its hard ceiling: encoding at 8 kHz brickwalls at 4 kHz far more steeply than any practical filter cascade.
Source code in src/xil_pipeline/audio_fx.py
ffmpeg_available
Report whether the configured ffmpeg binary can be executed.
Returns:
-
bool–True if ffmpeg responds to
-version, False otherwise.
Source code in src/xil_pipeline/audio_fx.py
encoder_available
Report whether this ffmpeg build ships a given encoder.
ffmpeg builds differ in what they bundle, and the codec-backed treatments degrade quietly when an encoder is absent rather than failing. This is what lets a caller — a test, or someone wondering why a treatment sounds different on another machine — tell the two situations apart.
Notably libgsm is present in Debian's ffmpeg but not in the GitHub
macOS or Windows runner builds, so phone renders band-limited but
without its codec grit on those platforms.
Parameters:
-
name(str) –Encoder name, e.g.
"libgsm".
Returns:
-
bool–True if ffmpeg lists the encoder, False if it does not or cannot run.
Source code in src/xil_pipeline/audio_fx.py
missing_codecs
Map each named treatment to the encoder it needs but cannot find.
A treatment whose codec is missing still renders — it degrades to the filter-only result — so nothing fails and the difference is easy to miss until two machines produce masters that do not match. This is what lets a render stage say so up front instead of one warning buried mid-run.
Parameters:
Returns:
-
dict[str, str]–{treatment_name: encoder_name}for treatments that declare a codec -
dict[str, str]–this ffmpeg build does not ship. Empty when everything is available,
-
dict[str, str]–including when no named treatment uses a codec at all.
Source code in src/xil_pipeline/audio_fx.py
run_ffmpeg_filter
run_ffmpeg_filter(segment: AudioSegment, graph: str, *, preserve_length: bool = True, label: str = '', codec: str | None = None, container: str | None = None, codec_rate: int | None = None) -> AudioSegment
Push a segment through an ffmpeg -filter_complex graph.
Parameters:
-
segment(AudioSegment) –Input audio.
-
graph(str) –Filter graph producing an
[out]pad.{rate}and{layout}placeholders are substituted fromsegment. -
preserve_length(bool, default:True) –Trim or pad the result back to the input length. Leave enabled unless the caller genuinely wants a length change; the pipeline's cue timeline assumes stems keep their duration.
-
label(str, default:'') –Treatment name, used in log messages and cache keys.
-
codec(str | None, default:None) –Optional encoder to round-trip the filtered audio through, for treatments whose character is a real codec artifact.
-
container(str | None, default:None) –Muxer for that round-trip; required alongside codec.
-
codec_rate(int | None, default:None) –Sample rate to encode at, or
Noneto keep the input's.
Returns:
-
AudioSegment–The treated segment, or
segmentunchanged if ffmpeg is unavailable -
AudioSegment–or the invocation fails and strict mode is off.
Raises:
-
AudioFxError–On failure when
XIL_STRICT_FXis set.
Source code in src/xil_pipeline/audio_fx.py
473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 | |
apply_treatment
Apply a named treatment from :data:TREATMENTS to a segment.
Parameters:
-
segment(AudioSegment) –Input audio.
-
name(str) –Treatment name, e.g.
"film"or"speakerphone".
Returns:
-
AudioSegment–The treated segment, or
segmentunchanged if the name is unknown -
AudioSegment–or the ffmpeg invocation fails outside strict mode.
Source code in src/xil_pipeline/audio_fx.py
clear_cache
Empty the treatment result cache.
Intended for tests; the cache is otherwise self-limiting.