To decode an MP3 in C#, add the RecursiveAI.OneAudio package and call Audio.Load("song.mp3"): you get the whole file as 32-bit float samples with its sample rate and channel count. Audio.Transcode("song.mp3", "song.wav") converts it to WAV, and Audio.Open streams it block by block with sample-accurate seeking. OneAudio is written in C# with no native code and no dependency beyond .NET, so the same code runs on Windows, Linux and macOS. It reads the LAME/Xing header, so the decoded length matches the original recording, without the encoder’s padding. Every sample below was compiled and run against OneAudio 1.2.1 on .NET 10, and the outputs shown are from those runs.
Before you start
dotnet new console -o Mp3Decode cd Mp3Decode dotnet add package RecursiveAI.OneAudio --version 1.2.1
OneAudio targets .NET 10 (net10.0) and is licensed MIT OR Apache-2.0. The samples use music.mp3, a 30-second stereo 44.1 kHz test piece encoded with LAME at -V 2. Put any MP3 of your own next to the program under that name.
Decode an MP3 to float samples
using OneAudio; // Decode the whole MP3 to 32-bit float samples, interleaved by channel. AudioClip clip = Audio.Load("music.mp3"); AudioFormat format = clip.Format; Console.WriteLine($"{format.SampleRate} Hz, {format.Channels} channels, {format.NativeType}"); Console.WriteLine($"{clip.Frames} frames = {clip.Frames / (double)format.SampleRate:F3} s"); // Frame i of channel c is at [i * channels + c]. Full scale is -1 to +1. ReadOnlySpan<float> samples = clip.Samples.Span; float peak = 0; foreach (float s in samples) peak = Math.Max(peak, Math.Abs(s)); Console.WriteLine($"{samples.Length} samples, peak {20 * Math.Log10(peak):F1} dBFS");
44100 Hz, 2 channels, Lossy 1323000 frames = 30.000 s 2646000 samples, peak -5.1 dBFS
- The format is detected from the bytes, not the extension. The same call reads WAV, Ogg Vorbis, Opus and FLAC, and they all come back as the same
AudioClip. - A frame is one sample per channel.
Samplesis interleaved, so a stereo clip of N frames holds 2N floats, with full scale at ±1. Decoded samples are never clipped. NativeTypeisLossyfor MP3: there are no original integer samples to recover, only floats.Audio.Loadholds the whole file in memory and refuses more than 16 minutes at 48 kHz unless you raiseLoadOptions.MaxFrames. For long files, stream instead (below).
Convert MP3 to WAV, on disk or in memory
Converting MP3 to WAV in memory is a common need when a speech or analysis service wants WAV bytes and you don’t want temporary files. Audio.Transcode works on paths and on streams, and can change the sample rate and channel count on the way:
using OneAudio; // MP3 file to WAV file. The output format comes from the extension. TranscodeResult result = Audio.Transcode("music.mp3", "music-decoded.wav"); Console.WriteLine($"{result.Frames} frames at {result.Input.SampleRate} Hz -> " + $"{new FileInfo("music-decoded.wav").Length:N0} bytes"); // MP3 bytes to WAV bytes in memory, e.g. for an upload or a speech API. // Converted to 16 kHz mono on the way. byte[] mp3 = File.ReadAllBytes("music.mp3"); using var input = new MemoryStream(mp3); using var wav = new MemoryStream(); Audio.Transcode(input, wav, new TranscodeOptions { Format = AudioFileFormat.Wav, SampleRate = 16000, Channels = 1, }); byte[] wavBytes = wav.ToArray(); Console.WriteLine($"in memory: {mp3.Length:N0} MP3 bytes -> {wavBytes.Length:N0} WAV bytes");
1323000 frames at 44100 Hz -> 5,292,080 bytes in memory: 526,113 MP3 bytes -> 960,080 WAV bytes
- With a path, the output format comes from the extension:
.wav,.flac, or.opusand.oggfor Ogg Opus. With streams, setTranscodeOptions.Format. - A lossy source is written as 16-bit PCM by default. Set
Wav = new WavEncoderSettings { SampleType = NativeSampleType.PcmSigned24 }(orIeeeFloat32) for another sample type. - The 16 kHz mono WAV is exactly 30 seconds: 480,000 frames of 2 bytes plus an 80-byte header. The conversion streams block by block, so memory use doesn’t grow with the length of the file.
- If a file conversion fails, the partial output file is deleted.
Stream a long MP3 and seek
For long files, or when you process audio as it arrives, open a decoder and read it in blocks. This version registers only the MP3 decoder, so an app published trimmed or with NativeAOT doesn’t carry the other codecs:
using OneAudio; // Register only the MP3 decoder, so a trimmed or NativeAOT app carries no other codec. var registry = new AudioCodecRegistry().AddMp3(); using IAudioDecoder decoder = AudioDecoder.OpenFile("music.mp3", registry); AudioFormat format = decoder.Format; Console.WriteLine($"{format.SampleRate} Hz, {format.Channels} ch, length {decoder.LengthFrames} frames"); // Read in blocks of 4096 frames: memory stays flat however long the file is. float[] block = new float[4096 * format.Channels]; long total = 0; int frames; while ((frames = decoder.Read(block)) > 0) // frames, not samples; 0 means the end { // block[..(frames * format.Channels)] holds this block's interleaved samples. total += frames; } Console.WriteLine($"decoded {total} frames"); // Seek to exactly 12.5 seconds and read from there. decoder.Seek((long)(12.5 * format.SampleRate)); frames = decoder.Read(block); Console.WriteLine($"after seek: position {decoder.PositionFrames}, read {frames} frames");
44100 Hz, 2 ch, length 1323000 frames decoded 1323000 frames after seek: position 555346, read 4096 frames
Readfills the span you pass and returns a number of frames, which can be fewer than you asked for. 0 means the end. Decoders allocate nothing per read.Seekis exact: 12.5 seconds is frame 551,250, and after reading 4,096 frames the position is 555,346. The decoder resumes up to 48 MP3 frames before the target to rebuild the bit reservoir, so the samples after a seek are identical to a straight decode.AudioDecoder.Openalso takes aStream, including one that can’t seek, such as a network response. OnlySeekneeds a seekable stream.Audio.Open(path)does the same asAudioDecoder.OpenFilewith every codec registered. The streaming recipe has more.
Gapless: why the length is exactly 30 seconds
An MP3 encoder adds silence at the start (the encoder delay) and pads the end to a whole frame. LAME, and ffmpeg when it writes MP3, record both numbers in the Xing/Info header with the LAME extension. OneAudio reads them, skips the delay and drops the padding, so LengthFrames and the decoded audio match the source: 1,323,000 frames for this 30-second file. Files without that header are decoded in full, with nothing trimmed.
That matters when you loop music, splice tracks or line decoded audio up with a timeline. Decoding the same file through the NLayer route shown below gives 1,323,648 frames (30.015 s), 648 more than the source.
Decoding MP3 with NAudio on Linux and macOS
NAudio is no longer Windows-only. NAudio 3 made NAudio.Core and NAudio.Midi cross-platform and added NAudio.Alsa for playback and recording on Linux, as Mark Heath wrote in his NAudio 3 release post (August 15, 2026). MP3 decoding, though, still depends on the platform. In NAudio 3.3.0, the current release, the package README says AudioFileReader “reads PCM/float WAV and AIFF everywhere, and uses Media Foundation (MP3, AAC, WMA, FLAC…) and ACM (compressed WAV) when running on Windows”. On Linux it says so at run time:
using NAudio.Wave; try { using var reader = new AudioFileReader("music.mp3"); Console.WriteLine($"{reader.WaveFormat}, {reader.TotalTime}"); } catch (Exception ex) { Console.WriteLine($"{ex.GetType().Name}: {ex.Message}"); }
NotSupportedException: MP3 is only supported by AudioFileReader on Windows, where it is decoded by Media Foundation or ACM. On other platforms use SoundFileReader from the NAudio.SoundFile package, or the Extended Audio File Services readers in NAudio.MacOS.Mp3FileReader uses “the ACM MP3 codec that is present on most versions of Windows”, according to NAudio’s MP3 to WAV guide. On Linux the compiler flags it as Windows-only (warning CA1416), and at run time it fails to load Msacm32.dll. That leaves two cross-platform routes in NAudio:
- NAudio.SoundFile, which calls a system installation of libsndfile. Its guide says no binary is shipped, so you install libsndfile on each machine, and the codecs available depend on how that libsndfile was built.
- NLayer, a managed MP3 decoder (MIT), plugged into
Mp3FileReaderBasethrough theNLayer.NAudioSupportpackage, as in the same MP3 guide. This runs on Linux with NAudio.Core 3.3.0 and NLayer.NAudioSupport 3.0.0:
using NAudio.Wave; using NLayer.NAudioSupport; var infile = "music.mp3"; var outfile = "converted.wav"; using (var reader = new Mp3FileReaderBase(infile, wf => new Mp3FrameDecompressor(wf))) { WaveFileWriter.CreateWaveFile(outfile, reader); } using var check = new WaveFileReader(outfile); Console.WriteLine($"{check.WaveFormat}, {check.SampleCount} frames, {check.TotalTime.TotalSeconds:F3} s");
32 bit IEEEFloat: 44100Hz 2 channels, 1323648 frames, 30.015 sIf NAudio is already your playback stack, the NLayer route keeps everything in one pipeline. If you only need samples or files, OneAudio gives you MP3 plus WAV, Ogg Vorbis, Opus and FLAC decoding, and WAV, FLAC and Opus encoding, in one managed package. To hear the result, its companion package RecursiveAI.OneAudio.Devices plays it on the default output device: see play audio in C#. Moving file code across is covered in migrating from NAudio.
Speed and memory
In the project’s published decode benchmark, OneAudio decoded a 30-second 128 kbit/s stereo MP3 in 37.0 ms, against 107 ms for NLayer 1.16.0, and allocated 132 KB against NLayer’s 36 MB. Both were measured with BenchmarkDotNet 0.15.8 on an AMD Ryzen 9 7950X3D under Linux with .NET 10.0.13. The README also lists the native mpg123 decoder, run as a separate process, for comparison. Decoding allocates a fixed few hundred kilobytes, whatever the length of the file.
The decoder was also checked against the ISO/IEC 11172-4 compliance streams: all 11 decode within the standard’s full-accuracy limits, per the README’s conformance table.
What the MP3 decoder covers
- Formats: MPEG-1, MPEG-2 and MPEG-2.5 Layer III at constant, variable and free-format bitrates; mono, stereo, joint stereo and dual channel. MP3 files with Layer I or II audio raise
UnsupportedAudioFormatException. - Tags: ID3v2 tags in front of the audio are skipped. ID3v1, APE and Lyrics3 tags at the end are ignored.
- Damaged files: frames that can’t be decoded come out as silence and decoding continues, so a cut or corrupted download still plays to the end.
- No MP3 encoder: OneAudio writes WAV, FLAC and Opus. For a smaller lossy file, encode to Opus with
Audio.Transcode("in.wav", "out.opus").
Where next
- Getting started with OneAudio: decode, convert, resample, mix and normalize.
- Decode MP3 and convert audio files, the cookbook recipes.
- Resample audio in C#, for example to 16 kHz for speech recognition.
- Play an MP3 in C# on Windows, Linux and macOS.
- The API overview has the MP3 details: seeking, gapless trimming and damaged data.