• DirectX编程:[初级]C# 中利用 DirectSound 录音


    来源:http://www.cnblogs.com/stg609/archive/2008/10/24/1318931.html

          花了一阵子,把DirectX安装后自带的帮助文件中的那部分关于DirectSound录音这块给看完了,顺便把那部分翻译成了中文,有些地方可能翻译的不是很通顺,不过总体上还是能看得懂的。查看中文翻译,建议大家在进行学习前可以先去看看。
          期间也看了些别人的相关文章,感觉有点思路后就开始动手了,很高兴最后能顺利通过测试。不过我这个功能很简单,也不完善,只是最基本的可以录音。还待以后深入学习。

          开发平台:VS.NET 2005 ,Windows XP SP2 ,DirectX SDK(June 2008)下载页面 。
          必须的硬件设备:麦克风、声卡(集成或独立均可)、音响(能播放声音就行)

          首先,我们来温故下声卡和声音的基础知识。

          [摘自网络]  声音其实是一种能量波,因此也有频率和振幅的特征,频率对应于时间轴线,振幅对应于电平轴线。波是无限光滑的,弦线其实由无数点组成,由于存储空间是相对有限的,数字编码过程中,必须对弦线的点进行采样。采样的过程就是抽取某点的频率值,很显然,在一秒中内抽取得点越多,获取得频率信息更丰富,为了复原波形,一次振动中,必须有2个点的采样,人耳能够感觉到的最高频率为20kHz,因此要满足人耳的听觉要求,则需要至少每秒进行40k次采样,用40kHz表达,这个40kHz就是采样率。我们常见的CD,采样率为44.1kHz。光有频率信息是不够的,我们还必须获得该频率的能量值并量化,用于表示信号强度。量化值为2的整数次幂,我们常见的CD位16bit的采样大小,即2的16次方。


    上图中A线条表示原始信号,而线条B和C表示不同采样率和采样大小的数字信号。采样率和采样大小的值越大,记录的波形更接近原始信号。


          采样频率一般分为22.05kHz、44.1kHz、48kHz三个等级,22.05kHz只能达到FM(调频)广播的声音品质,44.1kHz则是理论上的CD音质极限,48kHz则更加精确一些。对于高于48kHz的采样频率,人耳已无法听到,所以在电脑中也没有多少实用价值。
          目前的声卡基本都具有输入和输出信号的能力,这也是声卡具有最基本功能(录制声音、播放声音)的基础。如果可以同时输出和输入信号,这块声卡就得支持全双工的工作模式,这便是网络上进行语音通讯的基础。
          我们要讲的录音,就是将采集自麦克风的模拟信号转换成数字信号(ADC),一般麦克风只能提供模拟信号。
          一般声卡采集到的数据会被存放到缓存区后进行处理,如果是集成声卡,那就是先把数据放在内存中后再处理,你可以通过任务管理器来查看WINDOWS自带的录音机在录音时候内存的变化。
     

          对以上内容有所了解后,我们接着来了解下利用DirectSound录制声音的基本步骤。

           1 DirectSound是什么?
           DirectSound 是微软提供的DirectX API 的一部分。它使你能以极低的时延播放声音,使应用程序可以高度利用硬件资源。

           2 DirectSound能做什么?
           2.1 按照WAV格式播放声音。
           2.2 可以同时播放多种声音。
           2.3 将高优先级的声音分配给由硬件控制的缓冲区。
           2.4 将普通的声音融入自定的3D环境中。
           2.5 可以给声音添加不同的效果,比如回声,合唱等。
           2.6 从麦克风或其它音频输入设备中捕获WAV声音。

           3 DirectSound有哪些主要对象?

    对象 说明 作用 .Net中的类或结构体
       设备对象   每个应用程序只有一个设备对象   用来管理设备,创建辅助缓冲区   Microsoft.DirectX.DirectSound.Capture
       主缓冲区   一个应用程序只有一个主缓冲区   操控声音捕捉缓冲区和产生混音效果的区域  Microsoft.DirectX.DirectSound.CaptureBuffer
       辅助缓冲区   每一个声音对应一个辅助缓冲区,可以有多个辅助缓冲区   用来存储要播放的声音文件,可建立多个辅助缓冲区来放多个要播放的声音文 Microsoft.DirectX.DirectSound.SecondaryBuffer
      事件通知对象   一个缓冲区可以有多个通知对象   用于在缓冲区的特定点触发通知事件,来通知程序执行操作   Microsoft.DirectX.DirectSound.Notify

           4 其它辅助对象

    对象 作用 .Net中的类或结构体
       音频格式   定义WAV音频格式,如采样频率、量化位数、声道数等   Microsoft.DirectX.DirectSound.WaveFormat
       通知的事件   通知正在等待的线程已发生事件   System.Threading.AutoResetEvent


          5 必需知道的关键点
          实在是太不厚道了!!!!!辛苦写了半天,提交前没有事先复制一份,结果提交了半天给我展示了一个“无法显示该页面”,害得我又得从上次保存的地方开始写。真晕呀!!还不知道能不能记起刚才写的。都有点不想写了,可又觉得可惜。可恶!!可恶!!
          5.1 WAVE格式
          WAVE是录音时用的标准的WINDOWS文件格式,扩展名为“WAV”,
          我们使用DirectSound采集的WAV声音,其音频数据是按照PCM(脉冲编码调制,对连续变化的模拟信号进行抽样、量化和编码产生的数据,0和1的组合)调制后放入缓冲区的。
          WAVE文件格式采用RIFF文件格式结构,对PCM数据和其它一些音频信息进行相应的编排,从而最终形成的WAVE文件才能被音频播放器识别,才能进行播放。

           5.2 缓冲区指针
           缓冲区是存放音频数据的地方,并且它还提供了我们两个指针:读指针和捕捉指针。它们的位置按照相对于缓冲区起始位置的偏移量计算。读指针位于当前已经被完全捕捉到缓冲区的数据末尾。捕捉指针位于当前将要从硬件中复制的数据块的末尾。如果你想从缓冲区中读取数据,则只能从已经完全写入缓冲区的数据中读取,也就是说我们只能从偏移量小于读指针的地方读取。

           5.3 缓冲区通知
           大家应该都知道时间相同的音频文件,WAVE文件会比其它格式的音频文件大得多,这是因为WAVE文件没有对数据进行压缩。如果录音的时候,不限制缓冲区大小,那么你录制很短的时间可能就会占用很多内存,说不定不过多久,你的1G内存就不够用了。因此我们必须对缓冲区的大小进行限制,而且当缓冲区满了之后,还可以重新从缓冲区起始处开始,用新的数据覆盖旧的数据。那旧的数据怎么办呢?如果你不想丢失旧的数据,那就得在旧的数据被覆盖之前,将它转移到其它地方。
          如何才能在旧的数据没有被覆盖之前,将它转移走呢?如果是你,你会采用什么办法?
          有人提出通过轮询的办法,经常询问缓冲区是否满,满了则进行转移操作。可是这样做会相当耗费性能。微软提供了我们一个解决办法:“通知”。我们可以在缓冲区中的某些位置处设置通知,当读指针到达通知位置的时候,就会触发相应的事件执行转移操作。是不是有点像操作系统中的“响应中断”呢?

          6 录音大致过程
          6.1 设置PCM格式(很多人喜欢说是设置WAVE格式,但是个人觉得这样说并不恰当,因为PCM才是用来描述数据采集的,而WAVE只是一种文件格式。),设置相关的参数,如:采样频率、量化位数等。
          6.2 创建WAVE文件,有没有搞错呀?数据还没开始采集,怎么就先创建文件了呢?我可以很明确得告诉你没有错。因为RIFF结构的WAVE文件除了音频数据之外,还有其它数据,比如音频格式、格式长度等类似于文件头的数据。有了文件头后,接下来就只需要把接收到的数据添加在这个后面就好了。当然你一定要最后写的话,也不是不可以。
          6.3 建立设备对象,建立缓冲区对象。
          6.4 设置缓冲区通知,设置通知被触发后的事件。
          6.5 准备就绪后,就可以开始录音了。
          6.6 当通知被触发后,建立一个新的线程来处理数据转移的事件。(建立一个新的线程,就是为了防止录音过程被中断)。
          6.7 录音结束,写入WAV文件尾。这样一个可以播放的WAVE文件就OK了。


          具体代码
          不知道怎么形容现在的感觉了,真得很高兴大部分内容还记得。不知道你现在是何心情,是否已经没什么热情看下去了?本来想分两篇写的,但后来想想还是不浪费首面原创区的空间了。如果你前面的已经看懂了,那下面对你来说可能只是写写代码的事了。

          1.需要引用的命名空间和外部dll。

            两个外部DLL为:Microsoft.DirectX.dll 和 Microsoft.DirectX.DirectSound.dll

    using System.Threading;
    using System.IO;
    using Microsoft.DirectX.DirectSound;
    using Microsoft.DirectX;


           2.用户变量


            private string strRecSaveFile = string.Empty;//文件保存路径
            private Notify myNotify = null;//缓冲区提示事件
            private FileStream fsWav = null;//保存的文件流
            private int iNotifyNum = 16;//通知的个数
            private int iBufferOffset = 0;//本次数据起始点, 上一次数据的终点。
            private int iSampleSize = 0;//所采集到的数据大小
            private int iNotifySize = 0;//通知所在区域大小
            private int iBufferSize = 0;//缓冲区大小
            private BinaryWriter mWriter;
            
    private Capture capture = null;//捕捉设备对象
            private CaptureBuffer capturebuffer = null;//捕捉缓冲区
            private AutoResetEvent notifyevent = null;
            
    private Thread notifythread = null;
            
    private WaveFormat mWavFormat;//PCM格式


           3.设置PCM格式

    复制代码
            private WaveFormat SetWaveFormat()
            {
                WaveFormat format 
    = new WaveFormat();
                format.FormatTag 
    = WaveFormatTag.Pcm;//设置音频类型
                format.SamplesPerSecond = 22050;//采样率(单位:赫兹)典型值:11025、22050、44100Hz
                format.BitsPerSample = 16;//采样位数
                format.Channels = 1;//声道
                format.BlockAlign = (short)(format.Channels * (format.BitsPerSample / 8));//单位采样点的字节数
                format.AverageBytesPerSecond = format.BlockAlign * format.SamplesPerSecond;
                
    return format;
                
    //按照以上采样规格,可知采样1秒钟的字节数为22050*2=55100B 约为 53K
            }
    复制代码


           4.创建WAVE文件  


            private void CreateWaveFile(string strFileName)
            {
                fsWav 
    = new FileStream(strFileName, FileMode.CreateNew);
                mWriter 
    = new BinaryWriter(fsWav);
                
    /**************************************************************************
                   Here is where the file will be created. A
                   wave file is a RIFF file, which has chunks
                   of data that describe what the file contains.
                   A wave RIFF file is put together like this:
                   The 12 byte RIFF chunk is constructed like this:
                   Bytes 0 - 3 :  'R' 'I' 'F' 'F'
                   Bytes 4 - 7 :  Length of file, minus the first 8 bytes of the RIFF description.
                                     (4 bytes for "WAVE" + 24 bytes for format chunk length +
                                     8 bytes for data chunk description + actual sample data size.)
                    Bytes 8 - 11: 'W' 'A' 'V' 'E'
                    The 24 byte FORMAT chunk is constructed like this:
                    Bytes 0 - 3 : 'f' 'm' 't' ' '
                    Bytes 4 - 7 : The format chunk length. This is always 16.
                    Bytes 8 - 9 : File padding. Always 1.
                    Bytes 10- 11: Number of channels. Either 1 for mono,  or 2 for stereo.
                    Bytes 12- 15: Sample rate.
                    Bytes 16- 19: Number of bytes per second.
                    Bytes 20- 21: Bytes per sample. 1 for 8 bit mono, 2 for 8 bit stereo or
                                    16 bit mono, 4 for 16 bit stereo.
                    Bytes 22- 23: Number of bits per sample.
                    The DATA chunk is constructed like this:
                    Bytes 0 - 3 : 'd' 'a' 't' 'a'
                    Bytes 4 - 7 : Length of data, in bytes.
                    Bytes 8 -: Actual sample data.
                  **************************************************************************
    */
                
    char[] ChunkRiff = { 'R''I''F''F' };
                
    char[] ChunkType = { 'W''A''V''E' };
                
    char[] ChunkFmt = { 'f''m''t'' ' };
                
    char[] ChunkData = { 'd''a''t''a' };
                
    short shPad = 1;                // File padding
                int nFormatChunkLength = 0x10;  // Format chunk length.
                int nLength = 0;                // File length, minus first 8 bytes of RIFF description. This will be filled in later.
                short shBytesPerSample = 0;     // Bytes per sample.
                
    // 一个样本点的字节数目
                if (8 == mWavFormat.BitsPerSample && 1 == mWavFormat.Channels)
                    shBytesPerSample 
    = 1;
                
    else if ((8 == mWavFormat.BitsPerSample && 2 == mWavFormat.Channels) || (16 == mWavFormat.BitsPerSample && 1 == mWavFormat.Channels))
                    shBytesPerSample 
    = 2;
                
    else if (16 == mWavFormat.BitsPerSample && 2 == mWavFormat.Channels)
                    shBytesPerSample 
    = 4;
                
    // RIFF 块
                mWriter.Write(ChunkRiff);
                mWriter.Write(nLength);
                mWriter.Write(ChunkType);
                
    // WAVE块
                mWriter.Write(ChunkFmt);
                mWriter.Write(nFormatChunkLength);
                mWriter.Write(shPad);
                mWriter.Write(mWavFormat.Channels);
                mWriter.Write(mWavFormat.SamplesPerSecond);
                mWriter.Write(mWavFormat.AverageBytesPerSecond);
                mWriter.Write(shBytesPerSample);
                mWriter.Write(mWavFormat.BitsPerSample);
                
    // 数据块
                mWriter.Write(ChunkData);
                mWriter.Write((
    int)0);   // The sample length will be written in later.
            }


           5.建立两个对象


            private bool CreateCaputerDevice()
            {
                
    //首先要玫举可用的捕捉设备
                CaptureDevicesCollection capturedev = new CaptureDevicesCollection();
                Guid devguid;
                
    if (capturedev.Count > 0)
                {
                    devguid 
    = capturedev[0].DriverGuid;
                }
                
    else
                {
                    MessageBox.Show(
    "当前没有可用于音频捕捉的设备""系统提示");
                    
    return false;
                }
                
    //利用设备GUID来建立一个捕捉设备对象
                capture = new Capture(devguid);
                
    return true;
            }

            
    private void CreateCaptureBuffer()
            {
    //想要创建一个捕捉缓冲区必须要两个参数:缓冲区信息(描述这个缓冲区中的格式等),缓冲设备。

                CaptureBufferDescription bufferdescription 
    = new CaptureBufferDescription();
                bufferdescription.Format 
    = mWavFormat;//设置缓冲区要捕捉的数据格式
                iNotifySize = 1024;//设置通知大小
                iBufferSize = iNotifyNum * iNotifySize;
                bufferdescription.BufferBytes 
    = iBufferSize;
                capturebuffer 
    = new CaptureBuffer(bufferdescription, capture);//建立设备缓冲区对象
            }


           6.设置通知以及相应的事件


            //设置通知
            private void CreateNotification()
            {
                BufferPositionNotify[] bpn 
    = new BufferPositionNotify[iNotifyNum];//设置缓冲区通知个数
    //设置通知事件
                notifyevent = new AutoResetEvent(false);
                notifythread 
    = new Thread(RecoData);
                notifythread.Start();
                
    for (int i = 0; i < iNotifyNum; i++)
                {
                    bpn[i].Offset 
    = iNotifySize + i * iNotifySize-1;//设置具体每个的位置
                    bpn[i].EventNotifyHandle = notifyevent.Handle;
                }
                myNotify 
    = new Notify(capturebuffer);
                myNotify.SetNotificationPositions(bpn);
                
            }
            
    //线程中的事件
            private void RecoData()
            {
                
    while (true)
                {
                    
    // 等待缓冲区的通知消息
                    notifyevent.WaitOne(Timeout.Infinite, true);
                    
    // 录制数据
                    RecordCapturedData();
                }
            }

            
    //真正转移数据的事件,其实就是把数据转移到WAV文件中。
            private void RecordCapturedData()
            {
                
    byte[] capturedata = null;
                
    int readpos = 0, capturepos = 0, locksize = 0;
                capturebuffer.GetCurrentPosition(
    out capturepos, out readpos);
                locksize 
    = readpos - iBufferOffset;//这个大小就是我们可以安全读取的大小
                if (locksize == 0)
                {
                    
    return;
                }
                
    if (locksize < 0)
                {
    //因为我们是循环的使用缓冲区,所以有一种情况下为负:当文以载读指针回到第一个通知点,而Ibuffeoffset还在最后一个通知处
                    locksize += iBufferSize;
                }

                capturedata 
    = (byte[])capturebuffer.Read(iBufferOffset, typeof(byte), LockFlag.FromWriteCursor, locksize);
                mWriter.Write(capturedata, 
    0, capturedata.Length);//写入到文件
                iSampleSize += capturedata.Length;
                iBufferOffset 
    += capturedata.Length;
                iBufferOffset 
    %= iBufferSize;//取模是因为缓冲区是循环的。
            }


           7.开始捕捉
           调用缓冲区的START方法就可以开始捕捉了。

           8.结束捕捉并写入WAV文件尾


            private void stoprec()
            {
                capturebuffer.Stop();
    //调用缓冲区的停止方法。停止采集声音
                if (notifyevent != null)
                    notifyevent.Set();
    //关闭通知
                notifythread.Abort();//结束线程
                RecordCapturedData();//将缓冲区最后一部分数据写入到文件中

                
    //写WAV文件尾
                mWriter.Seek(4, SeekOrigin.Begin);
                mWriter.Write((
    int)(iSampleSize + 36));   // 写文件长度
                mWriter.Seek(40, SeekOrigin.Begin);
                mWriter.Write(iSampleSize);                
    // 写数据长度
                mWriter.Close();
                fsWav.Close();
                mWriter 
    = null;
                fsWav 
    = null;

            }


           这样, 基本就完成了。但是并没有进行完善。这个还待日后改善。
          
           感叹呀~~写这篇,真得好不容易,感觉卡得就像个...。

           参考:http://blog.donews.com/uplook/archive/2005/12/14/657145.aspx
                   http://www.cnblogs.com/onlytiancai/archive/2008/08/02/p2p_sound_chat.html

  • 相关阅读:
    Windows环境中Java多个JDK之间相互切换
    百度地图调用,传递经纬度到后台
    富文本的使用-KindEditor
    Play框架的@OneToMany、@ManyToOne级联操作
    Play框架文件上传
    [20171211][转载]如何实现dbms_output输出没有打开serveroutput on.txt
    [20171211]ora-16014 11g.txt
    [20171206]rman与truncate2.txt
    [20171206]rman与truncate.txt
    [20171205]uniq命令的输入输出.txt
  • 原文地址:https://www.cnblogs.com/luoshupeng/p/2890804.html
Copyright © 2020-2023  润新知