---
title: "FFmpeg Normalize Audio: Two-Pass Voiceover on Mac"
description: "Normalize local narration with FFmpeg loudnorm, inspect two-pass JSON and encoded WAV measurements, and download a tested Mac workflow."
canonical: "https://www.murmurtts.com/blog/resources/ffmpeg-normalize-voiceover-audio"
---
[Murmur](https://www.murmurtts.com/)/[Resources](https://www.murmurtts.com/blog/resources)/FFmpeg Normalize Audio: Two-Pass Voiceover on Mac

Guide

# FFmpeg Normalize Audio: Two-Pass Voiceover on Mac

Normalize local narration with FFmpeg loudnorm, inspect two-pass JSON and encoded WAV measurements, and download a tested Mac workflow.

Murmur·Published October 9, 2026·7 min read

On this page

Choose peak adjustment or loudness normalizationPick an illustrative target and an approved takeMeasure once, then process with those valuesRead the results from the actual encoded WAVStop on silence or an output collisionFinish the reel and check its final audioFrequently asked questionsSources

[All resource guides](https://www.murmurtts.com/blog/resources)

**Direct answer:** to normalize audio with FFmpeg for a local voiceover, measure the complete file with `loudnorm`, pass those measurements into a second processing pass, then measure the encoded WAV. Keep the original take and write a new filename. This workflow targets integrated loudness and true peak, which are different from raising the highest sample to a chosen level. It is useful when finishing narration for a promotional reel before adding music or exporting the final video.

We executed this recipe on October 9, 2026 with FFmpeg 9.0.2. The tests included an original 24-second file with six different level plateaus, a previously generated 7.12-second local narration, and silence. Download the [complete Python workflow source](https://www.murmurtts.com/downloads/ffmpeg-voiceover-loudness-workflow.txt). It needs no Python packages and prints the measurements and log locations. The examples below demonstrate processing; they do not establish a delivery standard or listening-quality score.

## Choose peak adjustment or loudness normalization

Integrated loudness, reported in LUFS, summarizes the measured loudness of a program. True peak estimates peaks between samples and is reported here in dBTP. Loudness range, or LRA, describes level variation in LU. A fixed gain changes the whole track equally. It can match overall level while leaving a quiet sentence quieter than the next loud sentence.

| Your task | Useful starting point | Limit to check |
| --- | --- | --- |
| Raise a file to a sample-peak level | The browser audio normalizer | It does not measure LUFS or true peak. |
| Match overall narration level while retaining dynamics | Two-pass loudnorm, with linear processing if feasible | Gain can make existing noise louder. |
| Reduce large changes inside one clip | Review edits, then consider dynamic processing | The requested LRA is not guaranteed. |

The [browser audio normalizer](https://www.murmurtts.com/tools/audio-normalizer) applies constant gain toward a sample peak of -1 dBFS and exports WAV. Choose this FFmpeg recipe when you need integrated measurements and a true-peak constraint. Both workflows can produce louder files, but the measurements answer different questions. Neither repairs distorted syllables or restores words missing from the source.

## Pick an illustrative target and an approved take

This example requests -16 LUFS integrated loudness, a maximum true peak of -1.5 dBTP, and LRA 11 LU. These are working settings for the test, not universal YouTube, podcast, broadcast, or audiobook requirements. Use the specification for your actual destination when one exists. A peak ceiling is a maximum: a result below it can be valid without being equally loud at every moment.

Start with a narration take whose wording, pronunciation, and timing you have checked. Export WAV from Murmur, or use an already completed local CLI or MCP job. The [MCP setup guide](https://www.murmurtts.com/blog/resources/connect-murmur-mcp-local-voiceover) explains that earlier generation step. This recipe only processes the audio you provide. FFmpeg and Python 3 are separate Terminal dependencies; running the installed Murmur app does not install them for this workflow.

```
ffmpeg -version
ffmpeg -hide_banner -h filter=loudnorm
python3 --version
```

Keep the source file unchanged and choose a new output name. The helper selects the first audio stream, preserves its channel count, and writes 48 kHz, 24-bit PCM WAV. It does not force mono or implement a stereo-delivery assumption. Check channel layout when importing a mono narration into a stereo timeline, because the eventual mix changes what needs to be measured.

## Measure once, then process with those values

The first pass analyzes the whole file without creating audio. FFmpeg prints JSON after its normal log messages. This manual command shows that measurement stage; the download runs it and captures its output automatically in a fresh temporary directory.

```
ffmpeg -nostdin -hide_banner -i "narration.wav" -map 0:a:0 \
  -af "loudnorm=I=-16:TP=-1.5:LRA=11:print_format=json" \
  -f null -
```

For the second pass, the helper maps `input_i` to `measured_I`, `input_tp` to `measured_TP`, `input_lra` to `measured_LRA`, and `input_thresh` to `measured_thresh`. It also supplies the first pass's `target_offset` as `offset`. Copying numbers from someone else's clip would defeat this step. Run the downloaded text file directly with Python; its contents are the executable source.

```
python3 ffmpeg-voiceover-loudness-workflow.txt \
  "narration.wav" "narration-normalized.wav"
```

The helper requests `linear=true`, which can preserve relative levels using constant gain. FFmpeg can switch to dynamic processing when the requested LRA is below the measured range or the gain would exceed the true-peak ceiling. Read `second_pass.normalization_type` instead of assuming the flag determined the outcome. An explicit output sample rate also prevents dynamic mode's internal high-rate processing from determining your WAV delivery rate.

## Read the results from the actual encoded WAV

The script performs a final analysis after writing the candidate WAV. In `encoded_wav`, read `input_i`, `input_tp`, and `input_lra`: those measure the encoded file. The first pass's `output_*` fields describe its proposed processing, so they are not proof of your saved result. Second-pass statistics can differ from a later measurement too.

| Input and request | Second-pass mode | Encoded WAV: loudnorm I / TP / LRA |
| --- | --- | --- |
| 24-second varying levels; LRA 11 | linear | -16.00 LUFS / -2.41 dBTP / 9.10 LU |
| Same input; LRA 3 | dynamic | -16.27 LUFS / -1.78 dBTP / 6.00 LU |
| 7.12-second development narration; LRA 11 | dynamic | -16.07 LUFS / -1.50 dBTP / 1.50 LU |

The varying-level input measured -32.69 LUFS and -19.10 dBTP in the first pass. Its linear output retained the level differences: each four-second segment gained 16.69 dB. Requesting LRA 3 triggered dynamic processing but did not produce LRA 3. The short speech file came from an earlier development helper reporting Murmur 1.0.18; it does not prove parity with the released app. We made no new speech-generation request or listening-quality claim for these tests.

The separate `ebur128` meter reported the linear file as -15.6 LUFS and the dynamic file as -16.0 LUFS, versus the table's loudnorm results. Keep the meter and version recorded, and use your delivery workflow's measurement for acceptance. Successful execution means a file was processed; it does not guarantee an exact target or platform compliance.

## Stop on silence or an output collision

- Silent or extremely quiet input can return non-finite measurements such as -inf. The helper rejects them before processing; do not replace them with zero.
- An existing output filename is refused. Choose another name, including for a retry, and preserve previous exports.
- Read the printed log path if FFmpeg fails or returns invalid statistics. Confirm that the selected stream contains the intended voiceover.
- Inspect the measured result and listen before accepting it. Dynamic gain can change delivery and raise the noise floor; missing words and clipping need earlier repairs.

The silence test stopped without producing an output. Reusing an existing output name also stopped, and its file hash stayed unchanged. Processing uses a temporary candidate and installs the result exclusively, so a late filename collision cannot replace another export. Logs remain in the printed temporary directory and can include filenames and metadata; keep them private when sharing the recipe.

## Finish the reel and check its final audio

```
ffprobe -v error -select_streams a:0 \
  -show_entries stream=codec_name,sample_rate,channels,duration \
  -of json "narration-normalized.wav"

ffmpeg -hide_banner -loglevel error \
  -i "narration-normalized.wav" -f null -
```

The tested outputs decoded completely and retained their input durations. These checks catch format or decoding problems, but they do not assess sound quality. Import the accepted WAV into your reel, set the music underneath it, and remeasure after mixing and final encoding. AAC or MP3 conversion can change peaks, and narration-only loudness is not the loudness of a complete soundtrack.

Continue with the [local promotional reel workflow](https://www.murmurtts.com/blog/resources/ai-promo-reel-local-voiceover-mac) for the render, or the [Remotion voiceover guide](https://www.murmurtts.com/blog/resources/remotion-local-voiceover-mac) for a React composition. Normalize only an approved take and keep scene timing tied to that file. If the finished mix misses your delivery specification, revise and verify it before publishing.

## Frequently asked questions

Can FFmpeg normalize audio in one pass?

Yes. For a saved file, two passes let the processor use measurements of the complete source. This guide adds a final encoded-file analysis so you can inspect what was actually saved.

Does linear=true guarantee constant-gain processing?

No. Read normalization_type in the second-pass JSON. FFmpeg may switch to dynamic processing when the requested range or true-peak condition cannot be satisfied by linear scaling.

Will setting LRA 3 make every sentence equally loud?

No. Our varying-level test requested LRA 3 and remeasured 6.00 LU with loudnorm. Review uneven edits and use listening plus the final measurements rather than treating a parameter as a guarantee.

Why does the recipe write WAV instead of MP3?

It creates a PCM working file for the editor and makes codec settings explicit. Export the final delivery format later and remeasure it after encoding and mixing.

Does this require Murmur?

FFmpeg can process a local file from another source. Murmur supplies the local narration when that is your workflow; this helper does not synthesize speech, install a model, or change app settings.

## Sources

- [FFmpeg loudnorm filter and normalization modes](https://ffmpeg.org/ffmpeg-filters.html#loudnorm)Accessed 2026-10-09
- [FFmpeg command-line options and overwrite behavior](https://ffmpeg.org/ffmpeg.html)Accessed 2026-10-09
- [FFprobe audio-stream inspection](https://ffmpeg.org/ffprobe.html)Accessed 2026-10-09
- [Murmur refund policy and Mac requirements](https://www.murmurtts.com/refund)Accessed 2026-10-09

## Create local narration for your next reel

Murmur generates voiceovers on Apple Silicon Macs with macOS 15 or later. The website edition costs $49 one-time, has no free trial, and offers a 7-day refund policy. Create an approved take, then finish its loudness with your chosen editing workflow.

[Get Murmur](https://murmur-licenses.tarunyadav9761.workers.dev/checkout)[Download Murmur](https://murmur-updates.tarunyadav9761.workers.dev/download/latest)
