You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

解码PCM音频通过WASAPI播放无声音问题求助

WASAPI播放解码后PCM无声音问题排查

问题描述

读取AAC音频帧,通过Media Foundation解码为48000kHz、双声道、16位的PCM格式,尝试用WASAPI播放。解码后的PCM写入full.pcm文件可在Audacity正常播放,但通过WASAPI输出到默认DAC设备时无声音。所有WASAPI相关代码HRESULT均返回成功,排查无方向。

相关代码

#include "AudioDecoder.h"
#include <vector>
#include <__msvc_chrono.hpp>
#include <string>
#include <fstream>
#include <cassert>
#include <filesystem>

#include <mmdeviceapi.h>
#include <endpointvolume.h>
#include <functiondiscoverykeys.h> 
#include <audioclient.h>

int fps_counter = 0;
int frame_index = 0;

IAudioClient* audio_client;
IAudioRenderClient* render_client = nullptr;

int setup_audio_playback()
{
    HRESULT hr = S_OK;

    IMMDeviceEnumerator* pEnumerator = nullptr;
    IMMDevice* pDevice = nullptr;

    ATLENSURE_SUCCEEDED(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&pEnumerator));

    ATLENSURE_SUCCEEDED(pEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &pDevice));

    IPropertyStore* ips;
    ATLENSURE_SUCCEEDED(pDevice->OpenPropertyStore(STGM_READ, &ips));

    PROPVARIANT varName;
    // Initialize container for property value.
    PropVariantInit(&varName);
    ATLENSURE_SUCCEEDED(ips->GetValue(PKEY_Device_FriendlyName, &varName));

    std::wcout << L"Device name: " << varName.pwszVal << std::endl;

    ATLENSURE_SUCCEEDED(pDevice->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, (void**)&audio_client));

    WAVEFORMATEX* format;
    ATLENSURE_SUCCEEDED(audio_client->GetMixFormat(&format));

    ATLENSURE_SUCCEEDED(audio_client->Initialize(AUDCLNT_SHAREMODE_SHARED, 0, 10000000, 0, format, NULL));

    uint32_t bufferFrameCount;
    ATLENSURE_SUCCEEDED(audio_client->GetBufferSize(&bufferFrameCount));

    ATLENSURE_SUCCEEDED(audio_client->GetService(__uuidof(IAudioRenderClient), (void**)&render_client));

    ATLENSURE_SUCCEEDED(audio_client->Start());

    return hr;
}

int main()
{
    HRESULT hr = S_OK;

    std::ofstream fout_all_frames_pcm;

    std::filesystem::remove(std::filesystem::current_path() / "full.pcm");

    fout_all_frames_pcm.open("full.pcm", std::ios::binary | std::ios::out);

    if (FAILED(hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED)))
        return hr;
    if (FAILED(hr = MFStartup(MF_VERSION)))
        return hr;

    setup_audio_playback();

    AudioDecoder* ad = new AudioDecoder();

    std::vector<uint8_t> data;

    while (true)
    {
        std::chrono::time_point<std::chrono::steady_clock> iteration_time = std::chrono::high_resolution_clock::now();

        // Read frame data
        std::ifstream fin("Encoded Audio Frames\\frame" + std::to_string(frame_index) + ".aac", std::ios::binary | std::ios::in);

        if (fin.fail())
        {
            //throw std::runtime_error("Invalid file path specified");
            break;
        }

        // Get file length
        fin.seekg(0, std::ios::end);
        size_t const length = fin.tellg();
        fin.seekg(0, std::ios::beg);

        if (length > data.size())
        {
            static size_t constexpr const granularity = 64 << 10;
            data.resize((length + (granularity - 1)) & ~(granularity - 1));
            assert(length <= data.size());
        }

        // Copy frame data from file to array;
        fin.read(reinterpret_cast<char*>(data.data()), length);
        fin.close();

        CComPtr<IMFSample> pcm_sample;
        while (!ad->decode_sync(data.data(), length, &pcm_sample))
        {
            if (pcm_sample == nullptr) // This will happen if the color converter isn't able to produce output, so we will continue in that case
                continue;

            CComPtr<IMFMediaBuffer> buffer;
            if (FAILED(hr = pcm_sample->ConvertToContiguousBuffer(&buffer)))
                return hr;

            unsigned char* datas;
            DWORD length;
            if (FAILED(hr = buffer->GetCurrentLength(&length)))
                return hr;

            if (FAILED(hr = buffer->Lock(&datas, nullptr, &length)))
                return hr;

            fout_all_frames_pcm.write((char*)datas, length);

            // Does nothing
            //Sleep(120);

            // Grab all the available space in the shared buffer.
            uint8_t* pData;
            ATLENSURE_SUCCEEDED(render_client->GetBuffer(1, &pData));

            memcpy(pData, datas, length);

            DWORD flags = 0;
            ATLENSURE_SUCCEEDED(render_client->ReleaseBuffer(1, flags));

            pcm_sample.Release();
        }

        frame_index++;
    }

    audio_client->Stop();

    return 0;
}

