
Mastering · 8 October 2026 · 12 min
432 Hz mastering: LUFS and true peak after a retune
A 432 retune changes pitch, not loudness: in our tests integrated LUFS moved by about 0.1 LU. Peaks are a different story. Resampling rebuilds the waveform, so inter-sample peaks hiding in a crushed master become real samples above 0 dBFS, and a fixed-point render clips them. Retune into 32-bit float, measure the result, then deliver any LUFS target as a separate, named, two-pass step, and always keep an un-normalized 432 master.
Here's a question that comes up the moment somebody likes a 432 version: "Cool, so what LUFS should it be?" Which is a fair question with a slightly awkward answer. The retune doesn't decide that. Where the file is going decides that. The retune only decides whether you've got a peak problem waiting for you.
So let's take the two things apart properly, look at what really happens to loudness and peaks when you resample a finished master, and then put them back together in an order that doesn't wreck the file or rig the comparison.
Pitch is not loudness
LUFS is an estimate of perceived program loudness, built on the ITU BS.1770 measurement, with a weighting curve that mostly ignores the deepest lows and leans a little on the upper mids. A 432 conversion slides every frequency down by 0.981818, about 31.77 cents. That moves a tiny bit of energy relative to the weighting curve, but it doesn't make the record meaningfully louder or quieter. A crushed master stays crushed, a dynamic one stays dynamic.
That has one important consequence for listening tests: don't normalize only one side. If the 432 version gets a loudness pass and the 440 doesn't, you're comparing loudness, not pitch. Decide whether the lower version works first, level-matched, the way How to run a 432 vs 440 listening test you can trust describes. Then think about delivery.
What actually moves: the peaks
A sample peak meter reads the stored sample values. A true-peak meter estimates the waveform between those samples, which is what a DAC, a sample-rate converter, or a codec will actually reconstruct. True peak is what codecs clip covers the difference in depth. The short version: a loud master can show a sample peak of −0.3 dBFS while its true peak sits above zero.
Classic retuning is a resample. It reinterprets the sample rate and then reconstructs new samples at different points in time. Those new points can land right on top of the inter-sample peaks the original was hiding. So a peak that was only "virtual" in the source becomes a real number above 1.0 in the output. In a 32-bit float file that's harmless and recoverable. In a 24-bit or 16-bit fixed-point file, it's clipped the moment it's written.
A test on three synthetic masters
This script builds three synthetic 40-second masters (a heavily limited loud one, a dynamic quiet one, and a hard-clipped one), retunes each with ffmpeg's soxr resampler into 32-bit float, measures loudness, loudness range, true peak, sample peak, and the number of samples over full scale, then normalizes the 432 renders to −14 LUFS and −1 dBTP with a proper two-pass loudnorm.
#!/usr/bin/env python3
"""lufs_after_retune.py: what a 440->432 retune does to loudness and peaks,
and how to deliver a LUFS target afterwards without cheating.
Usage: python3 lufs_after_retune.py
Needs: numpy, ffmpeg (with soxr) on PATH.
Builds three SYNTHETIC 40 s masters (loud/limited, dynamic/quiet, hard-
clipped), retunes each with asetrate + soxr into 32-bit float, measures
integrated loudness, loudness range, true peak, sample peak and overs, then
runs ffmpeg loudnorm to -14 LUFS / -1 dBTP in two passes and reports
whether it could stay linear or fell back to dynamic processing."""
import json, re, subprocess
import numpy as np
SR, D = 48000, 40.0
t = np.arange(int(SR * D)) / SR
rng = np.random.default_rng(14)
def ff(*a, inp=None):
return subprocess.run(["ffmpeg", "-v", "error", "-y", *a], input=inp, capture_output=True, check=True)
def write(name, x):
st = np.stack([x, np.roll(x, 31)], 1).astype(np.float32)
ff("-f", "f32le", "-ar", str(SR), "-ac", "2", "-i", "-", "-c:a", "pcm_f32le", name, inp=st.tobytes())
def decode(name):
return np.frombuffer(ff("-i", name, "-f", "f32le", "-").stdout, dtype=np.float32)
def meas(name):
err = subprocess.run(["ffmpeg", "-nostats", "-hide_banner", "-i", name, "-af",
"ebur128=peak=true:framelog=quiet", "-f", "null", "-"],
capture_output=True, text=True).stderr
s = err[err.rfind("Summary:"):]
g = lambda pat: float(re.search(pat, s).group(1))
x = decode(name)
sp = 20 * np.log10(np.max(np.abs(x)))
return g(r"I:\s+(-?[\d.]+) LUFS"), g(r"LRA:\s+(-?[\d.]+) LU"), g(r"Peak:\s+(-?[\d.]+) dBFS"), sp, int(np.sum(np.abs(x) > 1.0))
beat = (t * 92 / 60) % 1.0
music = (0.5 * np.sin(2 * np.pi * 49 * t) * np.exp(-2.5 * beat) # 808
+ sum(0.12 / h * np.sin(2 * np.pi * 196 * h * t) for h in range(1, 9)) # chord bed
+ 0.15 * rng.standard_normal(t.size) * np.exp(-40 * ((t * 92 / 30) % 1.0))) # hats/snare noise
music *= 1 + 0.5 * np.sin(2 * np.pi * t / 20) # verse/chorus swell
loud = np.tanh(3.2 * music / np.max(np.abs(music))) * 0.966 # heavy limiting, ~-0.3 dBFS
quiet = 0.6 * music / np.max(np.abs(music)) # dynamic, lots of headroom
clipped = np.clip(1.8 * music / np.max(np.abs(music)), -0.891, 0.891) # hard clip at -1 dBFS
write("syn_loud_440.wav", loud); write("syn_quiet_440.wav", quiet); write("syn_clipped_440.wav", clipped)
RT = f"asetrate={SR*432/440:.6f},aresample={SR}:resampler=soxr:precision=28"
print("SYNTHETIC masters, before and after a classic 440->432 retune (32-bit float render)")
print(f"{'file':22s} {'LUFS-I':>7s} {'LRA':>5s} {'dBTP':>6s} {'sample pk':>9s} {'overs>0dBFS':>11s}")
for k in ("loud", "quiet", "clipped"):
ff("-i", f"syn_{k}_440.wav", "-af", RT, "-c:a", "pcm_f32le", f"syn_{k}_432.wav")
for v in ("440", "432"):
i, lra, tp, sp, overs = meas(f"syn_{k}_{v}.wav")
print(f"{k+' '+v:22s} {i:7.1f} {lra:5.1f} {tp:+6.1f} {sp:+9.2f} {overs:11d}")
print("\nTwo-pass loudnorm to -14 LUFS / -1.0 dBTP / LRA 11, on the 432 renders")
for k in ("loud", "quiet"):
p1 = subprocess.run(["ffmpeg", "-nostats", "-hide_banner", "-i", f"syn_{k}_432.wav", "-af",
"loudnorm=I=-14:TP=-1:LRA=11:print_format=json", "-f", "null", "-"],
capture_output=True, text=True).stderr
m = json.loads(p1[p1.rfind("{"):p1.rfind("}") + 1])
af = (f"loudnorm=I=-14:TP=-1:LRA=11:linear=true:measured_I={m['input_i']}:measured_TP={m['input_tp']}"
f":measured_LRA={m['input_lra']}:measured_thresh={m['input_thresh']}:offset={m['target_offset']}:print_format=json")
p2 = subprocess.run(["ffmpeg", "-nostats", "-hide_banner", "-y", "-i", f"syn_{k}_432.wav", "-af", af,
"-ar", str(SR), "-c:a", "pcm_s24le", f"syn_{k}_432_-14.wav"], capture_output=True, text=True).stderr
r = json.loads(p2[p2.rfind("{"):p2.rfind("}") + 1])
i, lra, tp, sp, _ = meas(f"syn_{k}_432_-14.wav")
print(f" {k:6s} in {float(m['input_i']):6.1f} LUFS {float(m['input_tp']):+5.1f} dBTP -> "
f"mode={r['normalization_type']:7s} out {i:6.1f} LUFS {tp:+5.1f} dBTP, LRA {float(m['input_lra']):.1f} -> {lra:.1f}")
print("\nPlain gain instead of loudnorm (what a linear target would need)")
for k in ("loud", "quiet"):
i, _, tp, _, _ = meas(f"syn_{k}_432.wav")
g = -14 - i
print(f" {k:6s}: gain {g:+5.1f} dB -> true peak would be {tp+g:+5.1f} dBTP "
f"({'fits' if tp + g <= -1 else 'over the -1 dBTP ceiling, needs limiting or a lower target'})")
subprocess.run("rm -f syn_*.wav", shell=True)
Real output from this machine. All three masters are synthetic signals made by the script, not real releases.
SYNTHETIC masters, before and after a classic 440->432 retune (32-bit float render)
file LUFS-I LRA dBTP sample pk overs>0dBFS
loud 440 -9.3 7.2 +1.6 -0.33 0
loud 432 -9.4 7.3 +1.4 +1.32 40
quiet 440 -22.1 8.8 -4.3 -4.44 0
quiet 432 -22.2 8.8 -4.2 -4.21 0
clipped 440 -12.6 8.7 +1.2 -1.00 0
clipped 432 -12.7 8.7 +1.2 +1.05 31
Two-pass loudnorm to -14 LUFS / -1.0 dBTP / LRA 11, on the 432 renders
loud in -9.5 LUFS +1.4 dBTP -> mode=linear out -13.9 LUFS -3.1 dBTP, LRA 7.0 -> 7.3
quiet in -22.3 LUFS -4.2 dBTP -> mode=dynamic out -14.2 LUFS -0.7 dBTP, LRA 8.5 -> 7.0
Plain gain instead of loudnorm (what a linear target would need)
loud : gain -4.6 dB -> true peak would be -3.2 dBTP (fits)
quiet : gain +8.2 dB -> true peak would be +4.0 dBTP (over the -1 dBTP ceiling, needs limiting or a lower target)
What those numbers say
Loudness barely moved. Every master lost about 0.1 LU of integrated loudness and kept essentially the same loudness range. That's the "pitch is not loudness" point in numbers.
The loud master was already over, and the retune made it visible. Look at the first two rows. The 440 source had a sample peak of −0.33 dBFS and looked fine on a sample meter, but its true peak was +1.6 dBTP. After the retune, the sample peak jumped to +1.32 dBFS with 40 samples over full scale. The retune didn't create the problem. It revealed it. Write that file at 24-bit and those 40 samples clip.
The clipped master stayed damaged. It went in with flat tops at −1 dBFS and came out with 31 overs, because resampling rounds off flattened peaks and overshoots around them. Nothing about a retune, or a normalizer, puts the missing waveform back.
The quiet master was fine. Plenty of headroom in, plenty out. Most of what follows is about the loud case, because that's the kind of master most people retune.
A thirty-second QC check for any render
You don't need the whole script to catch this on a real file. This little wrapper prints the format, duration, integrated loudness, loudness range, sample peak, and true peak in one go, using nothing but ffprobe and ffmpeg's ebur128 meter.
#!/usr/bin/env bash
# check_render.sh: one-glance QC for a retuned render.
# Usage: ./check_render.sh file.wav
set -euo pipefail
f="$1"
ffprobe -v error -show_entries stream=sample_rate,channels,codec_name:format=duration -of csv=p=0 "$f" | paste -sd' '
ffmpeg -nostats -hide_banner -i "$f" -af "ebur128=peak=sample+true:framelog=quiet" -f null - 2>&1 \
| awk '/Summary:/{s=1} s&&/Sample peak:/{k="sample peak"} s&&/True peak:/{k="true peak"}
s&&/ I:/{print " integrated", $2, $3} s&&/LRA:/{print " LRA", $2, $3} s&&/Peak:/{print " " k, $2, $3}'
Here it is on the synthetic loud master from above, then on two 432 renders of it, one written as 32-bit float and one written straight to 24-bit:
$ ./check_render.sh syn_loud_440.wav
pcm_s24le,48000,2 40.000000
integrated -9.3 LUFS
LRA 7.2 LU
sample peak -0.3 dBFS
true peak 1.6 dBFS
$ ./check_render.sh syn_loud_432_float.wav
pcm_f32le,48000,2 40.740979
integrated -9.4 LUFS
LRA 7.3 LU
sample peak 1.3 dBFS
true peak 1.4 dBFS
$ ./check_render.sh syn_loud_432_24bit.wav
pcm_s24le,48000,2 40.740979
integrated -9.4 LUFS
LRA 7.3 LU
sample peak 0.0 dBFS
true peak 1.4 dBFS
Read the last two blocks side by side. Same loudness, same duration (40.74 seconds, the expected 1.85% longer). The float file keeps its +1.3 dBFS sample peak, which you can still trim cleanly. The 24-bit file reports a sample peak of exactly 0.0 dBFS. That isn't headroom, it's the ceiling of the format: the overs were chopped off as the file was written, and the true-peak reading still sits at +1.4 because the flattened tops reconstruct above full scale. If you ever see 0.0 dBFS on a fresh retune, assume it clipped.
Retune first, measure second, deliver third
The order matters more than any single setting:
- Start from the best source. If you have a pre-limiter master or the session, retune that and re-limit afterwards. If you only have the final master, accept that you're working on a file with no headroom left.
- Render the retune in 32-bit float. Overs survive intact and you can deal with them deliberately.
- Measure the render. Integrated loudness, true peak, sample peak, duration. Don't assume the source numbers carried over.
- Bring true peak under the ceiling. A simple trim of a dB or two, or a transparent true-peak limiter, on the float file. This is your clean 432 master.
- Only then make delivery copies. Each one named for its target, each one made from the clean 432 master, never from another delivery copy.
Two-pass loudnorm, and when it quietly stops being linear
If you normalize with ffmpeg's loudnorm, use two passes. The first pass measures the whole file. The second applies a correction using those measurements, which a single pass can't do properly because it hasn't heard the ending yet.
There's a catch the demo shows clearly. The ffmpeg documentation says linear mode only holds if the target loudness range isn't below the source's and the gain change wouldn't push true peak past the ceiling. If either condition fails, loudnorm reverts to dynamic mode. The loud master needed −4.6 dB to hit −14 LUFS, which fit easily, so it stayed linear: a clean gain change landing at −13.9 LUFS and −3.1 dBTP. The quiet master needed +8.2 dB, which would have put true peak near +4.0 dBTP. So loudnorm switched to dynamic processing, squeezed its loudness range from 8.5 to 7.0 LU, and still came out at −0.7 dBTP, slightly over the −1.0 ceiling I asked for.
That's not a bug report, it's a reminder. Always read the normalization_type in the second-pass output, and re-measure the file you actually wrote. Two more practical notes from the same docs: in dynamic mode loudnorm upsamples to 192 kHz internally, so set your output rate explicitly with -ar, and decide whether you'd rather have a lower target than a dynamically processed dynamic record.
Pick a destination, not a number
Targets belong to destinations. Spotify's loudness normalization guide describes a −14 LUFS playback reference, recommends keeping true peak below −1 dBTP, and suggests below −2 dBTP if your master is louder than −14. EBU R 128 is a broadcast recommendation built around −23 LUFS with a maximum true-peak descriptor; it's not a music-mastering target. A club file can stay louder as long as its peaks are legal. And an archive master shouldn't be normalized at all. Why streaming platforms turn loud masters down explains why chasing loudness past the platform reference buys you nothing.
| File | Made from | Processing | Loudness | True peak |
|---|---|---|---|---|
| Artist_Title_440Master_48k_24b.wav | Approved session | None; never touched | As mastered | As mastered |
| Artist_Title_432Master_48k_32f.wav | 440 master | Classic retune, float render | Within ~0.1 LU of source | Can exceed 0 dBFS; trim before use |
| Artist_Title_432Master_48k_24b.wav | 432 float master | Trim or true-peak limit only | Near source | ≤ −1 dBTP |
| Artist_Title_432_-14LUFS_48k_24b.wav | 432 24-bit master | Two-pass loudnorm, linear if possible | −14 LUFS | ≤ −1 dBTP (−2 if loud) |
| Artist_Title_432_-23LUFS_48k_24b.wav | 432 24-bit master | Two-pass loudnorm for broadcast | −23 LUFS | Per broadcaster spec |
The naming is doing real work there. A LUFS suffix appears only on files that were intentionally normalized. The float file is labeled as float so nobody uploads it by accident. And the untouched 440 master stays exactly that.
Leave room for the encoder
Whatever target you choose, remember the file you hand over is rarely the file people hear. Lossy encoders reconstruct the waveform again and can create fresh inter-sample peaks. That's exactly what Re-encoding eats true-peak headroom measures. A retuned master that sits at −0.1 dBTP is one encode away from distortion. A dB of true-peak margin is cheap insurance.
What loudness can't do
Normalization moves gain. A limiter contains peaks. Neither restores detail lost to clipping, low-bitrate encoding, heavy noise reduction, or repeated conversion. A louder render can sound more impressive for ten seconds while actually having less transient detail. That's the whole reason to choose your pitch reference first, at matched loudness, and treat loudness as a delivery job afterwards.
If you want the background on why a classic retune also changes duration, which matters for any loudness measurement that includes silence at the ends, What 432 Hz actually changes in a finished master and Resampling, pitch shifting, and tempo change are three different jobs cover it.
- Does converting to 432 Hz change LUFS?
- Barely. In our synthetic tests integrated loudness moved by about 0.1 LU. Pitch and loudness are separate properties, so a retuned file is as loud or quiet as its source. Measure the render anyway.
- Why did my 432 version clip when the original didn't?
- Resampling reconstructs the waveform between the original samples. If the source had inter-sample peaks above 0 dBFS, they become real sample values in the new file. In a 24-bit fixed-point render those clip. Render in 32-bit float or leave true-peak headroom first.
- Should I normalize the 432 version to -14 LUFS?
- Only if that matches where it's going. Spotify documents a -14 LUFS playback reference and recommends keeping true peak below -1 dBTP. Broadcast under EBU R 128 targets -23 LUFS. Keep an un-normalized master and make target-specific copies.
- What is two-pass loudness normalization?
- The first pass measures the whole file. The second applies gain using those measurements. With ffmpeg loudnorm, the second pass stays a clean linear gain change only if the target fits under the true-peak ceiling; otherwise it switches to dynamic processing.
- Can a retune or normalization fix a clipped master?
- No. Gain changes and limiters can't restore waveform detail that clipping flattened. Retuning a clipped file just moves the damage down in pitch. Go back to an unclipped source if you have one.