diff --git a/CMakeLists.txt b/CMakeLists.txt index dade7a63583b..6b5d555ffa9c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -363,7 +363,7 @@ if(NOT ANDROID) message("bluez NOT found, disabling bluetooth support") endif(BLUEZ_FOUND) - check_lib(PULSEAUDIO libpulse-simple QUIET) + check_lib(PULSEAUDIO libpulse QUIET) if(PULSEAUDIO_FOUND) add_definitions(-DHAVE_PULSEAUDIO=1) message("PulseAudio found, enabling PulseAudio sound backend") diff --git a/Source/Core/AudioCommon/PulseAudioStream.cpp b/Source/Core/AudioCommon/PulseAudioStream.cpp index 13307a8fe0f7..fe68d0eb6caf 100644 --- a/Source/Core/AudioCommon/PulseAudioStream.cpp +++ b/Source/Core/AudioCommon/PulseAudioStream.cpp @@ -11,30 +11,28 @@ namespace { -const size_t BUFFER_SAMPLES = 512; +const size_t BUFFER_SAMPLES = 512; // ~10 ms const size_t CHANNEL_COUNT = 2; -const size_t BUFFER_SIZE = BUFFER_SAMPLES * CHANNEL_COUNT; +const size_t BUFFER_SIZE = BUFFER_SAMPLES * CHANNEL_COUNT * sizeof(s16); } PulseAudio::PulseAudio(CMixer *mixer) : SoundStream(mixer) - , mix_buffer(BUFFER_SIZE) - , thread() - , run_thread() - , pa() + , m_thread() + , m_run_thread() {} bool PulseAudio::Start() { - run_thread = true; - thread = std::thread(std::mem_fun(&PulseAudio::SoundLoop), this); + m_run_thread = true; + m_thread = std::thread(std::mem_fun(&PulseAudio::SoundLoop), this); return true; } void PulseAudio::Stop() { - run_thread = false; - thread.join(); + m_run_thread = false; + m_thread.join(); } void PulseAudio::Update() @@ -49,11 +47,11 @@ void PulseAudio::SoundLoop() if (PulseInit()) { - while (run_thread) - { - m_mixer->Mix(&mix_buffer[0], mix_buffer.size() / CHANNEL_COUNT); - Write(&mix_buffer[0], mix_buffer.size() * sizeof(s16)); - } + while (m_run_thread.load() && m_pa_connected == 1 && m_pa_error >= 0) + m_pa_error = pa_mainloop_iterate(m_pa_ml, 1, NULL); + + if(m_pa_error < 0) + ERROR_LOG(AUDIO, "PulseAudio error: %s", pa_strerror(m_pa_error)); PulseShutdown(); } @@ -61,39 +59,117 @@ void PulseAudio::SoundLoop() bool PulseAudio::PulseInit() { - pa_sample_spec ss = {}; + m_pa_error = 0; + m_pa_connected = 0; + + // create pulseaudio main loop and context + // also register the async state callback which is called when the connection to the pa server has changed + m_pa_ml = pa_mainloop_new(); + m_pa_mlapi = pa_mainloop_get_api(m_pa_ml); + m_pa_ctx = pa_context_new(m_pa_mlapi, "dolphin-emu"); + m_pa_error = pa_context_connect(m_pa_ctx, NULL, PA_CONTEXT_NOFLAGS, NULL); + pa_context_set_state_callback(m_pa_ctx, StateCallback, this); + + // wait until we're connected to the pulseaudio server + while (m_pa_connected == 0 && m_pa_error >= 0) + m_pa_error = pa_mainloop_iterate(m_pa_ml, 1, NULL); + + if (m_pa_connected == 2 || m_pa_error < 0) + { + ERROR_LOG(AUDIO, "PulseAudio failed to initialize: %s", pa_strerror(m_pa_error)); + return false; + } + + // create a new audio stream with our sample format + // also connect the callbacks for this stream + pa_sample_spec ss; ss.format = PA_SAMPLE_S16LE; ss.channels = 2; ss.rate = m_mixer->GetSampleRate(); - - int error; - pa = pa_simple_new(nullptr, "dolphin-emu", PA_STREAM_PLAYBACK, - nullptr, "audio", &ss, nullptr, nullptr, &error); - - if (!pa) + m_pa_s = pa_stream_new(m_pa_ctx, "Playback", &ss, NULL); + pa_stream_set_write_callback(m_pa_s, WriteCallback, this); + pa_stream_set_underflow_callback(m_pa_s, UnderflowCallback, this); + + // connect this audio stream to the default audio playback + // limit buffersize to reduce latency + m_pa_ba.fragsize = -1; + m_pa_ba.maxlength = -1; // max buffer, so also max latency + m_pa_ba.minreq = -1; // don't read every byte, try to group them _a bit_ + m_pa_ba.prebuf = -1; // start as early as possible + m_pa_ba.tlength = BUFFER_SIZE; // designed latency, only change this flag for low latency output + pa_stream_flags flags = pa_stream_flags(PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_ADJUST_LATENCY | PA_STREAM_AUTO_TIMING_UPDATE); + m_pa_error = pa_stream_connect_playback(m_pa_s, NULL, &m_pa_ba, flags, NULL, NULL); + if (m_pa_error < 0) { - ERROR_LOG(AUDIO, "PulseAudio failed to initialize: %s", - pa_strerror(error)); + ERROR_LOG(AUDIO, "PulseAudio failed to initialize: %s", pa_strerror(m_pa_error)); return false; } - else - { - NOTICE_LOG(AUDIO, "Pulse successfully initialized."); - return true; - } + + INFO_LOG(AUDIO, "Pulse successfully initialized"); + return true; } void PulseAudio::PulseShutdown() { - pa_simple_free(pa); + pa_context_disconnect(m_pa_ctx); + pa_context_unref(m_pa_ctx); + pa_mainloop_free(m_pa_ml); } -void PulseAudio::Write(const void *data, size_t length) +void PulseAudio::StateCallback(pa_context* c) { - int error; - if (pa_simple_write(pa, data, length, &error) < 0) + pa_context_state_t state = pa_context_get_state(c); + switch (state) { - ERROR_LOG(AUDIO, "PulseAudio failed to write data: %s", - pa_strerror(error)); + case PA_CONTEXT_FAILED: + case PA_CONTEXT_TERMINATED: + m_pa_connected = 2; + break; + case PA_CONTEXT_READY: + m_pa_connected = 1; + break; + default: + break; } } +// on underflow, increase pulseaudio latency in ~10ms steps +void PulseAudio::UnderflowCallback(pa_stream* s) +{ + m_pa_ba.tlength += BUFFER_SIZE; + pa_stream_set_buffer_attr(s, &m_pa_ba, NULL, NULL); + + WARN_LOG(AUDIO, "pulseaudio underflow, new latency: %d bytes", m_pa_ba.tlength); +} + +void PulseAudio::WriteCallback(pa_stream* s, size_t length) +{ + // fetch dst buffer directly from pulseaudio, so no memcpy is needed + void* buffer; + m_pa_error = pa_stream_begin_write(s, &buffer, &length); + + if (!buffer || m_pa_error < 0) + return; // error will be printed from main loop + + m_mixer->Mix((s16*) buffer, length / sizeof(s16) / CHANNEL_COUNT); + m_pa_error = pa_stream_write(s, buffer, length, NULL, 0, PA_SEEK_RELATIVE); +} + +// Callbacks that forward to internal methods (required because PulseAudio is a C API). + +void PulseAudio::StateCallback(pa_context* c, void* userdata) +{ + PulseAudio* p = (PulseAudio*) userdata; + p->StateCallback(c); +} + +void PulseAudio::UnderflowCallback(pa_stream* s, void* userdata) +{ + PulseAudio* p = (PulseAudio*) userdata; + p->UnderflowCallback(s); +} + +void PulseAudio::WriteCallback(pa_stream* s, size_t length, void* userdata) +{ + PulseAudio* p = (PulseAudio*) userdata; + p->WriteCallback(s, length); +} diff --git a/Source/Core/AudioCommon/PulseAudioStream.h b/Source/Core/AudioCommon/PulseAudioStream.h index 8be8eae11dd9..3164a4ee9417 100644 --- a/Source/Core/AudioCommon/PulseAudioStream.h +++ b/Source/Core/AudioCommon/PulseAudioStream.h @@ -6,17 +6,16 @@ #define _PULSE_AUDIO_STREAM_H #if defined(HAVE_PULSEAUDIO) && HAVE_PULSEAUDIO -#include -#include +#include #endif +#include + #include "Common.h" #include "SoundStream.h" #include "Thread.h" -#include - class PulseAudio : public SoundStream { #if defined(HAVE_PULSEAUDIO) && HAVE_PULSEAUDIO @@ -32,18 +31,31 @@ class PulseAudio : public SoundStream virtual void Update(); + void StateCallback(pa_context *c); + void WriteCallback(pa_stream *s, size_t length); + void UnderflowCallback(pa_stream *s); + private: virtual void SoundLoop(); bool PulseInit(); void PulseShutdown(); - void Write(const void *data, size_t bytes); - std::vector mix_buffer; - std::thread thread; - volatile bool run_thread; + // wrapper callback functions, last parameter _must_ be PulseAudio* + static void StateCallback(pa_context *c, void *userdata); + static void WriteCallback(pa_stream *s, size_t length, void *userdata); + static void UnderflowCallback(pa_stream *s, void *userdata); + + std::thread m_thread; + std::atomic m_run_thread; - pa_simple* pa; + int m_pa_error; + int m_pa_connected; + pa_mainloop *m_pa_ml; + pa_mainloop_api *m_pa_mlapi; + pa_context *m_pa_ctx; + pa_stream *m_pa_s; + pa_buffer_attr m_pa_ba; #else public: PulseAudio(CMixer *mixer) : SoundStream(mixer) {}