问题排查与修复方案

1. 音频格式不匹配

当前代码直接使用设备的混合格式(GetMixFormat返回值)初始化WASAPI,但解码后的PCM是固定的48000Hz、双声道、16位,两者可能不兼容。共享模式下WASAPI虽支持格式转换,但错误的格式配置会导致无声。

修复:手动构造与解码后PCM匹配的WAVEFORMATEX结构,替换GetMixFormat的返回值:

// 构造匹配解码后PCM的格式
WAVEFORMATEX pcmFormat = {};
pcmFormat.wFormatTag = WAVE_FORMAT_PCM;
pcmFormat.nChannels = 2;                // 双声道
pcmFormat.nSamplesPerSec = 48000;       // 48kHz采样率
pcmFormat.wBitsPerSample = 16;          // 16位深度
pcmFormat.nBlockAlign = (pcmFormat.nChannels * pcmFormat.wBitsPerSample) / 8;
pcmFormat.nAvgBytesPerSec = pcmFormat.nSamplesPerSec * pcmFormat.nBlockAlign;
pcmFormat.cbSize = 0;

// 用自定义格式初始化AudioClient
ATLENSURE_SUCCEEDED(audio_client->Initialize(AUDCLNT_SHAREMODE_SHARED, 0, 10000000, 0, &pcmFormat, NULL));

注意:如果设备不支持该格式,Initialize会返回错误,此时需通过Media Foundation的格式转换组件将PCM转换为设备支持的格式。

2. GetBuffer/ReleaseBuffer帧数量错误

代码中调用render_client->GetBuffer(1, &pData)仅请求1帧,但解码后的PCM数据长度对应的帧数量远大于1,导致大部分数据写入了非法内存区域,播放内容完全错误。

修复:计算当前PCM数据对应的帧数量,用该数值调用GetBuffer和ReleaseBuffer:

// 计算每帧字节数:双声道*16位/8 = 4字节
uint32_t bytesPerFrame = pcmFormat.nChannels * (pcmFormat.wBitsPerSample / 8);
// 计算当前PCM数据对应的帧数量
uint32_t frameCount = length / bytesPerFrame;

// 请求对应数量的缓冲区
uint8_t* pData;
ATLENSURE_SUCCEEDED(render_client->GetBuffer(frameCount, &pData));
memcpy(pData, datas, length);
ATLENSURE_SUCCEEDED(render_client->ReleaseBuffer(frameCount, flags));

3. 缺少播放同步逻辑

当前代码解码一帧就立即写入缓冲区,若写入速度远快于播放速度,会导致缓冲区溢出,系统直接丢弃多余数据。需根据WASAPI的播放状态控制写入节奏。

修复:通过GetCurrentPadding获取缓冲区已填充帧数,计算可用空间后再写入:

uint32_t paddingFrames;
ATLENSURE_SUCCEEDED(audio_client->GetCurrentPadding(&paddingFrames));
// 获取缓冲区总大小(之前通过GetBufferSize获取的bufferFrameCount)
uint32_t availableFrames = bufferFrameCount - paddingFrames;

// 确保写入帧数不超过可用空间
uint32_t writeFrames = min(frameCount, availableFrames);
if (writeFrames > 0) {
    ATLENSURE_SUCCEEDED(render_client->GetBuffer(writeFrames, &pData));
    memcpy(pData, datas, writeFrames * bytesPerFrame);
    ATLENSURE_SUCCEEDED(render_client->ReleaseBuffer(writeFrames, flags));
    
    // 若有剩余数据,需缓存到后续循环写入
    // ...
} else {
    // 缓冲区已满,短暂等待后重试
    Sleep(10);
}

4. 资源泄漏修复

setup_audio_playback中的IMMDeviceEnumerator、IMMDevice、IPropertyStore和WAVEFORMATEX未释放,会导致内存泄漏,需添加Release调用:

// 在setup_audio_playback函数末尾添加:
PropVariantClear(&varName);
ips->Release();
pDevice->Release();
pEnumerator->Release();
CoTaskMemFree(format); // GetMixFormat返回的格式需用CoTaskMemFree释放

内容的提问来源于stack exchange,提问作者Meme Machine

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 09:30:51