音频播放 via Waveform API

Waveform API 播放音频

Waveform API 从 Windows 3.0 时代就登上历史舞台了,至今依然可以运行在最新的 OS 上,不得不佩服 Windows 的兼容性。不过新的 waveform API 是基于 Core Audio 架构之上的,其实就是一个 wrapper。

从名字上看,Waveform API 只支持 Wav 格式的音频,但确切的说是带 WAVEFORMATEX 信息的 PCM 数据,所以其实它也可以搭配其他框架(如 FFmpeg,DShow,MF 等)在解码音频数据后用来播放。

播放流程图

waveform audio playback

播放代码

以下是整个 Waveform API 采集过程的概要代码(包装成 CWavOutHelper 类),略去各个函数的具体实现和资源释放。

hr = _openWavFile(wavFilePath);
mRes = waveOutOpen(&m_hWaveOut, WAVE_MAPPER, &m_wavSample.wavFormat, 
                   (DWORD_PTR)_wavOutCallback, (ULONG)this, CALLBACK_FUNCTION);                   
hr = _prepareHeaderAndPlay();

while (!m_stopped) {
    WaitForSingleObject(m_hEvent, INFINITE);
    for (int i = 0; i < WAV_HEADER_NUM; i++) {
        if (m_wavHeaders[i].dwFlags & WHDR_DONE) {
            hr = _fillAudioBuffer(i);
            if (S_FALSE == hr)
                break;
                
            mRes = waveOutWrite(m_hWaveOut, &m_wavHeaders[i], sizeof(WAVEHDR));
        }
    }
}

for (int i = 0; i < WAV_HEADER_NUM; i++)
    mRes = waveOutUnprepareHeader(m_hWaveOut, &m_wavHeaders[i], sizeof(WAVEHDR));
    
waveOutReset(m_hWaveOut);
waveOutClose(m_hWaveOut);

CWavOutHelper::_openWavFile 函数

这里调用了 CWavFileHelper 来获取 WaveFormat 和 PCM data。

HRESULT CWavOutHelper::_openWavFile(LPCTSTR pFileName)
{
    HRESULT hr = E_FAIL;

    CWavFileHelper wavFile;
    hr = wavFile.open(pFileName);
    RETURN_IF_FAILED(hr);

    m_wavSample.wavFormat = wavFile.getWavFormat();
    hr = wavFile.getAudioData(&m_wavSample.pData, &m_wavSample.dataSize, true);
    RETURN_IF_FAILED(hr);

    wavFile.close();
    return S_OK;
}
CWaveFileHelper::open 函数

WAV 文件的格式请参考我的另一篇博文:PCM和WAV数据结构

HRESULT CWaveFileHelper::open(LPCTSTR filePath)
{
    MMRESULT mRes = 0;

    HMMIO m_hFile = mmioOpen((LPTSTR)filePath, NULL, MMIO_READ | MMIO_DENYWRITE | MMIO_ALLOCBUF);
    RETURN_IF_NULL(m_hFile);

    MMCKINFO riffChunkInfo;
    riffChunkInfo.fccType = mmioFOURCC('W', 'A', 'V', 'E');
    mRes = mmioDescend(m_hFile, &riffChunkInfo, NULL, MMIO_FINDRIFF);
    RETURN_IF_FALSE_EX(mRes == MMSYSERR_NOERROR, HRESULT_FROM_WIN32(mRes));

    MMCKINFO fmtSubChunkInfo;
    fmtSubChunkInfo.ckid = mmioFOURCC('f', 'm', 't', ' ');
    mRes = mmioDescend(m_hFile, &fmtSubChunkInfo, &riffChunkInfo, MMIO_FINDCHUNK);
    RETURN_IF_FALSE_EX(mRes == MMSYSERR_NOERROR, HRESULT_FROM_WIN32(mRes));

    //  `fmt` chunk contains wave audio format described by WAVEFORMAT or WAVEFORMATEX
    LONG cbRead = mmioRead(m_hFile, (HPSTR)&m_waveFormat, fmtSubChunkInfo.cksize);
    RETURN_IF_FALSE(cbRead == fmtSubChunkInfo.cksize);

    mRes = mmioAscend(m_hFile, &fmtSubChunkInfo, 0);
    RETURN_IF_FALSE_EX(mRes == MMSYSERR_NOERROR, HRESULT_FROM_WIN32(mRes));

    //  in a valid `wav` file, after calling mmioDescend, the file position should be
    //  at the beginning of `data` chunk. however, we should never rely on this.
    MMCKINFO dataSubChunkInfo;
    dataSubChunkInfo.ckid = mmioFOURCC('d', 'a', 't', 'a');
    mRes = mmioDescend(m_hFile, &dataSubChunkInfo, &riffChunkInfo, MMIO_FINDCHUNK);
    RETURN_IF_FALSE_EX(mRes == MMSYSERR_NOERROR, HRESULT_FROM_WIN32(mRes));

    m_dataSize = dataSubChunkInfo.cksize;
    m_data = new BYTE[m_dataSize]();
    RETURN_IF_BADNEW(m_data);

    cbRead = mmioRead(m_hFile, (HPSTR)m_data, m_dataSize);
    RETURN_IF_FALSE(cbRead == m_dataSize);

    return S_OK;
}
CWaveFileHelper::getAudioData 函数

