---
title: "Loudness normalization in C#"
url: https://recursiveai.org/oneaudio/articles/loudness-normalization-csharp/
description: "Measure and normalize loudness in C#: integrated LUFS, loudness range and true peak per EBU R 128 and ITU-R BS.1770, to -14 or -23 LUFS, in pure .NET code."
date: 2026-10-26
author: OneAudio team
---

# Loudness normalization in C#

Measure and normalize loudness in C#: integrated LUFS, loudness range and true peak per EBU R 128 and ITU-R BS.1770, to -14 or -23 LUFS, in pure .NET code.

October 26, 2026 · OneAudio team · Updated · October 27, 2026

Loudness normalization sets a recording’s gain so that its measured loudness hits a target, in LUFS, without letting its true peak pass a ceiling. In C#, add the `RecursiveAI.OneAudio` package: `Audio.Measure("in.wav")` returns integrated loudness, loudness range and true peak per ITU-R BS.1770 and EBU R 128, and `Audio.Normalize("in.wav", "out.wav")` writes a copy at −14 LUFS with the true peak at or below −1 dBTP. Pass `TargetLufs = -23` for the EBU R 128 broadcast target. OneAudio is pure C# with no native code, and its meter passes the EBU Tech 3341 and Tech 3342 test cases. Every sample was compiled and run against OneAudio 1.2.1 on .NET 10.

## LUFS, true peak and loudness range

- **Integrated loudness (LUFS)** is the loudness of the whole programme, measured the way ITU-R BS.1770 describes: the signal is K-weighted (a filter that approximates how loud we hear different frequencies), averaged over 400 ms blocks, and gated, so silence and very quiet passages don’t pull the figure down. The gates are −70 LUFS absolute and 10 LU below the ungated level.
- **True peak (dBTP)** is the highest level of the reconstructed waveform, including peaks between samples, found by oversampling. It can be higher than the highest sample, and it is what clips after lossy encoding or in a D/A converter.
- **Loudness range (LU)**, from EBU Tech 3342, describes how much the loudness varies over the programme. A compressed pop master has a small range, a film soundtrack a large one.

