Why lossless?

Lossless audio is used on various media, including studio masters, CD, DVD-Audio (via MLP) and Blu-ray (via Dolby TrueHD, which is technically a rebrand of and an extension to MLP, and DTS-HD Master Audio). All of these, when decoded, will result in a pulse-code modulated signal identical to the source, unlike the popular MP3 format. MP3 performs a quality-file size trade-off by discarding or reducing frequencies less audible to human hearing.

PCM by design uses a constant bitrate, which is proportional to the sample rate, bit depth and number of audio channels, which results in very large file sizes with the increasing of each parameter, and/or duration of the audio track.

Solutions such as FLAC, TrueHD and DTS-HD MA are used to losslessly compress the source audio so that the rest of the medium (for example, a Blu-ray disc) can be used for more audio tracks, higher-bandwidth video or extras.

Out of the aforementioned, only FLAC is free to use—both TrueHD and DTS-HD MA encoders and decoders have to be licensed.

The first part of the series will explore the processing of uncompressed audio data with the FLAC API in C#. In case you prefer to use Visual Basic .NET, you can use this online converter.

Anatomy of a WAVE file

In order to process a digital audio signal, we have to know three key parameters:

  • The frequency, at which the signal was sampled. Usual sample rates are 44,100 Hz; 48,000 Hz, 96,000 Hz and rarely 192,000 Hz.
  • The “depth” of each sample, measured in bits. The FLAC encoder supports up to 24 bits. Our C# WAVE reader will support 16 and 24 bits of audio data.
  • The number of channels, which the recording consists of. This is usually mono, stereo (CD), 5.1 (DVD-Audio, Blu-ray) or 7.1 (Blu-ray).

The most common container for PCM audio data is the WAVE file format. As noted above, PCM has a constant bitrate of

SampleRate * BitDepth * Channels,

which makes it very easy to predict the size of each block of audio samples—a single second of audio data would be Bitrate / 8 bytes (8 bits in a byte)—e.g. 176.4KB for a second of CD-quality audio.

riff

We can create the initialization method of the WavReader class by starting with an input Stream object. We have to ensure that there is enough available data for the wave format header, and check that the file is indeed a RIFF/WAVE file to avoid unnecessary reading and processing.

uRiffHeader = reader.ReadInt32();
uRiffHeaderSize = reader.ReadInt32();
uWaveHeader = reader.ReadInt32();

if (uRiffHeader != 0×46464952 /* RIFF */ ||
    uWaveHeader != 0×45564157 /* WAVE */)
    throw new Exception(”Invalid WAVE header!”);

 

Right after the RIFF chunk there can be a number of JUNK (padding) chunks, which we can skip and data and fmt chunks, whose data we need.

// Read all WAVE chunks
while (reader.BaseStream.Position < reader.BaseStream.Length)
{
    int type = reader.ReadInt32();
    int size = reader.ReadInt32();

    long last = reader.BaseStream.Position;

    switch (type)
    {
        case 0×61746164: /* data */
            uDataHeader = type;
            nTotalAudioBytes = size;
            break;

        case 0×20746d66: /* fmt  */
            uFmtHeader = type;
            uFmtHeaderSize = size;

            format.wFormatTag = reader.ReadInt16();
            format.nChannels = reader.ReadInt16();
            format.nSamplesPerSec = reader.ReadInt32();
            format.nAvgBytesPerSec = reader.ReadInt32();
            format.nBlockAlign = reader.ReadInt16();
            format.wBitsPerSample = reader.ReadInt16();
            format.cbSize = reader.ReadInt16();
            break;
    }

    if (uDataHeader == 0) // Do not skip the ‘data’ chunk size
        reader.BaseStream.Position = last + size;
    else
        break;
}

Our WavReader class only supports 16 and 24-bit PCM samples, so we have to ensure that format format.wFormatTag is 1 (PCM) and format.wBitsPerSample ple is either 16 or 24. These limitations can be further removed by implementing sample rate conversion on-the-fly.

