Build Auphonic
KINDAback to the verdict
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.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- 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 - 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 - 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 - 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.
- 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.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
TARGET_LUFS | required | -16 | Integrated loudness target. Around -16 is usual for podcasts; streaming platforms normalise to about -14 and broadcast to -23. |
TRUE_PEAK_DB | required | -1.5 | True peak ceiling in dBTP. Headroom below zero prevents clipping when the file is encoded to a lossy format. |
LOUDNESS_RANGE | required | 11 | Target loudness range in LU. Speech usually sits lower than music. |
DENOISE_NR | required | 12 | Noise reduction strength. Low by default; raising it introduces artefacts around consonants. |
OUTPUT_DIR | required | ./out | Where processed files and reports are written. Never the source directory. |
WHISPER_MODEL_PATH | optional | ./models/ggml-base.en.bin | Optional. Leave unset to skip transcription entirely. |
The build, in order
Measure before you change anything
You can state the loudness of a file in the same units everyone else uses.
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.pyterminalffmpeg -i input.wav -af loudnorm=print_format=json -f null -
One row per file per stage. You will compare before and after, and that comparison is the report.
Files
src/db.pyGenerate a tone at a known level and confirm the measured figure matches. This calibrates your trust in everything downstream.
terminalffmpeg -f lavfi -i "sine=frequency=1000:duration=10" -af volume=-20dB tone.wav
done when · tick each as it passeswatch 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.
Normalising to a target that is right for speech
Every output lands on the same loudness target without being squashed to get there.
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.pyterminalffmpeg -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
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.pyRun 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 passeswatch 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.
Noise reduction that does not damage the voice
Steady background noise is reduced, and the speech still sounds like the person.
Rumble below the speech range carries no information and consumes loudness budget. A high pass is cheap and safe, unlike denoising.
Files
src/clean.pyThe afftdn filter at a light default. Expose the strength but do not raise the default because a test file improved.
terminalffmpeg -i in.wav -af "highpass=f=80,afftdn=nr=12:nf=-30" out.wav
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 passeswatch 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.
Multiple speakers and multiple files
A batch of recordings from different microphones ends up sounding like one conversation.
Enumerate audio files, measure all, then process all. Two passes over the batch, matching the two pass design.
Files
src/batch.pyFor multitrack input, normalise each track to the target, then mix with a documented gain reduction to avoid summing above the ceiling.
Files
src/mix.pyOne 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 passeswatch 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.
Transcript, chapters and the report
Each job produces the files and the evidence that they were processed correctly.
Downmix to mono 16 kHz 16 bit WAV for whisper.cpp specifically, from the source rather than the export.
terminalffmpeg -i out.wav -ar 16000 -ac 1 -c:a pcm_s16le for-whisper.wav
Long silences using ffmpeg's silence detection, or a supplied outline with timestamps. Do not guess from the transcript alone.
Files
src/chapters.pyterminalffmpeg -i in.wav -af silencedetect=n=-35dB:d=2 -f null -
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 passeswatch 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.
Presets, watch folders and running it for othersproduct builder
It runs unattended on a schedule and produces the same result every time.
Version controlled rather than stored in a database, so a change is reviewable and a past job's preset is recoverable.
Files
presets/podcast.jsonWait until the file size has been stable for a few seconds before touching it.
Files
src/watch.pyProcess the same input twice and compare measurements. A difference means something in the pipeline is not being fed the measured values properly.
Files
tests/test_determinism.py
done when · tick each as it passeswatch out- A watch folder that starts processing while a file is still being copied will read a truncated file and produce a broken export. Wait for the file size to stop changing, or require an accompanying marker file, before processing.
That is the whole plan for Auphonic. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Adaptive per-recording processing decisions.
- Cloud queues and processing that continues when your machine sleeps.
- Delivery integrations to podcast hosts.
- Licensed music and automatic ducking around it.
- proprietary adaptive audio processing, cloud queues, and broad format delivery
- remote studio reliability
- licensed music libraries
- hosting distribution
- advanced mastering and support
- Speaker diarisation to label who is talking in the transcript
- A loudness history chart across episodes, which catches drift over a season
- Automatic removal of long silences, with the threshold exposed
Need the files? The project pack on the verdict page hands your agent the whole brief · more podcasting.