Как использовать IAudioClient3 (WASAPI) с API Очереди Работы в реальном времени - PullRequest
1 голос
/ 29 сентября 2019

Я работаю над программным обеспечением MIDI-синтезатора с минимальной задержкой.Я знаю об ASIO и других альтернативах, но, поскольку они, по-видимому, внесли существенные улучшения в стек WASAPI (по крайней мере, в режиме совместного использования), мне интересно попробовать его.Сначала я написал простую управляемую событиями версию программы, но так как это не рекомендуемый способ создания аудио с низкой задержкой в ​​Windows 10 (согласно docs ), я пытаюсь перейти на Real-Time Work Queue API.

В документации по аудиосигналам с низкой задержкой говорится, что рекомендуется использовать API очереди в реальном времени или MFCreateMFByteStreamOnStreamEx с WASAPIдля того, чтобы ОС управляла рабочими элементами таким образом, чтобы избегал помех от незвуковых подсистем .Это кажется хорошей идеей, но для последнего варианта требуется некоторый управляемый код (продемонстрированный в в этом примере WindowsAudioSession ), о котором я ничего не знаю и которого желательно избегать (также заголовок Robytestream.h, который имеет defsдля IRandomAccessStream также не найден в моей системе).

Пример RTWQ, включенный в документы, неполон (не компилируется как таковой), и я сделал необходимые дополнения, чтобы сделать его компилируемым:

class my_rtqueue : IRtwqAsyncCallback {

public:
    IRtwqAsyncResult* pAsyncResult;
    RTWQWORKITEM_KEY workItemKey;
    DWORD WorkQueueId;

    STDMETHODIMP GetParameters(DWORD* pdwFlags, DWORD* pdwQueue)
    {
        HRESULT hr = S_OK;
        *pdwFlags = 0;
        *pdwQueue = WorkQueueId;
        return hr;
    }

    //-------------------------------------------------------
    STDMETHODIMP Invoke(IRtwqAsyncResult* pAsyncResult)
    {
        HRESULT hr = S_OK;
        IUnknown* pState = NULL;
        WCHAR className[20];
        DWORD  bufferLength = 20;
        DWORD taskID = 0;
        LONG priority = 0;

        BYTE* pData;

        hr = render_info.renderclient->GetBuffer(render_info.buffer_framecount, &pData);
        ERROR_EXIT(hr);
        update_buffer((unsigned short*)pData, render_info.framesize_bytes / (2*sizeof(unsigned short))); // 2 channels, sizeof(unsigned short) == 2
        hr = render_info.renderclient->ReleaseBuffer(render_info.buffer_framecount, 0);
        ERROR_EXIT(hr);

        return S_OK;
    }

    STDMETHODIMP QueryInterface(const IID &riid, void **ppvObject) {
        return 0;
    }

    ULONG AddRef() {
        return 0;
    }

    ULONG Release() {
        return 0;
    }

    HRESULT queue(HANDLE event) {
        HRESULT hr;
        hr = RtwqPutWaitingWorkItem(event, 1, this->pAsyncResult, &this->workItemKey);
        return hr;
    }

    my_rtqueue() : workItemKey(0) {
        HRESULT hr = S_OK;
        IRtwqAsyncCallback* callback = NULL;
        DWORD taskId = 0;

        WorkQueueId = RTWQ_MULTITHREADED_WORKQUEUE;
        //WorkQueueId = RTWQ_STANDARD_WORKQUEUE;

        hr = RtwqLockSharedWorkQueue(L"Pro Audio", 0, &taskId, &WorkQueueId);
        ERROR_THROW(hr);

        hr = RtwqCreateAsyncResult(NULL, reinterpret_cast<IRtwqAsyncCallback*>(this), NULL, &pAsyncResult);
        ERROR_THROW(hr);

    }

    int stop() {
        HRESULT hr;
        if (pAsyncResult)
            pAsyncResult->Release();

        if (0xFFFFFFFF != this->WorkQueueId) {
            hr = RtwqUnlockWorkQueue(this->WorkQueueId);
            if (FAILED(hr)) {
                printf("Failed with RtwqUnlockWorkQueue 0x%x\n", hr);
                return 0;
            }
        }
        return 1;
    }

};

