Search code examples
c#wav32-bit

C# create 24/32bit sound wav file


I'm following this tutorial on Microsoft Developer blog: https://blogs.msdn.microsoft.com/dawate/2009/06/24/intro-to-audio-programming-part-3-synthesizing-simple-wave-audio-using-c/

The tutorial is called "Intro to Audio Programming" and I followed all steps, but probably I missed something.

This is a configuration piece extracted from WaveFormatChunk class in his homonym method:

    sChunkID = "fmt ";
    dwChunkSize = 16;
    wFormatTag = 1;
    wChannels = 2;
    dwSamplesPerSec = 44100;
    wBitsPerSample = 16;
    wBlockAlign = (ushort)(wChannels * (wBitsPerSample / 8));
    dwAvgBytesPerSec = dwSamplesPerSec * wBlockAlign;

The wBitsPerSample set the bit depth of the generated function, pretty simple till now.

Then, running the program, all is working with this settings. Generating 1Hz frequency with amplitude 32760 at 16bit / 44.1ksample this is the result:

1Hz 16bit 44.1k

And that is obviously a correct output.

Now I quote it:

we use an array of shorts because we have 16-bit samples as specified in the format block. If you want to change to 8-bit audio, use an array of bytes. If you want to use 32-bit audio, use an array of floats.

Talking about shortArray in WaveDataChunk class

public class WaveDataChunk
{
    public string sChunkID;     // "data"
    public uint dwChunkSize;    // Length of header in bytes
    public short[] shortArray;

Then, for 32bit audio, changing the shortArray to float:

public float[] shortArray;

and wBitsPerSample to 32:

wBitsPerSample = 32;

This is the result: 1Hz 32bit 44.1k

Practically, the frequency is doubled and only half of the time is written. What did I do wrong?? What I've to do?


Solution

  • When you use IEEE floating points, the range should be -1..1 (instead of the min..max integer number). Also, the format is then no longer MS PCM (1) but IEEE FLOAT (3).

    The problem with the article is that its chunk definitions are initialized without IEEE FLOAT format in mind. Here's a suggestion of mine to make the code more generic (literally); the chunks will initialize the fields (which I made read-only) in the constructor depending on the type chosen:

    public class WaveFormatChunk<T> where T: struct, IConvertible
    {
        public readonly string sChunkID;         // Four bytes: "fmt "
        public readonly uint dwChunkSize;        // Length of chunk in bytes
        public readonly ushort wFormatTag;       // 1 (MS PCM)
        public readonly ushort wChannels;        // Number of channels
        public readonly uint dwSamplesPerSec;    // Frequency of the audio in Hz... 44100
        public readonly uint dwAvgBytesPerSec;   // for estimating RAM allocation
        public readonly ushort wBlockAlign;      // sample frame size, in bytes
        public readonly ushort wBitsPerSample;    // bits per sample
    
        /// <summary>
        /// Initializes a format chunk. Supported data types: byte, short, float
        /// </summary>
        public WaveFormatChunk(short channels, uint samplesPerSec)
        {
            sChunkID = "fmt ";
            dwChunkSize = 16;
            wFormatTag = typeof(T) == typeof(float) || typeof(T) == typeof(double) ? 3 : 1;
            wChannels = channels;
            dwSamplesPerSec = samplesPerSec;
            wBitsPerSample = (ushort)(Marshal.SizeOf<T>() * 8);
            wBlockAlign = (ushort)(wChannels * ((wBitsPerSample + 7) / 8));
            dwAvgBytesPerSec = dwSamplesPerSec * wBlockAlign;            
        }
    }
    
    public class WaveDataChunk<T> where T: struct, IConvertible
    {
        public readonly string sChunkID;     // "data"
        public readonly uint dwChunkSize;    // Length of data chunk in bytes
        public readonly T[] dataArray;  // 8-bit audio
    
        /// <summary>
        /// Initializes a new data chunk with a specified capacity.
        /// </summary>
        public WaveDataChunk(uint capacity)
        {
            dataArray = new T[capacity];
            dwChunkSize = (uint)(Marshal.SizeOf<T>() * capacity);
            sChunkID = "data";
        }   
    }
    
    public void FloatWaveGenerator(WaveExampleType type)
    {          
        // Init chunks
        header = new WaveHeader();
        format = new WaveFormatChunk<float>(2, 44100);
        data = new WaveDataChunk<float>(format.dwSamplesPerSec * format.wChannels);            
    
        // Fill the data array with sample data
        switch (type)
        {
            case WaveExampleType.ExampleSineWave:
                double freq = 440.0f;   // Concert A: 440Hz
    
                // The "angle" used in the function, adjusted for the number of channels and sample rate.
                // This value is like the period of the wave.
                double t = (Math.PI * 2 * freq) / format.dwSamplesPerSec;
    
                for (uint i = 0; i < format.dwSamplesPerSec - 1; i++)
                {
                    // Fill with a simple sine wave at max amplitude
                    for (int channel = 0; channel < format.wChannels; channel++)
                    {
                        data.dataArray[i * format.wChannels + channel] = (float)Math.Sin(t * i);
                    }                        
                }
    
                break;
        }          
    }
    

    Note that you'll need to adjust the variables used by the FloatWaveGenerator, and the foreach loop saving the data must write the correct data type as well. I leave this as an exercise to you. :)