使用 C# 将音频流浮点帧保存为 WAV

Save audio stream float frames as WAV with C#

我正在测试 C# 中的一个应用程序,该应用程序接收实时音频流,然后将其保存到 WAV 文件。音频流具有以下特征:频率或采样率:16000,通道:1,每通道帧样本:320,播放延迟毫秒:200。音频帧以浮点形式出现,我正在收集浮点帧并将它们存储到带有 Binarywriter 的 Memorystream。之后,我将 Memorystream 的内容转换为数组,然后该数组再次转换为 Float 数组。使用浮点数组,我开始处理 assemble WAV 文件。

我将接收到的浮点帧值与我用来构建 WAV 文件的浮点数组中的值进行了比较,它们是相同的。我在将浮点数组处理为 assemble WAV 文件时遇到问题。我不确定是我用ConvertAndWrite()方法做的数据转换不对,还是WAV header没有根据音频流的特点格式化好。

我可以看到正在创建的 WAV 文件,但是除了我认为的 header 之外,里面没有任何内容。任何指导将不胜感激。我整理了这个示例代码供您测试我在做什么:

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

我已将您的代码更新为扩展方法。

这个想法是您可以将数据附加到流中,例如文件流或内存流,显然这不适用于不可搜索的流。所以你可能会添加错误检查和验证。

我想我在看了规格后就得到了 header,它似乎至少可以玩。请注意,由于字节顺序,这并不是真正的跨平台。

我不太确定 rescaleFactor 但是我必须相信你。

但是,您应该能够修改它以接受不同格式的数据。

最后,我正在更新追加末尾的 header,您可以单独执行此操作,即继续添加到流中,然后在完成后更新一次,加入胡椒粉和盐调味.

用法

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

分机

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

改变 writer.Write((short)2); 变成 writer.Write((short)1); 并且生成的文件 (.wav) 将在 Windows(针对 windows 7 进行测试)和 android 设备上播放。

否则,Windows 媒体播放器将 says:having 播放文件时出现问题;android 播放速度会比预期的快。