Build Auphonic

KINDAback to the verdict

0%0 of 20 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A command line and small interface over ffmpeg that measures each file, normalises it to a podcast loudness target with a true peak ceiling, reduces steady noise, evens out speakers recorded at different levels, and exports production ready files with a report showing the measured figures before and after.

estimated effort weekendthe files for this build are in the project pack

Processingffmpeg with the loudnorm and afftdn filtersRuntimePython 3.12 driving ffmpeg as a subprocessMeasurementTwo pass loudnorm: measure, then apply with linear correctionTranscriptionwhisper.cpp locally, optionalStorageOriginal files untouched, outputs written alongside with a JSON report

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why It is the entire processing engine. A build without these filters cannot do any of this.

    Get it brew install ffmpeg, your package manager, or a static build from ffmpeg.org. Confirm the filters are present rather than assuming. open ↗

    Verify ffmpeg -filters lists loudnorm and afftdn

  2. installfree

    Why Runs the orchestration, measurement parsing and batch logic.

    Get it python.org or your package manager, then a virtual environment in the project directory.

    Verify python3 --version prints 3.12 or higher

  3. have readyfree

    Why A clean studio file proves nothing. You need one with steady hiss, one with two speakers at clearly different levels, and one already close to target.

    Get it Use your own recordings, or record them deliberately. Keep them as fixtures so a settings change can be compared against the same material.

    Verify All three are saved as fixtures with their initial measurements recorded

  4. have readyfree if you already have a pair

    Why Denoising decisions are made by ear, and laptop speakers hide exactly the artefacts you are trying to avoid.

    Get it Any headphones you have listened to enough to know how they sound. Familiarity matters more than price here.

  5. installfree, but transcription takes real time on your own machine

    Why Phase 5 only, for transcripts and chapter assistance. Everything else works without it.

    Get it Build whisper.cpp and download a model. Model sizes vary from tens of megabytes to several gigabytes, and the larger ones are much slower. open ↗

    Verify whisper-cli transcribes a short 16 kHz mono WAV

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- jobs(id, created_at, preset_id, state)
- files(id, job_id, source_path, output_path, duration_ms, channels, sample_rate)
- measurements(file_id, stage, input_i, input_tp, input_lra, input_thresh, target_offset) · stage is before or after
- presets(id, name, target_lufs, true_peak_db, denoise_strength, output_format, bitrate)
- The measurement rows are the product. A processed file without its before and after figures is a file you cannot defend.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
TARGET_LUFSrequired-16Integrated loudness target. Around -16 is usual for podcasts; streaming platforms normalise to about -14 and broadcast to -23.
TRUE_PEAK_DBrequired-1.5True peak ceiling in dBTP. Headroom below zero prevents clipping when the file is encoded to a lossy format.
LOUDNESS_RANGErequired11Target loudness range in LU. Speech usually sits lower than music.
DENOISE_NRrequired12Noise reduction strength. Low by default; raising it introduces artefacts around consonants.
OUTPUT_DIRrequired./outWhere processed files and reports are written. Never the source directory.
WHISPER_MODEL_PATHoptional./models/ggml-base.en.binOptional. Leave unset to skip transcription entirely.

