Заполнение буфера конечной точки звука, предоставляемого WASAPI, не воспроизводит звук

#c #windows #winapi #audio #wasapi

#c #Windows #winapi #Аудио #wasapi

Вопрос:

Я пытаюсь воспроизвести шум через средство визуализации конечной точки звука по умолчанию, используя интерфейс WASPAI. Я пытаюсь использовать код, предоставленный Microsoft на этой странице: https://docs.microsoft.com/en-us/windows/win32/coreaudio/rendering-a-stream . Я хочу написать класс, который может генерировать шум для этого примера кода.

Я попытался записать целочисленные значения со знаком и без знака в буфер средства визуализации конечной точки звука по умолчанию и вижу, что значения записываются в буфер, но звук не воспроизводится.

Для начала я создал заголовок с необходимыми методами и генератором случайных чисел.

 #pragma once

// RNG
#include <random>

template <typename T>
class Random {
public:
    Random(T low, T high) : mLow(low), mHigh(high), function(std::mt19937_64(__rdtsc())) {};

    T operator()() { 
        signed __int64 f =  function();

        return ((f  % ((signed __int64) mHigh   (signed __int64) mLow))   (signed __int64) mLow); }

private:
    T mLow;
    T mHigh;
    std::mt19937_64 function;
};

class Noise_Gen {

public:

    Noise_Gen() : nChannels(NULL), nSamplesPerSec(NULL), nAvgBytesPerSec(NULL), nByteAlign(NULL), wBitsPerSample(NULL), 
        wValidBitsPerSample(NULL), wSamplesPerBlock(NULL), dwChannelMask(NULL), rd(NULL) {};

    ~Noise_Gen() {
        if(rd != NULL) {
            delete rd;
        }
    };

    HRESULT SetFormat(WAVEFORMATEX*);

    HRESULT LoadData(UINT32 bufferFrameCount, BYTE* pData, DWORD* flags);

private:
    void* rd;

    // WAVEFORMATEX
    WORD nChannels;
    DWORD nSamplesPerSec;
    DWORD nAvgBytesPerSec;
    WORD nByteAlign;
    WORD wBitsPerSample;

    // WAVEFORMATEXTENSIBLE
    WORD wValidBitsPerSample;
    WORD wSamplesPerBlock;
    DWORD dwChannelMask;
};
 

Затем я добавил определения:

 // WASAPI
#include <Audiopolicy.h>
#include <Audioclient.h>

#include <time.h>

#include "Noise_Gen.h"

HRESULT Noise_Gen::SetFormat(WAVEFORMATEX* format) {
    nChannels = format->nChannels;
    nSamplesPerSec = format->nSamplesPerSec;
    nAvgBytesPerSec = format->nAvgBytesPerSec;
    nByteAlign = format->nBlockAlign;
    wBitsPerSample = format->wBitsPerSample;
    WORD  wFormatTag = format->wFormatTag;
    if(wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
        WAVEFORMATEXTENSIBLE* pWFE = reinterpret_cast<WAVEFORMATEXTENSIBLE*>(format);
        wValidBitsPerSample = pWFE->Samples.wValidBitsPerSample;
        wSamplesPerBlock = pWFE->Samples.wSamplesPerBlock;
        dwChannelMask = pWFE->dwChannelMask;
    } else {
        wValidBitsPerSample = wBitsPerSample;
    }
    double amplitude = std::pow(2.0, wValidBitsPerSample) - 1;
    switch(wBitsPerSample / 8) {
    case(1):
        rd = new Random<unsigned __int8>(0.0, amplitude);
        break;
    case(2): 
        rd = new Random<unsigned __int16>(0.0, amplitude);
        break;
    case(3):
        rd = new Random<unsigned __int32>(0.0, amplitude);
        break;
    case(4): 
        rd = new Random<signed __int32>(-amplitude, amplitude);
        break;
    case(5): 
        rd = new Random<unsigned __int64>(0.0, amplitude);
        break;
    case(6):
        rd = new Random<unsigned __int64>(0.0, amplitude);
        break;
    case(7): 
        rd = new Random<unsigned __int64>(0.0, amplitude);
        break;
    case(8):
        rd = new Random<unsigned __int64>(0.0, amplitude);
        break;
    default:
        return E_NOTIMPL;
    }
    return S_OK;
}

