在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进行。这里有两个地方需要注意

  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 = ,
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声音的更多相关文章

  1. DirectSound播放PCM(可播放实时采集的音频数据)

    前言 该篇整理的原始来源为http://blog.csdn.net/leixiaohua1020/article/details/40540147.非常感谢该博主的无私奉献,写了不少关于不同多媒体库的 ...

  2. 最简单的视音频播放示例9:SDL2播放PCM

    本文记录SDL播放音频的技术.在这里使用的版本是SDL2.实际上SDL本身并不提供视音频播放的功能,它只是封装了视音频播放的底层API.在Windows平台下,SDL封装了Direct3D这类的API ...

  3. 最简单的视音频播放示例8:DirectSound播放PCM

    本文记录DirectSound播放音频的技术.DirectSound是Windows下最常见的音频播放技术.目前大部分的音频播放应用都是通过DirectSound来播放的.本文记录一个使用Direct ...

  4. 使用AudioTrack播放PCM音频数据(android)

    众所周知,Android的MediaPlayer包含了Audio和video的播放功能,在Android的界面上,Music和Video两个应用程序都是调用MediaPlayer实现的.MediaPl ...

  5. 最简单的视音频播放演示样例8:DirectSound播放PCM

    ===================================================== 最简单的视音频播放演示样例系列文章列表: 最简单的视音频播放演示样例1:总述 最简单的视音频 ...

  6. Android 音视频深入 二 AudioTrack播放pcm(附源码下载)

    本篇项目地址,名字是录音和播放PCM,求starhttps://github.com/979451341/Audio-and-video-learning-materials 1.AudioTrack ...

  7. OpenAL播放pcm或wav数据流-windows/ios/android(一)

    OpenAL播放pcm或wav数据流-windows/iOS/Android(一)   最近在研究渲染问题,本文采用openal做pcm和wav数据流播放,并非本地文件,demo是windows的,i ...

  8. ffplay代码播放pcm数据

    摘抄雷兄 http://blog.csdn.net/leixiaohua1020/article/details/46890259 /** * 最简单的SDL2播放音频的例子(SDL2播放PCM) * ...

  9. OSX/iOS 播放系统声音

    方法1: 系统会自带了些声音,有时候一些操作用必要自己播放一下声音提醒一下,用bash的直接say something就ok了,写代码的时候呢?原来很简单的,一句: [[NSSound soundNa ...

随机推荐

  1. AC自动机(病毒侵袭 )

    题目链接:https://cn.vjudge.net/contest/280743#problem/B 题目大意:中文题目 具体思路:AC自动机模板题,编号的时候注意,是按照给定的id进行编号的.然后 ...

  2. C++(vs)多线程调试 (转)

      在一个程序中,这些独立运行的程序片断叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”.利用线程,用户可按下一个按钮,然后程序会立即作出响应,而不是让用户等待程序完成了当前任务以后 ...

  3. docker之容器访问和网络连接(三)

    前言 当一台服务器上部署了多个应用容器,它们直接可能需要相互通信,比如web应用容器需要访问mysql数据库容器. 主机访问容器 通过映射端口的形式我们可以在外部访问容器内的服务 # 将主机的127. ...

  4. RPM Database

    RPM Database RPM 不仅在安装.升级.卸载方面工作出色,而且在查询和验证方面也表现非凡.你很久前安装了一个数据库软件,但现在忘记了它的版本号,也不知道它的说明文档的位置,可以通过 RPM ...

  5. mysql主从复制跳过复制错误【转】

    跳过复制错误 mysql因为binlog机制问题,有些时候会出现从库重放sql执行失败的情况,特别是旧的STATEMENT模式最容易出现这种情况(因为函数和存储过程等原因),这也是为什么强调使用mix ...

  6. tomcat报错:java.net.SocketException: Permission denied["http-nio-80"]

    tomcat报错:java.net.SocketException: Permission denied["http-nio-80"] 问题:使用非root账户tomcat启动to ...

  7. 解决chrome运行报错unknown error: cannot get automation extension

    今天把默认浏览器改成chrome,结果一运行脚本就报错,具体错误信息如下. FAILED CONFIGURATION: @BeforeClass beforeClassorg.openqa.selen ...

  8. 【LOJ】#2080. 「JSOI2016」病毒感染

    题解 那个限制表示一回头要治完前面的所有病人 我们处理一个g[i][j]表示治疗i到j的病人至少会死多少病人 \(g[i][j] = g[i + 1][j] + sum[i + 1,j] + min( ...

  9. Codeforces 145E Lucky Queries 线段树

    Lucky Queries 感觉是很简单的区间合并, 但是好像我写的比较麻烦. #include<bits/stdc++.h> #define LL long long #define f ...

  10. 004.MySQL主库手动复制至从库

    一 主库手动复制至从库 1.1 Master主库锁表 mysql> flush tables with read lock; Query OK, 0 rows affected (0.00 se ...