在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. Ubuntu GNOME单击任务栏图标最小化设置

    在Ubuntu GNOME的发行版中,桌面使用的是GNOME,GNOME可以像Windows那样有一个底部任务栏,在Ubuntu GNOME中它称为 dash to dock,如下图: Windows ...

  2. 基于滑动窗口协议写的程序(UDP实现) .

    正好有一个大作业关于用socket实现滑动窗口协议,所以写了一个,模拟接收方与发送方窗口都是2,用两个线程实现. 下面是代码,注释的比较详细了. socket_udp.h #include<st ...

  3. 解决“tar:Exiting with failure status due to previous errors”【转】

    问题: 当我想试着用tar命令来创建一个压缩文件时,总在执行过程中失败,并且抛出一个错误说明"tar:由于前一个错误导致于失败状态中退出"("Exiting with f ...

  4. 促使团队紧密协作[高效能程序员的修炼-N1]

    在Jeff看来,团队里最重要的事情,是人与人之间地协作和沟通!所有的问题,其实都是人的问题.“不管什么问题,那总是人的问题”-温伯格.即,让你和团队陷入困境的最快的方法,就是认为技术是决定性的因素,而 ...

  5. springcloud Zuul中异常处理细节

    Spring Cloud Zuul对异常的处理整体来说还是比较方便的,流程也比较清晰,只是由于Spring Cloud发展较快,各个版本之间有差异,导致有的小伙伴在寻找这方面的资料的时候经常云里雾里, ...

  6. 012_iTerm2 快捷键大全

    标签 新建标签:command + t 关闭标签:command + w 切换标签:command + 数字 command + 左右方向键 切换全屏:command + enter 查找:comma ...

  7. 07 Go 1.7 Release Notes

    Go 1.7 Release Notes Introduction to Go 1.7 Changes to the language Ports Known Issues Tools Assembl ...

  8. 使用crontab命令添加计划任务

    Ubuntu 16.04, 计划任务 就是 有(时间)计划地执行(做)任务,有计划 包括 定时执行(在哪些时间点执行任务).按照周期执行(每隔多少时间执行任务). 那么,什么是任务呢?就是 自己想要干 ...

  9. 【前端node.js框架】node.js框架express

    server.js /* 以下代码等下会有详细的解释 */ var express = require('express'); // 用来引入express模块 var app = express() ...

  10. Excel学习笔记:vlookup基础及多条件查找

    一.vlookup基础 关于vlookup的基础不多记录,相信基本的使用方法大家都懂得. 使用格式:=vlookup(搜索值,搜索范围,列号,是否精准匹配) =VLOOKUP(E2,$B$2:$C$6 ...