Windowsの録音デバイス設定:サンプルレートとビットの深さを別プログラムで設定したい

目的

レトロフリーク(音声出力サンプリングレートが44.1[kHz]固定)でのゲームキャプチャーをしている関係で,キャプチャー側音声のサンプリングレート(サンプルレート)と量子化ビット数(ビットの深さ)を都度,手動で変更している.手動で設定する場合,マウスで3,4クリックが必要となる:

サンプルレートとビットの深さの設定の場所

そもそも,私の使用しているHDMIキャプチャーボードは,48[kHz]の取り込みしかできないことも問題ではある*1

結論

MMDevice APIとIPolicyConfigというものを用いて,所望の動作である特定の録音インターフェースに対して,44.1[kHz]⇔48[kHz]を切り替えることができた(以下のGitHubソースコード参照):

github.com

ざっくり言うと

  1. MMDevice APIで録音インターフェースを取得
  2. サウンド設定に表示される名前で該当する録音インターフェースを特定
  3. IPolicyConfigのGetDeviceFormatとGetMixFormatで現在のフォーマットを取得
  4. それぞれのフォーマットを所望のサンプルレートに設定および関連する設定値を計算
  5. IPolicyConfigのSetDeviceFormatでそれらを反映

という流れとなる.GetMixFormatで得られた値のほうだけ変更すると,まったく音が出なくなるので注意.実行してしまった場合は,デバイスの無効→有効にすると元に戻る.また,逆に,GetDeviceFormatで得られた値のほうだけ変更したらどうなるかは未確認.

現状2023/11/23では,一覧を表示すると見せかけて,録音インターフェースSPDIF-In (USB Sound Blaster HD)が存在したら,サンプルレートをトグルで切り替える動作となっている.もうちょっと改良したい.

#include <wchar.h>
#include "mmdeviceapi.h"
#include "Propidl.h"
#include "functiondiscoverykeys_devpkey.h"
#include "initguid.h"

/* refer to the following article: */
/* https://learn.microsoft.com/en-us/answers/questions/669471/how-to-control-enable-audio-enhancements-with-code */
DEFINE_GUID(CLSID_PolicyConfig, 0x870af99c, 0x171d, 0x4f9e, 0xaf, 0x0d, 0xe6, 0x3d, 0xf4, 0x0c, 0x2b, 0xc9);
MIDL_INTERFACE("f8679f50-850a-41cf-9c72-430f290290c8")
IPolicyConfig : public IUnknown
{
public:
    virtual HRESULT STDMETHODCALLTYPE GetMixFormat(PCWSTR pszDeviceName, WAVEFORMATEX * *ppFormat) = 0;
    virtual HRESULT STDMETHODCALLTYPE GetDeviceFormat(PCWSTR pszDeviceName, bool bDefault, WAVEFORMATEX** ppFormat) = 0;
    virtual HRESULT STDMETHODCALLTYPE ResetDeviceFormat(PCWSTR pszDeviceName) = 0;
    virtual HRESULT STDMETHODCALLTYPE SetDeviceFormat(PCWSTR pszDeviceName, WAVEFORMATEX* ppEndpointFormatFormat, WAVEFORMATEX* pMixFormat) = 0;
    virtual HRESULT STDMETHODCALLTYPE GetProcessingPeriod(PCWSTR pszDeviceName, bool bDefault, PINT64 pmftDefaultPeriod, PINT64 pmftMinimumPeriod) = 0;
    virtual HRESULT STDMETHODCALLTYPE SetProcessingPeriod(PCWSTR pszDeviceName, PINT64 pmftPeriod) = 0;
    virtual HRESULT STDMETHODCALLTYPE GetShareMode(PCWSTR pszDeviceName, struct DeviceShareMode* pMode) = 0;
    virtual HRESULT STDMETHODCALLTYPE SetShareMode(PCWSTR pszDeviceName, struct DeviceShareMode* pMode) = 0;
    virtual HRESULT STDMETHODCALLTYPE GetPropertyValue(PCWSTR pszDeviceName, BOOL bFxStore, const PROPERTYKEY& pKey, PROPVARIANT* pv) = 0;
    virtual HRESULT STDMETHODCALLTYPE SetPropertyValue(PCWSTR pszDeviceName, BOOL bFxStore, const PROPERTYKEY& pKey, PROPVARIANT* pv) = 0;
    virtual HRESULT STDMETHODCALLTYPE SetDefaultEndpoint(PCWSTR pszDeviceName, ERole eRole) = 0;
    virtual HRESULT STDMETHODCALLTYPE SetEndpointVisibility(PCWSTR pszDeviceName, bool bVisible) = 0;
};

#define TARGET_DEV_NAME (L"SPDIF-In (USB Sound Blaster HD)")

