解码PCM音频通过WASAPI播放无声音问题求助
问题描述
读取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