// (The size of an audio frame = nChannels * wBitsPerSample)
HRESULT Noise_Gen::LoadData(UINT32 bufferFrameCount, BYTE* pData, DWORD* flags) {
    for(UINT32 i = 0; i < nChannels *bufferFrameCount; i  ) {
        switch(wBitsPerSample / 8) {
        case(1):
            pData[i] = (((Random<unsigned __int8>*)rd)->operator()());
            break;
        case(2):{
            unsigned __int16* pData2 = (unsigned __int16*) pData;
            pData2[i] = (((Random<unsigned __int16>*)rd)->operator()());
            break;
        }
        case(3): {
            __int32 data = ((Random<unsigned __int32>*)rd)->operator()();
            unsigned char* cp = (unsigned char*) (amp;data);
            pData[(3 * i)] = cp[0];
            pData[1   (3 * i)] = cp[1];
            pData[2   (3 * i)] = cp[2];
            break;
        }
        case(4):{
            signed __int32* pData2 = (signed __int32*) pData;
            pData2[i] = (((Random<signed __int32>*)rd)->operator()());
            break;
        }
        case(5): {
            __int64 data = ((Random<unsigned __int64>*)rd)->operator()();
            unsigned char* cp = (unsigned char*) amp;data;
            pData[(5 * i)] = cp[0];
            pData[1   (5 * i)] = cp[1];
            pData[2   (5 * i)] = cp[2];
            pData[3   (5 * i)] = cp[3];
            pData[4   (5 * i)] = cp[4];
            break;
        }
        case(6): {
            __int64 data = ((Random<unsigned __int64>*)rd)->operator()();
            unsigned char* cp = (unsigned char*) amp;data;
            pData[(6 * i)] = cp[0];
            pData[1   (6 * i)] = cp[1];
            pData[2   (6 * i)] = cp[2];
            pData[3   (6 * i)] = cp[3];
            pData[4   (6 * i)] = cp[4];
            pData[5   (6 * i)] = cp[5];
            break;
        }
        case(7): {
            __int64 data = ((Random<unsigned __int64>*)rd)->operator()();
            unsigned char* cp = (unsigned char*) amp;data;
            pData[(7 * i)] = cp[0];
            pData[1   (7 * i)] = cp[1];
            pData[2   (7 * i)] = cp[2];
            pData[3   (7 * i)] = cp[3];
            pData[4   (7 * i)] = cp[4];
            pData[5   (7 * i)] = cp[5];
            pData[6   (7 * i)] = cp[6];
            break;
        }
        case(8): {
            unsigned __int64* pData2 = (unsigned __int64*) pData;
            pData2[i] = (((Random<unsigned __int64>*)rd)->operator()());
            break;
        }
        default:
            // For stopping playback
            (*flags) = AUDCLNT_BUFFERFLAGS_SILENT;
            return E_NOTIMPL;
        }
    }
    return S_OK;
}
 

Затем я добавил свой класс в шаблон, предоставленный Microsoft, и напечатал средство визуализации конечной точки звука по умолчанию на консоли.

 #include <InitGuid.h>
#include <iostream>
#include <Windows.h>
#include <dshow.h>

// Windows multimedia device
#include <Mmdeviceapi.h>
#include <Functiondiscoverykeys_devpkey.h>

// WASAPI
#include <Audiopolicy.h>
#include <Audioclient.h>

#include "Noise_Gen.h"

//-----------------------------------------------------------
// Play an audio stream on the default audio rendering
// device. The PlayAudioStream function allocates a shared
// buffer big enough to hold one second of PCM audio data.
// The function uses this buffer to stream data to the
// rendering device. The inner loop runs every 1/2 second.
//-----------------------------------------------------------

// REFERENCE_TIME time units per second and per millisecond
#define REFTIMES_PER_SEC  10000000
#define REFTIMES_PER_MILLISEC  10000