Two targets cover most work. [EBU R 128](https://tech.ebu.ch/publications/r128) (version 5.0, November 2023) recommends normalizing programmes to −23.0 LUFS, within ±0.5 LU for most programmes, and says the true peak “shall not exceed −1 dBTP” during production. [Spotify](https://support.spotify.com/us/artists/article/loudness-normalization/) adjusts tracks to −14 dB LUFS according to ITU 1770 and advises masters below −1 dB true peak. OneAudio’s default of −14 LUFS and −1 dBTP matches that streaming practice, and every value is configurable.

## Before you start

*Terminal · NuGet*

```
dotnet new console -o Loudness
cd Loudness
dotnet add package RecursiveAI.OneAudio --version 1.2.1
```

The samples use `episode.wav`, a 30-second 48 kHz stereo recording mixed quietly, and `snare.wav`, 20 seconds of quiet noise with sharp drum-like hits. Any WAV, MP3, Ogg Vorbis, Opus or FLAC file works.

## Measure loudness

*Program.cs · OneAudio 1.2.1 · .NET 10*

```
using OneAudio;
using OneAudio.Loudness;
 
// Integrated loudness (LUFS), loudness range (LU) and true peak (dBTP), per ITU-R BS.1770 / EBU R 128.
LoudnessResult r = Audio.Measure("episode.wav");
 
Console.WriteLine($"integrated   {r.IntegratedLufs:F1} LUFS");
Console.WriteLine($"range        {r.LoudnessRange:F1} LU");
Console.WriteLine($"true peak    {r.TruePeakDbtp:F1} dBTP");
Console.WriteLine($"max momentary {r.MaxMomentaryLufs:F1} LUFS, max short-term {r.MaxShortTermLufs:F1} LUFS");
```

*Output · Console*

```
integrated   -24.3 LUFS
range        2.8 LU
true peak    -15.1 dBTP
max momentary -21.3 LUFS, max short-term -23.6 LUFS
```

As a cross-check, ffmpeg’s `ebur128` filter with `peak=true` reports −24.3 LUFS integrated and a −15.1 dB true peak for the same file. `Audio.Measure` decodes the file as it reads it, so memory stays flat for long recordings. Pass `truePeak: false` when you only need the loudness, which makes the measurement much cheaper.

## Normalize to −14 LUFS or −23 LUFS

*Program.cs · OneAudio 1.2.1 · .NET 10*

```
using OneAudio;
using OneAudio.Loudness;
 
// Default target: -14 LUFS integrated, true peak at most -1 dBTP.
NormalizeResult web = Audio.Normalize("episode.wav", "episode-14.wav");
Report("episode-14.wav", web);
 
// Broadcast target from EBU R 128: -23 LUFS, true peak at most -1 dBTP.
NormalizeResult broadcast = Audio.Normalize("episode.wav", "episode-23.wav",
    new NormalizeOptions { TargetLufs = -23, TruePeakCeilingDbtp = -1 });
Report("episode-23.wav", broadcast);
 
// Sharp transients: reaching -14 LUFS would push the true peak over -1 dBTP,
// so the gain stops at the ceiling and the file ends up quieter than the target.
Report("snare-14.wav", Audio.Normalize("snare.wav", "snare-14.wav"));
 
static void Report(string path, NormalizeResult n)
{
    LoudnessResult after = Audio.Measure(path);
    Console.WriteLine($"{path}: {n.Before.IntegratedLufs:F1} LUFS, gain {n.GainDb:+0.0;-0.0} dB" +
                      $"{(n.LimitedByPeak ? " (held back by the peak ceiling)" : "")} -> " +
                      $"{after.IntegratedLufs:F1} LUFS, {after.TruePeakDbtp:F1} dBTP");
}
```

*Output · Console*

```
episode-14.wav: -24.3 LUFS, gain +10.3 dB -> -14.0 LUFS, -4.8 dBTP
episode-23.wav: -24.3 LUFS, gain +1.3 dB -> -23.0 LUFS, -13.8 dBTP
snare-14.wav: -20.9 LUFS, gain +3.0 dB (held back by the peak ceiling) -> -17.9 LUFS, -1.0 dBTP
```

- Normalization applies one gain to the whole file. It doesn’t compress or limit, so the dynamics and the loudness range stay as they were. Each output above measures at its target, to a tenth of a LU.
- The gain is cut back when the true peak would pass the ceiling. `snare.wav` needs +6.9 dB to reach −14 LUFS, but its hits would then peak near +3 dBTP, so it gets +3.0 dB, ends at −17.9 LUFS with a −1.0 dBTP peak, and `LimitedByPeak` tells you so. To get such material louder you need a limiter or compressor first, which is a mastering decision. `TruePeakCeilingDbtp = null` drops the ceiling, and then a boost may clip when the output is written as integers.
- `Audio.Normalize` measures, then decodes again to write, rather than holding the audio in memory. The output format comes from the extension (WAV, FLAC or Ogg Opus), and `NormalizeOptions.Output` can change the sample rate, channels or encoder settings in the same pass. The loudness is measured after that conversion.
- Silence is left alone: there is no gain that brings it to a target.
- For audio already in memory, `Audio.Normalize(samples, format)` scales a `Span<float>` in place and `Audio.Normalize(clip, out result)` returns a new clip.

## Measure while audio plays or streams

`LoudnessMeter` is the meter under `Audio.Measure`. Feed it interleaved floats in blocks of any size, and read momentary (400 ms), short-term (3 s) and integrated loudness at any time, for a loudness display, a live input or a mixer bus:

*Program.cs · OneAudio 1.2.1 · .NET 10*

```
using OneAudio;
using OneAudio.Loudness;
 
// A meter you feed block by block: for live input, a mixer bus, or a progress display.
using IAudioDecoder decoder = Audio.Open("episode.wav");
var meter = new LoudnessMeter(decoder.Format);   // true peak and loudness range on
 
int rate = decoder.Format.SampleRate, channels = decoder.Format.Channels;
float[] block = new float[rate / 10 * channels];  // 100 ms per block
long frames = 0;
int read;
while ((read = decoder.Read(block)) > 0)
{
    meter.Process(block.AsSpan(0, read * channels));
    frames += read;
    if (frames % (rate * 10) == 0)                // every 10 seconds
        Console.WriteLine($"{frames / rate,3} s  momentary {meter.MomentaryLufs:F1}  " +
                          $"short-term {meter.ShortTermLufs:F1}  integrated {meter.IntegratedLufs:F1} LUFS");
}
Console.WriteLine($"end    integrated {meter.IntegratedLufs:F1} LUFS, range {meter.LoudnessRange:F1} LU, " +
                  $"true peak {meter.TruePeakDbtp:F1} dBTP");
```

*Output · Console*

```
 10 s  momentary -23.9  short-term -23.8  integrated -24.0 LUFS
 20 s  momentary -27.4  short-term -25.9  integrated -24.6 LUFS
 30 s  momentary -30.0  short-term -24.3  integrated -24.3 LUFS
end    integrated -24.3 LUFS, range 2.8 LU, true peak -15.1 dBTP
```

- Everything is allocated in the constructor and `Process` allocates nothing and takes no locks, so the meter can run on a real-time audio thread.
- Results don’t depend on how the stream was split into blocks.
- Channels are weighted from the channel layout as BS.1770 specifies: 1.41 for surround channels, and the LFE channel left out of the loudness but not the true peak.
- The momentary reading drops at 30 s because the file fades out over its last half second.
- The OneAudio mixer can run the same meter on any bus. See [measure loudness](https://recursiveai.org/oneaudio/cookbook/measure-loudness/) in the cookbook.
- To meter what actually reaches the speakers, the `RecursiveAI.OneAudio.Devices` package runs a BS.1770 meter after its output limiter: set `DeviceStreamOptions.Metering = true` and read `output.Loudness`. Playback itself is covered in [play audio in C#](https://recursiveai.org/oneaudio/articles/play-audio-csharp/).

## Other options in .NET

A Stack Overflow question that comes up for this search, “[Loudness Normalisation LUFS in C# with NAudio](https://stackoverflow.com/questions/73717684/loudness-normalisation-lufs-in-c-with-naudio)”, asks how to do it with NAudio. NAudio decodes and plays, but measuring LUFS needs a BS.1770 meter on top of it. [R128Net](https://www.nuget.org/packages/R128Net/1.1.2) 1.1.2 (MIT) is one: its README describes it as a C# port of libebur128 that measures momentary, short-term and integrated loudness, loudness range and sample and true peak. It takes samples rather than files, so reading the file and applying the gain are up to you. Following its README, with OneAudio decoding the file:

*Program.cs · R128Net 1.1.2 + OneAudio 1.2.1*

```
using OneAudio;
using R128Net;
 
// R128Net measures; it doesn't read files, so the samples come from a decoder.
AudioClip clip = Audio.Load("episode.wav");
 
using LoudnessMeter meter = new(channels: clip.Format.Channels, sampleRate: clip.Format.SampleRate,
                                LoudnessModes.Integrated | LoudnessModes.TruePeak);
meter.AddFrames(clip.Samples.Span);
 
double truePeak = Math.Max(meter.GetTruePeak(0), meter.GetTruePeak(1));
Console.WriteLine($"integrated {meter.IntegratedLoudness:F1} LUFS, true peak {20 * Math.Log10(truePeak):F1} dBTP");
```

*Output · Console*

```
integrated -24.3 LUFS, true peak -15.1 dBTP
```

Both meters agree on this file. If you only need a number and already decode audio elsewhere, a meter alone is enough. If you need decoding, measurement, gain and encoding in one step, `Audio.Normalize` does all four.

## Where next

- [Normalize loudness](https://recursiveai.org/oneaudio/cookbook/normalize-loudness/) and [measure loudness](https://recursiveai.org/oneaudio/cookbook/measure-loudness/), the cookbook recipes.
- The [API overview](https://recursiveai.org/oneaudio/docs/api/#loudness): the meter, channel weights, sample rates and the normalization helper.
- [Resample audio in C#](https://recursiveai.org/oneaudio/articles/resample-audio-csharp/) and [choosing a C# audio library](https://recursiveai.org/oneaudio/articles/csharp-audio-library/).
