利用Waveform函数播放PCM数据流

前言

由于近期需要做一些语音合成的工作,因此,需要进行对语音合成的数据进行实时播放,到网上找了一下资料,参考MSDN的相关说明,写下了如下一个PCM播放数据流的类,多说无益,直接上码:

头文件 pcmspeaker.h

#pragma once

#include <Windows.h> 
#include "mmsystem.h" 

#pragma comment(lib, "winmm.lib") 

#define DEF_MAX_BUFFER_SIZE (1024 * 16)
#define DEF_MAX_BUFFER_COUNT 16

class CPcmSpeaker
{
public:
    CPcmSpeaker(int bufferSize = DEF_MAX_BUFFER_SIZE, int bufferCnt = DEF_MAX_BUFFER_COUNT);
    ~CPcmSpeaker();

    int init(int channels, int samplePerSec, int bitsPerSample);

    //添加PCM音频数据,等待播放
    int toSpeaker(const void *data, int len, int timeout = INFINITE);
    int clearPcmData();

private:

    typedef struct
    {
        WAVEHDR header;
        char *data;
    }WaveHeadandData;

    int m_maxBufferSize;
    int m_maxBufferCnt;
    
    WaveHeadandData *m_headAndDatas;
    static void CALLBACK waveOutProc(HWAVEOUT hwo, UINT uMsg, DWORD dwInstance, DWORD dwParam1, DWORD dwParam2);
    int writeToWave(const void *data, int len);
    int pcmtoWave(const void *data, int len, int timeout = INFINITE);

    // 公共信息
    WAVEFORMATEX m_waveFormat;
    HWAVEOUT m_hWaveOut; // WAVEOUT句柄
    HANDLE m_hBufferEvent;
    CRITICAL_SECTION m_BufferOpCriticalSection;
};

实现文件 pcmspeaker.cpp

#include "PcmSpeaker.h"

CPcmSpeaker::CPcmSpeaker(int bufferSize, int bufferCnt)
{
    m_hWaveOut = NULL;
    m_hBufferEvent = CreateEvent(NULL, FALSE, FALSE, NULL);

    InitializeCriticalSection(&m_BufferOpCriticalSection);

    //申请内存
    m_headAndDatas = new WaveHeadandData[bufferCnt];
    for (int i = 0; i < bufferCnt; i++)
    {
        memset(&m_headAndDatas[i].header, 0, sizeof(WAVEHDR));
        m_headAndDatas[i].header.dwFlags = WHDR_DONE;
        m_headAndDatas[i].data = new char[bufferSize];
    }

    m_maxBufferSize = bufferSize;
    m_maxBufferCnt = bufferCnt;
}


CPcmSpeaker::~CPcmSpeaker()
{
    //关闭Wave
    if (m_hWaveOut != NULL)
    {
        clearPcmData();
        waveOutClose(m_hWaveOut);
        m_hWaveOut = NULL;
    }

    //关闭一些句柄
    CloseHandle(m_hBufferEvent);

    //删除临界区
    DeleteCriticalSection(&m_BufferOpCriticalSection);

    //释放内存
    for (int i = 0; i < m_maxBufferCnt; i++)
        delete[] m_headAndDatas[i].data;

    delete[] m_headAndDatas;
}


int CPcmSpeaker::init(int channels, int samplePerSec, int bitsPerSample)
{
    if (m_hWaveOut != NULL) {
        return 0;// 已经进行了初始化
    }

    // 第一步: 获取waveformat信息
    m_waveFormat.wFormatTag = WAVE_FORMAT_PCM;
    m_waveFormat.nChannels = channels;
    m_waveFormat.wBitsPerSample = bitsPerSample;
    m_waveFormat.nSamplesPerSec = samplePerSec;
    m_waveFormat.nBlockAlign =
        m_waveFormat.nChannels * m_waveFormat.wBitsPerSample / 8;
    m_waveFormat.nAvgBytesPerSec =
        m_waveFormat.nSamplesPerSec * m_waveFormat.nBlockAlign;
    m_waveFormat.cbSize = sizeof(m_waveFormat);

    MMRESULT ret = waveOutOpen(NULL, WAVE_MAPPER, &m_waveFormat,
        NULL, NULL, WAVE_FORMAT_QUERY);
    if (MMSYSERR_NOERROR != ret) {
        return -1;
    }

    // 第二步: 获取WAVEOUT句柄
    ret = waveOutOpen(&m_hWaveOut, WAVE_MAPPER, &m_waveFormat,
        (DWORD_PTR)waveOutProc, (DWORD_PTR)this, CALLBACK_FUNCTION);

    if (MMSYSERR_NOERROR != ret) {
        return -1;
    }

    return 0;
}

void CALLBACK CPcmSpeaker::waveOutProc(HWAVEOUT hwo, UINT uMsg, DWORD dwInstance, DWORD dwParam1, DWORD dwParam2)
{
    CPcmSpeaker *render = (CPcmSpeaker *)dwInstance;
    //WAVEHDR *header = (WAVEHDR *)dwParam1;
    int i = 0;
    switch (uMsg)
    {
    case WOM_DONE:
        EnterCriticalSection(&render->m_BufferOpCriticalSection);
        SetEvent(render->m_hBufferEvent);
        LeaveCriticalSection(&render->m_BufferOpCriticalSection);
        break;
    case WOM_CLOSE:
        i = 1;
        break;
    case WOM_OPEN:
        i = 2;
        break;
    }
}

