通过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 = * * ;
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 = {};
wfx.wFormatTag = WAVE_FORMAT_PCM; //设置波形声音的格式
wfx.nChannels = ; //设置音频文件的通道数量
wfx.nSamplesPerSec = ; //设置每个声道播放和记录时的样本频率
wfx.wBitsPerSample = ; //每隔采样点所占的大小 wfx.nBlockAlign = wfx.nChannels * wfx.wBitsPerSample / ;
wfx.nAvgBytesPerSec = wfx.nBlockAlign * wfx.nSamplesPerSec; HANDLE wait = CreateEvent(NULL, , , NULL);
HWAVEOUT hwo;
waveOutOpen(&hwo, WAVE_MAPPER, &wfx, (DWORD_PTR)wait, 0L, CALLBACK_EVENT); //打开一个给定的波形音频输出装置来进行回放 int data_size = ;
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 ;
}
这里是首先预读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 = ,
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)(-);
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 / );
_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 = ;
_wh.dwLoops = ; 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, -);
} public void Dispose()
{
kernel32.CloseHandle(Hwnd);
Hwnd = IntPtr.Zero;
}
}
通过WinAPI播放PCM声音的更多相关文章
- DirectSound播放PCM(可播放实时采集的音频数据)
前言 该篇整理的原始来源为http://blog.csdn.net/leixiaohua1020/article/details/40540147.非常感谢该博主的无私奉献,写了不少关于不同多媒体库的 ...
- 最简单的视音频播放示例9:SDL2播放PCM
本文记录SDL播放音频的技术.在这里使用的版本是SDL2.实际上SDL本身并不提供视音频播放的功能,它只是封装了视音频播放的底层API.在Windows平台下,SDL封装了Direct3D这类的API ...
- 最简单的视音频播放示例8:DirectSound播放PCM
本文记录DirectSound播放音频的技术.DirectSound是Windows下最常见的音频播放技术.目前大部分的音频播放应用都是通过DirectSound来播放的.本文记录一个使用Direct ...
- 使用AudioTrack播放PCM音频数据(android)
众所周知,Android的MediaPlayer包含了Audio和video的播放功能,在Android的界面上,Music和Video两个应用程序都是调用MediaPlayer实现的.MediaPl ...
- 最简单的视音频播放演示样例8:DirectSound播放PCM
===================================================== 最简单的视音频播放演示样例系列文章列表: 最简单的视音频播放演示样例1:总述 最简单的视音频 ...
- Android 音视频深入 二 AudioTrack播放pcm(附源码下载)
本篇项目地址,名字是录音和播放PCM,求starhttps://github.com/979451341/Audio-and-video-learning-materials 1.AudioTrack ...
- OpenAL播放pcm或wav数据流-windows/ios/android(一)
OpenAL播放pcm或wav数据流-windows/iOS/Android(一) 最近在研究渲染问题,本文采用openal做pcm和wav数据流播放,并非本地文件,demo是windows的,i ...
- ffplay代码播放pcm数据
摘抄雷兄 http://blog.csdn.net/leixiaohua1020/article/details/46890259 /** * 最简单的SDL2播放音频的例子(SDL2播放PCM) * ...
- OSX/iOS 播放系统声音
方法1: 系统会自带了些声音,有时候一些操作用必要自己播放一下声音提醒一下,用bash的直接say something就ok了,写代码的时候呢?原来很简单的,一句: [[NSSound soundNa ...
随机推荐
- HTML5 defer和async的区别
在HTML页面中插入Javascript的主要方法,就是使用<script>元素.这个元素由Netscape创造并在Netscape Navigator 2中首先实现.后来,这个元素就被加 ...
- Angular 下的 directive (part 1)
directive 指令 Directive components 指令部分 使用指令自动引导一个AngularJS应用.ngApp指令指定应用程序的根元素,通常是放在页面的根元素如: < ...
- Hibernate的实体规则、主键生成策略、对象状态
一. hibernate的实体类有一定的规则,类似于mybatis的逆向工程导出的实体类.具体的规则以及原因如下: 1.持久化类需要提供无参的构造方法. 因为hibernate底层采用反射机制创建对象 ...
- 一些对外的服务(例如lnmp)都不用root执行
lnmp lamp等需要对外的服务,都不用root用户执行进程 缺点: 如果php程序使用root运行,万一你的程序有漏洞,被拿到了web shell,那么黑客将直接拥有root权限进入你的系统 对于 ...
- 360加固保的dex脱壳方法
完全测试了看雪论坛的方法,发现并没有脱壳,依然无法反编译 http://bbs.pediy.com/thread-213377.htm 使用上面方法的结果是会得到16个dex文件,只有一个是与程序相关 ...
- Android 拍摄(横\竖屏)视频的懒人之路
想一想,我们聊过AudioReord,AudioTrack,MediaPlayer,那多媒体四大金刚,就剩下了MediaRecorder了(SoundPool?我这里信号不好···).其实MediaR ...
- Tomcat不同版本所对应的Servlet/JSP规范
上午在别人机器上做演示,写好 Servlet居然访问不了.后来回想叻下,觉得应该是Tomcat版本不一致的问题,我用的是Tomcat 7.0建的Project,他们大多数都是用的6.0的版本.回来又仔 ...
- Java 组合
组合: 在新类中产生现有类的对象,由于新的类是由现有类的对象所组成,所以这种方法称为组合 组合和继承都允许在新的类中放置对象,组合时显示的这样做,而继承则是隐式的这样做 组合技术通常用于想在新类中使用 ...
- javaweb笔记四
得到表单数据:1.String str = request.getParameter(String)//根据表单名得到表单值,如果是多个同名的键值,返回第一个值.2.String[] str= req ...
- vue组件库(一):前期准备工作
前言 将近期项目内自行开发一个vue组件,做个总结,记录下自己的思维过程~~~ 正文 接到这个任务后,还是要做些准备工作的. 主要内容如下: 1.优化下所在团队前端开发流程 服务器搭建gitlab,采 ...