Сохранение плавающих кадров аудиопотока в формате WAV с помощью С#

Я тестирую приложение на С#, которое получает живой аудиопоток, а затем сохраняет его в файл WAV. Аудиопоток имеет следующие характеристики: частота или частота дискретизации: 16000, каналы: 1, количество выборок кадра на канал: 320, задержка воспроизведения в мс: 200. Поток памяти с Binarywriter. После этого я конвертирую содержимое Memorystream в массив, а затем этот массив снова преобразуется в массив Float. С помощью массива с плавающей запятой я начинаю процесс сборки WAV-файла.

Я сравнил полученные значения кадров с плавающей запятой со значениями внутри массива с плавающей запятой, который я использую для создания файла WAV, и они совпадают. У меня возникли проблемы с обработкой массива с плавающей запятой для сборки файла WAV. Я не уверен, что я неправильно выполняю преобразование данных с помощью метода ConvertAndWrite(), или заголовок WAV неправильно отформатирован в соответствии с характеристиками аудиопотока.

Я вижу, как создается файл WAV, но внутри нет содержимого, кроме заголовка. Любое руководство будет высоко оценено. Я собрал этот пример кода, чтобы вы могли проверить, что я делаю:

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

person Towerss    schedule 12.05.2018    source источник
comment
Можете ли вы объяснить мне эту строку floatsArray[i] * rescaleFactor, почему число с плавающей запятой умножается на 32767?   -  person TheGeneral    schedule 12.05.2018
comment
@TheGeneral идея преобразовать значение числа с плавающей запятой в эквивалентное 16-битное целое число со знаком.   -  person Towerss    schedule 12.05.2018
comment
Итак, как вы получаете эти данные, поток двойников памяти? о, я вижу, что они приходят как поплавки, и вы сохраняете их в потоке памяти, но ваши тестовые данные удваиваются   -  person TheGeneral    schedule 12.05.2018
comment
@TheGeneral, тестовые данные удваиваются только потому, что, чтобы использовать их в качестве числа с плавающей запятой для примера, мне пришлось бы добавлять F после каждого значения, такого как 0F, -0,003509521F, -0,003356934F, ... и так далее. Вот почему у меня есть метод ConvertDoubleToFloat().   -  person Towerss    schedule 12.05.2018


Ответы (2)


Я обновил ваш код в метод расширения.

Идея заключалась в том, чтобы вы могли добавлять свои данные к потоку, например к файловому потоку или потоку памяти, очевидно, что это не будет работать для потоков без поиска. Таким образом, вы, вероятно, могли бы добавить проверку ошибок и проверку.

Я думаю, что я получил заголовок сразу после просмотра спецификаций, кажется, по крайней мере, он играет. Обратите внимание, что на самом деле это не кросс-платформа из-за порядка байтов.

Я не совсем уверен, что за rescaleFactor, однако я должен вам доверять.

Однако вы должны иметь возможность изменить это, чтобы принимать данные в разных форматах.

Наконец, я обновляю заголовок в конце приложения, вы, вероятно, могли бы сделать это отдельно, то есть продолжать добавлять в поток, а затем обновлять его один раз, когда закончите, добавить перец и соль по вкусу.

Использование

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
person TheGeneral    schedule 12.05.2018
comment
Спасибо за помощь. С этой версией вашего кода каждый раз, когда я даю поплавок из потока в поток.AppendWaveData(receivedStreamSample); он создает новый файл wav. - person Towerss; 12.05.2018
comment
@Towerss, да, используйте FileMode.OpenOrCreate, я обновил свой ответ, иначе вы могли бы просто сохранить файловый поток, как раньше. - person TheGeneral; 12.05.2018
comment
Вы легенда! наконец, благодаря вам я сохраняю поток в файле WAV. Я сделал несколько изменений, и теперь я могу слушать запись без проблем: изменил Hz = 8000 и добавил в расширение новый метод для вызова UpdateHeader(Stream stream) в конце потока. Потрясающий!!! Теперь буду работать над улучшением качества :) - person Towerss; 12.05.2018
comment
@Towers рад, что смог помочь - person TheGeneral; 12.05.2018

измените writer.Write((short)2); на writer.Write((short)1);, и сгенерированный файл (.wav) будет хорошо воспроизводиться как в Windows (протестируйте против Windows 7), так и на устройстве Android.

в противном случае проигрыватель Windows Media сообщит: проблемы с воспроизведением файлов; Android будет воспроизводиться с большей скоростью, чем ожидалось.

person passedbylove    schedule 06.06.2019