#define EXIT_ON_ERROR(hres)  
              if (FAILED(hres)) { goto Exit; }
#define SAFE_RELEASE(punk)  
              if ((punk) != NULL)  
                { (punk)->Release(); (punk) = NULL; }

const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
const IID IID_IAudioClient = __uuidof(IAudioClient);
const IID IID_IAudioRenderClient = __uuidof(IAudioRenderClient);

HRESULT PlayAudioStream(Noise_Gen* pMySource) {
    HRESULT hr;
    REFERENCE_TIME hnsRequestedDuration = REFTIMES_PER_SEC;
    REFERENCE_TIME hnsActualDuration;
    IMMDeviceEnumerator* pEnumerator = NULL;
    IMMDevice* pDevice = NULL;
    IAudioClient* pAudioClient = NULL;
    IAudioRenderClient* pRenderClient = NULL;
    WAVEFORMATEX* pwfx = NULL;
    UINT32 bufferFrameCount;
    UINT32 numFramesAvailable;
    UINT32 numFramesPadding;
    BYTE* pData;
    DWORD flags = 0;
    IPropertyStore* pPropertyStore = NULL;
    PROPVARIANT name;

    hr = CoCreateInstance(CLSID_MMDeviceEnumerator, NULL,
                          CLSCTX_ALL, IID_IMMDeviceEnumerator,
                          (void**) amp;pEnumerator);
    EXIT_ON_ERROR(hr);
    hr = pEnumerator->GetDefaultAudioEndpoint(
        eRender, eConsole, amp;pDevice);

    hr = pDevice->OpenPropertyStore(STGM_READ, amp;pPropertyStore);
    PropVariantInit(amp;name);
    hr = pPropertyStore->GetValue(PKEY_Device_FriendlyName, amp;name);
    printf("%S", name.pwszVal);
    printf("n");
    EXIT_ON_ERROR(hr);
    hr = pDevice->Activate(IID_IAudioClient, CLSCTX_ALL,
                           NULL, (void**) amp;pAudioClient);
    EXIT_ON_ERROR(hr);
    hr = pAudioClient->GetMixFormat(amp;pwfx);
    EXIT_ON_ERROR(hr);
    hr = pAudioClient->Initialize(AUDCLNT_SHAREMODE_SHARED,
                                  0, hnsRequestedDuration,
                                  0, pwfx, NULL);
    EXIT_ON_ERROR(hr);
    // Tell the audio source which format to use.
    hr = pMySource->SetFormat(pwfx);
    EXIT_ON_ERROR(hr);
    // Get the actual size of the allocated buffer.
    hr = pAudioClient->GetBufferSize(amp;bufferFrameCount);
    EXIT_ON_ERROR(hr);
    hr = pAudioClient->GetService(IID_IAudioRenderClient,
                                  (void**) amp;pRenderClient);
    EXIT_ON_ERROR(hr);
    // Grab the entire buffer for the initial fill operation.
    hr = pRenderClient->GetBuffer(bufferFrameCount, amp;pData);
    EXIT_ON_ERROR(hr);
    // Load the initial data into the shared buffer.
    hr = pMySource->LoadData(bufferFrameCount, pData, amp;flags);
    EXIT_ON_ERROR(hr);
    hr = pRenderClient->ReleaseBuffer(bufferFrameCount, flags);
    EXIT_ON_ERROR(hr);
    // Calculate the actual duration of the allocated buffer.
    hnsActualDuration = (double) REFTIMES_PER_SEC * bufferFrameCount / pwfx->nSamplesPerSec;
    hr = pAudioClient->Start();  // Start playing.
    EXIT_ON_ERROR(hr);
    // Each loop fills about half of the shared buffer.
    while(flags != AUDCLNT_BUFFERFLAGS_SILENT) {
        // Sleep for half the buffer duration.
        Sleep((DWORD) (hnsActualDuration / REFTIMES_PER_MILLISEC / 2));
        // See how much buffer space is available.
        hr = pAudioClient->GetCurrentPadding(amp;numFramesPadding);
        EXIT_ON_ERROR(hr);
        numFramesAvailable = bufferFrameCount - numFramesPadding;
        // Grab all the available space in the shared buffer.
        hr = pRenderClient->GetBuffer(numFramesAvailable, amp;pData);
        EXIT_ON_ERROR(hr);
        // Get next 1/2-second of data from the audio source.
        hr = pMySource->LoadData(numFramesAvailable, pData, amp;flags);
        EXIT_ON_ERROR(hr);
        hr = pRenderClient->ReleaseBuffer(numFramesAvailable, flags);
        EXIT_ON_ERROR(hr);
    }
    // Wait for last data in buffer to play before stopping.
    Sleep((DWORD) (hnsActualDuration / REFTIMES_PER_MILLISEC / 2));
    hr = pAudioClient->Stop();  // Stop playing.
    EXIT_ON_ERROR(hr);
Exit:
    CoTaskMemFree(pwfx);
    SAFE_RELEASE(pEnumerator);
    SAFE_RELEASE(pDevice);
    SAFE_RELEASE(pAudioClient);
    SAFE_RELEASE(pRenderClient);
    return hr;
}