int main()
{
    HRESULT hRes = NULL;
    IMMDeviceEnumerator *pDevEnum = NULL;
    IMMDeviceCollection *pDev = NULL;
    UINT nCount = 0;
    IMMDevice *pDevId = NULL;
    LPWSTR wstrEndpointId = NULL;
    IPropertyStore *pPropStore = NULL;
    PROPVARIANT friendlyName;
    IPolicyConfig *pPolicyConfig = NULL;
    WAVEFORMATEX *pDevFormat = NULL, *pMixFormat = NULL;

    hRes = CoInitialize(NULL);
    if (FAILED(hRes))
    {
        return -1;   /* TODO: refactor error proc */
    }

    hRes = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL,
        CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (LPVOID*)&pDevEnum);
    if (FAILED(hRes))
    {
        CoUninitialize();
        return -1;   /* TODO: refactor error proc */
    }

    /* get list of active input audio devices */
    hRes = pDevEnum->EnumAudioEndpoints(eCapture/* eRender: output, eCapture: input */,
        DEVICE_STATE_ACTIVE, &pDev);
    if (FAILED(hRes))
    {
        pDevEnum->Release();
        CoUninitialize();
        return -1;   /* TODO: refactor error proc */
    }

    hRes = pDev->GetCount(&nCount);
    if (FAILED(hRes))
    {
        pDev->Release();
        pDevEnum->Release();
        CoUninitialize();
        return -1;   /* TODO: refactor error proc */
    }
    for (UINT i = 0; i < nCount; i++)
    {
        hRes = pDev->Item(i, &pDevId);
        if (FAILED(hRes))
        {
            pDev->Release();
            pDevEnum->Release();
            CoUninitialize();
            return -1;   /* TODO: refactor error proc */
        }

        hRes = pDevId->GetId(&wstrEndpointId);
        if (FAILED(hRes))
        {
            pDevId->Release();
            pDev->Release();
            pDevEnum->Release();
            CoUninitialize();
            return -1;   /* TODO: refactor error proc */
        }

        hRes = pDevId->OpenPropertyStore(STGM_READ, &pPropStore);
        if (FAILED(hRes))
        {
            pDevId->Release();
            pDev->Release();
            pDevEnum->Release();
            CoUninitialize();
            return -1;   /* TODO: refactor error proc */
        }

        /* get friendly names on Sound settings */
        PropVariantInit(&friendlyName);
        hRes = pPropStore->GetValue(PKEY_Device_FriendlyName, &friendlyName);
        if (FAILED(hRes))
        {
            pPropStore->Release();
            pDevId->Release();
            pDev->Release();
            pDevEnum->Release();
            CoUninitialize();
            return -1;   /* TODO: refactor error proc */
        }
        wprintf(L"Device No. %d: %ls\n", i, friendlyName.pwszVal);  /* FIXME: show multi-byte letters properly */

        hRes = CoCreateInstance(CLSID_PolicyConfig, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pPolicyConfig));
        if (FAILED(hRes))
        {
            pPropStore->Release();
            pDevId->Release();
            pDev->Release();
            pDevEnum->Release();
            CoUninitialize();
            return -1;   /* TODO: refactor error proc */
        }

        hRes = pPolicyConfig->GetDeviceFormat(wstrEndpointId,
            FALSE/* if TRUE, get Default Format on Restore Default click */,
            &pDevFormat);
        if (FAILED(hRes))
        {
            pPolicyConfig->Release();
            pPropStore->Release();
            pDevId->Release();
            pDev->Release();
            pDevEnum->Release();
            CoUninitialize();
            return -1;   /* TODO: refactor error proc */
        }
        wprintf(L"\tWAVEFORMATEX Data from GetDeviceFormat\n");
        wprintf(L"\t\twFormatTag: %d\n", pDevFormat->wFormatTag);
        wprintf(L"\t\tnChannels: %d\n", pDevFormat->nChannels);
        wprintf(L"\t\tnSamplesPerSec: %ld\n", pDevFormat->nSamplesPerSec);
        wprintf(L"\t\tnAvgBytesPerSec: %ld\n", pDevFormat->nAvgBytesPerSec);
        wprintf(L"\t\tnBlockAlign: %ld\n", pDevFormat->nBlockAlign);
        wprintf(L"\t\twBitsPerSample: %d\n", pDevFormat->wBitsPerSample);
        wprintf(L"\t\tcbSize: %d\n", pDevFormat->cbSize);

        hRes = pPolicyConfig->GetMixFormat(wstrEndpointId, &pMixFormat);
        if (FAILED(hRes))
        {
            pPolicyConfig->Release();
            pPropStore->Release();
            pDevId->Release();
            pDev->Release();
            pDevEnum->Release();
            CoUninitialize();
            return -1;   /* TODO: refactor error proc */
        }
        wprintf(L"\tWAVEFORMATEX Data from GetMixFormat\n");
        wprintf(L"\t\twFormatTag: %d\n", pMixFormat->wFormatTag);
        wprintf(L"\t\tnChannels: %d\n", pMixFormat->nChannels);
        wprintf(L"\t\tnSamplesPerSec: %ld\n", pMixFormat->nSamplesPerSec);
        wprintf(L"\t\tnAvgBytesPerSec: %ld\n", pMixFormat->nAvgBytesPerSec);
        wprintf(L"\t\tnBlockAlign: %ld\n", pMixFormat->nBlockAlign);
        wprintf(L"\t\twBitsPerSample: %d\n", pMixFormat->wBitsPerSample);
        wprintf(L"\t\tcbSize: %d\n", pMixFormat->cbSize);

        wprintf(L"\n");

        /* if corresponding device found, set desired sample rate and calculate value(s) with it */
        if (0 == _wcsicmp(friendlyName.pwszVal, TARGET_DEV_NAME))
        {
            /* NOTE: if change wBitsPerSample, calculate nBlockAlign with it */
            pDevFormat->nSamplesPerSec = (44100 == pDevFormat->nSamplesPerSec) ? 48000 : 44100;
            pDevFormat->nAvgBytesPerSec = pDevFormat->nSamplesPerSec * pDevFormat->nBlockAlign;
            pMixFormat->nSamplesPerSec = (44100 == pMixFormat->nSamplesPerSec) ? 48000 : 44100;
            pMixFormat->nAvgBytesPerSec = pMixFormat->nSamplesPerSec * pMixFormat->nBlockAlign;
            hRes = pPolicyConfig->SetDeviceFormat(wstrEndpointId, pDevFormat, pMixFormat);
            if (FAILED(hRes))
            {
                wprintf(L"*** FAILED to change into your desired format...\n");
                pPolicyConfig->Release();
                pPropStore->Release();
                pDevId->Release();
                pDev->Release();
                pDevEnum->Release();
                CoUninitialize();
                return -1;   /* TODO: refactor error proc */
            }
            else
            {
                wprintf(L"*** succeeded to change into your desired format as follows:\n");
                wprintf(L"Device No. %d: %ls\n", i, friendlyName.pwszVal);  /* FIXME: show multi-byte letters properly */
                wprintf(L"\tWAVEFORMATEX Data from GetDeviceFormat\n");
                wprintf(L"\t\twFormatTag: %d\n", pDevFormat->wFormatTag);
                wprintf(L"\t\tnChannels: %d\n", pDevFormat->nChannels);
                wprintf(L"\t\tnSamplesPerSec: %ld\n", pDevFormat->nSamplesPerSec);
                wprintf(L"\t\tnAvgBytesPerSec: %ld\n", pDevFormat->nAvgBytesPerSec);
                wprintf(L"\t\tnBlockAlign: %ld\n", pDevFormat->nBlockAlign);
                wprintf(L"\t\twBitsPerSample: %d\n", pDevFormat->wBitsPerSample);
                wprintf(L"\t\tcbSize: %d\n", pDevFormat->cbSize);
                wprintf(L"\tWAVEFORMATEX Data from GetMixFormat\n");
                wprintf(L"\t\twFormatTag: %d\n", pMixFormat->wFormatTag);
                wprintf(L"\t\tnChannels: %d\n", pMixFormat->nChannels);
                wprintf(L"\t\tnSamplesPerSec: %ld\n", pMixFormat->nSamplesPerSec);
                wprintf(L"\t\tnAvgBytesPerSec: %ld\n", pMixFormat->nAvgBytesPerSec);
                wprintf(L"\t\tnBlockAlign: %ld\n", pMixFormat->nBlockAlign);
                wprintf(L"\t\twBitsPerSample: %d\n", pMixFormat->wBitsPerSample);
                wprintf(L"\t\tcbSize: %d\n", pMixFormat->cbSize);
                wprintf(L"***\n\n");
            }
        }

        PropVariantClear(&friendlyName);
    }

    pPropStore->Release();
    pDevId->Release();
    pDev->Release();
    pDevEnum->Release();
    CoUninitialize();

    return 0;
}

