在使用C#进行录音和播放录音功能上,使用NAudio是个不错的选择。
NAudio是个开源,相对功能比较全面的类库,它包含录音、播放录音、格式转换、混音调整等操作,具体可以去Github上看看介绍和源码,附:Git地址
我使用到的是录制和播放wav格式的音频,对应调用NAudio的WaveFileWriter和WaveFileReader类进行开发,从源码上看原理就是
- 根据上层传入的因为文件类型格式(mp3、wav等格式)定义进行创建流文件,并添加对应header和format等信息
- 调用WinAPI进行数据采集,实时读取其传入数据并保存至上述流文件中
- 保存
附:WaveFileWriter源码
using System; using System.IO; using NAudio.Wave.SampleProviders; using NAudio.Utils; // ReSharper disable once CheckNamespace namespace NAudio.Wave { ////// This class writes WAV data to a .wav file on disk /// public class WaveFileWriter : Stream { private Stream outStream; private readonly BinaryWriter writer; private long dataSizePos; private long factSampleCountPos; private long dataChunkSize; private readonly WaveFormat format; private readonly string filename; /// /// Creates a 16 bit Wave File from an ISampleProvider /// BEWARE: the source provider must not return data indefinitely /// /// The filename to write to /// The source sample provider public static void CreateWaveFile16(string filename, ISampleProvider sourceProvider) { CreateWaveFile(filename, new SampleToWaveProvider16(sourceProvider)); } /// /// Creates a Wave file by reading all the data from a WaveProvider /// BEWARE: the WaveProvider MUST return 0 from its Read method when it is finished, /// or the Wave File will grow indefinitely. /// /// The filename to use /// The source WaveProvider public static void CreateWaveFile(string filename, IWaveProvider sourceProvider) { using (var writer = new WaveFileWriter(filename, sourceProvider.WaveFormat)) { var buffer = new byte[sourceProvider.WaveFormat.AverageBytesPerSecond * 4]; while (true) { int bytesRead = sourceProvider.Read(buffer, 0, buffer.Length); if (bytesRead == 0) { // end of source provider break; } // Write will throw exception if WAV file becomes too large writer.Write(buffer, 0, bytesRead); } } } /// /// Writes to a stream by reading all the data from a WaveProvider /// BEWARE: the WaveProvider MUST return 0 from its Read method when it is finished, /// or the Wave File will grow indefinitely. /// /// The stream the method will output to /// The source WaveProvider public static void WriteWavFileToStream(Stream outStream, IWaveProvider sourceProvider) { using (var writer = new WaveFileWriter(new IgnoreDisposeStream(outStream), sourceProvider.WaveFormat)) { var buffer = new byte[sourceProvider.WaveFormat.AverageBytesPerSecond * 4]; while(true) { var bytesRead = sourceProvider.Read(buffer, 0, buffer.Length); if (bytesRead == 0) { // end of source provider outStream.Flush(); break; } writer.Write(buffer, 0, bytesRead); } } } /// /// WaveFileWriter that actually writes to a stream /// /// Stream to be written to /// Wave format to use public WaveFileWriter(Stream outStream, WaveFormat format) { this.outStream = outStream; this.format = format; writer = new BinaryWriter(outStream, System.Text.Encoding.UTF8); writer.Write(System.Text.Encoding.UTF8.GetBytes("RIFF")); writer.Write((int)0); // placeholder writer.Write(System.Text.Encoding.UTF8.GetBytes("WAVE")); writer.Write(System.Text.Encoding.UTF8.GetBytes("fmt ")); format.Serialize(writer); CreateFactChunk(); WriteDataChunkHeader(); } /// /// Creates a new WaveFileWriter /// /// The filename to write to /// The Wave Format of the output data public WaveFileWriter(string filename, WaveFormat format) : this(new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.Read), format) { this.filename = filename; } private void WriteDataChunkHeader() { writer.Write(System.Text.Encoding.UTF8.GetBytes("data")); dataSizePos = outStream.Position; writer.Write((int)0); // placeholder } private void CreateFactChunk() { if (HasFactChunk()) { writer.Write(System.Text.Encoding.UTF8.GetBytes("fact")); writer.Write((int)4); factSampleCountPos = outStream.Position; writer.Write((int)0); // number of samples } } private bool HasFactChunk() { return format.Encoding != WaveFormatEncoding.Pcm && format.BitsPerSample != 0; } /// /// The wave file name or null if not applicable /// public string Filename => filename; /// /// Number of bytes of audio in the data chunk /// public override long Length => dataChunkSize; /// /// Total time (calculated from Length and average bytes per second) /// public TimeSpan TotalTime => TimeSpan.FromSeconds((double)Length / WaveFormat.AverageBytesPerSecond); /// /// WaveFormat of this wave file /// public WaveFormat WaveFormat => format; /// /// Returns false: Cannot read from a WaveFileWriter /// public override bool CanRead => false; /// /// Returns true: Can write to a WaveFileWriter /// public override bool CanWrite => true; /// /// Returns false: Cannot seek within a WaveFileWriter /// public override bool CanSeek => false; /// /// Read is not supported for a WaveFileWriter /// public override int Read(byte[] buffer, int offset, int count) { throw new InvalidOperationException("Cannot read from a WaveFileWriter"); } /// /// Seek is not supported for a WaveFileWriter /// public override long Seek(long offset, SeekOrigin origin) { throw new InvalidOperationException("Cannot seek within a WaveFileWriter"); } /// /// SetLength is not supported for WaveFileWriter /// /// public override void SetLength(long value) { throw new InvalidOperationException("Cannot set length of a WaveFileWriter"); } /// /// Gets the Position in the WaveFile (i.e. number of bytes written so far) /// public override long Position { get => dataChunkSize; set => throw new InvalidOperationException("Repositioning a WaveFileWriter is not supported"); } /// /// Appends bytes to the WaveFile (assumes they are already in the correct format) /// /// the buffer containing the wave data /// the offset from which to start writing /// the number of bytes to write [Obsolete("Use Write instead")] public void WriteData(byte[] data, int offset, int count) { Write(data, offset, count); } /// /// Appends bytes to the WaveFile (assumes they are already in the correct format) /// /// the buffer containing the wave data /// the offset from which to start writing /// the number of bytes to write public override void Write(byte[] data, int offset, int count) { if (outStream.Length + count > UInt32.MaxValue) throw new ArgumentException("WAV file too large", nameof(count)); outStream.Write(data, offset, count); dataChunkSize += count; } private readonly byte[] value24 = new byte[3]; // keep this around to save us creating it every time /// /// Writes a single sample to the Wave file /// /// the sample to write (assumed floating point with 1.0f as max value) public void WriteSample(float sample) { if (WaveFormat.BitsPerSample == 16) { writer.Write((Int16)(Int16.MaxValue * sample)); dataChunkSize += 2; } else if (WaveFormat.BitsPerSample == 24) { var value = BitConverter.GetBytes((Int32)(Int32.MaxValue * sample)); value24[0] = value[1]; value24[1] = value[2]; value24[2] = value[3]; writer.Write(value24); dataChunkSize += 3; } else if (WaveFormat.BitsPerSample == 32 && WaveFormat.Encoding == WaveFormatEncoding.Extensible) { writer.Write(UInt16.MaxValue * (Int32)sample); dataChunkSize += 4; } else if (WaveFormat.Encoding == WaveFormatEncoding.IeeeFloat) { writer.Write(sample); dataChunkSize += 4; } else { throw new InvalidOperationException("Only 16, 24 or 32 bit PCM or IEEE float audio data supported"); } } /// /// Writes 32 bit floating point samples to the Wave file /// They will be converted to the appropriate bit depth depending on the WaveFormat of the WAV file /// /// The buffer containing the floating point samples /// The offset from which to start writing /// The number of floating point samples to write public void WriteSamples(float[] samples, int offset, int count) { for (int n = 0; n < count; n++) { WriteSample(samples[offset + n]); } } /// /// Writes 16 bit samples to the Wave file /// /// The buffer containing the 16 bit samples /// The offset from which to start writing /// The number of 16 bit samples to write [Obsolete("Use WriteSamples instead")] public void WriteData(short[] samples, int offset, int count) { WriteSamples(samples, offset, count); } /// /// Writes 16 bit samples to the Wave file /// /// The buffer containing the 16 bit samples /// The offset from which to start writing /// The number of 16 bit samples to write public void WriteSamples(short[] samples, int offset, int count) { // 16 bit PCM data if (WaveFormat.BitsPerSample == 16) { for (int sample = 0; sample < count; sample++) { writer.Write(samples[sample + offset]); } dataChunkSize += (count * 2); } // 24 bit PCM data else if (WaveFormat.BitsPerSample == 24) { for (int sample = 0; sample < count; sample++) { var value = BitConverter.GetBytes(UInt16.MaxValue * (Int32)samples[sample + offset]); value24[0] = value[1]; value24[1] = value[2]; value24[2] = value[3]; writer.Write(value24); } dataChunkSize += (count * 3); } // 32 bit PCM data else if (WaveFormat.BitsPerSample == 32 && WaveFormat.Encoding == WaveFormatEncoding.Extensible) { for (int sample = 0; sample < count; sample++) { writer.Write(UInt16.MaxValue * (Int32)samples[sample + offset]); } dataChunkSize += (count * 4); } // IEEE float data else if (WaveFormat.BitsPerSample == 32 && WaveFormat.Encoding == WaveFormatEncoding.IeeeFloat) { for (int sample = 0; sample < count; sample++) { writer.Write((float)samples[sample + offset] / (float)(Int16.MaxValue + 1)); } dataChunkSize += (count * 4); } else { throw new InvalidOperationException("Only 16, 24 or 32 bit PCM or IEEE float audio data supported"); } } /// /// Ensures data is written to disk /// Also updates header, so that WAV file will be valid up to the point currently written /// public override void Flush() { var pos = writer.BaseStream.Position; UpdateHeader(writer); writer.BaseStream.Position = pos; } #region IDisposable Members /// /// Actually performs the close,making sure the header contains the correct data /// /// True if called from Dispose protected override void Dispose(bool disposing) { if (disposing) { if (outStream != null) { try { UpdateHeader(writer); } finally { // in a finally block as we don't want the FileStream to run its disposer in // the GC thread if the code above caused an IOException (e.g. due to disk full) outStream.Dispose(); // will close the underlying base stream outStream = null; } } } } /// /// Updates the header with file size information /// protected virtual void UpdateHeader(BinaryWriter writer) { writer.Flush(); UpdateRiffChunk(writer); UpdateFactChunk(writer); UpdateDataChunk(writer); } private void UpdateDataChunk(BinaryWriter writer) { writer.Seek((int)dataSizePos, SeekOrigin.Begin); writer.Write((UInt32)dataChunkSize); } private void UpdateRiffChunk(BinaryWriter writer) { writer.Seek(4, SeekOrigin.Begin); writer.Write((UInt32)(outStream.Length - 8)); } private void UpdateFactChunk(BinaryWriter writer) { if (HasFactChunk()) { int bitsPerSample = (format.BitsPerSample * format.Channels); if (bitsPerSample != 0) { writer.Seek((int)factSampleCountPos, SeekOrigin.Begin); writer.Write((int)((dataChunkSize * 8) / bitsPerSample)); } } } /// /// Finaliser - should only be called if the user forgot to close this WaveFileWriter /// ~WaveFileWriter() { System.Diagnostics.Debug.Assert(false, "WaveFileWriter was not disposed"); Dispose(false); } #endregion } }
WaveFileReader和WaveFileWriter相似,只是把写流文件变成了读流文件,具体可在源码中查看。
值得注意的是,在有需要对音频进行分析处理的需求时(如VAD)可以查看其DataAvailable事件,该事件会实时回调传递音频数据(byte[]),最后强调一点这个音频数据byte数组需要注意其写入时和读取时PCM所使用的bit数,PCM分别有8/16/24/32四种,在WaveFormat.BitsPerSample属性上可以查看,根据PCM不同类型这个byte数组的真实数据转换上也要转换不同类型,8bit是一个字节、16bit是两个字节、24.....32...等,在使用时根据这个进行对应转换才是正确的数值。
附PCM类型初始化对应部分代码:
public static ISampleProvider ConvertWaveProviderIntoSampleProvider(IWaveProvider waveProvider) { ISampleProvider sampleProvider; if (waveProvider.WaveFormat.Encoding == WaveFormatEncoding.Pcm) { // go to float if (waveProvider.WaveFormat.BitsPerSample == 8) { sampleProvider = new Pcm8BitToSampleProvider(waveProvider); } else if (waveProvider.WaveFormat.BitsPerSample == 16) { sampleProvider = new Pcm16BitToSampleProvider(waveProvider); } else if (waveProvider.WaveFormat.BitsPerSample == 24) { sampleProvider = new Pcm24BitToSampleProvider(waveProvider); } else if (waveProvider.WaveFormat.BitsPerSample == 32) { sampleProvider = new Pcm32BitToSampleProvider(waveProvider); } else { throw new InvalidOperationException("Unsupported bit depth"); } } else if (waveProvider.WaveFormat.Encoding == WaveFormatEncoding.IeeeFloat) { if (waveProvider.WaveFormat.BitsPerSample == 64) sampleProvider = new WaveToSampleProvider64(waveProvider); else sampleProvider = new WaveToSampleProvider(waveProvider); } else { throw new ArgumentException("Unsupported source encoding"); } return sampleProvider; } }
以上是查看源码和使用上的一些记录,具体录制和播放示例如下:示例
新接触,有些感悟,分享下