diff --git a/app/streaming/audio/renderers/portaudiorenderer.cpp b/app/streaming/audio/renderers/portaudiorenderer.cpp new file mode 100644 index 00000000..f6732d67 --- /dev/null +++ b/app/streaming/audio/renderers/portaudiorenderer.cpp @@ -0,0 +1,189 @@ +#include "portaudiorenderer.h" + +#include + +#include + +PortAudioRenderer::PortAudioRenderer() + : m_Stream(nullptr), + m_ChannelCount(0), + m_WriteIndex(0), + m_ReadIndex(0) +{ + PaError error = Pa_Initialize(); + if (error != paNoError) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Pa_Initialize() failed: %s", + Pa_GetErrorText(error)); + return; + } + + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Initialized PortAudio: %s", + Pa_GetVersionText()); +} + +PortAudioRenderer::~PortAudioRenderer() +{ + if (m_Stream != nullptr) { + Pa_CloseStream(m_Stream); + } + + Pa_Terminate(); +} + +bool PortAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig) +{ + PaStreamParameters params = {}; + + m_ChannelCount = opusConfig->channelCount; + + PaDeviceIndex outputDeviceIndex = Pa_GetDefaultOutputDevice(); + if (outputDeviceIndex == paNoDevice) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "No output device available"); + return false; + } + + const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo(outputDeviceIndex); + if (deviceInfo == nullptr) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Pa_GetDeviceInfo() failed"); + return false; + } + + params.channelCount = opusConfig->channelCount; + params.sampleFormat = paInt16; + params.device = outputDeviceIndex; + params.suggestedLatency = deviceInfo->defaultLowOutputLatency; + + PaError error = Pa_OpenStream(&m_Stream, nullptr, ¶ms, + opusConfig->sampleRate, + SAMPLES_PER_FRAME, + paNoFlag, + paStreamCallback, this); + if (error != paNoError) { + m_Stream = nullptr; + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Pa_OpenStream() failed: %s", + Pa_GetErrorText(error)); + return false; + } + + error = Pa_StartStream(m_Stream); + if (error != paNoError) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Pa_StartStream() failed: %s", + Pa_GetErrorText(error)); + return false; + } + + return true; +} + +void PortAudioRenderer::submitAudio(short* audioBuffer, int audioSize) +{ + SDL_assert(audioSize == SAMPLES_PER_FRAME * m_ChannelCount * 2); + + // Check if there is space for this sample in the buffer. Again, this can race + // but in the worst case, we'll not see the sample callback having consumed a sample. + if (((m_WriteIndex + 1) % CIRCULAR_BUFFER_SIZE) == m_ReadIndex) { + return; + } + + SDL_memcpy(&m_AudioBuffer[m_WriteIndex * CIRCULAR_BUFFER_STRIDE], audioBuffer, audioSize); + + // Fence with release semantics ensures m_AudioBuffer[m_WriteIndex] is written before the + // consumer observes m_WriteIndex incrementing. + std::atomic_thread_fence(std::memory_order_release); + + // This can race with the reader in the sample callback, however this is a benign + // race since we'll either read the original value of m_WriteIndex (which is safe, + // we just won't consider this sample) or the new value of m_WriteIndex + m_WriteIndex = (m_WriteIndex + 1) % CIRCULAR_BUFFER_SIZE; +} + +bool PortAudioRenderer::testAudio(int audioConfiguration) +{ + PaStreamParameters params = {}; + + PaDeviceIndex outputDeviceIndex = Pa_GetDefaultOutputDevice(); + if (outputDeviceIndex == paNoDevice) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "No output device available"); + return false; + } + + switch (audioConfiguration) + { + case AUDIO_CONFIGURATION_STEREO: + params.channelCount = 2; + break; + case AUDIO_CONFIGURATION_51_SURROUND: + params.channelCount = 6; + break; + default: + SDL_assert(false); + return false; + } + + params.sampleFormat = paInt16; + params.device = outputDeviceIndex; + + PaError error = Pa_IsFormatSupported(nullptr, ¶ms, 48000); + if (error == paFormatIsSupported) { + return true; + } + else { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Pa_IsFormatSupported() failed: %s", + Pa_GetErrorText(error)); + return false; + } +} + +int PortAudioRenderer::detectAudioConfiguration() +{ + const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo(Pa_GetDefaultOutputDevice()); + if (deviceInfo == nullptr) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Pa_GetDeviceInfo() failed"); + return false; + } + + if (deviceInfo->maxOutputChannels > 2) { + return AUDIO_CONFIGURATION_51_SURROUND; + } + else { + return AUDIO_CONFIGURATION_STEREO; + } +} + +int PortAudioRenderer::paStreamCallback(const void*, void* output, unsigned long frameCount, const PaStreamCallbackTimeInfo*, PaStreamCallbackFlags statusFlags, void* userData) +{ + auto me = reinterpret_cast(userData); + + SDL_assert(frameCount == SAMPLES_PER_FRAME); + + // If the indexes aren't equal, we have a sample + if (me->m_WriteIndex != me->m_ReadIndex) { + // Copy data to the audio buffer + SDL_memcpy(output, + &me->m_AudioBuffer[me->m_ReadIndex * CIRCULAR_BUFFER_STRIDE], + frameCount * me->m_ChannelCount * sizeof(short)); + + // Fence with acquire semantics ensures m_AudioBuffer[m_ReadIndex] is read before the + // producer observes m_ReadIndex incrementing. + std::atomic_thread_fence(std::memory_order_acquire); + + // This can race with the reader in the submitAudio function. This is not a problem + // because at worst, it just won't see that we've consumed this sample yet. + me->m_ReadIndex = (me->m_ReadIndex + 1) % CIRCULAR_BUFFER_SIZE; + } + else { + // No data, so play silence + SDL_memset(output, 0, frameCount * me->m_ChannelCount * sizeof(short)); + } + + return paContinue; +} diff --git a/app/streaming/audio/renderers/portaudiorenderer.h b/app/streaming/audio/renderers/portaudiorenderer.h new file mode 100644 index 00000000..f39e0e3d --- /dev/null +++ b/app/streaming/audio/renderers/portaudiorenderer.h @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include "renderer.h" + +#define CIRCULAR_BUFFER_SIZE 32 +#define MAX_CHANNEL_COUNT 6 + +#define CIRCULAR_BUFFER_STRIDE (MAX_CHANNEL_COUNT * SAMPLES_PER_FRAME) + +class PortAudioRenderer : public IAudioRenderer +{ +public: + PortAudioRenderer(); + + virtual ~PortAudioRenderer(); + + virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig); + + virtual void submitAudio(short* audioBuffer, int audioSize); + + virtual bool testAudio(int audioConfiguration); + + virtual int detectAudioConfiguration(); + + static int paStreamCallback(const void *input, + void *output, + unsigned long frameCount, + const PaStreamCallbackTimeInfo *timeInfo, + PaStreamCallbackFlags statusFlags, + void *userData); + +private: + PaStream* m_Stream; + int m_ChannelCount; + int m_WriteIndex; + int m_ReadIndex; + short m_AudioBuffer[CIRCULAR_BUFFER_SIZE * CIRCULAR_BUFFER_STRIDE]; +}; +