int main() {
    HRESULT hr = CoInitialize(nullptr);
    if(FAILED(hr)) { return hr; }
    Noise_Gen* ng = new Noise_Gen();
    PlayAudioStream(ng);
    delete ng;
    CoUninitialize();
}
 

Средство визуализации конечной точки звука по умолчанию в моей системе использует 32-разрядные значения, поэтому код начинался с записи в буфер 32-разрядных значений без знака. Затем я попытался использовать значения со знаком, которые можно увидеть в приведенном выше коде. В обоих этих случаях звук не воспроизводился. Я проверил содержимое буфера во время отладки, и они действительно меняются. Я напечатал средство визуализации конечной точки звука по умолчанию на консоль, и это динамик моей системы. Windows даже показывает мое приложение в микшере громкости, но звук не отображается даже при максимальной громкости. Затем я проверил время ожидания, чтобы убедиться, что он находится в спящем режиме, поэтому система имеет доступ к буферу, и он переходит в спящий режим в течение 500 мс между записями в буфер.

Обновление: я обнаружил, что использую подформат KSDATAFORMAT_SUBTYPE_IEEEE_FLOAT, и попытался передать плавающие значения буфера в диапазоне от амплитуды до амплитуды, от 0 до амплитуды, от -1 до 1 и от 0 до 1.

Чего мне не хватает?

Ответ №1:

Ваш код распределения случайных чисел работает некорректно для форматов с плавающей запятой (который, насколько я знаю, в основном всегда будет форматом микширования в общем режиме).

Это неправильно даже для целых чисел. Я предполагаю, что вы хотели написать

 ((f  % ((signed __int64) mHigh - (signed __int64) mLow))   (signed __int64) mLow); 
 

(обратите внимание на минус),
но вам все равно не следует использовать необработанный модуль, потому что он слегка смещен.

Для форматов с плавающей запятой вы всегда используете диапазон от -1 до 1.

Я адаптировал ваш код для использования std ::uniform_real_distribution, и я слышу шум, воспроизводимый в моих динамиках.

 #include <cstdio>
#include <Windows.h>

// Windows multimedia device
#include <Mmdeviceapi.h>
#include <Functiondiscoverykeys_devpkey.h>

// WASAPI
#include <Audiopolicy.h>
#include <Audioclient.h>

#include <random>


class Noise_Gen {
public:
    Noise_Gen() : format(), engine(__rdtsc()), float_dist(-1.f, 1.f) {};

    void SetFormat(WAVEFORMATEX* wfex) {
        if(wfex->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
            format = *reinterpret_cast<WAVEFORMATEXTENSIBLE*>(wfex);
        } else {
            format.Format = *wfex;
            format.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
            INIT_WAVEFORMATEX_GUID(amp;format.SubFormat, wfex->wFormatTag);
            format.Samples.wValidBitsPerSample = format.Format.wBitsPerSample;
            format.dwChannelMask = 0;
        }
    }