After all headers are read, nTotalAudioBytes will contain the total count of audio data bytes in the WAVE file. To determine the duration of the audio file, we can simply divide it by the block size (Bitrate / 8 bytes).

The input stream will now be at the start of the audio samples. Every 16th or 24th bit (respectively, 2nd or 3rd byte) will mark the beginning of each sample. All audio samples are interleaved so the stream consists of:

channel 0, sample 0

channel 1, sample 0

channel n, sample 0

channel 0, sample 1

Now that we have reached the audio samples, we can start feeding them to FLAC.

Free Lossless Audio Codec

The FLAC encoder is an open-source C/C++ project. In order to use it in a C# application we have to use PInvoke to call its application programming interface—LibFlac.dll. The latest version of LibFlac can be found at http://sourceforge.net/projects/flac/files/flac-win/.

When the encoder processes a block of audio samples, our callback functions will write the compressed data to the output stream.

In a nutshell, what our FlacWriter class is going to do is:

  1. Create a new encoder instance.
  2. Set the three key parameters (sample rate, bits per sample,
  3. number of channels).
  4. Optionally set a compression level (default is 5).
  5. Initialize the encoder by passing references to the callback functions (Write, Tell and Seek).
  6. Pass all (desired) audio samples to the encoder, which will return their compressed counterpart.
  7. Finalize and delete the encoder instance. This will release all memory allocated by FLAC.

// Callbacks
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate int WriteCallback(IntPtr context, IntPtr buffer, int bytes, uint samples, uint current_frame, IntPtr userData);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate int SeekCallback(IntPtr context, long absoluteOffset, IntPtr userData);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate int TellCallback(IntPtr context, out long absoluteOffset, IntPtr userData);

When a buffer of PCM samples has been read, we can pass it to FlacWriter. Internally it has to pad all 16 or 24-bit sample to a 32-bit window (using little-endian format).

padded = new byte[buffer.Length * 8 / inputBitDepth];

if (inputBitDepth == 16)
    for (int i = 0; i < paddedSamples; i++)
        padded[i] = buffer[i * bytes + 1] << 8 |
                    buffer[i * bytes + 0];

else if (inputBitDepth == 24)
    for (int i = 0; i < paddedSamples; i++)
        padded[i] = buffer[i * bytes + 2] << 16 |
                    buffer[i * bytes + 1] << 8 |
                    buffer[i * bytes + 0];

Wav2Flac

The main program will use a combination of WavReader and FlacWriter to perform the encoding of WAVE files. Because both classes implement the IDisposable interface, they close all input/output streams as well as the FLAC encoder instance when the Dispose() method is called, or a using statement is used.

using (WavReader wav = new WavReader(inputFile))
{
    using (FlacWriter flac = new FlacWriter(
            File.Create(outputFile),
            wav.BitDepth,
            wav.Channels,
            wav.SampleRate))
    {
        // Buffer for 1 second’s worth of audio data
        byte[] buffer = new byte[wav.Bitrate / 8];
        int bytesRead;
        do
        {
            bytesRead = wav.InputStream.Read(buffer, 0, buffer.Length);
            flac.Write(buffer, 0, bytesRead);
        } while (bytesRead > 0);

        // Finished!
    }
}

Testing the program

All source code and the compiled 32-bit FLAC library can be downloaded from here.

The test program uses three sample WAVE files from the ‘wav’ folder and encodes them to ‘flac’. You can download the sample files from here, courtesy of 2L (Username: HD   Password: 2L), and place them in ‘wav’:

The test program also uses the ConsoleProgress class I have posted earlier.

The code can be further optimized and extended to support various other bit depths. The next part of the series will explore the decoding of DTS-HD Master Audio and its encoding using FlacWriter.

I would appreciate any questions, suggestions or corrections!


