通过WinAPI播放PCM声音

栏目: ASP.NET · 发布时间: 5年前

内容简介:在Windows平台上,播放PCM声音使用的API通常有如下两种。在Windows Vista以后,推出了更加强大的对于前面的两个API,在.net平台下有如下封装:

在Windows平台上,播放PCM声音使用的API通常有如下两种。

在Windows Vista以后,推出了更加强大的 WASAPI   ,并用 WASAPI 封装了 MME 以及 DirectSound API

对于前面的两个API,在.net平台下有如下封装:

WSAPI可能由于更加复杂,没有什么比较完善的封装,codeproject上有篇文章介绍了如何简单的封装WSAPI: Recording and playing PCM audio on Windows 8 (VB)

最近一个项目中使用到了PCM文件的播放,本来想用NAudio实现的,但使用过程中发现它自己提供的BlockAlignReductionStream播放实时数据是效果不是蛮好(方法可以参考这篇 文章 ),总是有一些卡顿的现象。

究其原因是其Buffer的机制,要求每次都填充满buffer,对于文件播放这个不是问题,但对于实时pcm数据,buffer过大播放的时候得不到足够的数据,buffer过小丢数据的情况。

于是,我便研究了一下微软的MMEAPI,官方文档: Using Waveform and Auxiliary Audio 。发现MMEAPI也并不复杂,一个简单的示例如下 


#include <Windows.h>
#include <stdio.h>
#pragma comment(lib, "winmm.lib")
 
int main()
{
    const int buf_size = 1024 * 1024 * 30;
    char* buf = new char[buf_size];
 
    FILE* thbgm; //文件
 
    fopen_s(&thbgm, R"(r:\re_sample.pcm)", "rb");
    fread(buf, sizeof(char), buf_size, thbgm); //预读取文件
    fclose(thbgm);
 
    WAVEFORMATEX wfx = {0};
    wfx.wFormatTag = WAVE_FORMAT_PCM; //设置波形声音的格式
    wfx.nChannels = 2;            //设置音频文件的通道数量
    wfx.nSamplesPerSec = 44100; //设置每个声道播放和记录时的样本频率
    wfx.wBitsPerSample = 16;    //每隔采样点所占的大小
 
    wfx.nBlockAlign = wfx.nChannels * wfx.wBitsPerSample / 8;
    wfx.nAvgBytesPerSec = wfx.nBlockAlign * wfx.nSamplesPerSec;
  
    HANDLE wait = CreateEvent(NULL, 0, 0, NULL);
    HWAVEOUT hwo;
    waveOutOpen(&hwo, WAVE_MAPPER, &wfx, (DWORD_PTR)wait, 0L, CALLBACK_EVENT); //打开一个给定的波形音频输出装置来进行回放
 
    int data_size = 20480;
    char* data_ptr = buf;
    WAVEHDR wh;
 
    while (data_ptr - buf < buf_size)
    {
        //这一部分需要特别注意的是在循环回来之后不能花太长的时间去做读取数据之类的工作,不然在每个循环的间隙会有“哒哒”的噪音
        wh.lpData = data_ptr;
        wh.dwBufferLength = data_size;
        wh.dwFlags = 0L;
        wh.dwLoops = 1L;
 
        data_ptr += data_size;
 
        waveOutPrepareHeader(hwo, &wh, sizeof(WAVEHDR)); //准备一个波形数据块用于播放
        waveOutWrite(hwo, &wh, sizeof(WAVEHDR)); //在音频媒体中播放第二个函数wh指定的数据
 
        WaitForSingleObject(wait, INFINITE); //等待
    }
    waveOutClose(hwo);
    CloseHandle(wait);
 
    
    return 0;
}

View Code

这里是首先预读pcm文件到内存,然后通过事件回调的方式同步写入声音数据。 整个播放过程大概也就用到了五六个API,主要过程如下:

设置音频参数

音频参数定义在一个 WAVEFORMATEX 对象中,这里只介绍PCM的设置方法,主要设置声道数、采样率、和采样位数。

