3

I am finding very difficult to find a way to store the audio captured using OpenTk.NetStandard into a proper .WAV file in NetCore C#.

What I am looking for is a solution which will work when running on a Raspberry pi, so NAudio or any Windows specific method won't solve my problem.

I found a couple of other SO answers which show how to capture audio using opentk , but nothing about how to store it in a wav file.

This is an extract of the code which should read data from the microphone I took from anther SO question, I see the AudioCapture class is the one to :

  const byte SampleToByte = 2;
  short[] _buffer = new short[512];
  int _sampling_rate = 16000;
  double _buffer_length_ms = 5000;
  var _recorders = AudioCapture.AvailableDevices;
  int buffer_length_samples = (int)((double)_buffer_length_ms  * _sampling_rate * 0.001 / BlittableValueType.StrideOf(_buffer));

  using (var audioCapture = new AudioCapture(_recorders.First(), _sampling_rate, ALFormat.Mono16, buffer_length_samples))
  {
      audioCapture.Start();
      int available_samples = audioCapture.AvailableSamples;        

      _buffer = new short[MathHelper.NextPowerOfTwo((int)(available_samples * SampleToByte / (double)BlittableValueType.StrideOf(_buffer) + 0.5))];

      if (available_samples > 0)
      {
          audioCapture.ReadSamples(_buffer, available_samples);

          int buf = AL.GenBuffer();
          AL.BufferData(buf, ALFormat.Mono16, buffer, (int)(available_samples * BlittableValueType.StrideOf(_buffer)), audio_capture.SampleFrequency);
          AL.SourceQueueBuffer(src, buf);

         // TODO: I assume this is where the save to WAV file logic should be placed...
      }

  }

Any help would be appreciate!

bre_dev
  • 562
  • 3
  • 21
  • You'll have to build the WAV header manually. Have you looked at how WAV files are structured? I would start there. – Andy Sep 18 '20 at 00:49
  • @Andy thanks for the comment, I improved the question with some code that should capture the audio samples. Any idea how to implement the actual save to WAV logic? – bre_dev Sep 18 '20 at 07:14
  • would NAudio help? https://github.com/naudio/NAudio – null Sep 18 '20 at 07:21
  • https://stackoverflow.com/questions/2665362/convert-byte-array-to-wav-file – null Sep 18 '20 at 07:22
  • @null as I specified in the question description, any Windows specific method calls cannot be used in RaspberryPI... both your comments refer to something that works in Windows only. – bre_dev Sep 18 '20 at 11:18
  • 1
    it's an open source project, the code can be copied and used, the code to produce a wav file does not use windows specific technology. It uses streams from File.IO which are available in .NET Core. – null Sep 18 '20 at 13:27

1 Answers1

5

Here is a .NET Core console program that will write a WAV file using Mono 16-bit data. There are links in the source code you should read through to understand what's going on with the values that are being written.

This will record 10 seconds of data and save it to a file in WAV format:

using OpenTK.Audio;
using OpenTK.Audio.OpenAL;
using System;
using System.IO;
using System.Threading;

