Skip to content

Latest commit

 

History

History
126 lines (98 loc) · 4.65 KB

File metadata and controls

126 lines (98 loc) · 4.65 KB

Studio

Post-production for spoken-word recordings: several tracks, each needing its own cleanup, assembled in order, delivered at a specified loudness.

This is the application the rest of the stack exists to make possible. With AuK alone it is a shell script of thirty auk-infer calls, manual chunking around the thirty-second window, and no way to tell whether any of it worked.

A recipe

{
  "name": "episode-12",
  "target_lufs": -16.0,
  "true_peak_db": -1.0,
  "gap_seconds": 0.4,
  "fade_out": 0.3,
  "tracks": [
    {
      "path": "raw/cold-open.wav",
      "label": "cold open",
      "request": "remove the noise and reverb, drop the breaths"
    },
    {
      "path": "raw/host.wav",
      "label": "host",
      "request": "clean it up and calm the delivery",
      "target_lufs": -18.0
    },
    {
      "path": "raw/guest.wav",
      "label": "guest",
      "request": "clean it up and take the phone-line sound off",
      "target_lufs": -18.0,
      "lead_in": 0.5
    }
  ],
  "output": "episode-12.wav"
}
openspeech studio episode.json -o build/
[1/3] cold open
[2/3] host
[3/3] guest
episode-12: 3 tracks -> build/episode-12.wav
  cold open            18.4s ->   18.4s   -24.1 ->  -16.0 LUFS  [pass]
  host                 42.0s ->   42.0s   -28.4 ->  -18.0 LUFS  [pass]
  guest                36.0s ->   36.0s   -31.2 ->  -18.0 LUFS  [warn]
    warning: 2 chunk level corrections hit the 6 dB limit
  master             -16.00 LUFS, true peak -1.00 dBTP, 97.6s

YAML works too with the yaml extra; JSON needs nothing.

Fields

Production

Field Default Meaning
name "production" Label used in reports
tracks required At least one track
output "<name>.wav" Output filename
target_lufs -16.0 Delivery loudness. -16 for podcasts, -23 for EBU R128 broadcast
true_peak_db -1.0 True-peak ceiling in dBTP
gap_seconds 0.35 Silence inserted between consecutive tracks
fade_in, fade_out 0.0, 0.25 Fades on the assembled episode
sample_rate highest input Assembly rate
seed none Set for a reproducible run

Track

Field Default Meaning
path required Relative to the recipe's directory, or absolute
request none What to do, in plain language. Omit to pass through untouched
label filename stem Name in reports
lead_in 0.0 Silence before this track
target_lufs none Per-track levelling, applied before assembly

Why per-track levelling

One quiet guest otherwise forces the master into a choice between an audible host and an inaudible answer. Levelling each track to a common target before assembly makes the final master a light touch rather than a rescue.

Set target_lufs slightly below the episode target (-18 against -16 is a reasonable start) so the master has headroom to work with.

The order of operations

  1. Per track — run its request through the full pipeline: planning, chunking, quality gates, repair.
  2. Per track — level to target_lufs if one is set.
  3. Assemble — reconcile sample rates, insert lead_in and gap_seconds, concatenate.
  4. Master — fades, then normalise the whole episode to target_lufs with the true-peak ceiling held.

Loudness is measured on the finished assembly, which is the only place the number means anything: an edit after mastering invalidates it.

The report

from openspeech.apps import load_recipe, run_production
from openspeech.engine import create_engine

result = run_production(load_recipe("episode.json"), create_engine("auto"), output_dir="build")

result.ok                      # every track processed without error or failure
result.master["output_lufs"]   # measured, not requested
result.master["true_peak_dbfs"]
result.master["clipped_frames"]

for track in result.tracks:
    print(track.label, track.verdict, track.input_lufs, "->", track.output_lufs)
    for step in track.steps:
        print("   ", step["op"], step["chunks"], step["verdict"])

result.as_dict() is JSON-serialisable, so a production run can be archived alongside the audio and diffed against the next one.

A missing track does not kill the episode

A file that cannot be read is recorded as an error on that track; the remaining tracks still produce an episode, and result.ok is False so a build script can decide what to do. Losing three good tracks because the fourth was misnamed is not a useful behaviour.

Reproducibility

Set seed in the recipe and the same recipe against the same inputs produces the same output. Worth doing for anything you might need to rebuild.