From: Mike Brady Date: Wed, 17 May 2017 07:49:40 +0000 (+0100) Subject: Initial very rough full-fat pulseaudio backend X-Git-Tag: 3.1d16~10 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b3ae05ea60787a6a25cc17ac5bb92baaa61b2940;p=thirdparty%2Fshairport-sync.git Initial very rough full-fat pulseaudio backend --- diff --git a/Makefile.am b/Makefile.am index 1727ba85..01cb2c15 100644 --- a/Makefile.am +++ b/Makefile.am @@ -59,6 +59,10 @@ if USE_PULSE shairport_sync_SOURCES += audio_pulse.c endif +if USE_PA +shairport_sync_SOURCES += audio_pa.c +endif + if USE_CONVOLUTION shairport_sync_SOURCES += FFTConvolver/AudioFFT.cpp FFTConvolver/FFTConvolver.cpp FFTConvolver/Utilities.cpp FFTConvolver/convolver.cpp AM_CXXFLAGS = -std=c++11 -O2 diff --git a/audio.c b/audio.c index 915cb802..c4f6e6a7 100644 --- a/audio.c +++ b/audio.c @@ -41,6 +41,9 @@ extern audio_output audio_soundio; #ifdef CONFIG_PULSE extern audio_output audio_pulse; #endif +#ifdef CONFIG_PA +extern audio_output audio_pa; +#endif #ifdef CONFIG_ALSA extern audio_output audio_alsa; #endif @@ -64,6 +67,9 @@ static audio_output *outputs[] = { #ifdef CONFIG_PULSE &audio_pulse, #endif +#ifdef CONFIG_PA + &audio_pa, +#endif #ifdef CONFIG_AO &audio_ao, #endif diff --git a/audio_alsa.c b/audio_alsa.c index 4032ad90..30183505 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -843,6 +843,12 @@ static void play(short buf[], int samples) { pthread_mutex_lock(&alsa_mutex); snd_pcm_sframes_t current_delay = 0; int err, ignore; + if (snd_pcm_state(alsa_handle) == SND_PCM_STATE_XRUN) { + if ((err = snd_pcm_prepare(alsa_handle))) { + ignore = snd_pcm_recover(alsa_handle, err, 1); + debug(1, "Error preparing after underrun: \"%s\".", snd_strerror(err)); + } + } if ((snd_pcm_state(alsa_handle) == SND_PCM_STATE_PREPARED) || (snd_pcm_state(alsa_handle) == SND_PCM_STATE_RUNNING)) { if (buf == NULL) diff --git a/audio_pa.c b/audio_pa.c new file mode 100644 index 00000000..5ba52afc --- /dev/null +++ b/audio_pa.c @@ -0,0 +1,334 @@ +// Based on +// http://stackoverflow.com/questions/29977651/how-can-the-pulseaudio-asynchronous-library-be-used-to-play-raw-pcm-data + +#include "audio.h" +#include "common.h" +#include +#include +#include +#include +#include +#include +#include + +// note -- these are hacked and hardwired into this code. +#define FORMAT PA_SAMPLE_S16NE +#define RATE 44100 + +#define buffer_allocation 88200 * 2 * 2 + +static pthread_mutex_t buffer_mutex = PTHREAD_MUTEX_INITIALIZER; + +static struct { + char *server; + char *sink; + char *service_name; +} pulse_options = {.server = NULL, .sink = NULL, .service_name = NULL}; + +pa_threaded_mainloop *mainloop; +pa_mainloop_api *mainloop_api; +pa_context *context; +pa_stream *stream; +char *audio_lmb, *audio_umb, *audio_toq, *audio_eoq; +size_t audio_size = buffer_allocation; +size_t audio_occupancy; + +void context_state_cb(pa_context *context, void *mainloop); +void stream_state_cb(pa_stream *s, void *mainloop); +void stream_success_cb(pa_stream *stream, int success, void *userdata); +void stream_write_cb(pa_stream *stream, size_t requested_bytes, void *userdata); + + +static int init(int argc, char **argv) { + + config.audio_backend_buffer_desired_length = 0.25; + config.audio_backend_latency_offset = 0; + + // allocate space for the audio buffer + audio_lmb = malloc(audio_size); + if (audio_lmb == NULL) + die("Can't allocate %d bytes for pulseaudio buffer.", audio_size); + audio_toq = audio_eoq = audio_lmb; + audio_umb = audio_lmb + audio_size; + audio_occupancy = 0; + + return 0; +} + +static void deinit(void) {} + +static void start(int sample_rate, int sample_format) { + + + uint32_t buffer_size_in_bytes = (uint32_t)2 * 2 * RATE * 0.1; // hard wired in here + debug(1, "pa_buffer size is %u bytes.", buffer_size_in_bytes); + + // Get a mainloop and its context + mainloop = pa_threaded_mainloop_new(); + assert(mainloop); + mainloop_api = pa_threaded_mainloop_get_api(mainloop); + context = pa_context_new(mainloop_api, "pcm-playback"); + assert(context); + + // Set a callback so we can wait for the context to be ready + pa_context_set_state_callback(context, &context_state_cb, mainloop); + + // Lock the mainloop so that it does not run and crash before the context is ready + pa_threaded_mainloop_lock(mainloop); + + // Start the mainloop + assert(pa_threaded_mainloop_start(mainloop) == 0); + assert(pa_context_connect(context, NULL, PA_CONTEXT_NOAUTOSPAWN, NULL) == 0); + + // Wait for the context to be ready + for (;;) { + pa_context_state_t context_state = pa_context_get_state(context); + assert(PA_CONTEXT_IS_GOOD(context_state)); + if (context_state == PA_CONTEXT_READY) + break; + pa_threaded_mainloop_wait(mainloop); + } + + // Create a playback stream + pa_sample_spec sample_specifications; + sample_specifications.format = FORMAT; + sample_specifications.rate = RATE; + sample_specifications.channels = 2; + + pa_channel_map map; + pa_channel_map_init_stereo(&map); + + stream = pa_stream_new(context, "Playback", &sample_specifications, &map); + pa_stream_set_state_callback(stream, stream_state_cb, mainloop); + pa_stream_set_write_callback(stream, stream_write_cb, mainloop); + // pa_stream_set_latency_update_callback(stream, stream_latency_cb, mainloop); + + // recommended settings, i.e. server uses sensible values + pa_buffer_attr buffer_attr; + buffer_attr.maxlength = (uint32_t) -1; + buffer_attr.tlength = buffer_size_in_bytes; + buffer_attr.prebuf = (uint32_t) 0; + buffer_attr.minreq = (uint32_t)-1; + + // Settings copied as per the chromium browser source + pa_stream_flags_t stream_flags; + stream_flags = PA_STREAM_START_CORKED | PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_NOT_MONOTONIC | + // PA_STREAM_AUTO_TIMING_UPDATE; + PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY; + + // Connect stream to the default audio output sink + assert(pa_stream_connect_playback(stream, NULL, &buffer_attr, stream_flags, NULL, NULL) == 0); + + // Wait for the stream to be ready + for (;;) { + pa_stream_state_t stream_state = pa_stream_get_state(stream); + assert(PA_STREAM_IS_GOOD(stream_state)); + if (stream_state == PA_STREAM_READY) + break; + pa_threaded_mainloop_wait(mainloop); + } + + pa_threaded_mainloop_unlock(mainloop); +} + +static void play(short buf[], int samples) { + // debug(1,"pa_play of %d samples.",samples); + char *bbuf = (char *)buf; + // copy the samples into the queue + size_t bytes_to_transfer = samples * 2 * 2; + size_t space_to_end_of_buffer = audio_umb - audio_eoq; + if (space_to_end_of_buffer >= bytes_to_transfer) { + memcpy(audio_eoq, bbuf, bytes_to_transfer); + audio_occupancy += bytes_to_transfer; + pthread_mutex_lock(&buffer_mutex); + audio_eoq += bytes_to_transfer; + pthread_mutex_unlock(&buffer_mutex); + } else { + memcpy(audio_eoq, bbuf, space_to_end_of_buffer); + bbuf += space_to_end_of_buffer; + memcpy(audio_lmb, bbuf, bytes_to_transfer - space_to_end_of_buffer); + pthread_mutex_lock(&buffer_mutex); + audio_occupancy += bytes_to_transfer; + pthread_mutex_unlock(&buffer_mutex); + audio_eoq = audio_lmb + bytes_to_transfer - space_to_end_of_buffer; + } + if ((audio_occupancy >= 11025 * 2 * 2) && (pa_stream_is_corked(stream))) { + // debug(1,"Uncorked"); + pa_stream_cork(stream, 0, stream_success_cb, mainloop); + } +} + +int pa_delay(long *the_delay) { + long result = 0; + int reply = -ENODEV; + pa_usec_t latency; + int negative; + int gl = pa_stream_get_latency(stream, &latency, &negative); + if (gl == PA_ERR_NODATA) { + debug(1, "No latency data yet."); + reply = -ENODEV; + } else if (gl != 0) { + // debug(1,"Error %d getting latency.",gl); + reply = -EIO; + } else { + result = (audio_occupancy / (2 * 2)) + (latency * 44100) / 1000000; + reply = 0; + } + *the_delay = result; + return reply; +} + +void flush(void) { + // Cork the stream so it will stop playing + pa_stream_cork(stream, 1, stream_success_cb, mainloop); + pa_stream_flush(stream, stream_success_cb, NULL); + audio_toq = audio_eoq = audio_lmb; + audio_umb = audio_lmb + audio_size; + audio_occupancy = 0; +} + +static void stop(void) { + // Cork the stream so it will stop playing + pa_stream_cork(stream, 1, stream_success_cb, mainloop); + pa_stream_flush(stream, stream_success_cb, NULL); + audio_toq = audio_eoq = audio_lmb; + audio_umb = audio_lmb + audio_size; + audio_occupancy = 0; + + // debug(1, "pa deinit start"); + pa_stream_disconnect(stream); + pa_threaded_mainloop_stop(mainloop); + pa_threaded_mainloop_free(mainloop); + // debug(1, "pa deinit done"); +} + +static void help(void) { + printf(" no settings.\n"); +} + +audio_output audio_pa = {.name = "pa", + .help = &help, + .init = &init, + .deinit = &deinit, + .start = &start, + .stop = &stop, + .flush = &flush, + .delay = &pa_delay, + .play = &play, + .volume = NULL, + .parameters = NULL, + .mute = NULL}; + +void context_state_cb(pa_context *context, void *mainloop) { + pa_threaded_mainloop_signal(mainloop, 0); +} + +void stream_state_cb(pa_stream *s, void *mainloop) { pa_threaded_mainloop_signal(mainloop, 0); } + +void stream_write_cb(pa_stream *stream, size_t requested_bytes, void *userdata) { + + // play with timing information + const struct pa_timing_info *ti = pa_stream_get_timing_info(stream); + + if ((ti == NULL) || (ti->write_index_corrupt)) { + debug(1, "Timing info invalid"); + } else { + struct timeval time_now; + + pa_gettimeofday(&time_now); + + uint64_t time_now_fp = ((uint64_t)time_now.tv_sec << 32) + + ((uint64_t)time_now.tv_usec << 32) / 1000000; // types okay + uint64_t time_of_ti_fp = ((uint64_t)(ti->timestamp.tv_sec) << 32) + + ((uint64_t)(ti->timestamp.tv_usec) << 32) / 1000000; // types okay + + if (time_now_fp >= time_of_ti_fp) { + uint64_t estimate_age = ((time_now_fp - time_of_ti_fp) * 1000000) >> 32; + uint64_t bytes_in_buffer = ti->write_index - ti->read_index; + pa_usec_t microseconds_to_write_buffer = (bytes_in_buffer * 1000000) / (44100 * 2 * 2); + pa_usec_t ea = (pa_usec_t)estimate_age; + pa_usec_t pa_latency = ti->sink_usec + ti->transport_usec + microseconds_to_write_buffer; + pa_usec_t estimated_latency = pa_latency - estimate_age; + // debug(1,"Estimated latency is %d microseconds.",estimated_latency); + + } else { + debug(1, "Time now is earlier than time of timing information"); + } + } + + int bytes_to_transfer = requested_bytes; + int bytes_transferred = 0; + uint8_t *buffer = NULL; + + while ((bytes_to_transfer > 0) && (audio_occupancy > 0)) { + size_t bytes_we_can_transfer = bytes_to_transfer; + if (audio_occupancy < bytes_we_can_transfer) { + debug(2, "Underflow? We have %d bytes but we are asked for %d bytes", audio_occupancy, + bytes_we_can_transfer); + bytes_we_can_transfer = audio_occupancy; + } + + // bytes we can transfer will never be greater than the bytes available + + pa_stream_begin_write(stream, (void **)&buffer, &bytes_we_can_transfer); + if (bytes_we_can_transfer <= (audio_umb - audio_toq)) { + // the bytes are all in a row in the audo buffer + memcpy(buffer, audio_toq, bytes_we_can_transfer); + audio_toq += bytes_we_can_transfer; + // lock + pthread_mutex_lock(&buffer_mutex); + audio_occupancy -= bytes_we_can_transfer; + pthread_mutex_unlock(&buffer_mutex); + // unlock + pa_stream_write(stream, buffer, bytes_we_can_transfer, NULL, 0LL, PA_SEEK_RELATIVE); + bytes_transferred += bytes_we_can_transfer; + } else { + // the bytes are in two places in the audio buffer + size_t first_portion_to_write = audio_umb - audio_toq; + if (first_portion_to_write != 0) + memcpy(buffer, audio_toq, first_portion_to_write); + char *new_buffer = buffer + first_portion_to_write; + memcpy(new_buffer, audio_lmb, bytes_we_can_transfer - first_portion_to_write); + pa_stream_write(stream, buffer, bytes_we_can_transfer, NULL, 0LL, PA_SEEK_RELATIVE); + bytes_transferred += bytes_we_can_transfer; + audio_toq = audio_lmb + bytes_we_can_transfer - first_portion_to_write; + // lock + pthread_mutex_lock(&buffer_mutex); + audio_occupancy -= bytes_we_can_transfer; + pthread_mutex_unlock(&buffer_mutex); + // unlock + } + bytes_to_transfer -= bytes_we_can_transfer; + // debug(1,"audio_toq is %llx",audio_toq); + } + + // debug(1,"<< 0) { + uint8_t *buffer = NULL; + size_t bytes_to_fill = 44100; + size_t i; + + if (bytes_to_fill > bytes_remaining) + bytes_to_fill = bytes_remaining; + + pa_stream_begin_write(stream, (void **)&buffer, &bytes_to_fill); + + for (i = 0; i < bytes_to_fill; i += 2) { + buffer[i] = (i % 100) * 40 / 100 + 44; + buffer[i + 1] = (i % 100) * 40 / 100 + 44; + } + + pa_stream_write(stream, buffer, bytes_to_fill, NULL, 0LL, PA_SEEK_RELATIVE); + + bytes_remaining -= bytes_to_fill; + } +} + +void stream_success_cb(pa_stream *stream, int success, void *userdata) { return; } + diff --git a/configure.ac b/configure.ac index ef9013f7..5b4c64c3 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # Process this file with autoconf to produce a configure script. AC_PREREQ([2.50]) -AC_INIT([shairport-sync], [3.1d15], [mikebrady@eircom.net]) +AC_INIT([shairport-sync], [3.1d16], [mikebrady@eircom.net]) AM_INIT_AUTOMAKE AC_CONFIG_SRCDIR([shairport.c]) AC_CONFIG_HEADERS([config.h]) @@ -233,6 +233,21 @@ AC_ARG_WITH(soundio, [ --with-soundio = choose soundio API support.], [ AC_CHECK_LIB([soundio], [soundio_create], , AC_MSG_ERROR(soundio support requires the soundio library!))], ) AM_CONDITIONAL([USE_SOUNDIO], [test "x$HAS_SOUNDIO" = "x1"]) +# Look for new pulseaudio flag +AC_ARG_WITH(pa, [ --with-pa = choose PulseAudio support.], [ + AC_MSG_RESULT(>>Including a PulseAudio back end.) + HAS_PA=1 + AC_DEFINE([CONFIG_PA], 1, [Needed by the compiler.]) + if test "x${with_pkg_config}" = xyes ; then + PKG_CHECK_MODULES( + [PULSEAUDIO], [libpulse >= 0.9.2], + [LIBS="${PULSEAUDIO_LIBS} ${LIBS}"],[AC_MSG_ERROR(PulseAudio support requires the libpulse-dev library!)]) + else + AC_CHECK_LIB([pulse-simple], [pa_simple_new], , AC_MSG_ERROR(PulseAudio support requires the libpulse library!)) + AC_CHECK_LIB([pulse], [pa_stream_peek], , AC_MSG_ERROR(PulseAudio support requires the libpulse-dev library.)) + fi ]) +AM_CONDITIONAL([USE_PA], [test "x$HAS_PA" = "x1"]) + # Look for pulseaudio flag AC_ARG_WITH(pulseaudio, [ --with-pulseaudio = choose PulseAudio API support. N.B. no synchronisation -- so underflow or overflow is inevitable!], [ AC_MSG_RESULT(>>Including a PulseAudio back end. N.B. no synchronisation -- so underflow or overflow is inevitable!) diff --git a/player.c b/player.c index 03d8e1a2..096a2ca6 100644 --- a/player.c +++ b/player.c @@ -863,8 +863,12 @@ static abuf_t *buffer_get_frame(rtsp_conn_info* conn) { conn->first_packet_time_to_play = reference_timestamp_time - delta_fp_sec; } - int64_t max_dac_delay = config.output_rate / 10; - int64_t filler_size = max_dac_delay; // 0.1 second -- the maximum we'll add to the DAC + int64_t initial_filler_size = config.output_rate / 40; // to prevent underrun, start it with this + + int64_t filler_size = 352; + if (have_sent_prefiller_silence==0) + filler_size = initial_filler_size; + if (local_time_now >= conn->first_packet_time_to_play) { // we've gone past the time... @@ -887,11 +891,14 @@ static abuf_t *buffer_get_frame(rtsp_conn_info* conn) { debug(1, "Error %d getting dac_delay in buffer_get_frame.", resp); dac_delay = 0; } - } else + } else { dac_delay = 0; + } + //debug(1,"DAC Delay: %d",dac_delay); int64_t gross_frame_gap = ((conn->first_packet_time_to_play - local_time_now) * config.output_rate) >> 32; int64_t exact_frame_gap = gross_frame_gap - dac_delay; + // debug(1,"Gross gap %lld, exact frame gap %lld, dac delay %lld.",gross_frame_gap, exact_frame_gap,dac_delay); if (exact_frame_gap < 0) { // we've gone past the time... // debug(1,"Run a bit past the exact start time by %lld frames, with time now of @@ -904,42 +911,49 @@ static abuf_t *buffer_get_frame(rtsp_conn_info* conn) { conn->first_packet_timestamp = 0; conn->first_packet_time_to_play = 0; } else { - int64_t fs = filler_size; - if (fs > (max_dac_delay - dac_delay)) - fs = max_dac_delay - dac_delay; - if (fs < 0) { - debug(2, "frame size (fs) < 0 with max_dac_delay of %lld and dac_delay of %ld", - max_dac_delay, dac_delay); - fs = 0; + if (filler_size < 0) { + debug(2, "frame size (filler_size) < 0 with dac_delay of %ld", dac_delay); + filler_size = 0; } - if ((exact_frame_gap <= fs) || (exact_frame_gap <= conn->max_frames_per_packet * 2)) { - fs = exact_frame_gap; + if ((exact_frame_gap <= filler_size) || (exact_frame_gap <= conn->max_frames_per_packet * 2)) { + filler_size = exact_frame_gap; + // debug(1,"Filler size changed to: %d.",fs); // debug(1,"Exact frame gap is %llu; play %d frames of silence. Dac_delay is %d, // with %d packets, ab_read is %04x, ab_write is // %04x.",exact_frame_gap,fs,dac_delay,seq_diff(ab_read, // ab_write),ab_read,ab_write); conn->ab_buffering = 0; } - signed short *silence; - // if (fs==0) - // debug(2,"Zero length silence buffer needed with gross_frame_gap of %lld and - // dac_delay of %lld.",gross_frame_gap,dac_delay); - // the fs (number of frames of silence to play) can be zero in the DAC doesn't start - // ouotputting frames for a while -- it could get loaded up but not start responding - // for many milliseconds. - if (fs != 0) { - silence = malloc(conn->output_bytes_per_frame * fs); - if (silence == NULL) - debug(1, "Failed to allocate %d byte silence buffer.", fs); - else { - memset(silence, 0, conn->output_bytes_per_frame * fs); - // debug(1,"Frames to start: %llu, DAC delay %ld, buffer: %d packets.",exact_frame_gap,dac_delay,seq_diff(conn->ab_read, conn->ab_write, conn->ab_read)); - config.output->play(silence, fs); - free(silence); - have_sent_prefiller_silence = 1; + // it doesn't really make much sense to send a prefiller if the back end does not have a delay procedure + // because you can't monitor its effect, so you end up doing it blindly. +// if (config.output->delay) { +// if (0) { + signed short *silence; + // if (fs==0) + // debug(2,"Zero length silence buffer needed with gross_frame_gap of %lld and + // dac_delay of %lld.",gross_frame_gap,dac_delay); + // the fs (number of frames of silence to play) can be zero in the DAC doesn't start + // ouotputting frames for a while -- it could get loaded up but not start responding + // for many milliseconds. + if (filler_size != 0) { + silence = malloc(conn->output_bytes_per_frame * filler_size); + if (silence == NULL) + debug(1, "Failed to allocate %d byte silence buffer.", filler_size); + else { + memset(silence, 0, conn->output_bytes_per_frame * filler_size); + // debug(1,"Frames to start: %llu, DAC delay %ld, buffer: %d packets.",exact_frame_gap,dac_delay,seq_diff(conn->ab_read, conn->ab_write, conn->ab_read)); + + if (config.output->delay) { +// if (0) { + //debug(1, "Sending %d frames of silence",filler_size); + config.output->play(silence, filler_size); + } + free(silence); + have_sent_prefiller_silence = 1; + } } } - } +// } } } if (conn->ab_buffering == 0) { @@ -948,6 +962,7 @@ static abuf_t *buffer_get_frame(rtsp_conn_info* conn) { get_reference_timestamp_stuff(&conn->play_segment_reference_frame, &reference_timestamp_time, &conn->play_segment_reference_frame_remote_time, conn); conn->play_segment_reference_frame *= conn->output_sample_ratio; + // debug(1,"Finished waiting"); #ifdef CONFIG_METADATA send_ssnc_metadata('prsm', NULL, 0, 0); // "resume", but don't wait if the queue is locked diff --git a/shairport.c b/shairport.c index 4320f659..8865ebc0 100644 --- a/shairport.c +++ b/shairport.c @@ -145,6 +145,9 @@ char *get_version_string() { #ifdef CONFIG_AO strcat(version_string, "-ao"); #endif +#ifdef CONFIG_PA + strcat(version_string, "-pa"); +#endif #ifdef CONFIG_PULSE strcat(version_string, "-pulse"); #endif