Normalize audio loudness in C# with oneaudio: write a copy at −14 LUFS, or −23 LUFS for EBU R 128 broadcast, with a true-peak ceiling, or scale samples in memory.
using OneAudio; using OneAudio.Loudness; // File to file at the default target: -14 LUFS, true peak at most -1 dBTP. NormalizeResult web = Audio.Normalize("music.wav", "music-14.wav"); Console.WriteLine($"-14 LUFS: was {web.Before.IntegratedLufs:F1} LUFS, " + $"gain {web.GainDb:+0.00;-0.00} dB, limited: {web.LimitedByPeak}"); // EBU R 128 broadcast delivery: -23 LUFS. The output format follows the extension. var broadcastOptions = new NormalizeOptions { TargetLufs = -23 }; NormalizeResult broadcast = Audio.Normalize("game-mix.wav", "game-mix-23.flac", broadcastOptions); Console.WriteLine($"-23 LUFS: was {broadcast.Before.IntegratedLufs:F1} LUFS, " + $"gain {broadcast.GainDb:+0.00;-0.00} dB"); // A target the ceiling won't allow: the gain stops where the true peak // reaches -1 dBTP. var loudOptions = new NormalizeOptions { TargetLufs = -9 }; NormalizeResult loud = Audio.Normalize("game-mix.wav", "game-mix-9.wav", loudOptions); LoudnessResult after = Audio.Measure("game-mix-9.wav"); Console.WriteLine($"-9 LUFS: gain {loud.GainDb:+0.00;-0.00} dB, limited: {loud.LimitedByPeak}, " + $"result {after.IntegratedLufs:F1} LUFS at {after.TruePeakDbtp:F1} dBTP"); // In memory: a new clip, or samples scaled in place. AudioClip clip = Audio.Load("voice.wav"); var voiceOptions = new NormalizeOptions { TargetLufs = -16 }; AudioClip level = Audio.Normalize(clip, out NormalizeResult v, voiceOptions); Console.WriteLine($"voice clip: gain {v.GainDb:+0.00;-0.00} dB, " + $"now {Audio.Measure(level).IntegratedLufs:F1} LUFS"); // Only the number: the gain to reach a target, without touching the audio. NormalizationGain g = Audio.Measure("music.flac").GainFor(targetLufs: -16); Console.WriteLine($"music.flac needs {g.GainDb:+0.00;-0.00} dB (x{g.Linear:F3}) for -16 LUFS");
-14 LUFS: was -23.0 LUFS, gain +8.98 dB, limited: False -23 LUFS: was -18.7 LUFS, gain -4.32 dB -9 LUFS: gain +0.76 dB, limited: True, result -17.9 LUFS at -1.0 dBTP voice clip: gain +1.60 dB, now -16.0 LUFS music.flac needs +6.98 dB (x2.234) for -16 LUFS
How it works
Audio.Normalizemeasures the integrated loudness and true peak, works out the gain that reaches the target, and writes the audio with that gain. It decodes the input twice, once to measure and once to write, so it never holds the whole file in memory.- The default target is −14 LUFS with a true-peak ceiling of −1 dBTP.
NormalizeOptions.TargetLufssets any target, such as −23 for EBU R 128. - If the gain would push the true peak over the ceiling, the gain is cut back to meet it, and
LimitedByPeakis true. The target isn’t reached. In the −9 LUFS case above,game-mix.wavpeaks at −1.8 dBTP, so it could be raised only 0.76 dB. oneaudio applies a gain and nothing else: it doesn’t compress or limit the audio to get louder. TruePeakCeilingDbtp = nullturns the ceiling off, and then a boost can clip when it’s written as integers.- Silence gets a gain of 0 dB.
- The output format comes from the extension, as in
Audio.Transcode.NormalizeOptions.Outputtakes the same conversion and encoder options.
In memory
Audio.Normalize(clip, out result, options)returns a new clip.Audio.Normalize(span, format, options)scales samples in place.LoudnessResult.GainFor(target, ceiling)returns the gain without applying it. Note that its default target is −23 LUFS, whereAudio.Normalizedefaults to −14.
To measure without changing anything, see Measure loudness.
A longer guide to EBU R 128 and streaming targets: Loudness normalization in C#.