From e28c44bdb996f7c3519195a9c9755e5750b605ed Mon Sep 17 00:00:00 2001 From: Colby Klein Date: Mon, 9 Nov 2009 06:56:56 +0000 Subject: [PATCH] Add PulseAudio support for Linux (may need some cleanup? From http://www.stepmania.com/forums/showthread.php?t=21348). Also commented out something in ScreenSelectMaster that causes a crash while trying to use cursors. --- stepmania/configure.ac | 8 + stepmania/src/Makefile.am | 4 + stepmania/src/ScreenSelectMaster.cpp | 3 + .../arch/Sound/RageSoundDriver_PulseAudio.cpp | 365 ++++++++++++++++++ .../arch/Sound/RageSoundDriver_PulseAudio.h | 68 ++++ stepmania/src/arch/arch_default.h | 2 +- 6 files changed, 449 insertions(+), 1 deletion(-) create mode 100644 stepmania/src/arch/Sound/RageSoundDriver_PulseAudio.cpp create mode 100644 stepmania/src/arch/Sound/RageSoundDriver_PulseAudio.h diff --git a/stepmania/configure.ac b/stepmania/configure.ac index 96a57b0a8e..ba70be7cd6 100644 --- a/stepmania/configure.ac +++ b/stepmania/configure.ac @@ -208,6 +208,13 @@ AC_ARG_ENABLE(force-oss, AS_HELP_STRING([--enable-force-oss],[Force OSS]), force AC_CHECK_HEADER(stdint.h, , [AC_DEFINE(MISSING_STDINT_H, 1, [stdint.h is missing])]) AC_CHECK_HEADERS([inttypes.h endian.h machine/endian.h alloca.h]) +have_pulse=no +AC_CHECK_LIB(pulse, pa_stream_new, have_pulse=yes) +if test x$have_pulse = xyes; then + AC_DEFINE(HAVE_PULSE, 1, [pulseaudio support available]) + LIBS="$LIBS -lpulse" +fi + AC_MSG_CHECKING(if cstdlib breaks llabs) AC_LANG_PUSH(C++) AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[#include @@ -227,6 +234,7 @@ AC_DEFINE(__STDC_FORMAT_MACROS, 1, [Use PRId64 and similar]) AM_CONDITIONAL(HAVE_ALSA, test x$alsa != xfalse ) AM_CONDITIONAL(HAVE_GTK, test "$enable_gtk2" != "no" ) AM_CONDITIONAL(HAVE_OSS, test x$ac_cv_header_sys_soundcard_h = xyes ) +AM_CONDITIONAL(HAVE_PULSE, test x$have_pulse = xyes) AM_CONDITIONAL(USE_CRASH_HANDLER, test "$use_crash_handler" = "yes" ) if test x$force_oss = xyes && test x$ac_cv_header_sys_soundcard_h = xyes; then diff --git a/stepmania/src/Makefile.am b/stepmania/src/Makefile.am index 4aba95c023..c6725c8655 100644 --- a/stepmania/src/Makefile.am +++ b/stepmania/src/Makefile.am @@ -198,6 +198,10 @@ if HAVE_OSS Sound += arch/Sound/RageSoundDriver_OSS.cpp arch/Sound/RageSoundDriver_OSS.h endif +if HAVE_PULSE +Sound += arch/Sound/RageSoundDriver_PulseAudio.cpp arch/Sound/RageSoundDriver_PulseAudio.h +endif + if HAVE_ALSA Sound += arch/Sound/ALSA9Dynamic.cpp arch/Sound/ALSA9Dynamic.h arch/Sound/ALSA9Functions.h \ arch/Sound/ALSA9Helpers.cpp arch/Sound/ALSA9Helpers.h \ diff --git a/stepmania/src/ScreenSelectMaster.cpp b/stepmania/src/ScreenSelectMaster.cpp index 4c3a0a10dd..3f3f74e5d3 100644 --- a/stepmania/src/ScreenSelectMaster.cpp +++ b/stepmania/src/ScreenSelectMaster.cpp @@ -392,8 +392,11 @@ void ScreenSelectMaster::BeginScreen() { if( GAMESTATE->IsHumanPlayer(pn) ) continue; + /* + XXX: this code causes crashes if( SHOW_CURSOR ) m_sprCursor[pn]->SetVisible( false ); + */ if( SHOW_SCROLLER ) m_Scroller[pn].SetVisible( false ); } diff --git a/stepmania/src/arch/Sound/RageSoundDriver_PulseAudio.cpp b/stepmania/src/arch/Sound/RageSoundDriver_PulseAudio.cpp new file mode 100644 index 0000000000..136d0d9f0e --- /dev/null +++ b/stepmania/src/arch/Sound/RageSoundDriver_PulseAudio.cpp @@ -0,0 +1,365 @@ +#include "global.h" +#include "RageSoundDriver_PulseAudio.h" +#include "RageLog.h" +#include "RageSound.h" +#include "RageSoundManager.h" +#include "RageUtil.h" +#include "RageTimer.h" +#include "PrefsManager.h" +#include +#include +#include + +/* Register the RageSoundDriver_Pulseaudio class as sound driver "Pulse" */ +REGISTER_SOUND_DRIVER_CLASS2( Pulse, PulseAudio ); + +/* Constructor */ +RageSoundDriver_PulseAudio::RageSoundDriver_PulseAudio() + : RageSoundDriver(), + m_LastPosition(0), m_SampleRate(0), m_Error(NULL), + m_Sem("Pulseaudio Synchronization Semaphore"), + m_PulseMainLoop(NULL), m_PulseCtx(NULL), m_PulseStream(NULL) +{ + m_SampleRate = PREFSMAN->m_iSoundPreferredSampleRate; + if( m_SampleRate == 0 ) + m_SampleRate = 44100; +} + +RageSoundDriver_PulseAudio::~RageSoundDriver_PulseAudio() +{ + pa_context_disconnect(m_PulseCtx); + pa_context_unref(m_PulseCtx); + pa_threaded_mainloop_stop(m_PulseMainLoop); + pa_threaded_mainloop_free(m_PulseMainLoop); + if(m_Error != NULL) + { + free(m_Error); + } +} + +/* Initialization */ +RString RageSoundDriver_PulseAudio::Init() +{ + int error = 0; + + LOG->Trace("Pulse: pa_threaded_mainloop_new()..."); + m_PulseMainLoop = pa_threaded_mainloop_new(); + if(m_PulseMainLoop == NULL) + { + return "pa_threaded_mainloop_new() failed!"; + } + +#ifdef PA_PROP_APPLICATION_NAME /* proplist available only since 0.9.11 */ + pa_proplist *plist = pa_proplist_new(); + pa_proplist_sets(plist, PA_PROP_APPLICATION_NAME, PACKAGE_NAME); + pa_proplist_sets(plist, PA_PROP_APPLICATION_VERSION, PACKAGE_VERSION); + pa_proplist_sets(plist, PA_PROP_MEDIA_ROLE, "game"); + LOG->Trace("Pulse: pa_context_new_with_proplist()..."); + m_PulseCtx = pa_context_new_with_proplist( + pa_threaded_mainloop_get_api(m_PulseMainLoop), + "StepMania", plist); + pa_proplist_free(plist); + if(m_PulseCtx == NULL) + { + return "pa_context_new_with_proplist() failed!"; + } +#else + LOG->Trace("Pulse: pa_context_new()..."); + m_PulseCtx = pa_context_new( + pa_threaded_mainloop_get_api(m_PulseMainLoop), + "Stepmania"); + if(m_PulseCtx == NULL) + { + return "pa_context_new() failed!"; + } +#endif + + pa_context_set_state_callback(m_PulseCtx, StaticCtxStateCb, this); + + LOG->Trace("Pulse: pa_context_connect()..."); + error = pa_context_connect(m_PulseCtx, NULL, + (pa_context_flags_t)0, NULL); + if(error < 0) + { + return ssprintf("pa_contect_connect(): %s", + pa_strerror(pa_context_errno(m_PulseCtx))); + } + + LOG->Trace("Pulse: pa_threaded_mainloop_start()..."); + error = pa_threaded_mainloop_start(m_PulseMainLoop); + if(error < 0) + { + return ssprintf("pa_threaded_mainloop_start() returned %i", + error); + } + + /* Create the decode thread, this will be needed for Mix(), that we + * will use as soon as a stream is ready. */ + StartDecodeThread(); + + /* Wait for the pulseaudio stream to be ready before returning. + * An error may occur, if it appends, m_Error becomes non-NULL. */ + m_Sem.Wait(); + + if(m_Error == NULL) + { + return ""; + } + else + { + return m_Error; + } +} + +void RageSoundDriver_PulseAudio::m_InitStream(void) +{ + int error; + pa_sample_spec ss; + pa_channel_map map; + + /* init sample spec */ + ss.format = PA_SAMPLE_S16LE; + ss.channels = 2; + ss.rate = PREFSMAN->m_iSoundPreferredSampleRate; + if(ss.rate == 0) + { + ss.rate = 44100; + } + + /* init channel map */ + pa_channel_map_init_stereo(&map); + + /* check sample spec */ + if(!pa_sample_spec_valid(&ss)) + { + if(asprintf(&m_Error, "invalid sample spec!") == -1) + { + m_Error = NULL; + } + m_Sem.Post(); + return; + } + + /* log the used sample spec */ + char specstring[PA_SAMPLE_SPEC_SNPRINT_MAX]; + pa_sample_spec_snprint(specstring, sizeof(specstring), &ss); + LOG->Trace("Pulse: using sample spec: %s", specstring); + + /* create the stream */ + LOG->Trace("Pulse: pa_stream_new()..."); + m_PulseStream = pa_stream_new(m_PulseCtx, "Stepmania Audio", &ss, &map); + if(m_PulseStream == NULL) + { + if(asprintf(&m_Error, "pa_stream_new(): %s", + pa_strerror(pa_context_errno(m_PulseCtx))) == -1) + { + m_Error = NULL; + } + m_Sem.Post(); + return; + } + + /* set the write callback, it will be called when the sound server + * needs data */ + pa_stream_set_write_callback(m_PulseStream, StaticStreamWriteCb, this); + /* set the state callback, it will be called the the stream state will + * change */ + pa_stream_set_state_callback(m_PulseStream, StaticStreamStateCb, this); + + /* configure attributes of the stream */ + pa_buffer_attr attr; + memset(&attr, 0x00, sizeof(attr)); + + /* tlength: Target length of the buffer. + * + * "The server tries to assure that at least tlength bytes are always + * available in the per-stream server-side playback buffer. It is + * recommended to set this to (uint32_t) -1, which will initialize + * this to a value that is deemed sensible by the server. However, + * this value will default to something like 2s, i.e. for applications + * that have specific latency requirements this value should be set to + * the maximum latency that the application can deal with." + * + * We don't want the default here, we want a small latency. + * We use pa_usec_to_bytes() to convert a latency to a buffer size. + */ + attr.tlength = pa_usec_to_bytes(20*PA_USEC_PER_MSEC, &ss); + + /* maxlength: Maximum length of the buffer + * + * "Setting this to (uint32_t) -1 will initialize this to the maximum + * value supported by server, which is recommended." + * + * (uint32_t)-1 is NOT working here, setting it to tlength*2, like + * openal-soft-pulseaudio does. + */ + attr.maxlength = attr.tlength*2; + + /* minreq: Minimum request + * + * "The server does not request less than minreq bytes from the client, + * instead waits until the buffer is free enough to request more bytes + * at once. It is recommended to set this to (uint32_t) -1, which will + * initialize this to a value that is deemed sensible by the server." + * + * (uint32_t)-1 is NOT working here, setting it to 0, like + * openal-soft-pulseaudio does. + */ + attr.minreq = 0; + + /* prebuf: Pre-buffering + * + * "The server does not start with playback before at least prebuf + * bytes are available in the buffer. It is recommended to set this + * to (uint32_t) -1, which will initialize this to the same value as + * tlength" + */ + attr.prebuf = (uint32_t)-1; + + /* log the used target buffer length */ + LOG->Trace("Pulse: using target buffer length of %i bytes", + attr.tlength); + + /* connect the stream for playback */ + LOG->Trace("Pulse: pa_stream_connect_playback()..."); + error = pa_stream_connect_playback(m_PulseStream, NULL, &attr, + PA_STREAM_AUTO_TIMING_UPDATE, NULL, NULL); + if(error < 0) + { + if(asprintf(&m_Error, "pa_stream_connect_playback(): %s", + pa_strerror(pa_context_errno(m_PulseCtx))) == -1) + { + m_Error = NULL; + } + m_Sem.Post(); + return; + } + + m_SampleRate = ss.rate; +} + +void RageSoundDriver_PulseAudio::CtxStateCb(pa_context *c) +{ + switch (pa_context_get_state(m_PulseCtx)) + { + case PA_CONTEXT_CONNECTING: + LOG->Trace("Pulse: Context connecting..."); + break; + case PA_CONTEXT_AUTHORIZING: + LOG->Trace("Pulse: Context authorizing..."); + break; + case PA_CONTEXT_SETTING_NAME: + LOG->Trace("Pulse: Context setting name..."); + break; + case PA_CONTEXT_READY: + LOG->Trace("Pulse: Context ready now."); + m_InitStream(); + break; + case PA_CONTEXT_TERMINATED: + case PA_CONTEXT_FAILED: + if(asprintf(&m_Error, "context connection failed: %s", + pa_strerror(pa_context_errno(m_PulseCtx))) + == -1) + { + m_Error = NULL; + } + m_Sem.Post(); + return; + break; + } +} + +void RageSoundDriver_PulseAudio::StreamStateCb(pa_stream *s) +{ + switch(pa_stream_get_state(m_PulseStream)) + { + case PA_STREAM_CREATING: + LOG->Trace("Pulse: Stream creating..."); + break; + case PA_STREAM_READY: + LOG->Trace("Pulse: Stream ready now/"); + m_Sem.Post(); + return; + break; + case PA_STREAM_UNCONNECTED: + case PA_STREAM_TERMINATED: + case PA_STREAM_FAILED: + if(asprintf(&m_Error, "stream connection failed: %s", + pa_strerror(pa_context_errno(m_PulseCtx))) + == -1) + { + } + m_Sem.Post(); + return; + break; + } +} + +int64_t RageSoundDriver_PulseAudio::GetPosition() const +{ + return m_LastPosition; +} + +void RageSoundDriver_PulseAudio::StreamWriteCb(pa_stream *s, size_t length) +{ +#if PA_API_VERSION <= 11 + /* We have to multiply the requested length by 2 on 0.9.10 + * maybe the requested length is given in frames instead of bytes */ + length *= 2; +#endif + size_t nbframes = length / sizeof(int16_t); /* we use 16-bit frames */ + int16_t buf[nbframes]; + int64_t pos1 = m_LastPosition; + int64_t pos2 = pos1 + nbframes/2; /* Mix() position in stereo frames */ + this->Mix( buf, pos2-pos1, pos1, pos2); + if(pa_stream_write(m_PulseStream, buf, length, NULL, 0, PA_SEEK_RELATIVE) < 0) + { + RageException::Throw("Pulse: pa_stream_write()"); + } + m_LastPosition = pos2; +} + +/* Static wrappers, because pulseaudio is a C API, it uses callbacks. + * So we have to write wrappers that will call our objects callbacks. */ + +void RageSoundDriver_PulseAudio::StaticCtxStateCb(pa_context *c, void *user) +{ + RageSoundDriver_PulseAudio *obj = (RageSoundDriver_PulseAudio*)user; + obj->CtxStateCb(c); +} +void RageSoundDriver_PulseAudio::StaticStreamStateCb(pa_stream *s, void *user) +{ + RageSoundDriver_PulseAudio *obj = (RageSoundDriver_PulseAudio*)user; + obj->StreamStateCb(s); +} +void RageSoundDriver_PulseAudio::StaticStreamWriteCb(pa_stream *s, + size_t length, void *user) +{ + RageSoundDriver_PulseAudio *obj = (RageSoundDriver_PulseAudio*)user; + obj->StreamWriteCb(s, length); +} + +/* + * (c) 2009 Damien Thebault + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, provided that the above + * copyright notice(s) and this permission notice appear in all copies of + * the Software and that both the above copyright notice(s) and this + * permission notice appear in supporting documentation. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF + * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS + * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT + * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS + * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ diff --git a/stepmania/src/arch/Sound/RageSoundDriver_PulseAudio.h b/stepmania/src/arch/Sound/RageSoundDriver_PulseAudio.h new file mode 100644 index 0000000000..f4f3df4ce6 --- /dev/null +++ b/stepmania/src/arch/Sound/RageSoundDriver_PulseAudio.h @@ -0,0 +1,68 @@ +#ifndef RAGE_SOUND_PULSEAUDIO_H +#define RAGE_SOUND_PULSEAUDIO_H + +#include "RageSound.h" +#include "RageThreads.h" +#include "RageSoundDriver.h" +#include + +class RageSoundDriver_PulseAudio: public RageSoundDriver +{ +public: + RageSoundDriver_PulseAudio(); + virtual ~RageSoundDriver_PulseAudio(); + + RString Init(); + + inline int64_t GetPosition() const; + inline int GetSampleRate() const { return m_SampleRate; }; + +protected: + int64_t m_LastPosition; + int m_SampleRate; + char *m_Error; + + void m_InitStream(); + RageSemaphore m_Sem; + + pa_threaded_mainloop *m_PulseMainLoop; + pa_context *m_PulseCtx; + pa_stream *m_PulseStream; + +public: + void CtxStateCb(pa_context *c); + void StreamStateCb(pa_stream *s); + void StreamWriteCb(pa_stream *s, size_t length); + + static void StaticCtxStateCb(pa_context *c, void *user); + static void StaticStreamStateCb(pa_stream *s, void *user); + static void StaticStreamWriteCb(pa_stream *s, + size_t length, void *user); +}; + +#endif /* RAGE_SOUND_PULSEAUDIO_H */ + +/* + * (c) 2009 Damien Thebault + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, provided that the above + * copyright notice(s) and this permission notice appear in all copies of + * the Software and that both the above copyright notice(s) and this + * permission notice appear in supporting documentation. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF + * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS + * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT + * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS + * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ diff --git a/stepmania/src/arch/arch_default.h b/stepmania/src/arch/arch_default.h index 3826fe271c..5752dacadc 100644 --- a/stepmania/src/arch/arch_default.h +++ b/stepmania/src/arch/arch_default.h @@ -49,7 +49,7 @@ #define DEFAULT_INPUT_DRIVER_LIST "X11" #endif #define DEFAULT_MOVIE_DRIVER_LIST "Theora,FFMpeg,Null" -#define DEFAULT_SOUND_DRIVER_LIST "ALSA-sw,OSS,Null" +#define DEFAULT_SOUND_DRIVER_LIST "pulse,ALSA-sw,OSS,Null" #else #error Which arch? #endif