Итак, фактическийКод WASAPI (HRESULT проверка ошибок для ясности опущена):

void thread_main(LPVOID param) {

    HRESULT hr;
    REFERENCE_TIME hnsRequestedDuration = 0;
    IMMDeviceEnumerator* pEnumerator = NULL;
    IMMDevice* pDevice = NULL;
    IAudioClient3* pAudioClient = NULL;
    IAudioRenderClient* pRenderClient = NULL;
    WAVEFORMATEX* pwfx = NULL;
    HANDLE hEvent = NULL;
    HANDLE hTask = NULL;
    UINT32 bufferFrameCount;
    BYTE* pData;
    DWORD flags = 0;

    hr = RtwqStartup();

    // also, hr is checked for errors every step of the way

    hr = CoInitialize(NULL);

    hr = CoCreateInstance(
        CLSID_MMDeviceEnumerator, NULL,
        CLSCTX_ALL, IID_IMMDeviceEnumerator,
        (void**)&pEnumerator);

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

    hr = pDevice->Activate(
        IID_IAudioClient, CLSCTX_ALL,
        NULL, (void**)&pAudioClient);


    WAVEFORMATEX wave_format = {};
    wave_format.wFormatTag = WAVE_FORMAT_PCM;
    wave_format.nChannels = 2;
    wave_format.nSamplesPerSec = 48000;
    wave_format.nAvgBytesPerSec = 48000 * 2 * 16 / 8;
    wave_format.nBlockAlign = 2 * 16 / 8;
    wave_format.wBitsPerSample = 16;

    UINT32 DP, FP, MINP, MAXP;
    hr = pAudioClient->GetSharedModeEnginePeriod(&wave_format, &DP, &FP, &MINP, &MAXP);
    printf("DefaultPeriod: %u, Fundamental period: %u, min_period: %u, max_period: %u\n", DP, FP, MINP, MAXP);

    hr = pAudioClient->InitializeSharedAudioStream(AUDCLNT_STREAMFLAGS_EVENTCALLBACK, MINP, &wave_format, 0);

    my_rtqueue* workqueue = NULL;
    try {
        workqueue = new my_rtqueue();
    }
    catch (...) {
        hr = E_ABORT;
        ERROR_EXIT(hr);
    }

    hr = pAudioClient->GetBufferSize(&bufferFrameCount);

    PWAVEFORMATEX wf = &wave_format;
    UINT32 current_period;
    pAudioClient->GetCurrentSharedModeEnginePeriod(&wf, &current_period);

    INT32 FrameSize_bytes = bufferFrameCount * wave_format.nChannels * wave_format.wBitsPerSample / 8;
    printf("bufferFrameCount: %u, FrameSize_bytes: %d, current_period: %u\n", bufferFrameCount, FrameSize_bytes, current_period);

    hr = pAudioClient->GetService(
        IID_IAudioRenderClient,
        (void**)&pRenderClient);

    render_info.framesize_bytes = FrameSize_bytes;
    render_info.buffer_framecount = bufferFrameCount;
    render_info.renderclient = pRenderClient;

    hEvent = CreateEvent(nullptr, false, false, nullptr);
    if (hEvent == INVALID_HANDLE_VALUE) { ERROR_EXIT(0); }

    hr = pAudioClient->SetEventHandle(hEvent);

    const size_t num_samples = FrameSize_bytes / sizeof(unsigned short);

    DWORD taskIndex = 0;
    hTask = AvSetMmThreadCharacteristics(TEXT("Pro Audio"), &taskIndex);

    if (hTask == NULL) {
        hr = E_FAIL;
    }

    hr = pAudioClient->Start();  // Start playing.

    running = 1;
    while (running) {
        workqueue->queue(hEvent);
    }

    workqueue->stop();
    hr = RtwqShutdown();

    delete workqueue;

    running = 0;

    return 1;
}

Это похоже на работу (т. Е. Выводится звук), но при при каждом другом вызовеmy_rtqueue::Invoke(), IAudioRenderClient::GetBuffer() возвращает HRESULT из 0x88890006 (-> AUDCLNT_E_BUFFER_TOO_LARGE), и реальный аудиовыход определенно не тот, который я намереваюсь сделать.

Какие проблемы возникают с моим кодом?Это правильный способ использования RTWQ с WASAPI?

...