WAVEFORMATEX     wfx = { 0 };
wfx. wFormatTag  =  WAVE_FORMAT_PCM ;     //
设置波形声音的格式
wfx. nChannels  = 2;                     //
设置音频文件的道数量
wfx. nSamplesPerSec  = 44100;             //
设置每个声道播放和记录时的样本频率
wfx. wBitsPerSample  = 16;             //
每隔采样点所占的大小

除此之外,还需要设置两个参数nBlockAlign和nAvgBytesPerSec。对于PCM,它们的计算公式如下:

wfx. nBlockAlign  = wfx. nChannels  * wfx. wBitsPerSample  / 8; 
wfx. nAvgBytesPerSec  = wfx. nBlockAlign  * wfx. nSamplesPerSec

打开音频输出

打开音频输出需要定义一个 HWAVEOUT 对象,它代表一个波形对象,通过 waveOutOpen 函数打开它。

HWAVEOUT hwo;
waveOutOpen (&hwo,  WAVE_MAPPER , &wfx, ( DWORD_PTR )wait, 0L,  CALLBACK_EVENT ); 

这个函数前三个参数分别是波形对象,输出设备(WAVE_MAPPER为-1,表示默认输出设备),音频参数。 后面三个参数分别是回调相关参数,因为音频数据一次只写入一小段,播放是由系统在另一个线程中进行的,当数据播放完成后,需要通过回调的方式通知写入新数据。

MMEAPI支持多种回调方式。具体参看MSDN文档: waveOutOpen function 。具体常见的回调方式有如下几种:

  • CALLBACK_NULL        不回调,需要主动掌握写入数据时机,常用于实时音频流

  • CALLBACK_EVENT        需要数据时写事件,在另外一个独立的线程上等待该事件写入数据

  • CALLBACK_FUNCTION        需要数据时执行回调函数,在回调函数中写入数据

这里是示例通过事件的方式回调的

写入音频数据

音频的播放操作是一个生产者消费者模型,调用waveOutOpen后,系统会在后台启动一个播放线程(WinForm程序也可以设置为使用UI线程)。当需要数据时,调用回调函数,写入相应的数据。

首先定义一个WAVEHDR对象:

int data_size = 20480;
char * data_ptr = buf;
WAVEHDR  wh;

每次写入的操作过程如下:

wh. lpData  = data_ptr;
wh. dwBufferLength  = data_size;
wh. dwFlags  = 0L;
wh. dwLoops  = 1L;
data_ptr += data_size;
waveOutPrepareHeader (hwo, &wh,  sizeof ( WAVEHDR ));  //
准备一个波形数据块用于播放
waveOutWrite (hwo, &wh,  sizeof ( WAVEHDR ));  //
在音频媒体中播放第二个函数 wh 指定的数据

写入主要是通过两个函数 waveOutPrepareHeaderwaveOutWrite 进行。这里有两个地方需要注意

  1. 每次写入data_size不要太小,太小了会出现声音不流畅

  2. 从它调用回调到写入的时间间隔不能过长,否则会出现声音断流而出现的哒哒声。

这两个地方的原因实际上都是一个,消费者线程没有足够的数据。要解决这个问题需要采取缓冲模型,对数据源预读。

另外,写入操作waveOutPrepareHeader和waveOutWrite这两个函数是并不要求一定非要在等待通知后才执行的,当写入的速度和播放的速度不一致时,出现声音快进会慢速播放现象。

关闭音频输出

关闭音频输出只需要使用接口即可。

waveOutClose (hwo);

.net接口封装

了解各接口功能后,自己封装一个也比较简单了。用起来也方便多了。