int CPcmSpeaker::clearPcmData()
{
    if (m_hWaveOut != NULL)
    {
        EnterCriticalSection(&m_BufferOpCriticalSection);
        for (int i = 0; i < m_maxBufferCnt; i++)
        {
            if (m_headAndDatas[i].header.dwFlags & WHDR_PREPARED) //有数据被Prepered
                waveOutUnprepareHeader(m_hWaveOut, &m_headAndDatas[i].header, sizeof(WAVEHDR));
        }

        waveOutReset(m_hWaveOut);
        LeaveCriticalSection(&m_BufferOpCriticalSection);
    }
    return 0;
}

int CPcmSpeaker::writeToWave(const void *data, int len)
{
    MMRESULT mmres;
    int i;
    EnterCriticalSection(&m_BufferOpCriticalSection);
    for (i = 0; i < m_maxBufferCnt; i++)
        if (m_headAndDatas[i].header.dwFlags & WHDR_DONE)
        {
            //查看是否需要释放之前已经Prepared资源
            if (m_headAndDatas[i].header.dwFlags & WHDR_PREPARED) //有数据被Prepered
                waveOutUnprepareHeader(m_hWaveOut, &m_headAndDatas[i].header, sizeof(WAVEHDR));
            
            //写入新的数据到音频缓冲区      
            memcpy(m_headAndDatas[i].data, data, len);
            m_headAndDatas[i].header.lpData = m_headAndDatas[i].data;
            m_headAndDatas[i].header.dwBufferLength = len;
            m_headAndDatas[i].header.dwFlags = 0;

            mmres = waveOutPrepareHeader(m_hWaveOut, &m_headAndDatas[i].header, sizeof(WAVEHDR));
            if (MMSYSERR_NOERROR == mmres)
                mmres = waveOutWrite(m_hWaveOut, &m_headAndDatas[i].header, sizeof(WAVEHDR));
            

            break;
        }
    LeaveCriticalSection(&m_BufferOpCriticalSection);

    if (i == m_maxBufferCnt)
        return -2;

    return (mmres == MMSYSERR_NOERROR) ? 0 : -1;
}

//添加PCM音频数据,等待播放
int CPcmSpeaker::pcmtoWave(const void *data, int len, int timeout)
{
    int res;

    if (len > m_maxBufferSize)
        return -1;

    res = writeToWave(data, len);

    //缓冲区已满,需要等待
    if (res == -2)
    {       
        if (WAIT_OBJECT_0 == WaitForSingleObject(m_hBufferEvent, timeout))
            res = writeToWave(data, len);
    }
    
    return res;
}

int CPcmSpeaker::toSpeaker(const void *data, int len, int timeout)
{
    int res;
    int n, l, ptr;

    //对大数据做分段处理
    n = len / m_maxBufferSize;
    l = len % m_maxBufferSize;
    ptr = 0;

    for (int i = 0; i < n; i++)
    {
        res = pcmtoWave(((char *)data) + ptr, m_maxBufferSize, timeout);
        ptr += m_maxBufferSize;
        if (res != 0)
            return -1;
    }

    return pcmtoWave(((char *)data) + ptr, l, timeout);
}

用法

用法非常简单,如下:

  1. 定义实例:CPcmSpeaker ps;
  2. 初始化参数:ps.init(1, 16000, 16);三个参数分别为:通道数,采样速率,单次采样数据位
  3. 填PCM数据到喇叭:ps.toSpeaker(data, data_len);两个参数分别为PCM数据指针和数据长度。

而外说明

微软的这个Waveform相关的函数,感觉比较原始,用的时候,需要如下注意事项:

  • 如MSDN所言,waveOutProc中不能调用任何Waveform相关函数,原文如下:

Applications should not call any system-defined functions from inside a callback function, except for EnterCriticalSection, LeaveCriticalSection, midiOutLongMsg, midiOutShortMsg, OutputDebugString, PostMessage, PostThreadMessage, SetEvent, timeGetSystemTime, timeGetTime, timeKillEvent, and timeSetEvent. Calling other wave functions will cause deadlock.

  • CPcmSpeaker的构造函数定义为:CPcmSpeaker(int bufferSize = DEF_MAX_BUFFER_SIZE, int bufferCnt = DEF_MAX_BUFFER_COUNT),其有两个有默认值的参数,分别为每次写入系统音频缓冲区的数据的最大大小,以及CPcmSpeaker自己的缓冲区个数,bufferCnt不要太小(最好大于2,根据具体的情况设置大小,建议值为32),否则会出现卡顿现象。

  • toSpeaker函数,带有第三个参数(默认为INFINITEtimeout,表示函数调用超时时间。换句话说,该函数在某种程度上是阻塞式的,即,如果写入的太快,使得系统来不及播放数据,导致CPcmSpeaker类内部的缓冲区已经满了,那么toSpeaker函数将会等待有新的缓冲区数据被播放后,腾出空间后,才返回,当然如果你不想死等,可以设置一个超时值,超时后,也会返回。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容