如何在选定的输出设备上使用本机API在Windows上播放声音(mp3 / wav)

时间:2015-02-09 16:16:09

标签: c++ windows

我只是希望能够为我的程序创建选项,以便用户可以选择将用于播放声音的输出设备,例如MS Lync中的这个:

enter image description here

我最初在Qt中创建了我的程序,我在这里询问了类似(但不完全相同)的问题Qt5+ How to set default audio device for QMediaPlayer

我发现Qt对此漏洞过多,这是不可能的,所以我降低了我的要求,我将使用原生的Windows API,因为这些可能只是解决方案。不幸的是,这需要重写我的程序的某些部分,现在我在msdn上遵循这个指南:https://msdn.microsoft.com/en-us/library/windows/desktop/dd371455%28v=vs.85%29.aspx

我基本上希望能够做到以下几点:

  • 列出所有可用的输出设备并将其显示在首选项表单上 - 我已经使用IMMDeviceEnumerator
  • 创建了一个可用的代码
  • 让用户选择他们想要用来输出我的程序的设备 - 我已经有了那部分
  • 创建一个函数,我们称之为PlaySound(string path),如果使用.wav或.mp3文件的路径调用,则会使用首选的IMMDevice并通过它播放文件 - 这就是我的意思需要帮助

因为到目前为止我使用的是Qt而且我几乎不知道MS windows内部,我不知道如何将文件存储在磁盘上的某个位置并使用Windows API播放它,特别是使用选择的{{1}哪个用户在他们的首选项中设置。我在谷歌搜索并搜索文档,但我只能处理极其复杂和奇怪的解决方案,例如https://msdn.microsoft.com/en-us/library/windows/desktop/dd316756%28v=vs.85%29.aspx

我甚至可以找到一些可以使用IMMDevice设备播放mp3文件的示例,但这并没有真正解释如何更改首选输出设备,所以它对我的使用并不是很有用。

据我所知,低级API可能不会提供一些简单的“playmyfile”功能,但至少要有一些超简单解决方案的示例或者使用选定输出播放媒体文件的教程会很好Windows上的设备,以便我可以将其用作起始参考。我有一个活跃的MCI,现在我只需要通过它播放mp3 / wav文件就可以了。

注意:这不是一些通用的“如何在Windows上播放声音”的问题。我需要能够在选定的音频输出设备上播放该声音。仅适用于我的程序(就像MS Lync,VLC媒体播放器或任何其他高级音频程序一样)。我不想更改系统全局首选项(默认设备等)。

1 个答案:

答案 0 :(得分:0)

我设法做到这一点,但令人惊讶的是使用名为“DirectShow”的Windows本机库,它们主要用于视频渲染,但也可以处理音频。

如何:

枚举输出设备 此函数迭代OS检测到的所有音频设备,并将它们存储在列表中。

void Options::Initialize()
{
#ifdef WIN
    HRESULT hr;
    ICreateDevEnum *pSysDevEnum = NULL;
    hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, (void **)&pSysDevEnum);
    if (FAILED(hr))
        return;

    IEnumMoniker *pEnumCat = NULL;
    hr = pSysDevEnum->CreateClassEnumerator(CLSID_AudioRendererCategory, &pEnumCat, 0);
    if (hr == S_OK)
    {
        // Enumerate the monikers.
        IMoniker *pMoniker = NULL;
        ULONG cFetched;
        while (pEnumCat->Next(1, &pMoniker, &cFetched) == S_OK)
        {
            IPropertyBag *pPropBag;
            hr = pMoniker->BindToStorage(0, 0, IID_IPropertyBag, (void **)&pPropBag);
            if (SUCCEEDED(hr))
            {
                // To retrieve the filter's friendly name, do the following:
                VARIANT varName;
                VariantInit(&varName);
                hr = pPropBag->Read(L"FriendlyName", &varName, 0);
                if (SUCCEEDED(hr))
                {
                    OutputDevice device;
                    device.Name = QString((QChar*)varName.bstrVal, wcslen(varName.bstrVal));
                    Options::devices.append(device);
                }
                VariantClear(&varName);
                pPropBag->Release();
            }
            pMoniker->Release();
        }
        pEnumCat->Release();
    }
    pSysDevEnum->Release();
#endif
}

为用户选择的设备创建过滤器 再次迭代所有设备并为用户选择的设备制作过滤器

HRESULT hr;
ICreateDevEnum *pSysDevEnum = NULL;
hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, (void **)&pSysDevEnum);
if (FAILED(hr))
{
    Error("Failed SystemDeviceEnum");
    return;
}

IEnumMoniker *pEnumCat = NULL;
QSettings s;
hr = pSysDevEnum->CreateClassEnumerator(CLSID_AudioRendererCategory, &pEnumCat, 0);
IBaseFilter *pFilter = NULL;
if (hr == S_OK)
{
    // Enumerate the monikers.
    IMoniker *pMoniker = NULL;
    ULONG cFetched;
    int i = 0;
    while (pEnumCat->Next(1, &pMoniker, &cFetched) == S_OK)
    {
        IPropertyBag *pPropBag;
        hr = pMoniker->BindToStorage(0, 0, IID_IPropertyBag, (void **)&pPropBag);
        if (SUCCEEDED(hr))
        {
            // retrieve the filter's friendly name now
            VARIANT varName;
            VariantInit(&varName);
            hr = pPropBag->Read(L"FriendlyName", &varName, 0);
            if (SUCCEEDED(hr))
            {
                QString name = QString((QChar*)varName.bstrVal, wcslen(varName.bstrVal));
                if (s.value("d:" + name).toBool())
                {
                    hr = pMoniker->BindToObject(NULL, NULL, IID_IBaseFilter, (void**)&pFilter);
                    // now we got the filter in pFilter so we can play sound using that filter
                    PlayWin(pFilter, path);
                }
            }
            VariantClear(&varName);
            pPropBag->Release();
        }
        pMoniker->Release();
    }
    pEnumCat->Release();
}
pSysDevEnum->Release();

使用我们设备的过滤器播放声音 在此函数中,device是来自先前函数的pFilter

HRESULT hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, __uuidof(IGraphBuilder), (void **)&x->pGraph);
if (FAILED(hr))
{
    Error("ERROR - Could not create the Filter Graph Manager.");
    return;
}

hr = x->pGraph->QueryInterface(IID_IBasicAudio, (void**)&x->pOutput);

if (FAILED(hr))
{
    Error("ERROR - Could not create the IBasicAudio.");
    return;
}

x->pFlx = device;
if (device)
    x->pGraph->AddFilter(device, L"fd");
hr = x->pGraph->QueryInterface(__uuidof(IMediaControl), (void **)&x->pControl);
hr = x->pGraph->QueryInterface(__uuidof(IMediaEvent), (void **)&x->pEvent);

// Build the graph.
hr = x->pGraph->RenderFile(path, NULL);
if (SUCCEEDED(hr))
{
    // Run the graph.
    hr = x->pControl->Run();
}
else
{
    Error("Unable to play: " + QString::fromWCharArray(path));
}

这个代码本身当然不会开箱即用,但它简明扼要地为你提供了如何做到这一点的线索:

  1. 检索所有设备的列表并将其存储在某处,以便我们可以为用户创建对话框
  2. 在播放声音之前,我们会检查选择的设备用户并为其创建过滤器
  3. 我们将过滤器应用于DirectShow BasicAudio,它本身可以播放系统编解码器支持的任何媒体文件。
  4. msdn上的文档:https://msdn.microsoft.com/en-us/library/windows/desktop/dd407292%28v=vs.85%29.aspx