Recursive AI

Read and write FLAC in C#

Decode and encode FLAC in C# on any OS with managed code: exact integer samples, MD5 verification, tags, WAV to FLAC at levels 0 to 8, and 24-bit output.

To read a FLAC file in C#, add the RecursiveAI.OneAudio package and call Audio.Open("song.flac") to stream it or Audio.Load("song.flac") to decode it all; to write one, call Audio.Transcode("song.wav", "song.flac") or feed samples to a FlacEncoder. The decoder returns the exact integers stored in the file and checks them against the file’s MD5 signature, and the encoder writes compression levels 0 to 8 like the flac command line. It is all managed C#, so the same code runs on Windows, Linux and macOS with no libFLAC or Media Foundation. Every sample below was compiled and run against OneAudio 1.2.1 on .NET 10.

Before you start

TerminalNuGet
dotnet new console -o FlacDemo
cd FlacDemo
dotnet add package RecursiveAI.OneAudio --version 1.2.1

OneAudio targets .NET 10 and is licensed MIT OR Apache-2.0. The samples use music.wav, 30 seconds of 16-bit stereo at 44.1 kHz, and music.flac, the same audio encoded by ffmpeg.

Read a FLAC file

Program.csOneAudio 1.2.1 · .NET 10
using OneAudio;
using OneAudio.Flac;
 
// Open a FLAC file: the concrete decoder exposes the tags, the true bit depth and the MD5 signature.
using var flac = (FlacDecoder)Audio.Open("music.flac");
Console.WriteLine($"{flac.Format.SampleRate} Hz, {flac.Format.Channels} ch, {flac.BitsPerSample}-bit, {flac.LengthFrames} frames");
Console.WriteLine($"vendor: {flac.Vendor}, tags: {flac.Comments.Count}");
Console.WriteLine($"MD5 matches: {flac.VerifyMd5()}");   // decodes the whole file and compares with STREAMINFO
 
// Exact integers, as stored: compare with the WAV the file was made from.
using IAudioDecoder wav = Audio.Open("music.wav");
int[] a = new int[4096 * 2], b = new int[4096 * 2];
long frames = 0, differences = 0;
int n;
while ((n = flac.ReadInteger(a)) > 0)
{
    wav.ReadInteger(b.AsSpan(0, n * 2));
    for (int i = 0; i < n * 2; i++)
        if (a[i] != b[i]) differences++;
    frames += n;
}
Console.WriteLine($"{frames} frames compared with music.wav, {differences} differences");
OutputConsole
44100 Hz, 2 ch, 16-bit, 1323000 frames
vendor: Lavf62.3.100, tags: 1
MD5 matches: True
1323000 frames compared with music.wav, 0 differences
  • Audio.Open detects FLAC from the bytes and returns a FlacDecoder, which adds BitsPerSample (the true depth), Vendor, Comments (the Vorbis comment tags, such as TITLE=…), Md5 and VerifyMd5() to the common decoder interface.
  • ReadInteger returns the samples exactly as stored; Read returns floats scaled by the stream’s own depth, so full scale is ±1 for 16-bit and 24-bit files alike. The comparison shows the round trip from WAV to FLAC and back is lossless: zero differences over 2,646,000 samples.
  • VerifyMd5 decodes the whole file and compares it with the signature in STREAMINFO, then puts the read position back. It returns null when the file stores no signature. Every frame is also CRC-checked as it is read.
  • Seek(frame) is sample-accurate, and Ogg FLAC files open through the same call.

Write FLAC: WAV to FLAC, and samples you produce

Program.csOneAudio 1.2.1 · .NET 10
using OneAudio;
using OneAudio.Flac;
 
// WAV to FLAC in one call, at the highest compression level, with tags.
Audio.Transcode("music.wav", "music-l8.flac", new TranscodeOptions
{
    Flac = new FlacEncoderSettings
    {
        CompressionLevel = 8,
        Comments = ["TITLE=Test piece", "ARTIST=OneAudio samples"],
    },
});
 
