c# 通过WinAPI播放PCM声音
作者:天方 发布时间:2021-10-22 12:35:18
在Windows平台上,播放PCM声音使用的API通常有如下两种。
waveOut and waveIn:传统的音频MMEAPI,也是使用的最多的
xAudio2:C++/COM API,主要针对游戏开发,是DirectSound的基础
在Windows Vista以后,推出了更加强大的WASAPI ,并用WASAPI封装了MME以及DirectSound API。
对于前面的两个API,在.net平台下有如下封装:
NAudio
Sharpdx
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;
}
这里是首先预读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指定的数据
写入主要是通过两个函数waveOutPrepareHeader和waveOutWrite进行。这里有两个地方需要注意
每次写入data_size不要太小,太小了会出现声音不流畅
从它调用回调到写入的时间间隔不能过长,否则会出现声音断流而出现的哒哒声。
这两个地方的原因实际上都是一个,消费者线程没有足够的数据。要解决这个问题需要采取缓冲模型,对数据源预读。
另外,写入操作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);
}
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;
}
}
来源:https://www.cnblogs.com/TianFang/p/9655225.html
猜你喜欢
- transport请求的发送和处理过程前一篇分析对nettytransport的启动及连接,本篇主要分析transport请求的发送和处理过
- 本文实例为大家分享了Java代码对HDFS进行增删改查操作的具体代码,供大家参考,具体内容如下import java.io.File;imp
- 前言:在新增的Concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程
- 缘起年前,因为项目需要进行配置的优化和架构的升级,领导给我来了个任务,让我去进行技术调研需要将配置中心的yaml配置文件里面的配置转为Jso
- 一、概要我们可以用java实现简单的登录界面。如上效果,直观但也需要一步一步来完成,从界面弹窗的设置,图片的插入,文本框的设置,到登录的按钮
- 本文实例讲述了Java线程之守护线程(Daemon)用法。分享给大家供大家参考。具体如下:守护线程(Daemon)Java有两种Thread
- 系列文章已完成,目录如下:jdk-logging log4j logback日志系统实现机制原理介绍commons-lo
- 本文实例为大家分享了Android九宫格图片展示的具体代码,供大家参考,具体内容如下1.RandomAccessFileRandomAcce
- 又忙了一周,事情差不多解决了,终于有可以继续写我的博客了(各位看官久等了)。这次我们来谈一谈Java里的一个很有意思的东西——回调。什么叫回
- 堆的性质堆是一棵完全二叉树,实际中可以通过一个数组来实现,它最重要的一个性质是:任意节点都小于(大于)等于其子节点。将根节点最小的堆称为最小
- C#利用缓存分块读写大文件,供大家参考,具体内容如下在日常生活中,可能会遇到大文件的读取,不论是什么格式,按照储存文件的格式读取大文件,就会
- 什么是Dozer?Dozer是一种Java Bean到Java Bean的映射器,递归地将数据从一个对象复制到另一个对象,它是一个强大的,通
- log4j2支持日志的异步打印,日志异步输出的好处在于,使用单独的进程来执行日志打印的功能,可以提高日志执行效率,减少日志功能对正常业务的影
- 一、数据输出SpringMVC将数据携带给页面的储存工具,有三种,map,ModelMap,model,它们在底层实质还是使用到了Bindi
- 1、运算符两边的变量为boolean变量时 先列出代码:public clas
- 开发环境:IntelliJ IDEA 2019.2.2Spring Boot版本:2.1.8一、发布REST服务1、IDEA新建一个名称为r
- 在我们做项目的过程中,有可能会遇到跨域请求,所以需要我们自己组装支持跨域请求的JSONP数据,而在4.1版本以后的SpringMVC中,为我
- springboot项目启动慢的问题排查springboot项目,随着时间的推移,启动耗时逐步增加,从几分钟慢慢的达到30多分钟,有点恐怖!
- 在java项目开发中。最开始换行符大家一般是在idea中设置新文件为LF,并且对旧文件通过IDEA下方的点击来更换换行符。很显然,对于几千文
- 相信大家肯定都在电商网站买过东西,当我们看中一件喜欢又想买的东西时,这时候你又不想这么快结账,这时候你就可以放入购物车;就像我们平时去超市买