    // (The size of an audio frame = nChannels * wBitsPerSample)
    void FillBuffer(UINT32 bufferFrameCount, BYTE* pData, DWORD* flags) {
        const UINT16 formatTag = EXTRACT_WAVEFORMATEX_ID(amp;format.SubFormat);
        if(formatTag == WAVE_FORMAT_IEEE_FLOAT) {
            float* fData = (float*)pData;
            for(UINT32 i = 0; i < format.Format.nChannels * bufferFrameCount; i  ) {
                fData[i] = float_dist(engine);
            }
        } else if(formatTag == WAVE_FORMAT_PCM) {
            using rndT = decltype(engine)::result_type;
            UINT32 iterations = format.Format.nBlockAlign * bufferFrameCount / sizeof(rndT);
            UINT32 leftoverBytes = format.Format.nBlockAlign * bufferFrameCount % sizeof(rndT);
            rndT* iData = (rndT*)pData;
            UINT32 i = 0;
            for(; i < iterations; i  ) {
                iData[i] = engine();
            }
            if(leftoverBytes != 0) {
                rndT lastRnd = engine();
                BYTE* pLastBytes = pData   i * sizeof(rndT);
                for(UINT32 j = 0; j < leftoverBytes;   j) {
                    pLastBytes[j] = lastRnd >> (j * 8) amp; 0xFF;
                }
            }
        } else {
            //memset(pData, 0, wfex.Format.nBlockAlign * bufferFrameCount);
            *flags = AUDCLNT_BUFFERFLAGS_SILENT;
        }
    }

private:
    WAVEFORMATEXTENSIBLE format;

    std::mt19937_64 engine;
    std::uniform_real_distribution<float> float_dist;
};

// REFERENCE_TIME time units per second and per millisecond
#define REFTIMES_PER_SEC  10000000ll
#define REFTIMES_PER_MILLISEC  10000

#define EXIT_ON_ERROR(hres)  
              if (FAILED(hres)) { goto Exit; }
#define SAFE_RELEASE(punk)  
              if ((punk) != NULL)  
                { (punk)->Release(); (punk) = NULL; }