class Program
{
    static void Main(string[] args)
    {
        var recorders = AudioCapture.AvailableDevices;
        for (int i = 0; i < recorders.Count; i++)
        {
            Console.WriteLine(recorders[i]);
        }
        Console.WriteLine("-----");

        const int samplingRate = 44100;     // Samples per second

        const ALFormat alFormat = ALFormat.Mono16;
        const ushort bitsPerSample = 16;    // Mono16 has 16 bits per sample
        const ushort numChannels = 1;       // Mono16 has 1 channel

        using (var f = File.OpenWrite(@"C:\users\andy\desktop\out.wav"))
        using (var sw = new BinaryWriter(f))
        {
            // Read This: http://soundfile.sapp.org/doc/WaveFormat/

            sw.Write(new char[] { 'R', 'I', 'F', 'F' });
            sw.Write(0); // will fill in later
            sw.Write(new char[] { 'W', 'A', 'V', 'E' });
            // "fmt " chunk (Google: WAVEFORMATEX structure)
            sw.Write(new char[] { 'f', 'm', 't', ' ' });
            sw.Write(16); // chunkSize (in bytes)
            sw.Write((ushort)1); // wFormatTag (PCM = 1)
            sw.Write(numChannels); // wChannels
            sw.Write(samplingRate); // dwSamplesPerSec
            sw.Write(samplingRate * numChannels * (bitsPerSample / 8)); // dwAvgBytesPerSec
            sw.Write((ushort)(numChannels * (bitsPerSample / 8))); // wBlockAlign
            sw.Write(bitsPerSample); // wBitsPerSample
            // "data" chunk
            sw.Write(new char[] { 'd', 'a', 't', 'a' });
            sw.Write(0); // will fill in later

            // 10 seconds of data. overblown, but it gets the job done
            const int bufferLength = samplingRate * 10;
            int samplesWrote = 0;

            Console.WriteLine($"Recording from: {recorders[0]}");

            using (var audioCapture = new AudioCapture(
                recorders[0], samplingRate, alFormat, bufferLength))
            {
                var buffer = new short[bufferLength];

                audioCapture.Start();
                for (int i = 0; i < 10; ++i)
                {
                    Thread.Sleep(1000); // give it some time to collect samples

                    var samplesAvailable = audioCapture.AvailableSamples;
                    audioCapture.ReadSamples(buffer, samplesAvailable);
                    for (var x = 0; x < samplesAvailable; ++x)
                    {
                        sw.Write(buffer[x]);
                    }

                    samplesWrote += samplesAvailable;

                    Console.WriteLine($"Wrote {samplesAvailable}/{samplesWrote} samples...");
                }
                audioCapture.Stop();
            }

            sw.Seek(4, SeekOrigin.Begin); // seek to overall size
            sw.Write(36 + samplesWrote * (bitsPerSample / 8) * numChannels);
            sw.Seek(40, SeekOrigin.Begin); // seek to data size position
            sw.Write(samplesWrote * (bitsPerSample / 8) * numChannels);
        }
    }
}
Andy
  • 12,859
  • 5
  • 41
  • 56
  • 1
    This is great! Thanks @Andy, I will test it over the next couple of days! – bre_dev Sep 18 '20 at 20:36
  • 1
    looking at the specs, I think this line: sw.Write(44 + samplesWrote..., Should be: sw.Write(36 + samplesWrote , instead. Am I right? – bre_dev Sep 19 '20 at 07:21
  • 1
    @bre_dev -- yeah.. was counting things as ints instead of shorts. updated – Andy Sep 19 '20 at 15:17
  • @bre_dev -- awesome! I didn't test it on the PI, so glad to hear it works. – Andy Sep 20 '20 at 22:58
  • The next step for me will be to stop the recording when a certain amount of silence is detected. The time based stop is not ideal. I will probably raise a new SO question. – bre_dev Sep 21 '20 at 05:49
  • the problem with your solution is that the Thread.Sleep pauses the audio capturing as well since everything is running on the same thread. Any idea how to solve this? – bre_dev Sep 26 '20 at 10:24
  • @bre_dev -- this was just an example on how to build a WAV file. You would have to collect the wav samples on a timer. So everything in the for loop would need to be added to a timer event. – Andy Sep 26 '20 at 16:31
  • OpenTK moved the AudioCapture class during 4.0 it seems. Looks like it's working by just rolling back a version to the latest 3.X: Install-Package OpenTK -Version 3.3.1 – Drew Delano Jan 26 '21 at 21:42
  • I wrote a writer class for IEEE-Float32 wave format based on this answer. Feel free to use it and/or extend it: https://github.com/Laubeee/Float32WavWriter/blob/main/Float32WavWriter.cs – N4ppeL Feb 24 '22 at 17:32