通过WinAPI播放PCM声音

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

  • waveOut and waveIn:传统的音频MMEAPI,也是使用的最多的
  • xAudio2:C++/COM API,主要针对游戏开发,是DirectSound的基础

在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

更多信息请参看MSDN文档:
https://msdn.microsoft.com/en-us/library/windows/desktop/dd757713(v=vs.85).aspx

 

打开音频输出

打开音频输出需要定义一个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

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要创建虚拟网口并分配虚拟IP,可以使用WinAPI中的以下函数: 1. CreateInterface函数,用于创建虚拟网络接口。 2. SetIpInterfaceEntry函数,用于设置虚拟网口的IP地址。 以下是一个示例代码,可以帮助你创建虚拟网口并分配虚拟IP: ```c++ #include <winsock2.h> #include <iphlpapi.h> #include <ws2tcpip.h> #include <stdio.h> #pragma comment(lib, "iphlpapi.lib") #pragma comment(lib, "ws2_32.lib") int main(int argc, char* argv[]) { WCHAR adapterName[MAX_ADAPTER_NAME_LENGTH]; GUID adapterGuid; PIP_ADAPTER_ADDRESSES adapterAddresses = NULL; PIP_ADAPTER_ADDRESSES adapter = NULL; ULONG adapterAddressesLength = 0; DWORD result = 0; // 获取网络接口列表长度 result = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, NULL, adapterAddresses, &adapterAddressesLength); if (result == ERROR_BUFFER_OVERFLOW) { // 分配内存 adapterAddresses = (PIP_ADAPTER_ADDRESSES)malloc(adapterAddressesLength); // 重新获取网络接口列表 result = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, NULL, adapterAddresses, &adapterAddressesLength); if (result != NO_ERROR) { printf("GetAdaptersAddresses failed with error: %lu\n", result); return -1; } } else { printf("GetAdaptersAddresses failed with error: %lu\n", result); return -1; } // 遍历网络接口列表 for (adapter = adapterAddresses; adapter != NULL; adapter = adapter->Next) { // 找到第一个物理网络接口 if (adapter->IfType == IF_TYPE_ETHERNET_CSMACD && adapter->OperStatus == IfOperStatusUp) { wcscpy_s(adapterName, MAX_ADAPTER_NAME_LENGTH, adapter->AdapterName); adapterGuid = adapter->AdapterGuid; break; } } if (adapter == NULL) { printf("No physical network adapter found.\n"); return -1; } // 创建虚拟网络接口 NET_LUID adapterLuid = { 0 }; result = ConvertInterfaceGuidToLuid(&adapterGuid, &adapterLuid); if (result != NO_ERROR) { printf("ConvertInterfaceGuidToLuid failed with error: %lu\n", result); return -1; } MIB_IF_ROW2 interfaceRow = { 0 }; interfaceRow.InterfaceLuid = adapterLuid; interfaceRow.InterfaceType = IF_TYPE_SOFTWARE_LOOPBACK; interfaceRow.InterfaceAndOperStatusFlags.HardwareInterface = FALSE; interfaceRow.InterfaceAndOperStatusFlags.OperStatus = IF_OPER_STATUS_UP; wcscpy_s(interfaceRow.Alias, IF_MAX_STRING_SIZE, L"MyVirtualAdapter"); result = CreateIfEntry2(&interfaceRow); if (result != NO_ERROR) { printf("CreateIfEntry2 failed with error: %lu\n", result); return -1; } // 设置虚拟网口的IP地址 MIB_IPINTERFACE_ROW ipInterfaceRow = { 0 }; ipInterfaceRow.InterfaceLuid = adapterLuid; ipInterfaceRow.Family = AF_INET; InetPton(AF_INET, "192.168.100.2", &(ipInterfaceRow.InterfaceLuid)); result = SetIpInterfaceEntry(&ipInterfaceRow); if (result != NO_ERROR) { printf("SetIpInterfaceEntry failed with error: %lu\n", result); return -1; } printf("Virtual network adapter created successfully.\n"); return 0; } ``` 这个示例代码中,我们首先使用GetAdaptersAddresses函数获取网络接口列表,然后找到第一个物理网络接口。接着,我们使用CreateIfEntry2函数创建虚拟网络接口,并使用SetIpInterfaceEntry函数设置虚拟网口的IP地址。注意,IP地址必须是未被使用的,否则会失败。 希望这个示例代码能够帮助你创建虚拟网口并分配虚拟IP。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值