Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reading 24-bit samples from a .WAV file

I understand how to read 8-bit, 16-bit & 32-bit samples (PCM & floating-point) from a .wav file, since (conveniently) the .Net Framework has an in-built integral type for those exact sizes. But, I don't know how to read (and store) 24-bit (3 byte) samples.

How can I read 24-bit audio? Is there maybe some way I can alter my current method (below) for reading 32-bit audio to solve my problem?

private List<float> Read32BitSamples(FileStream stream, int sampleStartIndex, int sampleEndIndex)
{
    var samples = new List<float>();
    var bytes = ReadChannelBytes(stream, Channels.Left, sampleStartIndex, sampleEndIndex); // Reads bytes of a single channel.

    if (audioFormat == WavFormat.PCM)  // audioFormat determines whether to process sample bytes as PCM or floating point.
    {
        for (var i = 0; i < bytes.Length / 4; i++)
        {
            samples.Add(BitConverter.ToInt32(bytes, i * 4) / 2147483648f);
        }
    }
    else
    {
        for (var i = 0; i < bytes.Length / 4; i++)
        {
            samples.Add(BitConverter.ToSingle(bytes, i * 4));
        }
    }

    return samples;
}
like image 366
Sam Avatar asked Jun 10 '14 22:06

Sam


1 Answers

Reading (and storing) 24-bit samples is very simple. Now, as you've rightly said, a 3 byte integral type does not exist within the framework, which means you're left with two choices; either create your own type, or, you can pad your 24-bit samples by inserting an empty byte (0) to the start of your sample's byte array therefore making them 32-bit samples (so you can then use an int to store/manipulate them).

I will explain and demonstrate how to do the later (which is also in my opinion the more simpler approach).


First we must look at how a 24-bit sample would be stored within an int,

~ ~ ~ ~ ~ ~ ~ ~ ~ ~ MSB ~ ~ 2ndMSB ~ ~ 2ndLSB ~ ~ LSB ~ ~

24-bit sample: 11001101 01101001 01011100 00000000

32-bit sample: 11001101 01101001 01011100 00101001

MSB = Most Significant Byte, LSB = Lest Significant Byte.

As you can see the LSB of the 24-bit sample is 0, therefore all you have to is declare a byte[] with 4 elements, then read the 3 bytes of the sample into the array (starting at element 1) so that your array looks like below (effectively bit shifting by 8 places to the left),

myArray[0]: 00000000

myArray[1]: 01011100

myArray[2]: 01101001

myArray[3]: 11001101

Once you have your byte array full you can pass it to BitConverter.ToInt32(myArray, 0);, you will then need to shift the sample by 8 places to the right to get the sample in it's proper 24-bit intergal representation (from -8388608 to 8388608); then divide by 8388608 to have it as a floating-point value.

So, putting that all together you should end up with something like this,

Note, I wrote the following code with the intention to be "easy-to-follow", therefore this will not be the most performant method, for a faster solution see the code below this one.

private List<float> Read24BitSamples(FileStream stream, int startIndex, int endIndex)
{
    var samples = new List<float>();
    var bytes = ReadChannelBytes(stream, Channels.Left, startIndex, endIndex);
    var temp = new List<byte>();
    var paddedBytes = new byte[bytes.Length / 3 * 4];
    
    // Right align our samples to 32-bit (effectively bit shifting 8 places to the left).
    for (var i = 0; i < bytes.Length; i += 3)
    {
        temp.Add(0);            // LSB
        temp.Add(bytes[i]);     // 2nd LSB
        temp.Add(bytes[i + 1]); // 2nd MSB
        temp.Add(bytes[i + 2]); // MSB
    }

    // BitConverter requires collection to be an array.
    paddedBytes = temp.ToArray();
    temp = null;
    bytes = null;

    for (var i = 0; i < paddedBytes.Length / 4; i++)
    {
        samples.Add(BitConverter.ToInt32(paddedBytes, i * 4) / 2147483648f); // Skip the bit shift and just divide, since our sample has been "shited" 8 places to the right we need to divide by 2147483648, not 8388608.
    }

    return samples;
}

For a faster1 implementation you can do the following instead,

private List<float> Read24BitSamples(FileStream stream, int startIndex, int endIndex)
{
    var bytes = ReadChannelBytes(stream, Channels.Left, startIndex, endIndex);
    var samples = new float[bytes.Length / 3];

    for (var i = 0; i < bytes.Length; i += 3)
    {
        samples[i / 3] = (bytes[i] << 8 | bytes[i + 1] << 16 | bytes[i + 2] << 24) / 2147483648f;
    }
    
    return samples.ToList();
}


1 After benchmarking the above code against the previous method, this solution is approximately 450% to 550% faster.

like image 110
Sam Avatar answered Sep 20 '22 20:09

Sam