2018-09-22 06:30:47 +00:00
|
|
|
#include "portaudiorenderer.h"
|
|
|
|
|
|
|
|
#include <SDL.h>
|
|
|
|
|
|
|
|
#include <atomic>
|
|
|
|
|
|
|
|
PortAudioRenderer::PortAudioRenderer()
|
|
|
|
: m_Stream(nullptr),
|
|
|
|
m_ChannelCount(0),
|
|
|
|
m_WriteIndex(0),
|
2018-09-29 23:52:40 +00:00
|
|
|
m_ReadIndex(0),
|
|
|
|
m_Started(false)
|
2018-09-22 06:30:47 +00:00
|
|
|
{
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
2018-09-23 04:00:44 +00:00
|
|
|
|
|
|
|
// Start the stream after we've written the first sample to it
|
2018-09-29 23:52:40 +00:00
|
|
|
if (!m_Started) {
|
2018-09-23 04:00:44 +00:00
|
|
|
PaError error = Pa_StartStream(m_Stream);
|
|
|
|
if (error != paNoError) {
|
|
|
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
|
|
"Pa_StartStream() failed: %s",
|
|
|
|
Pa_GetErrorText(error));
|
|
|
|
return;
|
|
|
|
}
|
2018-09-29 23:52:40 +00:00
|
|
|
|
|
|
|
m_Started = true;
|
2018-09-23 04:00:44 +00:00
|
|
|
}
|
2018-09-22 06:30:47 +00:00
|
|
|
}
|
|
|
|
|
2018-09-30 05:19:41 +00:00
|
|
|
bool PortAudioRenderer::testAudio(int audioConfiguration) const
|
2018-09-22 06:30:47 +00:00
|
|
|
{
|
|
|
|
PaStreamParameters params = {};
|
|
|
|
|
|
|
|
PaDeviceIndex outputDeviceIndex = Pa_GetDefaultOutputDevice();
|
|
|
|
if (outputDeviceIndex == paNoDevice) {
|
|
|
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
|
|
"No output device available");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-09-30 05:19:41 +00:00
|
|
|
const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo(outputDeviceIndex);
|
|
|
|
if (deviceInfo == nullptr) {
|
|
|
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
|
|
"Pa_GetDeviceInfo() failed");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-09-22 06:30:47 +00:00
|
|
|
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;
|
2018-09-30 05:19:41 +00:00
|
|
|
params.suggestedLatency = deviceInfo->defaultLowOutputLatency;
|
2018-09-22 06:30:47 +00:00
|
|
|
|
2018-09-30 05:19:41 +00:00
|
|
|
// We used to just use Pa_IsFormatSupported() but there are cases
|
|
|
|
// where Pa_IsFormatSupported() will fail but when we actually
|
|
|
|
// call Pa_OpenStream(), it fails with device unavailable.
|
|
|
|
PaStream* stream;
|
|
|
|
PaError error = Pa_OpenStream(&stream, nullptr, ¶ms,
|
|
|
|
48000,
|
|
|
|
SAMPLES_PER_FRAME,
|
|
|
|
paNoFlag,
|
|
|
|
nullptr, nullptr);
|
|
|
|
if (error != paNoError) {
|
2018-09-22 06:30:47 +00:00
|
|
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
2018-09-30 05:19:41 +00:00
|
|
|
"Pa_OpenStream() failed: %s",
|
2018-09-22 06:30:47 +00:00
|
|
|
Pa_GetErrorText(error));
|
|
|
|
return false;
|
|
|
|
}
|
2018-09-30 05:19:41 +00:00
|
|
|
|
2018-09-30 05:24:26 +00:00
|
|
|
error = Pa_StartStream(stream);
|
2018-09-30 05:19:41 +00:00
|
|
|
if (error != paNoError) {
|
|
|
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
|
|
"Pa_StartStream() failed: %s",
|
|
|
|
Pa_GetErrorText(error));
|
|
|
|
}
|
|
|
|
|
|
|
|
Pa_CloseStream(stream);
|
|
|
|
|
|
|
|
return error == paNoError;
|
2018-09-22 06:30:47 +00:00
|
|
|
}
|
|
|
|
|
2018-09-30 05:19:41 +00:00
|
|
|
int PortAudioRenderer::detectAudioConfiguration() const
|
2018-09-22 06:30:47 +00:00
|
|
|
{
|
|
|
|
const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo(Pa_GetDefaultOutputDevice());
|
|
|
|
if (deviceInfo == nullptr) {
|
|
|
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
|
|
"Pa_GetDeviceInfo() failed");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-09-22 23:51:45 +00:00
|
|
|
// PulseAudio reports max output channels that don't
|
|
|
|
// correspond to any output devices (32 channels), so
|
|
|
|
// only use 5.1 surround sound if the output channel count
|
|
|
|
// is reasonable. Additionally, PortAudio doesn't do remixing
|
|
|
|
// for quadraphonic, so only use 5.1 if we have 6 or more channels.
|
|
|
|
if (deviceInfo->maxOutputChannels == 6 || deviceInfo->maxOutputChannels == 8) {
|
2018-09-22 06:30:47 +00:00
|
|
|
return AUDIO_CONFIGURATION_51_SURROUND;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return AUDIO_CONFIGURATION_STEREO;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-02 01:46:16 +00:00
|
|
|
void PortAudioRenderer::adjustOpusChannelMapping(OPUS_MULTISTREAM_CONFIGURATION* opusConfig) const
|
|
|
|
{
|
|
|
|
const OPUS_MULTISTREAM_CONFIGURATION origConfig = *opusConfig;
|
|
|
|
|
|
|
|
const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo(Pa_GetDefaultOutputDevice());
|
|
|
|
if (deviceInfo == nullptr) {
|
|
|
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
|
|
"Pa_GetDeviceInfo() failed");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const PaHostApiInfo* hostApiInfo = Pa_GetHostApiInfo(deviceInfo->hostApi);
|
|
|
|
if (hostApiInfo == nullptr) {
|
|
|
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
|
|
"Pa_GetHostApiInfo() failed");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
|
|
"PortAudio host API: %s",
|
|
|
|
hostApiInfo->name);
|
|
|
|
|
|
|
|
if (hostApiInfo->type == paALSA) {
|
|
|
|
// The default mapping array has is: FL-FR-C-LFE-RL-RR
|
|
|
|
// ALSA expects: FL-FR-RL-RR-C-LFE
|
|
|
|
opusConfig->mapping[0] = origConfig.mapping[0];
|
|
|
|
opusConfig->mapping[1] = origConfig.mapping[1];
|
|
|
|
if (opusConfig->channelCount == 6) {
|
|
|
|
opusConfig->mapping[2] = origConfig.mapping[4];
|
|
|
|
opusConfig->mapping[3] = origConfig.mapping[5];
|
|
|
|
opusConfig->mapping[4] = origConfig.mapping[2];
|
|
|
|
opusConfig->mapping[5] = origConfig.mapping[3];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-23 00:12:54 +00:00
|
|
|
int PortAudioRenderer::paStreamCallback(const void*, void* output, unsigned long frameCount, const PaStreamCallbackTimeInfo*, PaStreamCallbackFlags, void* userData)
|
2018-09-22 06:30:47 +00:00
|
|
|
{
|
|
|
|
auto me = reinterpret_cast<PortAudioRenderer*>(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;
|
|
|
|
}
|