此处把 WAV 文件的内容一次性读到内存中。

HRESULT CWaveFileHelper::getAudioData(BYTE** ppBuf, DWORD* bufLen, bool deepCopy)
{
    RETURN_IF_NULL(ppBuf);
    RETURN_IF_NULL(bufLen);

    if (deepCopy) {
        *ppBuf = new BYTE[m_dataSize]();
        RETURN_IF_BADNEW(*ppBuf);
    
        memcpy_s(*ppBuf, m_dataSize, m_data, m_dataSize);
    }
    else
        *ppBuf = m_data;

    *bufLen = m_dataSize;
    return S_OK;
}

CWavOutHelper::_prepareHeaderAndPlay 函数

注意这里有多个 Wave Header,当一个 Wave Header 播放完之后再填充会产生一定的延迟,多个 Wave Header 可以确保播放不会出现中断。播放声音是通过 wavOutWrite API 实现的。

HRESULT CWavOutHelper::_prepareHeaderAndPlay()
{
    HRESULT hr = E_FAIL;
    MMRESULT mRes = 0;
    for (int i = 0; i < WAV_HEADER_NUM; i++) {
        m_wavHeaders[i].dwBufferLength = SAMPLE_SIZE;
        m_wavHeaders[i].lpData = m_audioBuffers[i]; 
        
        mRes = waveOutPrepareHeader(m_hWaveOut, &m_wavHeaders[i], sizeof(WAVEHDR));
        RETURN_IF_FALSE_EX(mRes == MMSYSERR_NOERROR, HRESULT_FROM_WIN32(mRes));

        hr = _fillAudioBuffer(i);
        RETURN_IF_FAILED(hr);

        mRes = waveOutWrite(m_hWaveOut, &m_wavHeaders[i], sizeof(WAVEHDR));
        RETURN_IF_FALSE_EX(mRes == MMSYSERR_NOERROR, HRESULT_FROM_WIN32(mRes));
    }

    return S_OK;
}

CWavOutHelper::_fillAudioBuffer 函数

此处 audio buffer 填充的是供 audio render 使用的 PCM 数据。

HRESULT CWavOutHelper::_fillAudioBuffer(int idx)
{
    if (m_wavSample.dataSize <= m_wavSample.dataPos) { // EOF
        m_stopped = true;
        return S_FALSE;
    }
    
    UINT bytesNotUsed = SAMPLE_SIZE;
    m_wavHeaders[idx].dwFlags &= ~WHDR_DONE;
    UINT remainBytes = m_wavSample.dataSize - m_wavSample.dataPos;
    
    if (remainBytes < bytesNotUsed) { // at last block of file
        memcpy(m_audioBuffers[idx], m_wavSample.pData + m_wavSample.dataPos, remainBytes);
        bytesNotUsed -= remainBytes;
        m_wavSample.dataPos = m_wavSample.dataSize;
    }
    else {
        memcpy(m_audioBuffers[idx], m_wavSample.pData + m_wavSample.dataPos, bytesNotUsed);
        m_wavSample.dataPos += SAMPLE_SIZE;
        bytesNotUsed = 0;
    }
    
    m_wavHeaders[idx].lpData = m_audioBuffers[idx];
    m_wavHeaders[idx].dwBufferLength = SAMPLE_SIZE - bytesNotUsed;
    return S_OK;
}

其他框架下的播放

请参考对应的文章。

Blueware
EOF

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值