HRESULT PlayAudioStream(Noise_Gen* pMySource) {
    HRESULT hr;
    REFERENCE_TIME hnsRequestedDuration = REFTIMES_PER_SEC;
    REFERENCE_TIME hnsActualDuration;
    IMMDeviceEnumerator* pEnumerator = NULL;
    IPropertyStore* pPropertyStore = NULL;
    IMMDevice* pDevice = NULL;
    IAudioClient* pAudioClient = NULL;
    IAudioRenderClient* pRenderClient = NULL;
    WAVEFORMATEX* pwfx = NULL;
    UINT32 bufferFrameCount;
    BYTE* pData;
    DWORD flags = 0;
    PROPVARIANT name;

    hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL,
        CLSCTX_ALL, IID_PPV_ARGS(amp;pEnumerator));
    EXIT_ON_ERROR(hr);
    hr = pEnumerator->GetDefaultAudioEndpoint(
        eRender, eConsole, amp;pDevice);
    EXIT_ON_ERROR(hr);

    hr = pDevice->OpenPropertyStore(STGM_READ, amp;pPropertyStore);
    EXIT_ON_ERROR(hr);
    PropVariantInit(amp;name);
    hr = pPropertyStore->GetValue(PKEY_Device_FriendlyName, amp;name);
    EXIT_ON_ERROR(hr);
    printf("%S", name.pwszVal);
    printf("n");
    hr = pDevice->Activate(__uuidof(pAudioClient), CLSCTX_ALL,
        NULL, (void**) amp;pAudioClient);
    EXIT_ON_ERROR(hr);
    hr = pAudioClient->GetMixFormat(amp;pwfx);
    EXIT_ON_ERROR(hr);

    hr = pAudioClient->Initialize(AUDCLNT_SHAREMODE_SHARED,
        0, hnsRequestedDuration,
        0, pwfx, NULL);
    EXIT_ON_ERROR(hr);
    // Tell the audio source which format to use.
    pMySource->SetFormat(pwfx);
    // Get the actual size of the allocated buffer.
    hr = pAudioClient->GetBufferSize(amp;bufferFrameCount);
    EXIT_ON_ERROR(hr);
    hr = pAudioClient->GetService(IID_PPV_ARGS(amp;pRenderClient));
    EXIT_ON_ERROR(hr);
    // Grab the entire buffer for the initial fill operation.
    hr = pRenderClient->GetBuffer(bufferFrameCount, amp;pData);
    EXIT_ON_ERROR(hr);

    // Load the initial data into the shared buffer.
    pMySource->FillBuffer(bufferFrameCount, pData, amp;flags);    

    hr = pRenderClient->ReleaseBuffer(bufferFrameCount, flags);
    EXIT_ON_ERROR(hr);
    // Calculate the actual duration of the allocated buffer.
    hnsActualDuration = REFTIMES_PER_SEC * bufferFrameCount / pwfx->nSamplesPerSec;
    hr = pAudioClient->Start();  // Start playing.
    EXIT_ON_ERROR(hr);
    // Each loop fills about half of the shared buffer.
    DWORD sleepTime;
    while(flags != AUDCLNT_BUFFERFLAGS_SILENT) {
        // Sleep for half the buffer duration.
        sleepTime = (DWORD) (hnsActualDuration / REFTIMES_PER_MILLISEC / 2);
        if(sleepTime != 0)
            Sleep(sleepTime);
        // See how much buffer space is available.
        UINT32 numFramesPadding;
        hr = pAudioClient->GetCurrentPadding(amp;numFramesPadding);
        EXIT_ON_ERROR(hr);

        UINT32 numFramesAvailable = bufferFrameCount - numFramesPadding;
        // Grab all the available space in the shared buffer.
        hr = pRenderClient->GetBuffer(numFramesAvailable, amp;pData);
        EXIT_ON_ERROR(hr);

        // Get next 1/2-second of data from the audio source.
        pMySource->FillBuffer(numFramesAvailable, pData, amp;flags);

        hr = pRenderClient->ReleaseBuffer(numFramesAvailable, flags);
        EXIT_ON_ERROR(hr);
    }
    // Wait for last data in buffer to play before stopping.
    sleepTime = (DWORD) (hnsActualDuration / REFTIMES_PER_MILLISEC / 2);
    if(sleepTime != 0)
        Sleep(sleepTime);
    hr = pAudioClient->Stop();  // Stop playing.
    EXIT_ON_ERROR(hr);

Exit:
    CoTaskMemFree(pwfx);
    SAFE_RELEASE(pRenderClient);
    SAFE_RELEASE(pAudioClient);
    SAFE_RELEASE(pDevice);
    SAFE_RELEASE(pPropertyStore); // you forgot to free the property store
    SAFE_RELEASE(pEnumerator);
    return hr;
}

int main() {
    HRESULT hr = CoInitialize(nullptr);
    if(FAILED(hr)) { return hr; }

    Noise_Gen ng;
    PlayAudioStream(amp;ng);
    
    CoUninitialize();
}
 

Комментарии:

1. Спасибо за ответ. Мне не хватало информации: «Для форматов с плавающей запятой вы всегда используете диапазон от -1 до 1». В итоге я использовал std::bind(std::uniform_real_distribution<>(low, high), std::default_random_engine()) для случайных чисел.

2. Возможно, я ошибаюсь, но я не думаю, что это заполняет весь циклический буфер. Когда numFramesAvailable размер меньше полного размера буфера, FillBuffer будет переходить обратно к индексу 0 pData в случае WAVE_FORMAT_IEEE_FLOAT .

3. pData будет указывать в буфер с правильным смещением уже тогда, когда мы получим его из IAudioRenderClient::GetBuffer, поэтому мы всегда должны начинать заполнять новые данные со смещением 0 из pData

4. @TheOm Это странно, поскольку я обнаружил, что это не относится к моей собственной реализации. Меня беспокоит то, что характер белого шума звука скрывает разрывы.

5. @TheOm GetBuffer не обновляет адрес pData в моем случае, и мне нужно самому отслеживать положение буфера.