English | 日本語
A complete C# port of libebur128, the loudness measurement library implementing EBU R128 and ITU-R BS.1770 by Jan Kokemüller.
Measurement runs without a single managed allocation, so the garbage collector never observes the analysis path.
All state comes from one native block, every hot routine is written with unsafe pointers, and the whole library is annotated for Native AOT.
Correctness is not asserted from reading the source: every stage is compared against golden data produced by the original C compiled with MSVC.
- Overview
- Requirements
- Installation
- Features
- API Reference
- Limitations
- Notes
- Disclaimer
- Third-Party Licenses
- License
R128Net ports libebur128 to C#. Momentary, short-term and integrated loudness, loudness range, the relative threshold, sample peak and true peak are all provided, together with the histogram algorithm and the aggregation of several measurement instances into a single figure.
The public surface is modern C#. Audio is passed as ReadOnlySpan<short>, ReadOnlySpan<int>, ReadOnlySpan<float> or ReadOnlySpan<double>, the mode set is a [Flags] enum that reproduces the implied bits of the original, options are a readonly record struct with init accessors, and results are properties on a single LoudnessMeter class. The pointer-based internals are not exposed.
All state is served by one 64-byte aligned block obtained from NativeMemory.AlignedAlloc at construction. The layout of that block is declared once as a Layout method on a dedicated type; a source generator reads that method's signature and emits both the size query and the binding call, so the reported size and the actual consumption cannot drift apart.
The original C is not vendored into this repository. The reference harness under reference/ clones libebur128 at a pinned commit, builds it with MSVC and dumps the internal state of every stage as raw doubles. The test suite loads those dumps and compares them against the C# results.
| Item | Requirement |
|---|---|
| OS | Windows, Linux or macOS supported by .NET 10 |
| SDK | .NET SDK 10.0 |
| Language | C# 14 or later (LangVersion is set to latest) |
| Unsafe code | Not required in the consuming project |
| Reference data | MSVC C toolset and Git, required only to regenerate the golden data used by the tests |
dotnet add package R128Net- Create a
LoudnessMeterwith the channel count, the sample rate and the modes you need. Request the lowest set of modes that suits your needs, because every mode costs processing time. - Feed interleaved audio through
AddFrames. Buffers of any length are accepted as long as they hold a whole number of frames. - Read the results as properties. They may be read at any point during the measurement.
- To run the test suite, generate the reference data first by executing
reference/build.bat, which clones libebur128, builds it with MSVC and writes the dumps toreference/data. - To produce a Native AOT binary of the sample application, run
publish-aot.bat.
The BS.1770 pre-filter is the cascade of a high shelf and a high pass, combined into one fourth-order section exactly as the original does. Gating blocks span 400 ms and advance by 100 ms, giving the 75 percent overlap of the 2011 revision.
Momentary loudness covers the last 400 ms and short-term loudness the last 3 s. Integrated loudness applies the absolute gate at -70 LUFS and then the relative gate 10 LU below the ungated mean. Loudness range follows EBU Tech 3342, taking the 10th and 95th percentile of the short-term blocks that survive a gate 20 dB below their mean. GetLoudnessOverWindow reports the loudness of any window up to the configured maximum.
Sample peak is the largest absolute sample divided by the scaling factor of the input format. True peak oversamples by four below 96 kHz and by two below 192 kHz using a 49-tap polyphase FIR interpolator, and leaves the signal unchanged at 192 kHz and above. Both are available for the whole measurement and for the most recent call to AddFrames.
The original writes the converted input to one buffer, writes every interpolated sample to a second buffer, and scans that buffer for the peak in a separate pass. Both buffers and both passes are fused away here. The stored value would be read back unchanged, and a maximum does not depend on the order of the scan, so the fusion leaves every result identical.
Channel weights follow BS.1770: the surround positions contribute a factor of 1.41, a channel marked as dual mono contributes a factor of two, and a channel marked as unused is skipped and is not written to the ring buffer at all.
The histogram algorithm quantises block energies into 1000 bins of 0.1 LU and is selected with LoudnessModes.Histogram. It bounds the memory of a long measurement at the cost of quantising the result. Without it the block energies are kept individually, which is the default and matches the original.
LoudnessMeter.GatedLoudness and LoudnessMeter.LoudnessRangeOf accept a span of meters and produce the figure that a single meter fed with all the material would have produced. This is the intended way to measure several files as one programme, or to measure one file on several threads. The meters must agree on whether they use the histogram algorithm.
The filtered ring buffer, the filter state, the channel map, the peak arrays, the histograms and the interpolator all live in one aligned native block allocated at construction. A type marked with [StateLayout] declares its requirement once as a Layout method generic over an allocator. Running it with the measuring allocator yields the required byte count without touching memory, and running it with the binding allocator performs the actual binding. The source generator emits GetRequiredBytes and Bind from that single signature.
The block energy history grows geometrically in native memory by default, which reproduces the effectively unbounded history of the original. PreallocateHistory sizes it once from the requested maximum history instead, after which the measurement performs no allocation of any kind.
The absence of managed allocation is measured, not asserted. GC.GetAllocatedBytesForCurrentThread reports a delta of zero bytes across AddFrames for all four input formats and across every query, including the sort that loudness range performs.
The test suite compares against dumps produced by the original C built with MSVC. Because the internal state of libebur128 is an incomplete type, the harness includes ebur128.c into its own translation unit and reaches the state directly. The following table records the agreement that the tests enforce.
| Stage | Agreement |
|---|---|
| Pre-filter coefficients, 20 sample rates | Bit-exact |
| Filtered output and filter state | Bit-exact |
| Filter state through a silent decay, 40 blocks | Bit-exact |
| Interpolator coefficients, both oversampling factors | Bit-exact |
| Gating block energies and short-term block energies | Bit-exact |
| Integrated loudness, momentary, short-term, window, relative threshold | Bit-exact |
| Loudness range, list algorithm and histogram algorithm | Bit-exact |
| Sample peak and true peak, current and previous | Bit-exact |
| Histogram bin counts, 1000 bins of each kind | Bit-exact |
| Aggregation across three instances | Bit-exact |
Those figures hold for eight configurations: stereo, five-channel surround, dual mono, the histogram algorithm, buffer lengths aligned and unaligned to the 100 ms boundary, 44100 Hz, 96000 Hz, and an adversarial signal whose amplitude cycles through 1e-310, 1e-40, 5e-324 and exact zero.
The transcendental functions are measured separately. Math.Tan and Math.Log return exactly the same doubles as the MSVC runtime over 20000 sampled arguments each. Math.Pow differs by at most one unit in the last place on fewer than one in a thousand of the sampled inputs, which is why the histogram boundary table is embedded rather than computed.
The suite contains 131 tests and all of them pass.
The tables compare the original C compiled with MSVC at /O2 against this port published with Native AOT. Both are compiled ahead of time, so the comparison is like for like. Each table measures both builds back to back in one session, so the ratio is the meaningful quantity while the absolute values move with the machine.
The first table is a fixed record taken on a workstation. It is not regenerated.
Intel Core i7-1360P under Windows 11, sample application published for an x86-64-v3 baseline, best of 12 runs in milliseconds, analysing 30 seconds of stereo 48 kHz audio in 100 ms buffers.
| Mode set | C with MSVC | This port with Native AOT | Ratio |
|---|---|---|---|
| Momentary only | 44.51 | 43.61 | 1.02x |
| Integrated | 60.96 | 56.18 | 1.09x |
| Loudness range | 51.92 | 52.66 | 0.99x |
| Integrated and sample peak | 62.63 | 63.69 | 0.98x |
| Integrated and true peak | 328.34 | 356.35 | 0.92x |
| Every mode | 326.98 | 350.55 | 0.93x |
The second table is regenerated by the benchmark workflow on a GitHub Actions runner. The hardware differs from the workstation, which makes it an independent check that the ratios do not depend on one particular machine.
Measured by CI on a GitHub Actions windows-latest runner with AMD EPYC 9V74 80-Core Processor. Figures are the best of 20 runs in milliseconds, analysing 30 seconds of stereo 48 kHz audio in 100 ms buffers. Both builds run back to back in the same job, so the ratio is the stable quantity; the absolute values move with the shared runner. Recorded on 2026-07-21 from commit 868c9f2.
| Mode set | C with MSVC | This port with Native AOT | Ratio |
|---|---|---|---|
| Momentary only | 32.86 | 34.69 | 0.95x |
| Integrated | 44.56 | 46.85 | 0.95x |
| Loudness range | 40.94 | 43.27 | 0.95x |
| Integrated and sample peak | 47.22 | 47.72 | 0.99x |
| Integrated and true peak | 172.37 | 172.98 | 1.00x |
| Every mode | 180.86 | 182.27 | 0.99x |
A ratio above 1.00 means this port is faster than the original C.
True peak dominates the cost. The following optimisations were applied to it, and every one of them leaves the output bit-exact.
| Optimisation | Effect |
|---|---|
| Delay line duplicated to twice its length, removing the wrap branch | Combined below |
| Two-frame unrolling, doubling the independent dependency chains | 2.1x together with the above |
| Delay line held already widened to double, removing a conversion per tap | 1.5x |
| Subfilters one to three vectorised across phase | 1.09x |
Each accumulation chain must keep the addition order of the original, so the chain itself cannot be shortened. Unrolling further to four frames was measured and rejected: it regressed by 13 percent under Native AOT because four accumulators exceed the vector register budget. The just-in-time measurement had suggested the opposite, which is why the decision was taken from the Native AOT figures.
The K-weighting filter is vectorised across channels, which is bit-exact because each lane performs the arithmetic of one channel in the original order. It yields 1.4x to 1.8x at four channels and above. A two-channel vector path was implemented and removed after measurement showed no gain beyond the noise floor.
Sample peak is vectorised across the interleaved buffer whenever the vector width is a multiple of the channel count. A maximum does not depend on the order in which it is taken, so the result is bit-exact, and the comparison reproduces the ternary of the original including its treatment of negative zero and of not a number.
using R128Net;
using LoudnessMeter meter = new(channels: 2, sampleRate: 48000, LoudnessModes.All);| Member | Description |
|---|---|
LoudnessMeter(int, int, LoudnessModes) |
Creates a meter with the default options |
LoudnessMeter(int, int, LoudnessModes, in LoudnessMeterOptions) |
Creates a meter with explicit options |
Channels, SampleRate, Modes |
The configuration the meter was created with |
MaxWindowMilliseconds, MaxHistoryMilliseconds |
The current window and history |
Dispose |
Releases the native state |
LoudnessModes reproduces the implied bits of the original: ShortTerm implies Momentary, Integrated implies Momentary, LoudnessRange implies ShortTerm, and TruePeak implies SamplePeak.
meter.AddFrames(interleaved);| Overload | Full-scale value |
|---|---|
AddFrames(ReadOnlySpan<short>) |
32768 |
AddFrames(ReadOnlySpan<int>) |
2147483648 |
AddFrames(ReadOnlySpan<float>) |
1.0 |
AddFrames(ReadOnlySpan<double>) |
1.0 |
Samples are interleaved by channel. The span must hold a whole number of frames.
| Member | Unit | Requires |
|---|---|---|
MomentaryLoudness |
LUFS | Momentary |
ShortTermLoudness |
LUFS | ShortTerm |
IntegratedLoudness |
LUFS | Integrated |
LoudnessRange |
LU | LoudnessRange |
RelativeThreshold |
LUFS | Integrated |
GetLoudnessOverWindow(long) |
LUFS | Momentary |
LoudnessMeter.GatedLoudness(ReadOnlySpan<LoudnessMeter>) |
LUFS | Integrated on every meter |
LoudnessMeter.LoudnessRangeOf(ReadOnlySpan<LoudnessMeter>) |
LU | LoudnessRange on every meter |
A loudness of negative infinity means the material is silent or entirely below the absolute gate. A loudness range of zero means too few short-term blocks survived the gate.
| Member | Requires |
|---|---|
GetSamplePeak(int) |
SamplePeak |
GetPreviousSamplePeak(int) |
SamplePeak |
GetTruePeak(int) |
TruePeak |
GetPreviousTruePeak(int) |
TruePeak |
A value of 1.0 is 0 dBFS, so the conversion to decibels is 20 * log10(value). The previous variants report the peak of the most recent call to AddFrames. True peak never reports less than sample peak.
| Member | Description |
|---|---|
SetChannel(int, ChannelPosition) |
Assigns a position to a channel |
GetChannel(int) |
Reads the position of a channel |
The default map assigns the BS.1770 layout for four and five channels, and otherwise assigns left, right, centre, unused, left surround and right surround from the first channel onwards. ChannelPosition.DualMono applies only to the single channel of a mono meter.
| Member | Default | Description |
|---|---|---|
MaxHistoryMilliseconds |
4294967295 | The history retained for integrated loudness and loudness range |
PreallocateHistory |
false |
Sizes the history once instead of growing it |
UseUpstreamWindowOverflow |
false |
Reproduces the integer overflow of the original on Windows |
- Bit-exactness has been verified against libebur128 compiled with MSVC on Windows x64. Other compilers, other runtimes and other architectures may round the transcendental functions differently, and the agreement above is not claimed for them.
- The histogram boundary table differs from the original at two of its 1001 entries by one unit in the last place. The embedded values are the correctly rounded ones; the MSVC
powis not, andpowis not required to be correctly rounded by IEEE 754. The difference is confined to the histogram algorithm. LoudnessMeteris not thread-safe. Concurrent measurement requires one meter per thread, which the aggregation functions are designed for.- Sample rates at which the pre-filter diverges are rejected at construction rather than accepted as the original accepts them. Every rate from 3364 Hz upwards is accepted.
ebur128_set_max_windowandebur128_change_parametersare not ported. Both reallocate the audio buffer, and the equivalent is to create a new meter.- Running the comparison tests requires the reference data. Without
reference/datathose tests cannot execute. - The sample application under
R128Net.Exampleswrites to the console and therefore allocates managed memory. The zero-allocation guarantee applies to the library.
- Processing cost: true peak is by far the most expensive mode, accounting for the large majority of the total. Omitting it from the mode set is the single most effective way to speed up a measurement. Sample peak and gating are minor by comparison.
- Denormal handling: the original enables the flush-to-zero bit of the MXCSR register while filtering. The .NET runtime exposes no equivalent, so the flush is emulated at the level of the individual operation. The emulation reproduces the state trajectory of the original through a silent decay exactly, and it also avoids the fifty-fold slowdown that denormal arithmetic would otherwise cause on silence.
- Meter reuse: the state is allocated once at construction. Creating a meter for every buffer defeats the purpose and reintroduces native allocation.
- Determinism: the measurement produces identical output across repeated runs and does not depend on the length of the buffers passed to
AddFrames. The test suite feeds the same material one frame at a time and in bulk and compares the results bit for bit. - State layout: a type marked with
[StateLayout]must expose aLayoutmethod generic overIStateAllocator. The generator emitsGetRequiredBytesandBindwith a matching parameter list, and skips whichever of the two the type already declares. - Native AOT: the library sets
IsAotCompatible, which enables the trim, single-file and AOT analyzers.publish-aot.batpublishes the sample application forwin-x64and requires the MSVC toolset for the native linker. It also places the Visual Studio installer directory on the path, because the linker probe of the AOT compiler fails whenvswhere.execannot be resolved. - Regenerating reference data:
reference/build.batclones libebur128 intoreference/ebur128-src, verifies that the checkout is the pinned commit, builds it, and writes the dumps. The clone and the build output are excluded from version control.
This library is published under the MIT License.
This software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement.
The author accepts no liability for any damage arising from the use of or the inability to use this library. Use it at your own risk.
R128Net is a derivative work of the software below. The full license text is stored in the repository under .github/LICENSE/libebur128.txt.
libebur128 is distributed under the MIT License, which requires that redistributions retain its copyright notice and permission notice. That file carries the text unmodified. No third-party source code is vendored into this repository, and the reference harness downloads libebur128 on demand.
| Software | Purpose | License | Copyright |
|---|---|---|---|
| libebur128 | Origin of every ported algorithm and of the reference implementation used for verification | MIT License | Copyright (c) 2011 Jan Kokemüller |