mirror of
https://github.com/moonlight-stream/moonlight-qt
synced 2024-12-13 21:02:28 +00:00
Add PoC PortAudio audio renderer
This commit is contained in:
parent
e20bf3fbd9
commit
2c960059ee
2 changed files with 230 additions and 0 deletions
189
app/streaming/audio/renderers/portaudiorenderer.cpp
Normal file
189
app/streaming/audio/renderers/portaudiorenderer.cpp
Normal file
|
@ -0,0 +1,189 @@
|
|||
#include "portaudiorenderer.h"
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#include <atomic>
|
||||
|
||||
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<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;
|
||||
}
|
41
app/streaming/audio/renderers/portaudiorenderer.h
Normal file
41
app/streaming/audio/renderers/portaudiorenderer.h
Normal file
|
@ -0,0 +1,41 @@
|
|||
#pragma once
|
||||
|
||||
#include <portaudio.h>
|
||||
|
||||
#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];
|
||||
};
|
||||
|
Loading…
Reference in a new issue