14 Responses to “Encoding uncompressed audio with FLAC in C#”  

  1. 1 Alex

    Simply a great article!

    For many months I tried to find a solution, thanks!

  2. 2 Nicolai Dvinge

    Thanks for sharing this nicely written code.
    However, when compressing waves to flac using your code, the Flac files are very big (only like 5 % compressionrate), compared to using the standard Flac commandline encoder. Do you have any idea how to achieve the same level of compression using your stream-based encoding?

  3. 3 hanan

    Hello,
    Thanks on the great code. I am using it to read WAV file and I am trying to display the wave on a graph. I have taken the buffer i have problem to convert it to value that I can display on graph. do you have an idea on how to display it?
    I tried to convert from unsigned byte to int but without success.

    public static int unsignedShortToInt(byte[] b)
    {
    int i = 0;
    i |= b[0] & 0xFF;
    i

  4. 4 codefire

    Hi,

    I have the same remark about result FLAC files which are lot bigger that the ones I generated using Flac-Fronted application.

    I really need to manage wav to flac conversion into my C# .NET application,

    I don’t know how to achieve such a good ratio.

    Thanks in advance for replies and help.

  5. 5 Stanimir Stoyanov

    @hanan Displaying the wave graph is as simple as taking the current amplitude value (preferably as a floating-point number in the range of [-1.0; 1.0]) and displaying it in some way. Here’s an example of how to convert a WORD/ushort value from byte[] to float:

    ushort v; float f; // 'ushort' because amplitudes are unsigned
    byte[] a;
    
    v = 32768; // Test with half of the maximum amplitude
    a = BitConverter.GetBytes(v); // Same as the 'b' array in your comment
    f = BitConverter.ToUInt16(a, 0) / (float)ushort.MaxValue; // Result is 0.5

    @codefire I will check this again, thanks, and update the post adequately.

  6. 6 codefire

    Thanks for you reply.

    When you’ll fix it; could you please also show me how to decode from flac to wav ?

    Because I have tried using open source codes, but have found nothing which works nice.

    Thanks

  7. 7 codefire

    I have also difficulties to make an application which encode/decode signals which have only 1 channel.

    I can’t find an open source code which work for X number of channel or X bits per samples :-(

  8. 8 Stanimir Stoyanov

    @codefire I have posted on the blog an article on FLAC decoding and have updated the original code. I am, however, still looking into the compression ratio issue.

  9. 9 codefire

    Plz look at the link I have pasted into “website” address.
    It’s a 7zip file I have sent through “yousendit”.

    That 7zip is a test-project I have made, re-using open-source code.

    It can encore wav to flac and decode flac to wav, with a really good ratio.

    I succeed to re-used existing code, but to be honest i’m quite lost about that libflac code and its API :-s

    So, I wish it will help you to have good ratio compression, for single and multi channels, as well as different bit rates.

    I am still trying to have c# code, to manage wav to flac encoding and flac to wav decoding, with such a good ratio.

    And also, important for my need, it should be able to work with stream.

    Thanks in advance for your help.

  10. 10 ac3 filter

    thanks for this piece of info i have a blog on regarding this ones.I Should admit you managed more cooler then me

  11. 11 yasotha

    i want to connect with maximum audio level from my PC to TV , i want in C#.net source code,
    please help me for this,,

  12. 12 Whinarn

    I came by this very old blog post and noticed the same problem as a person above me said, the uncompressed file is not at a very good ratio.

    In case anybody else finds this in the future. The solution is easy:
    Go into the FlacWriter.cs file, in the Write method replace the following:

    if (inputBitDepth == 16)
    for (int i = 0; i

  13. 13 Whinarn

    Apologize for my last post, didn’t think twice about characters being disallowed in the posts.

    Use this link instead:
    http://pastebin.com/GD5m2q0z

  14. 14 cdub

    I’ve spent many hours on this project and your sample is a great help.
    Now, I’m just at the point where I’m getting an error because the voicemail wave file is of format 7. Is there any sample as to how I can work with this format?

    Thank You very much for all the work you put into this.

Leave a Reply