Search code examples
c#unity-game-engineaudio-streamingaudio-recordingphoton

Save audio stream float frames as WAV with C#


I am testing an application in C# that receives a live audio stream and then saves it to a WAV file. The audio stream has these characteristics: frequency or sampling rate: 16000, channels: 1, frame Samples Per Channel: 320, play Delay in Ms: 200. The audio frames come as floats, and I am collecting the float frames and storing them into a Memorystream with Binarywriter. After that, I convert the content of the Memorystream into an array, and that array then is converted to a Float array again. With the float array, I start the process to assemble the WAV file.

I have compared the float frames values received with the ones inside the float array that I am using to build the WAV file and are the same. I am having trouble processing the float array to assemble the WAV file. I am not sure if I am doing the data conversion wrong with the ConvertAndWrite() method, or if the WAV header is not well formatted according to the characteristics of the audio stream.

I can see the WAV file being created, but there is no content inside apart from the header I think. Any guidance will be much appreciated. I put together this sample code for you to test what I am doing:

using System;
using System.IO;
using System.Text;

class SaveAudioStreamToWav
{

    //Sample as received from stream. Here as a double to avoid altering the sample adding F to each value.
    public double[] receivedStreamSample = { 0, -0.003509521, -0.003356934, 0.0002746582, -0.004516602, -0.0027771, -0.0003967285, -0.001739502, 0.004150391, 0.0008544922, 0.002593994, 0.00970459, 0.003631592, 0.001800537, 0.004760742, 0.004272461, -0.002655029, -0.001495361, -0.006835938, -0.004211426, -0.0008239746, 0.001525879, 0.006347656, 0.002532959, -0.002471924, -0.001342773, 0.001159668, 0.0006713867, -0.000793457, 0.001403809, -0.0006713867, -0.0006713867, -0.0007629395, 0.0009460449, -0.003662109, 0.00390625, -0.001312256, -0.001678467, 0.002288818, -0.001831055, -0.00579834, 0.001220703, -0.005096436, -0.003631592, -0.007019043, -0.0001220703, -0.0008850098, -0.0001220703, -0.005371094, 0.004608154, 0.004425049, 0.0027771, 0.005279541, 0.0001525879, 0.0009765625, 0.004150391, -0.002807617, 0.001678467, -0.004577637, -0.002685547, -0.004364014, -0.0008544922, 0.001281738, -0.0009155273, -0.008148193, -0.001983643, 9.155273E-05, 0.0008239746, 0.0004272461, 0.002807617, -0.00289917, 0.002075195, 0.008392334, 0.003479004, 0.005615234, 0.0009460449, 0.002471924, 0.0004272461, -0.006164551, 0.0003967285, -0.0007629395, -0.007476807, -0.002532959, 0.01495361, 0.01382446, 0.002288818, -0.009063721, -0.1129761, -0.05401611, 0.03497314, -0.03027344, 0.08999634, 0.01831055, 0.01037598, 0.03302002, 0.02667236, 0.04309082, -0.01806641, -0.0440979, 0.07125854, 0.00680542, -0.01242065, 0.001983643, -0.03710938, 0.009552002, 0.01013184, 0.002258301, 0.007446289, 0.004486084, -0.009063721, -0.007293701, 0.008239746, -0.0003967285, 0.001556396, 0.001586914, 0.002258301, 0.001281738, 0.001617432, -0.001831055, 0.001556396, -0.001525879, -0.002410889, 0.004516602, 0.000793457, -0.001403809, -0.004882813, -0.0005187988, -0.003540039, -0.004302979, 0.0004272461, 0.004974365, -0.002868652, -0.003875732, -0.0001220703, 0.001617432, 0.002258301, -0.005889893, -0.001068115, 0.003295898, 0.002410889, -0.00201416, 0.001068115, 0.003143311, -0.001464844, 0.000579834, 0.005310059, 0.001434326, 0.001403809, 0.001312256, -0.001617432, 0.0009460449, -0.0009765625, -0.0007324219, -0.001617432, -0.004730225, 0.001373291, -0.001586914, 0.0005187988, 0.001556396, -0.001647949, 0.0008544922, 0.001739502, 0.0027771, 0.001831055, 3.051758E-05, -0.04672241, 0.02276611, 0.02529907, -0.005249023, -0.02285767, -0.0378418, -0.1454468, 0.04385376, -0.04058838, -0.005249023, -3.051758E-05, -0.02166748, -0.006378174, -0.002380371, -0.0368042, 0.04330444, -0.008453369, 0.0300293, -0.01651001, -0.005554199, -0.01828003, 0.008972168, -0.01571655, -0.01202393, 0.01141357, -0.003997803, 0.004119873, -0.002532959, 0.004333496, -0.001495361, -0.001281738, -0.003692627, -0.001647949, -0.001861572, 0.000793457, -0.0003662109, -0.002532959, -0.001342773, 0.0003051758, 0.002075195, 0.002349854, 0.001464844, 0.001678467, -0.0008850098, -0.0001525879, 0.003723145, -0.0009155273, 0.002807617, -0.005157471, -0.001617432, 0.002471924, 0.002166748, -0.0003356934, 0.000213623, -0.000793457, -0.0008544922, -0.00100708, 0.000213623, 0.001037598, -0.003448486, 0.0009460449, -0.0006103516, -0.002655029, -0.009735107, -0.01101685, 0.01937866, 0.00994873, -0.02600098, 0.04592896, 0.1063843, 0.002441406, -0.0100708, 0.002990723, -0.01235962, -0.003448486, 0.01089478, -0.01480103, -0.02902222, 0.02990723, -0.01376343, 0.01275635, -0.008666992, 0.006469727, -0.009857178, 0.002655029, -0.0004882813, 0.003814697, 0.004943848, -0.002990723, -0.0003051758, -0.001678467, 0.003265381, 0.0009460449, -9.155273E-05, -0.001403809, 0.001739502, -0.002685547, -0.0009460449, -0.001281738, 0.0009765625, 0.001312256, 0.002288818, -0.0002746582, -0.001098633, -0.002319336, -0.000793457, 0.001464844, 0.001281738, -0.002319336, 6.103516E-05, 0.0003967285, -0.002532959, 0.0002441406, 0.001861572, 0.0009765625 };
    public float[] floatsArray;
    public FileStream fileStream;


