/****************************************************************************** * Start the PulseAudio mainloop background thread. * This function returns as soon as the PulseAudio thread is started. * Initialization of PulseAudio is done in the background thread. * In order to learn when initialization is finished, you need to wait until * context_state_cb() is called, and check the PulseAudio context state. *****************************************************************************/ static mbx_error_code init_pulseaudio(_mbx_out out) { if ((out->pa_ml = pa_threaded_mainloop_new()) == NULL ) { mbx_log_error(MBX_LOG_AUDIO_OUTPUT, "Unable to allocate a PulseAudio threaded main " "loop object."); return MBX_PULSEAUDIO_ERROR; } out->pa_props = pa_proplist_new(); /* TODO: Set properties in proplist */ pa_mainloop_api *pa_mlapi = pa_threaded_mainloop_get_api(out->pa_ml); out->pa_ctx = pa_context_new_with_proplist(pa_mlapi, NULL, out->pa_props); if ( out->pa_ctx == NULL ) { mbx_log_error(MBX_LOG_AUDIO_OUTPUT, "Unable to instantiate a pulseaudio connection " "context."); return MBX_PULSEAUDIO_ERROR; } pa_context_set_state_callback(out->pa_ctx, context_state_cb, out); if ( pa_context_connect(out->pa_ctx, NULL, 0, NULL) < 0 ) { mbx_log_error(MBX_LOG_AUDIO_OUTPUT, "Unable to connect to the pulseaudio server: %s", pa_msg(out)); return MBX_PULSEAUDIO_ERROR; } if ( pa_threaded_mainloop_start(out->pa_ml) ) { mbx_log_error(MBX_LOG_AUDIO_OUTPUT, "Failed to start pulseaudio mainloop " "in background thread."); return MBX_PULSEAUDIO_ERROR; } return MBX_SUCCESS; }
/* This callback is called when the stream drain is complete, i.e. the stream * can be disconnected and we can start draining the pulseaudio context. */ static void stream_drain_complete_cb(pa_stream*s, int success, void *userdata){ _mbx_out out = (_mbx_out) userdata; assert(out != NULL && out->stream == s); mbx_log_debug(MBX_LOG_AUDIO_OUTPUT, "Draining pulseaudio stream has completed."); if (!success) { mbx_log_error(MBX_LOG_AUDIO_OUTPUT, "Failed to complete pulseaudio stream drain: %s", pa_msg(out)); } pa_stream_disconnect(s); pa_stream_unref(s); pa_operation *o; o = pa_context_drain(out->pa_ctx, context_drain_complete_cb, out); if ( o == NULL ) { mbx_log_error(MBX_LOG_AUDIO_OUTPUT, "Failed to start pulseaudio context drain: %s", pa_msg(out)); /* In case of error, we do our best and continue manually */ context_drain_complete_cb(out->pa_ctx, out); } pa_operation_unref(o); }
/* Shutdown the application. This is called from the pulseaudio mainloop * thread in stream_write_cb() */ static void do_shutdown(_mbx_out out) { mbx_log_debug(MBX_LOG_AUDIO_OUTPUT, "Shutting down."); unset_all_callbacks(out); /* stream_drain_complete_cb will be called when drain is done */ pa_operation *o = pa_stream_drain(out->stream,stream_drain_complete_cb,out); if ( o == NULL ) { mbx_log_error(MBX_LOG_AUDIO_OUTPUT, "Failed to start pulseaudio stream drain: %s", pa_msg(out)); /* In case of error, we do our best and continue manually */ stream_drain_complete_cb(out->stream, 0, out); } pa_operation_unref(o); }
/****************************************************************************** * This is called when the PulseAudio context state becomes PA_CONTEXT_READY. * We initialize a new PulseAudio stream, configure it with our callbacks, * and connect the new stream to the PulseAudio server. *****************************************************************************/ static void context_ready(_mbx_out out) { out->stream = pa_stream_new(out->pa_ctx, "playback", &out->sample_spec, NULL); if ( out->stream == NULL ) { mbx_log_error(MBX_LOG_AUDIO_OUTPUT, "Unable to create pulseaudio stream: %s", pa_msg(out)); out->state = _MBX_OUT_PULSEAUDIO_ERROR; return; } /* will be called when pulseaudio requests audio data */ pa_stream_set_write_callback(out->stream, stream_write_cb, out); /* will be called when an buffer underflow occurs */ pa_stream_set_underflow_callback(out->stream, stream_underflow_cb, out); pa_buffer_attr bufattr = make_bufattr(); int r = pa_stream_connect_playback(out->stream, out->dev_name, &bufattr, PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_ADJUST_LATENCY | PA_STREAM_AUTO_TIMING_UPDATE, NULL, NULL); if (r < 0) { mbx_log_error(MBX_LOG_AUDIO_OUTPUT, "Failed to connect stream to pulseaudio sink: %s", pa_msg(out)); out->state = _MBX_OUT_PULSEAUDIO_ERROR; return; } out->state = _MBX_OUT_READY; }
/****************************************************************************** * stream_write_cb() * This will be called by PulseAudio when we need to provide audio data * for playback. *****************************************************************************/ static void stream_write_cb(pa_stream *s, size_t n_requested_bytes, void *userdata) { _mbx_out out = (_mbx_out ) userdata; sample_t *data_to_write = NULL; size_t n_bytes_written = 0; assert ( out != NULL && out->stream == s); if ( out->trigger_shutdown ) { do_shutdown(out); return; } while ( n_bytes_written < n_requested_bytes ) { int r; size_t n_bytes_to_write = n_requested_bytes - n_bytes_written; r = pa_stream_begin_write(s, (void**)&data_to_write, &n_bytes_to_write); assert(n_bytes_to_write % (2*sizeof(sample_t)) == 0); if ( r < 0 ) { mbx_log_error(MBX_LOG_AUDIO_OUTPUT, "Prepare writing data to the pulseaudio server" " failed: %s", pa_msg(out)); pa_stream_cancel_write(s); return; } if ( n_bytes_to_write > 0 ) { int i; assert ( n_bytes_to_write / sizeof(sample_t) < 44100L*2*8 ); sample_t left[44100L*2*8]; sample_t right[44100L*2*8]; out->cb(left, right, n_bytes_to_write / (2*sizeof(sample_t)), out->output_cb_userdata); for ( i=0; i<n_bytes_to_write/(2*sizeof(sample_t)); i++ ) { data_to_write[2*i] = left[i]; data_to_write[2*i+1] = right[i]; } pa_stream_write(s, data_to_write, n_bytes_to_write, NULL, 0, PA_SEEK_RELATIVE); n_bytes_written += n_bytes_to_write; } } }
/* The implementation of create_output_device_name_list() is based on the * example code in * http://www.ypass.net/blog/2009/10/ * pulseaudio-an-async-example-to-get-device-lists/ */ mbx_error_code mbx_create_output_device_name_list(char ***dev_names, size_t *n_devs) { pa_mainloop *pa_ml = pa_mainloop_new(); pa_mainloop_api *pa_mlapi = pa_mainloop_get_api(pa_ml); pa_context *pa_ctx = pa_context_new(pa_mlapi, "music box (listing output " "devices)"); pa_operation *pa_op; pa_context_state_t pa_context_state = PA_CONTEXT_UNCONNECTED; int do_iterate = 1; int error = 0; pa_context_connect(pa_ctx, NULL, 0, NULL); /* The state callback will update the state when we are connected to the * PulseAudio server, or if an error occurs. */ pa_context_set_state_callback(pa_ctx, pa_context_state_cb, &pa_context_state); while ( do_iterate ) { switch ( pa_context_state ) { case PA_CONTEXT_UNCONNECTED: case PA_CONTEXT_CONNECTING: case PA_CONTEXT_AUTHORIZING: case PA_CONTEXT_SETTING_NAME: pa_mainloop_iterate(pa_ml, 1, NULL); // we must wait. break; case PA_CONTEXT_READY: do_iterate = 0; break; case PA_CONTEXT_FAILED: mbx_log_error(MBX_LOG_AUDIO_OUTPUT, "Connection to PulseAudio server failed: " "%s", pa_strerror(pa_context_errno(pa_ctx))); error = 1; break; case PA_CONTEXT_TERMINATED: mbx_log_error(MBX_LOG_AUDIO_OUTPUT, "Connection to PulseAudio server " "terminated unexpectedly."); error = 1; break; default: mbx_log_error(MBX_LOG_AUDIO_OUTPUT, "The PulseAudio context has an unexpected " "state: %d", pa_context_state); error = 1; break; } if ( error ) { do_iterate = 0; } } if ( ! error ) { struct list_of_strings result = { NULL, 0, 0 }; pa_op = pa_context_get_sink_info_list(pa_ctx, pa_sinklist_cb, &result); while ( pa_operation_get_state(pa_op) == PA_OPERATION_RUNNING ) { pa_mainloop_iterate(pa_ml, 1, NULL); // wait. } pa_operation_unref(pa_op); *dev_names = result.strings; *n_devs = result.n_strings; } pa_context_disconnect(pa_ctx); pa_context_unref(pa_ctx); pa_mainloop_free(pa_ml); return error ? MBX_PULSEAUDIO_ERROR : MBX_SUCCESS; }