WinAPI封装:


    using HWAVEOUT = IntPtr;

    class winmm
    {
        [StructLayout(LayoutKind.Sequential)]
        public struct WAVEFORMATEX
        {
            /// <summary>
            /// 波形声音的格式
            /// </summary>
            public WaveFormat wFormatTag;

            /// <summary>
            /// 音频文件的通道数量
            /// </summary>
            public UInt16 nChannels; /* number of channels (i.e. mono, stereo...) */

            /// <summary>
            /// 采样频率
            /// </summary>
            public UInt32 nSamplesPerSec; /* sample rate */

            /// <summary>
            /// 每秒缓冲区
            /// </summary>
            public UInt32 nAvgBytesPerSec; /* for buffer estimation */


            public UInt16 nBlockAlign;    /* block size of data */
            public UInt16 wBitsPerSample; /* number of bits per sample of mono data */
            public UInt16 cbSize;         /* the count in bytes of the size of */
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct WAVEHDR
        {
            /// <summary>
            /// 缓冲区指针
            /// </summary>
            public IntPtr lpData;

            /// <summary>
            /// 缓冲区长度
            /// </summary>
            public UInt32 dwBufferLength;
            public UInt32 dwBytesRecorded; /* used for input only */
            public IntPtr dwUser;          /* for client's use */

            /// <summary>
            /// 设置标志
            /// </summary>
            public UInt32 dwFlags; 

            /// <summary>
            /// 循环控制
            /// </summary>
            public UInt32 dwLoops; 

            /// <summary>
            /// 保留字段
            /// </summary>
            public IntPtr lpNext;  

            /// <summary>
            /// 保留字段
            /// </summary>
            public IntPtr reserved;
        }


        [Flags]
        public enum WaveOpenFlags
        {
            CALLBACK_NULL     = 0,
            CALLBACK_FUNCTION = 0x30000,
            CALLBACK_EVENT    = 0x50000,
            CallbackWindow    = 0x10000,
            CallbackThread    = 0x20000,
        }

        public enum WaveMessage
        {
            WIM_OPEN  = 0x3BE,
            WIM_CLOSE = 0x3BF,
            WIM_DATA  = 0x3C0,
            WOM_CLOSE = 0x3BC,
            WOM_DONE  = 0x3BD,
            WOM_OPEN  = 0x3BB
        }


        [Flags]
        public enum WaveHeaderFlags
        {
            WHDR_BEGINLOOP = 0x00000004,
            WHDR_DONE      = 0x00000001,
            WHDR_ENDLOOP   = 0x00000008,
            WHDR_INQUEUE   = 0x00000010,
            WHDR_PREPARED  = 0x00000002
        }

        public enum WaveFormat : ushort
        {
            WAVE_FORMAT_PCM = 0x0001,
        }


        /// <summary>
        /// 默认设备
        /// </summary>
        public static IntPtr WAVE_MAPPER { get; } = (IntPtr)(-1);

        public delegate void WaveCallback(IntPtr hWaveOut, WaveMessage message, IntPtr dwInstance, WAVEHDR wavhdr,
                                          IntPtr dwReserved);

        [DllImport("winmm.dll")]
        public static extern int waveOutOpen(out HWAVEOUT hWaveOut,   IntPtr uDeviceID,  in WAVEFORMATEX lpFormat,
                                             WaveCallback dwCallback, IntPtr dwInstance, WaveOpenFlags   dwFlags);

        [DllImport("winmm.dll")]
        public static extern int waveOutOpen(out HWAVEOUT hWaveOut,   IntPtr uDeviceID,  in WAVEFORMATEX lpFormat,
                                             IntPtr       dwCallback, IntPtr dwInstance, WaveOpenFlags   dwFlags);

        [DllImport("winmm.dll")]
        public static extern int waveOutSetVolume(HWAVEOUT hwo, ushort dwVolume);

        [DllImport("winmm.dll")]
        public static extern int waveOutClose(in HWAVEOUT hWaveOut);

        [DllImport("winmm.dll")]
        public static extern int waveOutPrepareHeader(HWAVEOUT hWaveOut, in WAVEHDR lpWaveOutHdr, int uSize);

        [DllImport("winmm.dll")]
        public static extern int waveOutUnprepareHeader(HWAVEOUT hWaveOut, in WAVEHDR lpWaveOutHdr, int uSize);

        [DllImport("winmm.dll")]
        public static extern int waveOutWrite(HWAVEOUT hWaveOut, in WAVEHDR lpWaveOutHdr, int uSize);
    }

    class kernel32
    {
        [DllImport("kernel32.dll")]
        public static extern IntPtr CreateEvent(IntPtr lpEventAttributes, bool bManualReset, bool bInitialState, string lpName);

        [DllImport("kernel32.dll")]
        public static extern int WaitForSingleObject(IntPtr hHandle, int dwMilliseconds);

        [DllImport("kernel32.dll")]
        public static extern bool CloseHandle(IntPtr hHandle);
    }

View Code

PCM播放器:


    /// <summary>
    /// Pcm播放器
    /// </summary>
    public unsafe class PcmPlayer
    {
        /// <param name="channels">声道数目</param>
        /// <param name="sampleRate">采样频率</param>
        /// <param name="sampleSize">采样大小(bits)</param>
        public PcmPlayer(int channels, int sampleRate, int sampleSize)
        {
            _wfx = new winmm.WAVEFORMATEX
            {
                wFormatTag     = winmm.WaveFormat.WAVE_FORMAT_PCM,
                nChannels      = (ushort)channels,
                nSamplesPerSec = (ushort)sampleRate,
                wBitsPerSample = (ushort)sampleSize
            };

            _wfx.nBlockAlign     = (ushort)(_wfx.nChannels * _wfx.wBitsPerSample / 8);
            _wfx.nAvgBytesPerSec = _wfx.nBlockAlign * _wfx.nSamplesPerSec;
        }

        winmm.WAVEFORMATEX _wfx;
        IntPtr       _hwo;

        /// <summary>
        /// 以事件回调的方式打开设备
        /// </summary>
        /// <param name="waitEvent"></param>
        public void OpenEvent(IntPtr waitEvent)
        {
            winmm.waveOutOpen(out _hwo, winmm.WAVE_MAPPER, _wfx, waitEvent, IntPtr.Zero, winmm.WaveOpenFlags.CALLBACK_EVENT);
            Debug.Assert(_hwo != IntPtr.Zero);
        }

        public void OpenNone()
        {
            winmm.waveOutOpen(out _hwo, winmm.WAVE_MAPPER, _wfx, IntPtr.Zero, IntPtr.Zero, winmm.WaveOpenFlags.CALLBACK_NULL);
            Debug.Assert(_hwo != IntPtr.Zero);
        }


        winmm.WAVEHDR _wh;
        public void WriteData(ReadOnlyMemory<byte> buffer)
        {
            var hwnd = buffer.Pin();

            _wh.lpData         = (IntPtr)hwnd.Pointer;
            _wh.dwBufferLength = (uint)buffer.Length;
            _wh.dwFlags        = 0;
            _wh.dwLoops        = 1;

            winmm.waveOutPrepareHeader(_hwo, _wh, sizeof(winmm.WAVEHDR)); //准备一个波形数据块用于播放
            winmm.waveOutWrite(_hwo, _wh, sizeof(winmm.WAVEHDR));         //在音频媒体中播放第二个函数wh指定的数据
            hwnd.Dispose();
        }

        public void Dispose()
        {
            winmm.waveOutPrepareHeader(_hwo, _wh, sizeof(winmm.WAVEHDR));
            winmm.waveOutClose(_hwo);
            _hwo = IntPtr.Zero;
        }
    }

    public class WaitObject : IDisposable
    {

        public IntPtr Hwnd { get; set; }

        public WaitObject()
        {
            Hwnd = kernel32.CreateEvent(IntPtr.Zero, false, false, null);
        }

        public void Wait()
        {
            kernel32.WaitForSingleObject(Hwnd, -1);
        }

        public void Dispose()
        {
            kernel32.CloseHandle(Hwnd);
            Hwnd = IntPtr.Zero;
        }
    }

View Code

以上所述就是小编给大家介绍的《通过WinAPI播放PCM声音》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

刘强东自述

刘强东自述

刘强东 / 中信出版集团 / 2016-6-1 / 49.00

京东 1998年,京东还只是中关村一个经营光磁生意的小柜台,月营业额仅有几万元,如今则已经成长为中国营收规模超大的互联网企业,2015年全年营收1813亿,总交易额达到4627亿元; 为解决电商“最后一公里”的痛点,创立并自建B2C物流模式; 经常被争议,却始终坚持“不挣快钱”,选择上市不是因为“缺钱”,只为让合作伙伴睡得着觉,为用户和社会创造价值,由此成就让整个华尔街一片京东红......一起来看看 《刘强东自述》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

html转js在线工具
html转js在线工具

html转js在线工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具