    static void Main(string[] args)
    {
        var saveAudioStreamToWav = new SaveAudioStreamToWav();

        saveAudioStreamToWav.ConvertDoubleToFloat();
        saveAudioStreamToWav.CreateEmpty(saveAudioStreamToWav.SetNameAndPath());
        saveAudioStreamToWav.ConvertAndWrite();
        saveAudioStreamToWav.WriteHeader();
    }

    public void ConvertDoubleToFloat()
    {
        floatsArray = new float[receivedStreamSample.Length];
        floatsArray = Array.ConvertAll(receivedStreamSample, x => (float)x);
    }

    public string SetNameAndPath()
    {

        //Setting the name of the file
        string timeStamp = DateTime.Now.ToString("yyyyMMddHHmmssfff");
        string filename = "/TestSavingStreamToWav_" + timeStamp + ".wav";
        string path = Directory.GetCurrentDirectory();
        string filepath = path + filename;
        Console.WriteLine(filepath);
        return filepath;
    }

    public void CreateEmpty(string filepath)
    {
        const int HEADER_SIZE = 44;
        fileStream = new FileStream(filepath, FileMode.CreateNew, FileAccess.ReadWrite);
        byte emptyByte = new byte();

        for (int i = 0; i < HEADER_SIZE; i++) //preparing an empty space for the header
        {
            fileStream.WriteByte(emptyByte);
        }

    }

    public void ConvertAndWrite()
    {

        Int16[] intData = new Int16[floatsArray.Length];

        Byte[] bytesData = new Byte[floatsArray.Length * 2]; // bytesData array is twice the size of floatsArray array because a float converted in Int16 is 2 bytes.

        const float rescaleFactor = 32767; //to convert float to Int16

        for (var i = 0; i < floatsArray.Length; i++)
        {
            intData[i] = (short)(floatsArray[i] * rescaleFactor);
            var byteArr = new Byte[2];
            byteArr = BitConverter.GetBytes(intData[i]);
            byteArr.CopyTo(bytesData, i * 2);
        }
        fileStream.Write(bytesData, 0, bytesData.Length);


    }

    public void WriteHeader()
    {

        int hz = 16000; //frequency or sampling rate
        int headerSize = 44; //default for uncompressed wav

        fileStream.Seek(0, SeekOrigin.Begin);

        Byte[] riff = System.Text.Encoding.UTF8.GetBytes("RIFF"); //RIFF marker. Marks the file as a riff file. Characters are each 1 byte long. 
        fileStream.Write(riff, 0, 4);

        Byte[] chunkSize = BitConverter.GetBytes(fileStream.Length - 8); //file-size (equals file-size - 8). Size of the overall file - 8 bytes, in bytes (32-bit integer). Typically, you'd fill this in after creation.
        fileStream.Write(chunkSize, 0, 4);

        Byte[] wave = System.Text.Encoding.UTF8.GetBytes("WAVE"); //File Type Header. For our purposes, it always equals "WAVE".
        fileStream.Write(wave, 0, 4);

        Byte[] fmt = System.Text.Encoding.UTF8.GetBytes("fmt "); //Mark the format section. Format chunk marker. Includes trailing null. 
        fileStream.Write(fmt, 0, 4);

        Byte[] subChunk1 = BitConverter.GetBytes(16); //Length of format data.  Always 16. 
        fileStream.Write(subChunk1, 0, 4);

        UInt16 two = 2;
        UInt16 one = 1;

        Byte[] audioFormat = BitConverter.GetBytes(one); //Type of format (1 is PCM, other number means compression) . 2 byte integer. Wave type PCM
        fileStream.Write(audioFormat, 0, 2);

        Byte[] numChannels = BitConverter.GetBytes(one); //Number of Channels - 2 byte integer
        fileStream.Write(numChannels, 0, 2);

        Byte[] sampleRate = BitConverter.GetBytes(hz); //Sample Rate - 32 byte integer. Sample Rate = Number of Samples per second, or Hertz.
        fileStream.Write(sampleRate, 0, 4);

        Byte[] byteRate = BitConverter.GetBytes(hz * 2 * 1);// sampleRate * bytesPerSample * number of channels, here 16000*2*1.
        fileStream.Write(byteRate, 0, 4);

        UInt16 blockAlign = (ushort)(1 * 2); //channels * bytesPerSample, here 1 * 2  // Bytes Per Sample: 1=8 bit Mono,  2 = 8 bit Stereo or 16 bit Mono, 4 = 16 bit Stereo
        fileStream.Write(BitConverter.GetBytes(blockAlign), 0, 2);

        UInt16 sixteen = 16;
        Byte[] bitsPerSample = BitConverter.GetBytes(sixteen); //Bits per sample (BitsPerSample * Channels) ?? should be 8???
        fileStream.Write(bitsPerSample, 0, 2);

        Byte[] dataString = System.Text.Encoding.UTF8.GetBytes("data"); //"data" chunk header. Marks the beginning of the data section.
        fileStream.Write(dataString, 0, 4);

        Byte[] subChunk2 = BitConverter.GetBytes(fileStream.Length - headerSize); //Size of the data section. data-size (equals file-size - 44). or NumSamples * NumChannels * bytesPerSample ??
        fileStream.Write(subChunk2, 0, 4);

        fileStream.Close();
    }

}//end of class

