/* -*- 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 = MIN(s_remaining(out), avail_space); if (audio_size) { unsigned char *data; in_uint8p(out, data, audio_size); if (pa_stream_write (playback_stream, data, 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; } if (s_check_end(out)) { 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 (s_check_end(out)) 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; }