The build, in order

  1. Measure before you change anything

    You can state the loudness of a file in the same units everyone else uses.

    1. The filter prints a JSON block on the error stream in print_format=json mode. Parse it rather than scraping the human readable output, which changes between versions.

      Files src/measure.py

      terminal
      ffmpeg -i input.wav -af loudnorm=print_format=json -f null -
    2. One row per file per stage. You will compare before and after, and that comparison is the report.

      Files src/db.py

    3. Generate a tone at a known level and confirm the measured figure matches. This calibrates your trust in everything downstream.

      terminal
      ffmpeg -f lavfi -i "sine=frequency=1000:duration=10" -af volume=-20dB tone.wav
    done when · tick each as it passes
    watch out
    • Peak and true peak are different numbers. A sample peak meter reads the highest sample value, while true peak accounts for the higher levels that appear when the signal is reconstructed between samples. A file that peaks at 0 dB on a sample meter can clip audibly once encoded to a lossy format. Read the true peak figure, not the sample peak.
  2. Normalising to a target that is right for speech

    Every output lands on the same loudness target without being squashed to get there.

    1. Pass measured_I, measured_TP, measured_LRA and measured_thresh back to loudnorm with linear=true. Without them, the second pass measures again and compresses.

      Files src/normalise.py

      terminal
      ffmpeg -i in.wav -af loudnorm=I=-16:TP=-1.5:LRA=11:measured_I=-24.1:measured_TP=-3.2:measured_LRA=9.4:measured_thresh=-34.9:linear=true -ar 48000 out.wav
    2. Podcast, broadcast and streaming destinations differ. Store the target with the preset and record it in the report rather than hard coding one number.

      Files src/presets.py

    3. Run Phase 1's analysis on the result. This is a closed loop and it is the whole quality argument.

      Files src/verify.py

    done when · tick each as it passes
    watch out
    • Single pass loudnorm compresses dynamics to reach the target, which on speech sounds pumped and tiring. The two pass form feeds the measured values back in and applies a linear gain instead. This is the single biggest difference between output that sounds processed and output that sounds level.
    • A true peak ceiling of 0 dBTP will clip once encoded to a lossy format, because encoding introduces peaks above the original samples. Leave headroom; -1.5 dBTP is a common choice for exactly this reason.
  3. Noise reduction that does not damage the voice

    Steady background noise is reduced, and the speech still sounds like the person.

    1. Rumble below the speech range carries no information and consumes loudness budget. A high pass is cheap and safe, unlike denoising.

      Files src/clean.py

    2. The afftdn filter at a light default. Expose the strength but do not raise the default because a test file improved.

      terminal
      ffmpeg -i in.wav -af "highpass=f=80,afftdn=nr=12:nf=-30" out.wav
    3. A command that plays the same passage from source and output back to back. Denoising decisions must be made by ear, not by a number.

      Files src/compare.py

    done when · tick each as it passes
    watch out
    • Aggressive spectral denoising produces a watery, metallic artefact around consonants that is far more distracting than the hiss it removed. It also cannot be undone from the processed file. Default to a light setting, and require a deliberate choice to go further.
  4. Multiple speakers and multiple files

    A batch of recordings from different microphones ends up sounding like one conversation.

    1. Enumerate audio files, measure all, then process all. Two passes over the batch, matching the two pass design.

      Files src/batch.py

    2. For multitrack input, normalise each track to the target, then mix with a documented gain reduction to avoid summing above the ceiling.

      Files src/mix.py

    3. One unreadable file must not abort a batch of forty. Record the error against the file and continue.

      Files src/batch.py

    done when · tick each as it passes
    watch out
    • Normalising a mixed-down file cannot fix two speakers who were recorded at different levels, because by then they share one signal. Correct each track first, then mix. If you only have the mix, say so in the report rather than pretending the result is equivalent.
  5. Transcript, chapters and the report

    Each job produces the files and the evidence that they were processed correctly.

    1. Downmix to mono 16 kHz 16 bit WAV for whisper.cpp specifically, from the source rather than the export.

      terminal
      ffmpeg -i out.wav -ar 16000 -ac 1 -c:a pcm_s16le for-whisper.wav
    2. Long silences using ffmpeg's silence detection, or a supplied outline with timestamps. Do not guess from the transcript alone.

      Files src/chapters.py

      terminal
      ffmpeg -i in.wav -af silencedetect=n=-35dB:d=2 -f null -
    3. JSON and a readable summary. Before and after figures per file, the preset, and every warning. This is what makes the job auditable.

      Files src/report.py

    done when · tick each as it passes
    watch out
    • whisper.cpp accepts 16 bit WAV at 16 kHz and will refuse or misbehave with other formats, so convert before transcribing rather than feeding it your production master. Transcription is also slow, so make it optional and run it after the audio is already exported.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more podcasting.