Solution

  • I have updated your code into an extension method.

    The idea was so you could append you data to a stream, like a file stream or memory stream, obviously this won't work for non seekable streams. So you could probably add error checking and validation.

    I think I got the header right after looking at the specs, it seems to play at least. Note this is not really cross platform because of the endianness.

    I'm not really sure what the rescaleFactor however I'll have to trust you there.

    However, you should be able to modify this to accept data in different formats.

    Lastly, I am updating the header at the end of the append, you could probably do this separately, i.e keep adding to the stream and then update it once when finished, add pepper and salt to taste.

    Usage

    using (var stream = new FileStream(GetFileName(), FileMode.OpenOrCreate, FileAccess.ReadWrite))
    {
       stream.AppendWaveData(receivedStreamSample);
    }
    

    Extension

    public static class BinaryWriterExtensions
    {
       private const int HeaderSize = 44;
    
       private const int Hz = 16000; //frequency or sampling rate
    
       private const float RescaleFactor = 32767; //to convert float to Int16
    
       public static void AppendWaveData<T>(this T stream, float[] buffer)
          where T : Stream
       {
          if (stream.Length > HeaderSize)
          {
             stream.Seek(0, SeekOrigin.End);
          }
          else
          {
             stream.SetLength(HeaderSize);
             stream.Position = HeaderSize;
          }
    
          // rescale
          var floats = Array.ConvertAll(buffer, x => (short)(x * RescaleFactor));
    
          // Copy to bytes
          var result = new byte[floats.Length * sizeof(short)];
          Buffer.BlockCopy(floats, 0, result, 0, result.Length);
    
          // write to stream
          stream.Write(result, 0, result.Length);
    
          // Update Header
          UpdateHeader(stream);
       }
    
       public static void UpdateHeader(Stream stream)
       {
          var writer = new BinaryWriter(stream);
    
          writer.Seek(0, SeekOrigin.Begin);
    
          writer.Write(Encoding.ASCII.GetBytes("RIFF")); //RIFF marker. Marks the file as a riff file. Characters are each 1 byte long. 
          writer.Write((int)(writer.BaseStream.Length - 8)); //file-size (equals file-size - 8). Size of the overall file - 8 bytes, in bytes (32-bit integer). Typically, you'd fill this in after creation.
          writer.Write(Encoding.ASCII.GetBytes("WAVE")); //File Type Header. For our purposes, it always equals "WAVE".
          writer.Write(Encoding.ASCII.GetBytes("fmt ")); //Mark the format section. Format chunk marker. Includes trailing null. 
          writer.Write(16); //Length of format data.  Always 16. 
          writer.Write((short)1); //Type of format (1 is PCM, other number means compression) . 2 byte integer. Wave type PCM
          writer.Write((short)2); //Number of Channels - 2 byte integer
          writer.Write(Hz); //Sample Rate - 32 byte integer. Sample Rate = Number of Samples per second, or Hertz.
          writer.Write(Hz * 2 * 1); // sampleRate * bytesPerSample * number of channels, here 16000*2*1.
          writer.Write((short)(1 * 2)); //channels * bytesPerSample, here 1 * 2  // Bytes Per Sample: 1=8 bit Mono,  2 = 8 bit Stereo or 16 bit Mono, 4 = 16 bit Stereo
          writer.Write((short)16); //Bits per sample (BitsPerSample * Channels) ?? should be 8???
          writer.Write(Encoding.ASCII.GetBytes("data")); //"data" chunk header. Marks the beginning of the data section.    
          writer.Write((int)(writer.BaseStream.Length - HeaderSize)); //Size of the data section. data-size (equals file-size - 44). or NumSamples * NumChannels * bytesPerSample ??        
       }
    } //end of class