mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2026-06-17 22:23:31 +00:00
libsoundio audio renderer for Windows and Mac (#97)
This commit is contained in:
@@ -1,141 +0,0 @@
|
||||
#include "portaudiorenderer.h"
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#include <atomic>
|
||||
|
||||
PortAudioRenderer::PortAudioRenderer()
|
||||
: m_Stream(nullptr),
|
||||
m_ChannelCount(0),
|
||||
m_WriteIndex(0),
|
||||
m_ReadIndex(0),
|
||||
m_Started(false)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
bool 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 true;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Start the stream after we've written the first sample to it
|
||||
if (!m_Started) {
|
||||
PaError error = Pa_StartStream(m_Stream);
|
||||
if (error != paNoError) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Pa_StartStream() failed: %s",
|
||||
Pa_GetErrorText(error));
|
||||
return false;
|
||||
}
|
||||
|
||||
m_Started = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int PortAudioRenderer::paStreamCallback(const void*, void* output, unsigned long frameCount, const PaStreamCallbackTimeInfo*, PaStreamCallbackFlags, 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;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <portaudio.h>
|
||||
|
||||
#include "renderer.h"
|
||||
|
||||
#define CIRCULAR_BUFFER_SIZE 16
|
||||
#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 bool submitAudio(short* audioBuffer, int audioSize);
|
||||
|
||||
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;
|
||||
bool m_Started;
|
||||
short m_AudioBuffer[CIRCULAR_BUFFER_SIZE * CIRCULAR_BUFFER_STRIDE];
|
||||
};
|
||||
|
||||
@@ -3,11 +3,6 @@
|
||||
#include "renderer.h"
|
||||
#include <SDL.h>
|
||||
|
||||
#ifndef HAVE_SLVIDEO
|
||||
#error SDL audio backend is only available for Steam Link
|
||||
#error Please install PortAudio to build for Linux
|
||||
#endif
|
||||
|
||||
class SdlAudioRenderer : public IAudioRenderer
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
#include "soundioaudiorenderer.h"
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
SoundIoAudioRenderer::SoundIoAudioRenderer()
|
||||
: m_OpusChannelCount(0),
|
||||
m_SoundIo(nullptr),
|
||||
m_Device(nullptr),
|
||||
m_OutputStream(nullptr),
|
||||
m_RingBuffer(nullptr),
|
||||
m_Errored(false)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
SoundIoAudioRenderer::~SoundIoAudioRenderer()
|
||||
{
|
||||
if (m_OutputStream != nullptr) {
|
||||
soundio_outstream_destroy(m_OutputStream);
|
||||
}
|
||||
|
||||
// Must be destroyed after the stream is stopped
|
||||
// or we could still get sioWriteCallback() calls.
|
||||
if (m_RingBuffer != nullptr) {
|
||||
soundio_ring_buffer_destroy(m_RingBuffer);
|
||||
}
|
||||
|
||||
if (m_Device != nullptr) {
|
||||
soundio_device_unref(m_Device);
|
||||
}
|
||||
|
||||
if (m_SoundIo != nullptr) {
|
||||
soundio_destroy(m_SoundIo);
|
||||
}
|
||||
}
|
||||
|
||||
int SoundIoAudioRenderer::scoreChannelLayout(const struct SoundIoChannelLayout* layout, const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
|
||||
{
|
||||
int score = 50;
|
||||
|
||||
// Compute a score for this layout based on how many matching channels
|
||||
// we find (or acceptable alternatives).
|
||||
for (int i = 0; i < layout->channel_count; i++) {
|
||||
if (opusConfig->channelCount >= 2) {
|
||||
switch (layout->channels[i]) {
|
||||
case SoundIoChannelIdFrontLeft:
|
||||
case SoundIoChannelIdFrontRight:
|
||||
score += 2;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (opusConfig->channelCount >= 6) {
|
||||
switch (layout->channels[i]) {
|
||||
case SoundIoChannelIdFrontCenter:
|
||||
case SoundIoChannelIdLfe:
|
||||
score += 2;
|
||||
break;
|
||||
|
||||
case SoundIoChannelIdSideLeft:
|
||||
case SoundIoChannelIdSideRight:
|
||||
score++;
|
||||
break;
|
||||
|
||||
// Score layouts using the back L/R as higher
|
||||
// value than those using side L/R.
|
||||
case SoundIoChannelIdBackLeft:
|
||||
case SoundIoChannelIdBackRight:
|
||||
score += 2;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now subtract the difference between the desired and actual channel count
|
||||
// to punish layouts that have extra unused speakers.
|
||||
if (opusConfig->channelCount < layout->channel_count) {
|
||||
score -= layout->channel_count - opusConfig->channelCount;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
|
||||
{
|
||||
m_SoundIo = soundio_create();
|
||||
if (m_SoundIo == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_create() failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_SoundIo->app_name = "Moonlight";
|
||||
m_SoundIo->userdata = this;
|
||||
m_SoundIo->on_backend_disconnect = sioBackendDisconnect;
|
||||
m_SoundIo->on_devices_change = sioDevicesChanged;
|
||||
|
||||
int err = soundio_connect(m_SoundIo);
|
||||
if (err != SoundIoErrorNone) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_connect() failed: %s",
|
||||
soundio_strerror(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Audio backend: %s",
|
||||
soundio_backend_name(m_SoundIo->current_backend));
|
||||
|
||||
// Don't continue if we could only open the dummy backend
|
||||
if (m_SoundIo->current_backend == SoundIoBackendDummy) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Flush events to update with new device arrivals
|
||||
soundio_flush_events(m_SoundIo);
|
||||
|
||||
// Remember the actual channel count for later
|
||||
m_OpusChannelCount = opusConfig->channelCount;
|
||||
|
||||
int outputDeviceIndex = soundio_default_output_device_index(m_SoundIo);
|
||||
if (outputDeviceIndex < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"No output device found");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_Device = soundio_get_output_device(m_SoundIo, outputDeviceIndex);
|
||||
if (m_Device == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_get_output_device() failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Selected audio device: %s",
|
||||
m_Device->name);
|
||||
|
||||
m_OutputStream = soundio_outstream_create(m_Device);
|
||||
if (m_OutputStream == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_outstream_create() failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_OutputStream->format = SoundIoFormatS16NE;
|
||||
m_OutputStream->sample_rate = opusConfig->sampleRate;
|
||||
m_OutputStream->name = "Moonlight";
|
||||
m_OutputStream->userdata = this;
|
||||
m_OutputStream->error_callback = sioErrorCallback;
|
||||
m_OutputStream->write_callback = sioWriteCallback;
|
||||
|
||||
// This determines the size of the buffers we'll
|
||||
// get from CoreAudio. Since GFE sends us packets
|
||||
// in 5 ms chunks, we'll give them to the OS in
|
||||
// buffers of the same size.
|
||||
m_OutputStream->software_latency = 0.005;
|
||||
|
||||
SoundIoChannelLayout bestLayout = m_Device->current_layout;
|
||||
for (int i = 0; i < m_Device->layout_count; i++) {
|
||||
if (scoreChannelLayout(&bestLayout, opusConfig) <
|
||||
scoreChannelLayout(&m_Device->layouts[i], opusConfig)) {
|
||||
bestLayout = m_Device->layouts[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (bestLayout.channel_count < opusConfig->channelCount) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"No compatible channel layouts found. Some channels may not be played!");
|
||||
}
|
||||
|
||||
m_OutputStream->layout = bestLayout;
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Native layout: %s (%d channels)",
|
||||
m_OutputStream->layout.name ?
|
||||
m_OutputStream->layout.name : "<UNKNOWN>",
|
||||
m_OutputStream->layout.channel_count);
|
||||
|
||||
err = soundio_outstream_open(m_OutputStream);
|
||||
if (err != SoundIoErrorNone) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_outstream_open() failed: %s",
|
||||
soundio_strerror(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_OutputStream->layout_error) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Channel layout failed: %s",
|
||||
soundio_strerror(m_OutputStream->layout_error));
|
||||
|
||||
// ALSA through PulseAudio appears to fail snd_pcm_set_chmap()
|
||||
// even after claiming the layout is supported (and even on totally
|
||||
// standard layouts like Stereo). We'll just ignore this for ALSA
|
||||
// and only bail if we get an actual failure out of one of these APIs.
|
||||
if (m_SoundIo->current_backend != SoundIoBackendAlsa) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
m_EffectiveLayout = m_OutputStream->layout;
|
||||
for (int i = 0; i < m_EffectiveLayout.channel_count; i++) {
|
||||
// Fixup the layout to use back L/R so our channel position
|
||||
// logic in sioWriteCallback() works.
|
||||
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideLeft) {
|
||||
m_EffectiveLayout.channels[i] = SoundIoChannelIdBackLeft;
|
||||
}
|
||||
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideRight) {
|
||||
m_EffectiveLayout.channels[i] = SoundIoChannelIdBackRight;
|
||||
}
|
||||
}
|
||||
|
||||
// Buffer up to 6 packets of audio (30 ms) to smooth
|
||||
// out network packet delivery jitter
|
||||
m_RingBuffer = soundio_ring_buffer_create(m_SoundIo,
|
||||
m_OutputStream->bytes_per_sample *
|
||||
m_OpusChannelCount *
|
||||
SAMPLES_PER_FRAME * 6);
|
||||
if (m_RingBuffer == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_ring_buffer_create() failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
err = soundio_outstream_start(m_OutputStream);
|
||||
if (err != SoundIoErrorNone) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_outstream_start() failed: %s",
|
||||
soundio_strerror(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SoundIoAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
|
||||
{
|
||||
if (m_Errored) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Flush events to update with new device arrivals
|
||||
soundio_flush_events(m_SoundIo);
|
||||
|
||||
// We must always write a full frame of audio. If we don't,
|
||||
// the reader will get out of sync with the writer and our
|
||||
// channels will get all mixed up. To ensure this is always
|
||||
// the case, round our bytes free down to the next multiple
|
||||
// of our frame size.
|
||||
int bytesFree = soundio_ring_buffer_free_count(m_RingBuffer);
|
||||
int bytesPerFrame = m_OpusChannelCount * m_OutputStream->bytes_per_sample;
|
||||
int bytesToWrite = qMin(audioSize, (bytesFree / bytesPerFrame) * bytesPerFrame);
|
||||
memcpy(soundio_ring_buffer_write_ptr(m_RingBuffer), audioBuffer, bytesToWrite);
|
||||
soundio_ring_buffer_advance_write_ptr(m_RingBuffer, bytesToWrite);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SoundIoAudioRenderer::sioErrorCallback(SoundIoOutStream* stream, int err)
|
||||
{
|
||||
auto me = reinterpret_cast<SoundIoAudioRenderer*>(stream->userdata);
|
||||
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Audio rendering error: %s",
|
||||
soundio_strerror(err));
|
||||
|
||||
// Trigger reinitialization
|
||||
me->m_Errored = true;
|
||||
}
|
||||
|
||||
void SoundIoAudioRenderer::sioBackendDisconnect(SoundIo* soundio, int err)
|
||||
{
|
||||
auto me = reinterpret_cast<SoundIoAudioRenderer*>(soundio->userdata);
|
||||
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Audio backend disconnected: %s",
|
||||
soundio_strerror(err));
|
||||
|
||||
// Trigger reinitialization
|
||||
me->m_Errored = true;
|
||||
}
|
||||
|
||||
void SoundIoAudioRenderer::sioDevicesChanged(SoundIo* soundio)
|
||||
{
|
||||
auto me = reinterpret_cast<SoundIoAudioRenderer*>(soundio->userdata);
|
||||
|
||||
if (me->m_Device == nullptr) {
|
||||
// Ignore calls that take place during initialization
|
||||
return;
|
||||
}
|
||||
|
||||
int outputDeviceIndex = soundio_default_output_device_index(soundio);
|
||||
if (outputDeviceIndex >= 0) {
|
||||
struct SoundIoDevice* outputDevice = soundio_get_output_device(soundio, outputDeviceIndex);
|
||||
if (outputDevice == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_get_output_device() failed");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!soundio_device_equal(outputDevice, me->m_Device)) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Default audio output device changed");
|
||||
|
||||
// Trigger reinitialization
|
||||
me->m_Errored = true;
|
||||
}
|
||||
|
||||
soundio_device_unref(outputDevice);
|
||||
}
|
||||
}
|
||||
|
||||
// bytes_per_frame should never be used on the ring buffer! It's not always
|
||||
// the same number of bytes per frames as the output stream!
|
||||
void SoundIoAudioRenderer::sioWriteCallback(SoundIoOutStream* stream, int frameCountMin, int frameCountMax)
|
||||
{
|
||||
auto me = reinterpret_cast<SoundIoAudioRenderer*>(stream->userdata);
|
||||
char* readPtr = soundio_ring_buffer_read_ptr(me->m_RingBuffer);
|
||||
int framesLeft = soundio_ring_buffer_fill_count(me->m_RingBuffer) /
|
||||
(me->m_OpusChannelCount * stream->bytes_per_sample);
|
||||
int bytesRead = 0;
|
||||
|
||||
// Clamp framesLeft to frameCountMax
|
||||
framesLeft = qMin(framesLeft, frameCountMax);
|
||||
|
||||
// Place an upper-bound on audio stream latency to
|
||||
// avoid accumulating packets in queue-based backends
|
||||
// like WASAPI. This bound was set by testing on several
|
||||
// Windows machines. The highest latency was found on
|
||||
// a XPS 9343 running Windows 7 in Steam Big Picture
|
||||
// and the 5.1 audio test clip from Fraunhofer.
|
||||
if (me->m_SoundIo->current_backend == SoundIoBackendWasapi) {
|
||||
double latency;
|
||||
if (soundio_outstream_get_latency(stream, &latency) == SoundIoErrorNone) {
|
||||
if (latency > 0.050) {
|
||||
// If our latency is higher than desired, drop these samples to gracefully lower
|
||||
// the latency without glitching too much. Dropping the whole buffer causes
|
||||
// a much more noticeable glitch. This approach also ensures that we don't
|
||||
// accidentally underflow if the driver/kernel side is delayed and isn't
|
||||
// consuming data fast enough. Dropping a frame at a time and re-evaluating
|
||||
// each time ensures that we'll stop dropping if latency returns to normal.
|
||||
readPtr += framesLeft * stream->bytes_per_sample * me->m_OpusChannelCount;
|
||||
bytesRead += framesLeft * stream->bytes_per_sample * me->m_OpusChannelCount;
|
||||
framesLeft = 0;
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Latency exceeded drop cap: %f",
|
||||
latency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
int frameCount;
|
||||
int err;
|
||||
struct SoundIoChannelArea* areas;
|
||||
|
||||
// Always meet the minimum but don't write more than that
|
||||
// if we'll have to insert silence
|
||||
frameCount = qMax(framesLeft, frameCountMin);
|
||||
|
||||
if (frameCount == 0) {
|
||||
// Nothing more to write
|
||||
break;
|
||||
}
|
||||
|
||||
err = soundio_outstream_begin_write(stream, &areas, &frameCount);
|
||||
if (err != SoundIoErrorNone) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_outstream_begin_write() failed: %s",
|
||||
soundio_strerror(err));
|
||||
break;
|
||||
}
|
||||
|
||||
for (int frame = 0; frame < frameCount; frame++) {
|
||||
for (int ch = 0; ch < me->m_EffectiveLayout.channel_count; ch++) {
|
||||
// SoundIoChannelId - 1 happens to match Moonlight's channel layout.
|
||||
// For side L/R, this logic depends on us fixing those up
|
||||
// in m_EffectiveLayout to back L/R.
|
||||
int readPtrChannel = me->m_EffectiveLayout.channels[ch] - 1;
|
||||
|
||||
if (frame >= framesLeft || readPtrChannel >= me->m_OpusChannelCount) {
|
||||
// Write silence if we have no buffered frames left or
|
||||
// nothing in the audio stream for this channel
|
||||
memset(areas[ch].ptr, 0, stream->bytes_per_sample);
|
||||
}
|
||||
else {
|
||||
// Write audio data from our ring buffer
|
||||
memcpy(areas[ch].ptr,
|
||||
&readPtr[readPtrChannel * stream->bytes_per_sample],
|
||||
stream->bytes_per_sample);
|
||||
}
|
||||
|
||||
areas[ch].ptr += areas[ch].step;
|
||||
}
|
||||
|
||||
// Move on to the next frame if we aren't inserting silence
|
||||
if (frame < framesLeft) {
|
||||
readPtr += stream->bytes_per_sample * me->m_OpusChannelCount;
|
||||
bytesRead += stream->bytes_per_sample * me->m_OpusChannelCount;
|
||||
}
|
||||
}
|
||||
|
||||
err = soundio_outstream_end_write(stream);
|
||||
if (err != SoundIoErrorNone && err != SoundIoErrorUnderflow) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_outstream_end_write() failed: %s",
|
||||
soundio_strerror(err));
|
||||
break;
|
||||
}
|
||||
|
||||
if (framesLeft >= frameCount) {
|
||||
framesLeft -= frameCount;
|
||||
}
|
||||
else {
|
||||
framesLeft = 0;
|
||||
}
|
||||
|
||||
if (frameCountMin >= frameCount) {
|
||||
frameCountMin -= frameCount;
|
||||
}
|
||||
else {
|
||||
frameCountMin = 0;
|
||||
}
|
||||
}
|
||||
|
||||
soundio_ring_buffer_advance_read_ptr(me->m_RingBuffer, bytesRead);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include "renderer.h"
|
||||
|
||||
#include <soundio/soundio.h>
|
||||
|
||||
class SoundIoAudioRenderer : public IAudioRenderer
|
||||
{
|
||||
public:
|
||||
SoundIoAudioRenderer();
|
||||
|
||||
~SoundIoAudioRenderer();
|
||||
|
||||
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||
|
||||
virtual bool submitAudio(short* audioBuffer, int audioSize);
|
||||
|
||||
private:
|
||||
int scoreChannelLayout(const struct SoundIoChannelLayout* layout, const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||
|
||||
static void sioErrorCallback(struct SoundIoOutStream* stream, int err);
|
||||
|
||||
static void sioWriteCallback(struct SoundIoOutStream* stream, int frameCountMin, int frameCountMax);
|
||||
|
||||
static void sioBackendDisconnect(struct SoundIo* soundio, int err);
|
||||
|
||||
static void sioDevicesChanged(SoundIo* soundio);
|
||||
|
||||
int m_OpusChannelCount;
|
||||
struct SoundIo* m_SoundIo;
|
||||
struct SoundIoDevice* m_Device;
|
||||
struct SoundIoOutStream* m_OutputStream;
|
||||
struct SoundIoRingBuffer* m_RingBuffer;
|
||||
struct SoundIoChannelLayout m_EffectiveLayout;
|
||||
bool m_Errored;
|
||||
};
|
||||
Reference in New Issue
Block a user