From 9b98c3b2296f07b99b94aceb2c19131f32bde476 Mon Sep 17 00:00:00 2001 From: Henrik Andersson Date: Tue, 26 Sep 2017 15:07:14 +0200 Subject: [PATCH] Add support for pulseaudio backend This work is done by Nikita Krupenko which sent a patch to rdesktop in 2010 for version 1.6.0. I have now ported it over to trunk and it seems to work as expected. --- configure.ac | 23 +- rdpsnd.c | 8 +- rdpsnd.h | 1 + rdpsnd_pulse.c | 1382 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1411 insertions(+), 3 deletions(-) create mode 100644 rdpsnd_pulse.c diff --git a/configure.ac b/configure.ac index 63aee02..375faa8 100644 --- a/configure.ac +++ b/configure.ac @@ -256,7 +256,7 @@ AC_DEFINE_UNQUOTED(EGD_SOCKET, "$EGD_SOCKET") sound="yes" AC_ARG_WITH(sound, - [ --with-sound select sound system ("oss", "sgi", "sun", "alsa" or "libao") ], + [ --with-sound select sound system ("oss", "sgi", "sun", "alsa", "pulse" or "libao") ], [ sound="$withval" ]) @@ -272,6 +272,7 @@ AC_ARG_ENABLE(static-libsamplerate, if test -n "$PKG_CONFIG"; then PKG_CHECK_MODULES(LIBAO, ao, [HAVE_LIBAO=1], [HAVE_LIBAO=0]) + PKG_CHECK_MODULES(PULSE, libpulse, [HAVE_PULSE=1], [HAVE_PULSE=0]) PKG_CHECK_MODULES(ALSA, alsa, [HAVE_ALSA=1], [HAVE_ALSA=0]) PKG_CHECK_MODULES(LIBSAMPLERATE, samplerate, [HAVE_LIBSAMPLERATE=1], [HAVE_LIBSAMPLERATE=0]) if test x"$HAVE_LIBSAMPLERATE" = "x1"; then @@ -316,6 +317,13 @@ case $sound in AC_DEFINE(RDPSND_ALSA) fi + if test x"$HAVE_PULSE" = "x1"; then + SOUNDOBJ="$SOUNDOBJ rdpsnd_pulse.o" + CFLAGS="$CFLAGS $PULSE_CFLAGS" + LIBS="$LIBS $PULSE_LIBS" + AC_DEFINE(RDPSND_PULSE) + fi + if test x"$HAVE_LIBAO" = "x1"; then SOUNDOBJ="$SOUNDOBJ rdpsnd_libao.o" CFLAGS="$CFLAGS $LIBAO_CFLAGS" @@ -364,6 +372,17 @@ case $sound in fi ;; + pulse) + if test x"$HAVE_PULSE" = "x1"; then + SOUNDOBJ="$SOUNDOBJ rdpsnd_pulse.o" + CFLAGS="$CFLAGS $PULSE_CFLAGS" + LIBS="$LIBS $PULSE_LIBS" + AC_DEFINE(RDPSND_PULSE) + else + AC_MSG_ERROR([Selected sound system is not available.]) + fi + ;; + libao) if test x"$HAVE_LIBAO" = "x1"; then SOUNDOBJ="$SOUNDOBJ rdpsnd_libao.o" @@ -380,7 +399,7 @@ case $sound in *) AC_MSG_WARN([sound support disabled]) - AC_MSG_WARN([Currently supported systems are Open Sound System (oss), SGI AL (sgi), Sun/BSD (sun), ALSA (alsa) and libao]) + AC_MSG_WARN([Currently supported systems are Open Sound System (oss), SGI AL (sgi), Sun/BSD (sun), ALSA (alsa), PulseAudio (pulse) and libao]) ;; esac diff --git a/rdpsnd.c b/rdpsnd.c index 2f1ee29..4ef1667 100644 --- a/rdpsnd.c +++ b/rdpsnd.c @@ -3,6 +3,7 @@ Sound Channel Process Functions Copyright 2006-2010 Pierre Ossman for Cendio AB Copyright 2009-2011 Peter Astrand for Cendio AB + Copyright 2017 Henrik Andersson for Cendio AB Copyright (C) Matthew Chapman 2003-2008 Copyright (C) GuoJunBo 2003 @@ -119,7 +120,7 @@ rdpsnd_auto_select(void) current_driver->name); if (current_driver->wave_out_open()) { - logger(Sound, Debug, "rdpsnd_auto_select(), using driver '%s'", + logger(Sound, Verbose, "rdpsnd_auto_select(), using driver '%s'", current_driver->name); current_driver->wave_out_close(); return True; @@ -451,6 +452,11 @@ rdpsnd_register_drivers(char *options) /* The order of registrations define the probe-order when opening the device for the first time */ reg = &drivers; +#if defined(RDPSND_PULSE) + *reg = pulse_register(options); + assert(*reg); + reg = &((*reg)->next); +#endif #if defined(RDPSND_ALSA) *reg = alsa_register(options); assert(*reg); diff --git a/rdpsnd.h b/rdpsnd.h index 1723061..81e87c4 100644 --- a/rdpsnd.h +++ b/rdpsnd.h @@ -52,6 +52,7 @@ struct audio_driver }; /* Driver register functions */ +struct audio_driver *pulse_register(char *options); struct audio_driver *alsa_register(char *options); struct audio_driver *libao_register(char *options); struct audio_driver *oss_register(char *options); diff --git a/rdpsnd_pulse.c b/rdpsnd_pulse.c new file mode 100644 index 0000000..403b65c --- /dev/null +++ b/rdpsnd_pulse.c @@ -0,0 +1,1382 @@ +/* -*- c-basic-offset: 8 -*- + rdesktop: A Remote Desktop Protocol client. + Sound Channel Process Functions - PulseAudio + Copyright (C) Krupen'ko Nikita 2010 + Copyright (C) Henrik Andersson 2017 + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +*/ + +#include "rdesktop.h" +#include "rdpsnd.h" +#include "rdpsnd_dsp.h" +#include +#include +#include +#include +#include + +#include + +#ifndef PA_CHECK_VERSION +#define PA_CHECK_VERSION(major, minor, micro) (0) +#endif + +#include +#if PA_CHECK_VERSION(0,9,11) +#include +#endif +#include +#include +#include +#include + +#define DEFAULTDEVICE NULL + +/* Messages that may be sent from the PulseAudio thread */ +enum RDPSND_PULSE_MSG_TYPE +{ + RDPSND_PULSE_OUT_AVAIL, // Output space available in output stream + RDPSND_PULSE_IN_AVAIL, // Input data available in input stream + RDPSND_PULSE_OUT_ERR, // An error occured in the output stream + RDPSND_PULSE_IN_ERR // An error occured in the input strem +}; + +static pa_threaded_mainloop *mainloop; +#if PA_CHECK_VERSION(0,9,11) +static pa_proplist *proplist; +#endif +static pa_context *context; +static pa_stream *playback_stream; +static pa_stream *capture_stream; +static int pulse_ctl[2] = { -1, -1 }; // Pipe for comminicating with main thread + +/* Streams states for the possibility of the proper reinitialization */ +static RD_BOOL playback_started = False; +static RD_BOOL capture_started = False; + +/* Device's parameters */ +static const char *device; +static int playback_channels; +static int playback_samplerate; +static int playback_samplewidth; +static int capture_channels; +static int capture_samplerate; +static int capture_samplewidth; + +/* Internal audio buffer sizes (latency) */ +static const int playback_latency_part = 10; // Playback latency (in part of second) +static const int capture_latency_part = 10; // Capture latency (in part of second) + +/* Capture buffer */ +static void *capture_buf = NULL; +static size_t capture_buf_size = 0; + +static RD_BOOL pulse_init(void); +static void pulse_deinit(void); +static RD_BOOL pulse_context_init(void); +static void pulse_context_deinit(void); +static RD_BOOL pulse_stream_open(pa_stream ** stream, int channels, int samplerate, int samplewidth, pa_stream_flags_t flags); +static void pulse_stream_close(pa_stream ** stream); + +static void pulse_send_msg(int fd, char message); + +static RD_BOOL pulse_playback_start(void); +static RD_BOOL pulse_playback_stop(void); +static RD_BOOL pulse_playback_set_audio(int channels, int samplerate, int samplewidth); +static RD_BOOL pulse_play(void); + +static RD_BOOL pulse_capture_start(void); +static RD_BOOL pulse_capture_stop(void); +static RD_BOOL pulse_capture_set_audio(int channels, int samplerate, int samplewidth); +static RD_BOOL pulse_record(void); + +static RD_BOOL pulse_recover(pa_stream ** stream); + +/* Callbacks for the PulseAudio events */ +static void pulse_context_state_cb(pa_context * c, void * userdata); +static void pulse_stream_state_cb(pa_stream * p, void * userdata); +static void pulse_write_cb(pa_stream * , size_t nbytes, void * userdata); +static void pulse_read_cb(pa_stream * p, size_t nbytes, void * userdata); + +static void pulse_cork_cb(pa_stream * p, int success, void * userdata); +static void pulse_flush_cb(pa_stream * p, int success, void * userdata); +static void pulse_update_timing_cb(pa_stream * p, int success, void * userdata); + +static RD_BOOL +pulse_init(void) +{ + RD_BOOL ret = False; + + + do + { + /* PulsaAudio mainloop thread initialization */ + mainloop = pa_threaded_mainloop_new(); + if (mainloop == NULL) + { + logger(Sound, Error, "pulse_init(), Error creating PulseAudio threaded mainloop"); + break; + } + if (pa_threaded_mainloop_start(mainloop) != 0) + { + logger(Sound, Error, "pulse_init(), Error starting PulseAudio threaded mainloop"); + break; + } + #if PA_CHECK_VERSION(0,9,11) + /* PulseAudio proplist initialization */ + proplist = pa_proplist_new(); + if (proplist == NULL) + { + logger(Sound, Error, "pulse_init(), Error creating PulseAudio proplist"); + break; + } + if (pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, "rdesktop") != 0) + { + logger(Sound, Error, "pulse_init(), Error setting option to PulseAudio proplist"); + break; + } + #endif + + if (pulse_context_init() != True) + break; + + ret = True; + } + while (0); + + if (ret != True) + pulse_deinit(); + + return ret; +} + +static void +pulse_deinit(void) +{ + pulse_stream_close(&capture_stream); + pulse_stream_close(&playback_stream); + pulse_context_deinit(); + #if PA_CHECK_VERSION(0,9,11) + if (proplist != NULL) + { + pa_proplist_free(proplist); + proplist = NULL; + } + #endif + if (mainloop != NULL) + { + pa_threaded_mainloop_stop(mainloop); + pa_threaded_mainloop_free(mainloop); + mainloop = NULL; + } +} + +static RD_BOOL +pulse_context_init(void) +{ + pa_context_flags_t flags; + pa_context_state_t context_state; + int err; + RD_BOOL ret = False; + + + pa_threaded_mainloop_lock(mainloop); + + do + { + /* Pipe for the control information from the audio thread */ + if (pipe(pulse_ctl) != 0) + { + logger(Sound, Error, "pulse_context_init(), pipe: %s", strerror(errno)); + pulse_ctl[0] = pulse_ctl[1] = -1; + break; + } + if (fcntl(pulse_ctl[0], F_SETFL, O_NONBLOCK) == -1) + { + logger(Sound, Error, "pulse_context_init(), fcntl: %s", strerror(errno)); + break; + } + #if PA_CHECK_VERSION(0,9,11) + context = pa_context_new_with_proplist(pa_threaded_mainloop_get_api(mainloop), NULL, proplist); + #else + context = pa_context_new(pa_threaded_mainloop_get_api(mainloop), "rdesktop"); + #endif + if (context == NULL) + { + logger(Sound, Error, "pulse_context_init(), error creating PulseAudio context"); + break; + } + pa_context_set_state_callback(context, pulse_context_state_cb, mainloop); + /* PulseAudio context connection */ + #if PA_CHECK_VERSION(0,9,15) + flags = PA_CONTEXT_NOFAIL; + #else + flags = 0; + #endif + if (pa_context_connect(context, NULL, flags, NULL) != 0) + { + err = pa_context_errno(context); + logger(Sound, Error ,"pulse_context_init(), %s", pa_strerror(err)); + break; + } + do + { + context_state = pa_context_get_state(context); + if (context_state == PA_CONTEXT_READY || context_state == PA_CONTEXT_FAILED) + break; + else + pa_threaded_mainloop_wait(mainloop); + } + while (1); + if (context_state != PA_CONTEXT_READY) + { + err = pa_context_errno(context); + logger(Sound, Error ,"pulse_context_init(), %s", pa_strerror(err)); + break; + } + + ret = True; + } + while (0); + + pa_threaded_mainloop_unlock(mainloop); + + return ret; +} + +static void +pulse_context_deinit(void) +{ + int err; + + + if (context != NULL) + { + pa_threaded_mainloop_lock(mainloop); + + pa_context_disconnect(context); + pa_context_unref(context); + context = NULL; + + pa_threaded_mainloop_unlock(mainloop); + } + + if (pulse_ctl[0] != -1) + { + do + err = close(pulse_ctl[0]); + while (err == -1 && errno == EINTR); + if (err == -1) + logger(Sound, Error, "pulse_context_deinit(), close: %s", strerror(errno)); + pulse_ctl[0] = -1; + } + if (pulse_ctl[1] != -1) + { + do + err = close(pulse_ctl[1]); + while (err == -1 && errno == EINTR); + if (err == -1) + logger(Sound, Error, "pulse_context_deinit(), close: %s", strerror(errno)); + pulse_ctl[1] = 0; + } +} + +static RD_BOOL +pulse_stream_open(pa_stream ** stream, int channels, int samplerate, int samplewidth, pa_stream_flags_t flags) +{ + pa_sample_spec samples; + pa_buffer_attr buffer_attr; + pa_stream_state_t state; + int err; + RD_BOOL ret = False; + + + assert(stream != NULL); + assert(stream == &playback_stream || stream == &capture_stream); + + logger(Sound, Debug, "pulse_stream_open(), channels=%d, samplerate=%d, samplewidth=%d", + channels, samplerate, samplewidth); + + pa_threaded_mainloop_lock(mainloop); + + do + { + /* PulseAudio sample format initialization */ + #if PA_CHECK_VERSION(0,9,13) + if (pa_sample_spec_init(&samples) == NULL) + { + logger(Sound, Error, "pulse_stream_open(), error initializing PulseAudio sample format"); + break; + } + #endif + if (samplewidth == 2) + samples.format = PA_SAMPLE_S16LE; + else if (samplewidth == 1) + samples.format = PA_SAMPLE_U8; + else + { + logger(Sound, Error, "pulse_stream_open(), wrong samplewidth for the PulseAudio stream: %d", samplewidth); + break; + } + samples.rate = samplerate; + samples.channels = channels; + if (!pa_sample_spec_valid(&samples)) + { + logger(Sound, Error, "pulse_stream_open(), Invalid PulseAudio sample format"); + break; + } + /* PulseAudio stream creation */ + #if PA_CHECK_VERSION(0,9,11) + if (stream == &playback_stream) + *stream = pa_stream_new_with_proplist(context, "Playback Stream", &samples, NULL, proplist); + else + *stream = pa_stream_new_with_proplist(context, "Capture Stream", &samples, NULL, proplist); + #else + if (stream == &playback_stream) + *stream = pa_stream_new(context, "Playback Stream", &samples, NULL); + else + *stream = pa_stream_new(context, "Capture Stream", &samples, NULL); + #endif + if (*stream == NULL) + { + err = pa_context_errno(context); + logger(Sound, Error, "pulse_stream_open(), pa_stream_new: %s", pa_strerror(err)); + break; + } + pa_stream_set_state_callback(*stream, pulse_stream_state_cb, mainloop); + + buffer_attr.maxlength = (uint32_t) -1; + buffer_attr.minreq = (uint32_t) -1; + buffer_attr.prebuf = (uint32_t) -1; + buffer_attr.tlength = (uint32_t) -1; + buffer_attr.fragsize = (uint32_t) -1; + + /* PulseAudio stream connection */ + if (stream == &playback_stream) + { + #if PA_CHECK_VERSION(0,9,0) + buffer_attr.tlength = pa_usec_to_bytes(1000000/playback_latency_part, &samples); + #else + buffer_attr.tlength = (samples.rate/playback_latency_part) * samples.channels * (samples.format == PA_SAMPLE_S16LE ? 2 : 1); + #endif + buffer_attr.prebuf = 0; + buffer_attr.maxlength = buffer_attr.tlength; + } + else + { + #if PA_CHECK_VERSION(0,9,0) + buffer_attr.fragsize = pa_usec_to_bytes(1000000/capture_latency_part, &samples); + #else + buffer_attr.fragsize = (samples.rate/capture_latency_part) * samples.channels * (samples.format == PA_SAMPLE_S16LE ? 2 : 1); + #endif + buffer_attr.maxlength = buffer_attr.fragsize; + } + + #if !PA_CHECK_VERSION(0,9,16) + buffer_attr.minreq = (samples.rate/50) * samples.channels * (samples.format == PA_SAMPLE_S16LE ? 2 : 1); // 20 ms + #endif + + if (stream == &playback_stream) + err = pa_stream_connect_playback(*stream, device, &buffer_attr, flags, NULL, NULL); + else + err = pa_stream_connect_record(*stream, device, &buffer_attr, flags); + if (err) + { + err = pa_context_errno(context); + logger(Sound, Error, "pulse_stream_open(), error connecting PulseAudio stream: %s", pa_strerror(err)); + break; + } + do + { + state = pa_stream_get_state(*stream); + if (state == PA_STREAM_READY || state == PA_STREAM_FAILED) + break; + else + pa_threaded_mainloop_wait(mainloop); + } + while (1); + if (state != PA_STREAM_READY) + { + err = pa_context_errno(context); + logger(Sound, Error, "pulse_stream_open(), error connecting PulseAudio stream: %s", pa_strerror(err)); + break; + } + + #if PA_CHECK_VERSION(0,9,8) + logger(Sound, Debug, "pulse_stream_open(), opened PulseAudio stream on device %s", + pa_stream_get_device_name(*stream)); + #endif + #if PA_CHECK_VERSION(0,9,0) + const pa_buffer_attr *res_ba; + res_ba = pa_stream_get_buffer_attr(*stream); + logger(Sound, Debug, "pulse_stream_open(), PulseAudio stream buffer metrics: maxlength %u, minreq %u, prebuf %u, tlength %u, fragsize %u", + res_ba->maxlength, res_ba->minreq, res_ba->prebuf, res_ba->tlength, res_ba->fragsize); + #endif + + /* Set the data callbacks for the PulseAudio stream */ + if (stream == &playback_stream) + pa_stream_set_write_callback(*stream, pulse_write_cb, mainloop); + else + pa_stream_set_read_callback(*stream, pulse_read_cb, mainloop); + + ret = True; + } + while (0); + + pa_threaded_mainloop_unlock(mainloop); + + return ret; +} + +static void +pulse_stream_close(pa_stream ** stream) +{ + pa_stream_state_t state; + int err; + + + assert(stream != NULL); + + if (*stream != NULL) + { + pa_threaded_mainloop_lock(mainloop); + + state = pa_stream_get_state(*stream); + if (state == PA_STREAM_READY) + { + if (pa_stream_disconnect(*stream) != 0) + { + err = pa_context_errno(context); + logger(Sound, Error, "pulse_stream_close(), pa_stream_disconnect: %s\n", pa_strerror(err)); + } + } + pa_stream_unref(*stream); + *stream = NULL; + + pa_threaded_mainloop_unlock(mainloop); + } +} + +static void +pulse_send_msg(int fd, char message) +{ + int ret; + + + do + ret = write(fd, &message, sizeof message); + while (ret == -1 && errno == EINTR); + if (ret == -1) + logger(Sound, Error, "pulse_send_msg(), error writing message to the pipe: %s\n", strerror(errno)); +} + +static RD_BOOL +pulse_playback_start(void) +{ + RD_BOOL result = False; + int ret; + int err; + pa_operation *po; + + + if (playback_stream == NULL) + { + logger(Sound, Warning, "pulse_playback_start(), trying to start PulseAudio stream while it's not exists"); + return True; + } + + pa_threaded_mainloop_lock(mainloop); + + do + { + if (pa_stream_get_state(playback_stream) != PA_STREAM_READY) + { + logger(Sound, Warning, "pulse_playback_start(), trying to start PulseAudio stream while it's not ready"); + break; + } + #if PA_CHECK_VERSION(0,9,11) + ret = pa_stream_is_corked(playback_stream); + #else + ret = 1; + #endif + if (ret < 0) + { + err = pa_context_errno(context); + logger(Sound, Error, "pulse_playback_start(), pa_stream_is_corked: %s", pa_strerror(err)); + break; + } + else if (ret != 0) + { + po = pa_stream_cork(playback_stream, 0, pulse_cork_cb, mainloop); + if (po == NULL) + { + err = pa_context_errno(context); + logger(Sound, Error, "pulse_playback_start(), pa_stream_corked: %s", pa_strerror(err)); + break; + } + while (pa_operation_get_state(po) == PA_OPERATION_RUNNING) + pa_threaded_mainloop_wait(mainloop); + pa_operation_unref(po); + } + + result = True; + } + while (0); + + pa_threaded_mainloop_unlock(mainloop); + + return result; +} + +static RD_BOOL +pulse_playback_stop(void) +{ + RD_BOOL result = False; + int ret; + int err; + pa_operation *po; + + + if (playback_stream == NULL) + { + logger(Sound, Debug, "pulse_playback_stop(), trying to stop PulseAudio stream while it's not exists"); + return True; + } + + pa_threaded_mainloop_lock(mainloop); + + do + { + if (pa_stream_get_state(playback_stream) != PA_STREAM_READY) + { + logger(Sound, Error, "pulse_playback_stop(), trying to stop PulseAudio stream while it's not ready"); + break; + } + #if PA_CHECK_VERSION(0,9,11) + ret = pa_stream_is_corked(playback_stream); + #else + ret = 0; + #endif + if (ret < 0) + { + err = pa_context_errno(context); + logger(Sound, Error, "pulse_playback_stop(), pa_stream_is_corked: %s", pa_strerror(err)); + break; + } + else if (ret == 0) + { + po = pa_stream_cork(playback_stream, 1, pulse_cork_cb, mainloop); + if (po == NULL) + { + err = pa_context_errno(context); + logger(Sound, Error, "pulse_playback_stop(), pa_stream_cork: %s", pa_strerror(err)); + break; + } + while (pa_operation_get_state(po) == PA_OPERATION_RUNNING) + pa_threaded_mainloop_wait(mainloop); + pa_operation_unref(po); + } + po = pa_stream_flush(playback_stream, pulse_flush_cb, mainloop); + if (po == NULL) + { + err = pa_context_errno(context); + logger(Sound, Error, "pulse_playback_stop(), pa_stream_flush: %s", pa_strerror(err)); + break; + } + while (pa_operation_get_state(po) == PA_OPERATION_RUNNING) + pa_threaded_mainloop_wait(mainloop); + pa_operation_unref(po); + + result = True; + } + while (0); + + pa_threaded_mainloop_unlock(mainloop); + + return result; +} + +static RD_BOOL +pulse_playback_set_audio(int channels, int samplerate, int samplewidth) +{ + pa_stream_flags_t flags; + + + pulse_stream_close(&playback_stream); + + flags = PA_STREAM_START_CORKED | PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_AUTO_TIMING_UPDATE; + #if PA_CHECK_VERSION(0,9,11) + flags |= PA_STREAM_ADJUST_LATENCY; + #endif + if (pulse_stream_open(&playback_stream, channels, samplerate, samplewidth, flags) != True) + return False; + + return True; +} + +static RD_BOOL +pulse_capture_start(void) +{ + RD_BOOL result = False; + int ret; + int err; + pa_operation *po; + + + if (capture_stream == NULL) + { + logger(Sound, Warning, "pulse_capture_start(), trying to start PulseAudio stream while it's not exists"); + return True; + } + + pa_threaded_mainloop_lock(mainloop); + + do + { + if (pa_stream_get_state(capture_stream) != PA_STREAM_READY) + { + logger(Sound, Error, "pulse_capture_start(), trying to start PulseAudio stream while it's not exists"); + break; + } + #if PA_CHECK_VERSION(0,9,11) + ret = pa_stream_is_corked(capture_stream); + #else + ret = 1; + #endif + if (ret < 0) + { + err = pa_context_errno(context); + logger(Sound, Error, "pulse_capture_start(), pa_stream_is_corked: %s", pa_strerror(err)); + break; + } + else if (ret != 0) + { + po = pa_stream_cork(capture_stream, 0, pulse_cork_cb, mainloop); + if (po == NULL) + { + err = pa_context_errno(context); + logger(Sound, Error, "pulse_capture_start(), pa_stream_cork: %s\n", pa_strerror(err)); + break; + } + while (pa_operation_get_state(po) == PA_OPERATION_RUNNING) + pa_threaded_mainloop_wait(mainloop); + pa_operation_unref(po); + } + + result = True; + } + while (0); + + pa_threaded_mainloop_unlock(mainloop); + + return result; +} + +static RD_BOOL +pulse_capture_stop(void) +{ + RD_BOOL result = False; + int ret; + int err; + pa_operation *po; + + + if (capture_stream == NULL) + { + logger(Sound, Debug, "pulse_capture_stop(), trying to stop PulseAudio stream while it's not exists"); + return True; + } + + pa_threaded_mainloop_lock(mainloop); + + do + { + if (pa_stream_get_state(capture_stream) != PA_STREAM_READY) + { + logger(Sound, Error, "pulse_capture_stop(), trying to stop PulseAudio stream while it's not exists"); + break; + } + #if PA_CHECK_VERSION(0,9,11) + ret = pa_stream_is_corked(capture_stream); + #else + ret = 0; + #endif + if (ret < 0) + { + err = pa_context_errno(context); + logger(Sound, Error, "pulse_capture_stop(), pa_stream_is_corked: %s\n", pa_strerror(err)); + break; + } + else if (ret == 0) + { + po = pa_stream_cork(capture_stream, 1, pulse_cork_cb, mainloop); + if (po == NULL) + { + err = pa_context_errno(context); + logger(Sound, Error, "pulse_capture_stop(), pa_stream_cork: %s\n", pa_strerror(err)); + break; + } + while (pa_operation_get_state(po) == PA_OPERATION_RUNNING) + pa_threaded_mainloop_wait(mainloop); + pa_operation_unref(po); + } + + result = True; + } + while (0); + + pa_threaded_mainloop_unlock(mainloop); + + return result; +} + +static RD_BOOL +pulse_capture_set_audio(int channels, int samplerate, int samplewidth) +{ + pa_stream_flags_t flags; + pa_stream_state_t state; + int ret; + int err; + + + flags = PA_STREAM_START_CORKED; + #if PA_CHECK_VERSION(0,9,11) + flags |= PA_STREAM_ADJUST_LATENCY; + #endif + + if (capture_stream != NULL) + { + pa_threaded_mainloop_lock(mainloop); + state = pa_stream_get_state(capture_stream); + if (state == PA_STREAM_READY) + { + #if PA_CHECK_VERSION(0,9,11) + ret = pa_stream_is_corked(capture_stream); + #else + ret = (capture_started == False); + #endif + if (ret == 0) + flags &= ~PA_STREAM_START_CORKED; + else if (ret < 0) + { + err = pa_context_errno(context); + pa_threaded_mainloop_unlock(mainloop); + logger(Sound, Error, "pulse_capture_set_audio(), pa_stream_is_corked: %s\n", pa_strerror(err)); + return False; + } + } + pa_threaded_mainloop_unlock(mainloop); + } + + pulse_stream_close(&capture_stream); + + if (pulse_stream_open(&capture_stream, channels, samplerate, samplewidth, flags) != True) + return False; + + return True; +} + +static void +pulse_context_state_cb(pa_context * c, void * userdata) +{ + pa_context_state_t state; + + + assert(userdata != NULL); + + state = pa_context_get_state(c); + if (state == PA_CONTEXT_READY || state == PA_CONTEXT_FAILED) + pa_threaded_mainloop_signal((pa_threaded_mainloop *) userdata, 0); +} + +static void +pulse_stream_state_cb(pa_stream * p, void * userdata) +{ + pa_stream_state_t state; + + + assert(userdata != NULL); + + state = pa_stream_get_state(p); + if (state == PA_STREAM_FAILED) + { + if (p == playback_stream) + { + logger(Sound, Debug, "pulse_stream_state_cb(), PulseAudio playback stream is in a fail state"); + pulse_send_msg(pulse_ctl[1], RDPSND_PULSE_OUT_ERR); + } + else + { + logger(Sound, Debug, "pulse_stream_state_cb(), PulseAudio capture stream is in a fail state"); + pulse_send_msg(pulse_ctl[1], RDPSND_PULSE_IN_ERR); + } + } + if (state == PA_STREAM_READY || state == PA_STREAM_FAILED) + pa_threaded_mainloop_signal((pa_threaded_mainloop *) userdata, 0); +} + +static void +pulse_read_cb(pa_stream * p, size_t nbytes, void * userdata) +{ + assert(userdata != NULL); + + pulse_send_msg(pulse_ctl[1], RDPSND_PULSE_IN_AVAIL); +} + +static void +pulse_write_cb(pa_stream * p, size_t nbytes, void * userdata) +{ + assert(userdata != NULL); + + pulse_send_msg(pulse_ctl[1], RDPSND_PULSE_OUT_AVAIL); +} + +static void +pulse_cork_cb(pa_stream * p, int success, void * userdata) +{ + assert(userdata != NULL); + + if (!success) + { + if (p == playback_stream) + { + logger(Sound, Warning, "pulse_cork_cb(), fail to cork/uncork the PulseAudio playback stream: %s", + pa_strerror(pa_context_errno(context))); + pulse_send_msg(pulse_ctl[1], RDPSND_PULSE_OUT_ERR); + } + else + { + logger(Sound, Warning, "pulse_cork_cb(), fail to cork/uncork the PulseAudio capture stream: %s", + pa_strerror(pa_context_errno(context))); + pulse_send_msg(pulse_ctl[1], RDPSND_PULSE_IN_ERR); + } + } + + pa_threaded_mainloop_signal((pa_threaded_mainloop *) userdata, 0); +} + +static void +pulse_flush_cb(pa_stream * p, int success, void * userdata) +{ + assert(userdata != NULL); + + if (!success) + { + logger(Sound, Warning, "pulse_flush_cb(), Fail to flush the PulseAudio stream: %s", + pa_strerror(pa_context_errno(context))); + pulse_send_msg(pulse_ctl[1], RDPSND_PULSE_OUT_ERR); + } + + pa_threaded_mainloop_signal((pa_threaded_mainloop *) userdata, 0); +} + +static void +pulse_update_timing_cb(pa_stream * p, int success, void * userdata) +{ + assert(userdata != NULL); + + if (!success) + { + logger(Sound, Warning, "pulse_update_timing_cb(), fail to update timing info of the PulseAudio stream: %s", + pa_strerror(pa_context_errno(context))); + pulse_send_msg(pulse_ctl[1], RDPSND_PULSE_OUT_ERR); + } + + pa_threaded_mainloop_signal((pa_threaded_mainloop *) userdata, 0); +} + +void +pulse_add_fds(int *n, fd_set * rfds, fd_set * wfds, struct timeval *tv) +{ + if (pulse_ctl[0] != -1) + { + if (pulse_ctl[0] > *n) + *n = pulse_ctl[0]; + + FD_SET(pulse_ctl[0], rfds); + } +} + +void +pulse_check_fds(fd_set * rfds, fd_set * wfds) +{ + char audio_cmd; + int n; + + + if (pulse_ctl[0] == -1) + return; + + if (FD_ISSET(pulse_ctl[0], rfds)) + { + do + { + n = read(pulse_ctl[0], &audio_cmd, sizeof audio_cmd); + if (n == -1) + { + if (errno == EINTR) + continue; + else if (errno == EAGAIN || errno == EWOULDBLOCK) + break; + else + { + logger(Sound, Error, "pulse_check_fds(), read: %s\n", strerror(errno)); + return; + } + } + else if (n == 0) + { + logger(Sound, Warning, "pulse_check_fds(), audio control pipe was closed"); + break; + } + else + switch (audio_cmd) + { + case RDPSND_PULSE_OUT_AVAIL: + if (pulse_play() != True) + if (pulse_recover(&playback_stream) != True) + { + logger(Sound, Error, "pulse_check_fds(), PulseAudio playback error"); + return; + } + break; + case RDPSND_PULSE_IN_AVAIL: + if (pulse_record() != True) + if (pulse_recover(&capture_stream) != True) + { + logger(Sound, Error, "pulse_check_fds(), PulseAudio capture error"); + return; + } + break; + case RDPSND_PULSE_OUT_ERR: + if (pulse_recover(&playback_stream) != True) + { + logger(Sound, Error, "pulse_check_fds(), an error occured in audio thread with PulseAudio playback stream"); + return; + } + break; + case RDPSND_PULSE_IN_ERR: + if (pulse_recover(&capture_stream) != True) + { + logger(Sound, Error, "pulse_check_fds(), an error occured in audio thread with PulseAudio capture stream"); + return; + } + break; + default: + logger(Sound, Error, "pulse_check_fds(), wrong command from the audio thread: %d", audio_cmd); + break; + } + } + while (1); + } + + return; +} + +RD_BOOL +pulse_open_out(void) +{ + if (context == NULL || mainloop == NULL) + if (pulse_init() != True) + return False; + + return True; +} + +void +pulse_close_out(void) +{ + /* Ack all remaining packets */ + while (!rdpsnd_queue_empty()) + rdpsnd_queue_next(0); + + playback_started = False; + + if (playback_stream && pulse_playback_stop() != True) + if (pulse_recover(&playback_stream) != True) + { + logger(Sound, Error ,"pulse_close_out(), fail to close the PulseAudio playback stream"); + return; + } +} + +RD_BOOL +pulse_format_supported(RD_WAVEFORMATEX * pwfx) +{ + if (pwfx->wFormatTag != WAVE_FORMAT_PCM) + return False; + if ((pwfx->nChannels != 1) && (pwfx->nChannels != 2)) + return False; + if ((pwfx->wBitsPerSample != 8) && (pwfx->wBitsPerSample != 16)) + return False; + + return True; +} + +RD_BOOL +pulse_set_format_out(RD_WAVEFORMATEX * pwfx) +{ + if (playback_stream == NULL + || playback_channels != pwfx->nChannels + || playback_samplerate != pwfx->nSamplesPerSec + || playback_samplewidth != pwfx->wBitsPerSample / 8) + { + playback_channels = pwfx->nChannels; + playback_samplerate = pwfx->nSamplesPerSec; + playback_samplewidth = pwfx->wBitsPerSample / 8; + + if (pulse_playback_set_audio(pwfx->nChannels, pwfx->nSamplesPerSec, pwfx->wBitsPerSample / 8) != True) + if (pulse_recover(&playback_stream) != True) + { + logger(Sound, Error ,"pulse_set_format_out(), fail to open the PulseAudio playback stream"); + return False; + } + } + + playback_started = True; + + if (pulse_playback_start() != True) + if (pulse_recover(&playback_stream) != True) + { + logger(Sound, Error, "pulse_set_format_out(), fail to start the PulseAudio playback stream"); + return False; + } + + return True; +} + +RD_BOOL +pulse_play(void) +{ + struct audio_packet *packet; + STREAM out; + const pa_timing_info *ti; + pa_operation *po; + pa_seek_mode_t playback_seek; + size_t avail_space, audio_size; + pa_usec_t delay = 0; + int ret; + int err; + RD_BOOL result = False; + + + if (rdpsnd_queue_empty()) + return True; + + if (playback_stream == NULL) + return False; + + pa_threaded_mainloop_lock(mainloop); + + do + { + packet = rdpsnd_queue_current_packet(); + out = &packet->s; + + ti = pa_stream_get_timing_info(playback_stream); + if (ti == NULL) + { + err = pa_context_errno(context); + logger(Sound, Error, "pulse_play(), pa_stream_get_timing_info: %s", pa_strerror(err)); + break; + } + + if (ti->read_index_corrupt || ti->write_index_corrupt) + { + po = pa_stream_update_timing_info(playback_stream, pulse_update_timing_cb, mainloop); + if (po == NULL) + { + err = pa_context_errno(context); + logger(Sound, Error, "pulse_play(), pa_stream_update_timing_info: %s", pa_strerror(err)); + break; + } + while (pa_operation_get_state(po) == PA_OPERATION_RUNNING) + pa_threaded_mainloop_wait(mainloop); + pa_operation_unref(po); + } + + if (ti->read_index > ti->write_index) + { + logger(Sound, Debug, "pulse_play(), PulseAudio stream underflow %ld bytes", + (long) (ti->read_index - ti->write_index)); + playback_seek = PA_SEEK_RELATIVE_ON_READ; + } + else + playback_seek = PA_SEEK_RELATIVE; + + avail_space = pa_stream_writable_size(playback_stream); + audio_size = out->end - out->p <= avail_space ? out->end - out->p : avail_space; + if (audio_size) + { + if (pa_stream_write(playback_stream, out->p, audio_size, NULL, 0, playback_seek) != 0) + { + err = pa_context_errno(context); + logger(Sound, Error, "pulse_play(), pa_stream_write: %s", pa_strerror(err)); + break; + } + else if (playback_seek == PA_SEEK_RELATIVE_ON_READ) + playback_seek = PA_SEEK_RELATIVE; + } + + out->p += audio_size; + + if (out->p == out->end) + { + ret = pa_stream_get_latency(playback_stream, &delay, NULL); + if (ret != 0 && (err = pa_context_errno(context)) == PA_ERR_NODATA) + { + po = pa_stream_update_timing_info(playback_stream, pulse_update_timing_cb, mainloop); + if (po == NULL) + { + delay = 0; + err = pa_context_errno(context); + logger(Sound, Error, "pulse_play(), pa_stream_update_timing_info: %s", pa_strerror(err)); + break; + } + while (pa_operation_get_state(po) == PA_OPERATION_RUNNING) + pa_threaded_mainloop_wait(mainloop); + pa_operation_unref(po); + + ret = pa_stream_get_latency(playback_stream, &delay, NULL); + } + if (ret != 0) + { + delay = 0; + err = pa_context_errno(context); + logger(Sound, Error, "pulse_play(), pa_stream_get_latency: %s", pa_strerror(err)); + break; + } + + logger(Sound, Debug, "pulse_play(), PulseAudio playback stream latency %lu usec", (long) delay); + } + + result = True; + } + while (0); + + pa_threaded_mainloop_unlock(mainloop); + + if (out->p == out->end) + rdpsnd_queue_next(delay); + + return result; +} + +RD_BOOL +pulse_open_in(void) +{ + if (context == NULL || mainloop == NULL) + if (pulse_init() != True) + return False; + + return True; +} + +void +pulse_close_in(void) +{ + capture_started = False; + + if (capture_stream && pulse_capture_stop() != True) + if (pulse_recover(&capture_stream) != True) + { + logger(Sound, Error, "pulse_close_in(), fail to close the PulseAudio capture stream"); + return; + } +} + +RD_BOOL +pulse_set_format_in(RD_WAVEFORMATEX * pwfx) +{ + if (capture_stream == NULL + || capture_channels != pwfx->nChannels + || capture_samplerate != pwfx->nSamplesPerSec + || capture_samplewidth != pwfx->wBitsPerSample / 8) + { + capture_channels = pwfx->nChannels; + capture_samplerate = pwfx->nSamplesPerSec; + capture_samplewidth = pwfx->wBitsPerSample / 8; + + if (pulse_capture_set_audio(pwfx->nChannels, pwfx->nSamplesPerSec, pwfx->wBitsPerSample / 8) != True) + if (pulse_recover(&capture_stream) != True) + { + logger(Sound, Error, "pulse_set_format_in(), fail to open the PulseAudio capture stream"); + return False; + } + } + + capture_started = True; + + if (pulse_capture_start() != True) + if (pulse_recover(&capture_stream) != True) + { + logger(Sound, Error, "pulse_set_format_in(), fail to start the PulseAudio capture stream"); + return False; + } + + return True; +} + +RD_BOOL +pulse_record(void) +{ + const void *pulse_buf; + size_t audio_size; + RD_BOOL result = False; + + + if (capture_stream == NULL) + return False; + + pa_threaded_mainloop_lock(mainloop); + + do + { + if (pa_stream_peek(capture_stream, &pulse_buf, &audio_size) != 0) + { + logger(Sound, Error, "pulse_record(), pa_stream_peek: %s", pa_strerror(pa_context_errno(context))); + break; + } + + /* Stretch the buffer, if needed */ + if (capture_buf_size < audio_size) + { + capture_buf_size = audio_size; + if (capture_buf != NULL) + free(capture_buf); + capture_buf = malloc(capture_buf_size); + if (capture_buf == NULL) + { + logger(Sound, Error, "pulse_record(), malloc error"); + capture_buf_size = 0; + break; + } + } + + memcpy(capture_buf, pulse_buf, audio_size); + + if (pa_stream_drop(capture_stream) != 0) + { + logger(Sound, Error, "pulse_record(), pa_stream_drop: %s", pa_strerror(pa_context_errno(context))); + break; + } + + result = True; + } + while (0); + + pa_threaded_mainloop_unlock(mainloop); + + if (result == True) + rdpsnd_record(capture_buf, audio_size); + + return result; +} + +static RD_BOOL +pulse_recover(pa_stream ** stream) +{ + RD_BOOL playback, capture; + + + playback = capture = False; + + if (playback_stream != NULL) + playback = True; + if (capture_stream != NULL) + capture = True; + + if (stream == &playback_stream) + { + if (pulse_playback_set_audio(playback_channels, playback_samplerate, playback_samplewidth) == True) + if (playback_started != True || pulse_playback_start() == True) + return True; + } + else if (stream == &capture_stream) + { + if (pulse_capture_set_audio(capture_channels, capture_samplerate, capture_samplewidth) == True) + if (capture_started != True || pulse_capture_start() == True) + return True; + } + + pulse_deinit(); + + if (pulse_init() != True) + return False; + + do + { + if (playback == True) + { + if (pulse_playback_set_audio(playback_channels, playback_samplerate, playback_samplewidth) != True + || (playback_started == True && pulse_playback_start() != True)) + break; + } + if (capture == True) + { + if (pulse_capture_set_audio(capture_channels, capture_samplerate, capture_samplewidth) != True + || (capture_started == True && pulse_capture_start() != True)) + break; + } + + return True; + } + while (0); + + pulse_deinit(); + + return False; +} + +struct audio_driver * +pulse_register(char *options) +{ + static struct audio_driver pulse_driver; + + memset(&pulse_driver, 0, sizeof(pulse_driver)); + + pulse_driver.name = "pulse"; + pulse_driver.description = "PulseAudio output driver, default device: system dependent"; + + pulse_driver.add_fds = pulse_add_fds; + pulse_driver.check_fds = pulse_check_fds; + + pulse_driver.wave_out_open = pulse_open_out; + pulse_driver.wave_out_close = pulse_close_out; + pulse_driver.wave_out_format_supported = pulse_format_supported; + pulse_driver.wave_out_set_format = pulse_set_format_out; + pulse_driver.wave_out_volume = rdpsnd_dsp_softvol_set; + + pulse_driver.wave_in_open = pulse_open_in; + pulse_driver.wave_in_close = pulse_close_in; + pulse_driver.wave_in_format_supported = pulse_format_supported; + pulse_driver.wave_in_set_format = pulse_set_format_in; + pulse_driver.wave_in_volume = NULL; /* FIXME */ + + pulse_driver.need_byteswap_on_be = 0; + pulse_driver.need_resampling = 0; + + if (options != NULL) + { + device = xstrdup(options); + } + else + { + device = DEFAULTDEVICE; + } + + return &pulse_driver; +}