// Samples your own code produces: 24-bit FLAC written block by block.
var format = new AudioFormat(48000, 2);
using (FileStream file = File.Create("tone-24bit.flac"))
using (var encoder = new FlacEncoder(file, format, new FlacEncoderSettings { BitsPerSample = 24 }))
{
    float[] block = new float[1024 * 2];
    for (int b = 0; b < 48000 / 1024; b++)
    {
        for (int i = 0; i < 1024; i++)
            block[2 * i] = block[2 * i + 1] = 0.5f * MathF.Sin(2 * MathF.PI * 1000 * (b * 1024 + i) / 48000f);
        encoder.Write(block);
    }
    encoder.Finish();                                       // writes the length, MD5 and seek table
}
 
foreach (string f in new[] { "music.wav", "music-l8.flac", "tone-24bit.flac" })
    Console.WriteLine($"{f}: {new FileInfo(f).Length:N0} bytes");
using var check = (FlacDecoder)Audio.Open("music-l8.flac");
Console.WriteLine(string.Join(", ", check.Comments));
OutputConsole
music.wav: 5,292,078 bytes
music-l8.flac: 2,079,752 bytes
tone-24bit.flac: 68,628 bytes
TITLE=Test piece, ARTIST=OneAudio samples
  • At level 8 the music file shrinks to 39% of the WAV. Levels 0 to 8 (default 5) trade encoding time for size, as with the flac tool, and stay inside FLAC’s streamable subset. Block size, LPC order, partition order and coefficient precision can be set by hand.
  • Audio.Transcode takes any input OneAudio reads. From a lossless source (WAV or FLAC) the output is bit-exact: ffmpeg decodes music-l8.flac to the same PCM as music.wav, byte for byte.
  • FlacEncoder takes 4 to 32 bits per sample, 1 to 8 channels and any rate up to 1,048,575 Hz. Float input is rounded to the chosen depth; set Dither = true for triangular dither first. WriteInteger writes integers as they are.
  • On a seekable stream, Finish goes back and writes the length, the MD5 signature and a seek table. On a pipe or a network stream it writes STREAMINFO once, without them.
  • The same input and settings always produce the same bytes on a platform.

FLAC with NAudio

Many C# projects start with NAudio’s AudioFileReader. Its package README for NAudio 3.3.0 says it reads WAV and AIFF everywhere and “uses Media Foundation (MP3, AAC, WMA, FLAC…)” on Windows. On Linux, opening a FLAC file says so:

Program.csNAudio 3.3.0 · Linux
using NAudio.Wave;
 
try
{
    using var reader = new AudioFileReader("music.flac");
    Console.WriteLine($"{reader.WaveFormat}, {reader.TotalTime}");
}
catch (Exception ex)
{
    Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
}
OutputConsole
NotSupportedException: This file format 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.

The route the message points to, NAudio.SoundFile, reads and writes FLAC through a system installation of libsndfile, per its guide, so each machine needs libsndfile1 installed. If NAudio plays your audio, a OneAudio decoder can feed its player through an ISampleProvider, as shown in migrating from NAudio; the device package also plays FLAC directly (play audio in C#).

Conformance and speed

From the project’s published results:

  • Decoder: bit-exact against flac 1.4.3 and the STREAMINFO MD5 on all 64 files of the IETF CELLAR test suite’s subset; the 11 uncommon files behave as expected, and the 11 faulty files are cleanly rejected.
  • Encoder: output decoded by flac -d is bit-exact, and file sizes are within 0.4% of flac’s at levels 0 to 8.
  • Decode speed: on 16-bit 44.1 kHz, 24-bit 96 kHz and 24-bit 192 kHz files, OneAudio decoded 1.36 to 1.57 times as fast as PureFlac 1.0.0 and 2.07 to 8.09 times as fast as BunLabs.NAudio.Flac 2.0.1, allocating about 97 KB per file against 10 to 129 MB for the latter. Measured with BenchmarkDotNet 0.15.8 on an AMD Ryzen 9 7950X3D under Linux with .NET 10.0.13.

Where next