From 9c8b72a5febb9c1e1a038a0105c5e35f4bc49a0d Mon Sep 17 00:00:00 2001 From: Olivier Galibert Date: Fri, 16 Apr 2021 14:49:03 +0200 Subject: [PATCH] sound: Pulseaudio support --- scripts/src/osd/modules.lua | 29 ++ src/osd/modules/lib/osdobj_common.cpp | 3 + src/osd/modules/sound/pulse_sound.cpp | 405 ++++++++++++++++++++++++++ 3 files changed, 437 insertions(+) create mode 100644 src/osd/modules/sound/pulse_sound.cpp diff --git a/scripts/src/osd/modules.lua b/scripts/src/osd/modules.lua index d7987b5b80e..3f9e1948c6e 100644 --- a/scripts/src/osd/modules.lua +++ b/scripts/src/osd/modules.lua @@ -82,6 +82,7 @@ function osdmodulesbuild() MAME_DIR .. "src/osd/modules/sound/js_sound.cpp", MAME_DIR .. "src/osd/modules/sound/direct_sound.cpp", MAME_DIR .. "src/osd/modules/sound/pa_sound.cpp", + MAME_DIR .. "src/osd/modules/sound/pulse_sound.cpp", MAME_DIR .. "src/osd/modules/sound/coreaudio_sound.cpp", MAME_DIR .. "src/osd/modules/sound/sdl_sound.cpp", MAME_DIR .. "src/osd/modules/sound/xaudio2_sound.cpp", @@ -270,6 +271,12 @@ function osdmodulesbuild() } end + if _OPTIONS["NO_USE_PULSEAUDIO"]=="1" then + defines { + "NO_USE_PULSEAUDIO", + } + end + if _OPTIONS["NO_USE_MIDI"]=="1" then defines { "NO_USE_MIDI", @@ -501,6 +508,11 @@ function osdmodulestargetconf() } end + if _OPTIONS["NO_USE_PULSEAUDIO"]=="0" then + links { + ext_lib("pulse"), + } + end end @@ -578,6 +590,23 @@ if not _OPTIONS["NO_USE_PORTAUDIO"] then end end +newoption { + trigger = "NO_USE_PULSEAUDIO", + description = "Disable PulseAudio interface", + allowed = { + { "0", "Enable PulseAudio" }, + { "1", "Disable PulseAudio" }, + }, +} + +if not _OPTIONS["NO_USE_PULSEAUDIO"] then + if _OPTIONS["targetos"]=="linux" then + _OPTIONS["NO_USE_PULSEAUDIO"] = "0" + else + _OPTIONS["NO_USE_PULSEAUDIO"] = "1" + end +end + newoption { trigger = "MODERN_WIN_API", description = "Use Modern Windows APIs", diff --git a/src/osd/modules/lib/osdobj_common.cpp b/src/osd/modules/lib/osdobj_common.cpp index cedc39a17e0..5468cc1975c 100644 --- a/src/osd/modules/lib/osdobj_common.cpp +++ b/src/osd/modules/lib/osdobj_common.cpp @@ -225,6 +225,9 @@ void osd_common_t::register_options() REGISTER_MODULE(m_mod_man, SOUND_SDL); #ifndef NO_USE_PORTAUDIO REGISTER_MODULE(m_mod_man, SOUND_PORTAUDIO); +#endif +#ifndef NO_USE_PULSEAUDIO + REGISTER_MODULE(m_mod_man, SOUND_PULSEAUDIO); #endif REGISTER_MODULE(m_mod_man, SOUND_NONE); diff --git a/src/osd/modules/sound/pulse_sound.cpp b/src/osd/modules/sound/pulse_sound.cpp new file mode 100644 index 00000000000..fcee6c206e2 --- /dev/null +++ b/src/osd/modules/sound/pulse_sound.cpp @@ -0,0 +1,405 @@ +// license:BSD-3-Clause +// copyright-holders:Olivier Galibert +/*************************************************************************** + + pulse_sound.c + + PulseAudio interface. + +*******************************************************************c********/ + +#include "sound_module.h" +#include "modules/osdmodule.h" + +#ifndef NO_USE_PULSEAUDIO + +#define GNU_SOURCE +#include +#include +#include +#include +#include + +#include +#include + +#include "modules/lib/osdobj_common.h" + +class sound_pulse : public osd_module, public sound_module +{ +public: + sound_pulse() + : osd_module(OSD_SOUND_PROVIDER, "pulse"), sound_module() + { + } + virtual ~sound_pulse() { } + + virtual int init(osd_options const &options) override; + virtual void exit() override; + virtual void update_audio_stream(bool is_throttled, const s16 *buffer, int samples_this_frame) override; + virtual void set_mastervolume(int attenuation) override; + +private: + struct abuffer { + size_t cpos; + std::vector data; + }; + + std::thread *m_thread; + pa_mainloop *m_mainloop; + pa_context *m_context; + pa_stream *m_stream; + std::mutex m_mutex; + + std::vector m_buffers; + + u32 m_last_sample; + int m_new_volume_value; + bool m_setting_volume; + bool m_new_volume; + + int m_pipe_to_sub[2]; + int m_pipe_to_main[2]; + + static void i_volume_set_notify(pa_context *, int success, void *self); + void volume_set_notify(int success); + static void i_context_notify(pa_context *, void *self); + void context_notify(); + static void i_stream_notify(pa_stream *, void *self); + void stream_notify(); + static void i_stream_write_request(pa_stream *, size_t size, void *self); + void stream_write_request(size_t size); + + void make_pipes(); + + void mainloop_thread(); + void send_main(char c); + void send_sub(char c); + char get_main(); + char peek_main(); + void stop_mainloop(int err); + void generic_error(const char *msg); + void generic_pa_error(const char *msg, int err); +}; + +void sound_pulse::generic_error(const char *msg) +{ + perror(msg); + ::exit(1); +} + +void sound_pulse::generic_pa_error(const char *msg, int err) +{ + fprintf(stderr, "%s: %s\n", msg, pa_strerror(err)); + ::exit(1); +} + +char sound_pulse::peek_main() +{ + char c; + int err = read(m_pipe_to_main[0], &c, 1); + if(err != 1) { + if(err >= 0) { + fprintf(stderr, "peek_main: read returned %d, that's supposedly impossible\n", err); + ::exit(1); + } + if(errno == EAGAIN || errno == EWOULDBLOCK) { + // No data, no problem + return -1; + } + generic_error("peek_main: read"); + } + return c; +} + +char sound_pulse::get_main() +{ + pollfd pfds[1]; + pfds[0].fd = m_pipe_to_main[0]; + pfds[0].events = POLL_IN; + pfds[0].revents = 0; + int err = poll(pfds, 1, -1); + if(err < 0) + generic_error("get_main: poll"); + + char c; + err = read(m_pipe_to_main[0], &c, 1); + if(err != 1) { + if(err >= 0) { + fprintf(stderr, "get_main: read returned %d, that's supposedly impossible\n", err); + ::exit(1); + } + generic_error("get_main: read"); + } + return c; +} + +void sound_pulse::send_main(char c) +{ + int err = write(m_pipe_to_main[1], &c, 1); + if(err != 1) { + if(err >= 0) { + fprintf(stderr, "send_main: write returned %d, that's supposedly impossible\n", err); + ::exit(1); + } + if(errno == EAGAIN || errno == EWOULDBLOCK) { + fprintf(stderr, "send_main: write would block, pipe buffer overflowed, something is going very badly\n"); + ::exit(1); + } + generic_error("send_main: write"); + } +} + +void sound_pulse::send_sub(char c) +{ + int err = write(m_pipe_to_sub[1], &c, 1); + if(err != 1) { + if(err >= 0) { + fprintf(stderr, "send_sub: write returned %d, that's supposedly impossible\n", err); + ::exit(1); + } + if(errno == EAGAIN || errno == EWOULDBLOCK) { + fprintf(stderr, "send_sub: write would block, pipe buffer overflowed, something is going very badly\n"); + ::exit(1); + } + generic_error("send_sub: write"); + } +} + +void sound_pulse::make_pipes() +{ + if(pipe2(m_pipe_to_sub, O_NONBLOCK)) + generic_error("pipe2 pipe_to_sub"); + if(pipe2(m_pipe_to_main, O_NONBLOCK)) + generic_error("pipe2 pipe_to_main"); +} + +void sound_pulse::context_notify() +{ + pa_context_state state = pa_context_get_state(m_context); + if(state == PA_CONTEXT_READY) + send_main('r'); + + else if(state == PA_CONTEXT_FAILED) { + send_main('f'); + stop_mainloop(pa_context_errno(m_context)); + + } else if(state == PA_CONTEXT_TERMINATED) { + send_main('t'); + stop_mainloop(0); + } +} + +void sound_pulse::i_context_notify(pa_context *, void *self) +{ + static_cast(self)->context_notify(); +} + +void sound_pulse::stream_notify() +{ + pa_stream_state state = pa_stream_get_state(m_stream); + + if(state == PA_STREAM_READY) + send_main('r'); + + else if(state == PA_STREAM_FAILED) { + send_main('f'); + stop_mainloop(pa_context_errno(m_context)); + + } else if(state == PA_STREAM_TERMINATED) + pa_context_disconnect(m_context); +} + +void sound_pulse::i_stream_notify(pa_stream *, void *self) +{ + static_cast(self)->stream_notify(); +} + +void sound_pulse::stream_write_request(size_t size) +{ + if(size & 3) { + fprintf(stderr, "stream request with size %d not a multiple of 4.\n", int(size)); + ::exit(1); + } + size >>= 2; + + std::unique_lock lock(m_mutex); + while(size) { + if(m_buffers.empty()) { + std::vector zero(size, m_last_sample); + int err = pa_stream_write(m_stream, zero.data(), size << 2, nullptr, 0, PA_SEEK_RELATIVE); + if(err) + generic_pa_error("stream write", err); + size = 0; + + } else { + auto &buf = m_buffers[0]; + size_t csz = size; + size_t cur = buf.data.size() - buf.cpos; + if(csz > cur) + csz = cur; + int err = pa_stream_write(m_stream, buf.data.data() + buf.cpos, csz << 2, nullptr, 0, PA_SEEK_RELATIVE); + if(err) + generic_pa_error("stream write", err); + if(csz == cur) + m_buffers.erase(m_buffers.begin()); + else + buf.cpos += csz; + size -= csz; + } + } +} + +void sound_pulse::i_stream_write_request(pa_stream *, size_t size, void *self) +{ + static_cast(self)->stream_write_request(size); +} + + +void sound_pulse::mainloop_thread() +{ + int err = 0; + pa_mainloop_run(m_mainloop, &err); + if(err) + generic_pa_error("mainloop stopped", err); +} + +void sound_pulse::stop_mainloop(int err) +{ + pa_mainloop_quit(m_mainloop, err); +} + +int sound_pulse::init(osd_options const &options) +{ + m_last_sample = 0; + m_setting_volume = false; + m_new_volume = false; + m_new_volume_value = 0; + + m_mainloop = pa_mainloop_new(); + m_context = pa_context_new(pa_mainloop_get_api(m_mainloop), "MAME"); + pa_context_set_state_callback(m_context, i_context_notify, this); + int err = pa_context_connect(m_context, nullptr, PA_CONTEXT_NOFLAGS, nullptr); + + if(err) + generic_pa_error("pa_connect", err); + + make_pipes(); + m_thread = new std::thread(&sound_pulse::mainloop_thread, this); + + char res = get_main(); + + if(res != 'r') + return 1; + + pa_sample_spec ss; + ss.format = ENDIANNESS_NATIVE == ENDIANNESS_BIG ? PA_SAMPLE_S16BE : PA_SAMPLE_S16LE; + ss.rate = sample_rate(); + ss.channels = 2; + m_stream = pa_stream_new(m_context, "main output", &ss, nullptr); + pa_stream_set_state_callback(m_stream, i_stream_notify, this); + pa_stream_set_write_callback(m_stream, i_stream_write_request, this); + + pa_buffer_attr battr; + battr.fragsize = sample_rate() / 1000; + battr.maxlength = uint32_t(-1); + battr.minreq = sample_rate() / 1000; + battr.prebuf = uint32_t(-1); + battr.tlength = sample_rate() / 1000; + + err = pa_stream_connect_playback(m_stream, nullptr, &battr, PA_STREAM_ADJUST_LATENCY, nullptr, nullptr); + if(err) + generic_pa_error("stream connect playback", err); + + res = get_main(); + if(res != 'r') + return 1; + return 0; +} + +void sound_pulse::update_audio_stream(bool is_throttled, const s16 *buffer, int samples_this_frame) +{ + std::unique_lock lock(m_mutex); + m_buffers.resize(m_buffers.size() + 1); + auto &buf = m_buffers.back(); + buf.cpos = 0; + buf.data.resize(samples_this_frame); + memcpy(buf.data.data(), buffer, samples_this_frame*4); + m_last_sample = buf.data.back(); + + if(m_buffers.size() > 10) + // If there way too many buffers, drop some so only 10 are left (roughly 0.2s) + m_buffers.erase(m_buffers.begin(), m_buffers.begin() + m_buffers.size() - 10); + + else if(m_buffers.size() >= 5) + // If there are too many buffers, remove five sample per buffer + // to slowly resync to reduce latency (4 seconds to + // compensate one buffer roughly) + buf.cpos = 5; +} + +void sound_pulse::volume_set_notify(int success) +{ + std::unique_lock lock(m_mutex); + if(m_new_volume) { + m_new_volume = false; + pa_cvolume vol; + pa_cvolume_set(&vol, 2, pa_sw_volume_from_dB(m_new_volume_value)); + pa_context_set_sink_input_volume(m_context, pa_stream_get_index(m_stream), &vol, i_volume_set_notify, this); + } else + m_setting_volume = false; +} + +void sound_pulse::i_volume_set_notify(pa_context *, int success, void *self) +{ + static_cast(self)->volume_set_notify(success); +} + +void sound_pulse::set_mastervolume(int attenuation) +{ + if(!m_stream) + return; + + + std::unique_lock lock(m_mutex); + if(m_setting_volume) { + m_new_volume = true; + m_new_volume_value = attenuation; + } else { + m_setting_volume = true; + pa_cvolume vol; + pa_cvolume_set(&vol, 2, pa_sw_volume_from_dB(attenuation)); + pa_context_set_sink_input_volume(m_context, pa_stream_get_index(m_stream), &vol, i_volume_set_notify, this); + } +} + +void sound_pulse::exit() +{ + if(!m_stream) + return; + + pa_stream_disconnect(m_stream); + while(get_main() != 't') {} + pa_stream_unref(m_stream); + pa_context_unref(m_context); + m_thread->join(); + pa_mainloop_free(m_mainloop); + delete m_thread; + + close(m_pipe_to_sub[0]); + close(m_pipe_to_sub[1]); + close(m_pipe_to_main[0]); + close(m_pipe_to_main[1]); + + m_thread = nullptr; + m_mainloop = nullptr; + m_context = nullptr; + m_stream = nullptr; + m_buffers.clear(); +} + +#else + MODULE_NOT_SUPPORTED(sound_pulse, OSD_SOUND_PROVIDER, "pulse") +#endif + +MODULE_DEFINITION(SOUND_PULSEAUDIO, sound_pulse)