その他(感想)

最初にたどり着いたWASAPI(Windows Audio Session API)周りの情報では,どうもフォーマットが変更できなかった(以下twitter参照):

いろいろ方法はあるようで,そのうちのひとつが今回のMMDevice APIとIPolicyConfigを使う方法となった.MMDevice APIはまだしも,IPolicyConfigを使うという発想はどこから得ることができるのか,先人の知恵はすごい.

今日2023/11/23時点では,開発できる人以外は,まったく同じ録音インターフェースを使用している人にしか使えないものとなってしまっているため,mono-to-stereoを参考に

  1. インターフェース一覧表示
  2. 該当のインターフェース選択
  3. サンプルレート変更実行

というようにしたい.

改訂履歴

# 日付 内容
1 2023/11/23 とりあえずコマンドラインプログラム完成につき,情報まとめ!

参考サイト

  1. how to control enable audio enhancements with code
  2. Windowsの音声出力先を変えるショートカット作成
  3. Tauri + windows-rsでオーディオスイッチャーを作った話またはMicrosoftのRustへの本気度をCOM対応から見る的な何か
  4. C++&Win32 API MMDevice API (Core Audio APIs)によるデバイス情報の取得
  5. mono-to-stereo
  6. mono-to-stereo-gui
  7. WASAPIをプログラムしてみる

*1:PS3の映像・音声をキャプチャーできるので変更したくない