XAudio2 - 使用动态缓冲区时破解输出

XAudio2 - Cracking output when using a dynamic buffer

提供一点上下文。我正在尝试从我的 c# 应用程序中的摄像头输出实时音频。在做了一些研究之后,在 C++ 管理的 dll 中做这件事似乎很明显。我选择了 XAudio2 api 因为它应该很容易实现和与动态音频内容一起使用。

所以我的想法是使用空缓冲区在 c++ 中创建 XAudio 设备,并从 c# 代码端推送音频。音频块每 50 毫秒推送一次,因为我想让延迟尽可能小。

// SampleRate = 44100; Channels = 2; BitPerSample = 16;
var blockAlign = (Channels * BitsPerSample) / 8;
var avgBytesPerSecond = SampleRate * blockAlign;
var avgBytesPerMillisecond = avgBytesPerSecond / 1000;
var bufferSize = avgBytesPerMillisecond * Time;
_sampleBuffer = new byte[bufferSize];

每次计时器运行时,它都会获取音频缓冲区的指针,从音频中读取数据,将数据复制到指针并调用 PushAudio 方法。 我还使用秒表检查处理时间并再次计算计时器的间隔以包括处理时间。

private void PushAudioChunk(object sender, ElapsedEventArgs e)
{
    unsafe
    {
        _pushAudioStopWatch.Reset();
        _pushAudioStopWatch.Start();

        var audioBufferPtr = Output.AudioCapturerBuffer();
        FillBuffer(_sampleBuffer);
        Marshal.Copy(_sampleBuffer, 0, (IntPtr)audioBufferPtr, _sampleBuffer.Length);

        Output.PushAudio();

        _pushTimer.Interval = Time - _pushAudioStopWatch.ElapsedMilliseconds;
        _pushAudioStopWatch.Stop();
        DIX.Log.WriteLine("Push audio took: {0}ms", _pushAudioStopWatch.ElapsedMilliseconds);                
    }
}

这是c++部分的实现。

关于 msdn 上的文档,我创建了一个 XAudio2 设备并添加了 MasterVoice 和 SourceVoice。缓冲区一开始是空的,因为c#部分负责压入音频数据。

namespace Audio
{
    using namespace System;

    template <class T> void SafeRelease(T **ppT)
    {
        if (*ppT)
        {
            (*ppT)->Release();
            *ppT = NULL;
        }
    }

    WAVEFORMATEXTENSIBLE wFormat;

    XAUDIO2_BUFFER buffer = { 0 };

    IXAudio2* pXAudio2 = NULL;
    IXAudio2MasteringVoice* pMasterVoice = NULL;
    IXAudio2SourceVoice* pSourceVoice = NULL;           

    WaveOut::WaveOut(int bufferSize)
    {
        audioBuffer = new Byte[bufferSize];

        wFormat.Format.wFormatTag = WAVE_FORMAT_PCM;
        wFormat.Format.nChannels = 2;
        wFormat.Format.nSamplesPerSec = 44100;
        wFormat.Format.wBitsPerSample = 16;
        wFormat.Format.nBlockAlign = (wFormat.Format.nChannels * wFormat.Format.wBitsPerSample) / 8;
        wFormat.Format.nAvgBytesPerSec = wFormat.Format.nSamplesPerSec * wFormat.Format.nBlockAlign;
        wFormat.Format.cbSize = 0;
        wFormat.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;

        HRESULT hr = XAudio2Create(&pXAudio2, 0, XAUDIO2_DEFAULT_PROCESSOR);

        if (SUCCEEDED(hr))
        {
            hr = pXAudio2->CreateMasteringVoice(&pMasterVoice);
        }

        if (SUCCEEDED(hr))
        {
            hr = pXAudio2->CreateSourceVoice(&pSourceVoice, (WAVEFORMATEX*)&wFormat,
                0, XAUDIO2_DEFAULT_FREQ_RATIO, NULL, NULL, NULL);
        }

        buffer.pAudioData = (BYTE*)audioBuffer;
        buffer.AudioBytes = bufferSize;
        buffer.Flags = 0;

        if (SUCCEEDED(hr))
        {
            hr = pSourceVoice->Start(0);
        }
    }

    WaveOut::~WaveOut()
    {

    }

    WaveOut^ WaveOut::CreateWaveOut(int bufferSize)
    {
        return gcnew WaveOut(bufferSize);
    }

    uint8_t* WaveOut::AudioCapturerBuffer()
    {
        if (!audioBuffer)
        {
            throw gcnew Exception("Audio buffer is not initialized. Did you forget to set up the audio container?");
        }

        return (BYTE*)audioBuffer;
    }

    int WaveOut::PushAudio()
    {
        HRESULT hr = pSourceVoice->SubmitSourceBuffer(&buffer);

        if (FAILED(hr))
        {
            return -1;
        }

        return 0;
    }
}

我面临的问题是我的输出总是有一些裂纹。我试图增加计时器的间隔或稍微增加缓冲区大小。每次都是一样的结果。

我做错了什么?

更新:

我创建了 XAudio 引擎可以通过的 3 个缓冲区。裂缝消失了。现在缺少的部分是在正确的时间从c#部分填充缓冲区以避免缓冲区具有相同的数据。

void Render(void* param)
{
    std::vector<byte> audioBuffers[BUFFER_COUNT];
    size_t currentBuffer = 0;

    // Get the current state of the source voice
    while (BackgroundThreadRunning && pSourceVoice)
    {
        if (pSourceVoice)
        {
            pSourceVoice->GetState(&state);
        }

        while (state.BuffersQueued < BUFFER_COUNT)
        {
            std::vector<byte> resultData;
            resultData.resize(DATA_SIZE);
            CopyMemory(&resultData[0], pAudioBuffer, DATA_SIZE);

            // Retreive the next buffer to stream from MF Music Streamer
            audioBuffers[currentBuffer] = resultData;

            // Submit the new buffer
            XAUDIO2_BUFFER buf = { 0 };
            buf.AudioBytes = static_cast<UINT32>(audioBuffers[currentBuffer].size());
            buf.pAudioData = &audioBuffers[currentBuffer][0];

            pSourceVoice->SubmitSourceBuffer(&buf);

            // Advance the buffer index
            currentBuffer = ++currentBuffer % BUFFER_COUNT;

            // Get the updated state
            pSourceVoice->GetState(&state);
        }

        Sleep(30);
    }
}

XAudio2 不会 在您通过 SubmitSourceBuffer 提交时复制源数据缓冲区。您必须保持该数据(在您的应用程序内存中)有效,并且为 XAudio2 需要从中读出以处理数据的整个时间分配缓冲区。这样做是为了提高效率,以避免需要额外的副本,但会给您带来多线程负担,即在完成播放之前保持内存可用。这也意味着你不能修改播放缓冲区。

您当前的代码只是重复使用同一个缓冲区,当您在播放数据时更改数据时会导致弹出。您可以通过在 2 或 3 个缓冲区之间旋转来解决此问题。 XAudio2 Source Voice 具有状态信息,您可以使用它来确定播放缓冲区的时间,或者您可以注册显式回调,告诉您何时不再使用缓冲区。

See DirectX Tool Kit for Audio and classic XAudio2 samples for examples of using XAudio2.