#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; } /* * XXX: Something here is slow and causes arrows to stutter in gameplay. * This needs to be looked into (and for some reason the ALSA driver is * useless on my laptop). - Colby */ 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. */