![](https://seccdn.libravatar.org/avatar/e2145bc5cf53dda95c308a3c75e8fef3.jpg?s=120&d=mm&r=g)
Hello community, here is the log from the commit of package apulse for openSUSE:Factory checked in at 2017-05-10 20:53:41 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/apulse (Old) and /work/SRC/openSUSE:Factory/.apulse.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Package is "apulse" Wed May 10 20:53:41 2017 rev:7 rq:494104 version:0.1.10 Changes: -------- --- /work/SRC/openSUSE:Factory/apulse/apulse.changes 2017-03-12 20:04:13.163894268 +0100 +++ /work/SRC/openSUSE:Factory/.apulse.new/apulse.changes 2017-05-10 20:53:42.983763620 +0200 @@ -1,0 +2,13 @@ +Tue May 9 16:45:47 UTC 2017 - sor.alexei@meowr.ru + +- Update to version 0.1.10 (changes since 0.1.8): + * Fix the PA_SAMPLE_S16NE volume scaling bug. + * Fix video stuttering in Firefox by calling write callback as + soon as minreq bytes become available in write buffer. + * Make error messages from do_connect_pcm friendlier. + * Per-stream volumes. + * Implement source and sink info introspection functions. + * Implement timed events. + * Fix buffer xruns by increasing buffer sizes. + +------------------------------------------------------------------- Old: ---- apulse-0.1.8.tar.gz New: ---- apulse-0.1.10.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ apulse.spec ++++++ --- /var/tmp/diff_new_pack.99Xrpk/_old 2017-05-10 20:53:44.143599952 +0200 +++ /var/tmp/diff_new_pack.99Xrpk/_new 2017-05-10 20:53:44.147599388 +0200 @@ -18,7 +18,7 @@ %define __provides_exclude_from ^%{_libdir}/apulse/.*.so.*$ Name: apulse -Version: 0.1.8 +Version: 0.1.10 Release: 0 Summary: PulseAudio emulation for ALSA License: MIT ++++++ apulse-0.1.8.tar.gz -> apulse-0.1.10.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apulse-0.1.8/README.md new/apulse-0.1.10/README.md --- old/apulse-0.1.8/README.md 2017-03-08 17:38:10.000000000 +0100 +++ new/apulse-0.1.10/README.md 2017-04-18 17:55:19.000000000 +0200 @@ -3,9 +3,31 @@ PulseAudio emulation for ALSA. -Project is in stale state since its proclamation. The main objective, -working Skype test call, is reached. I don't have any plans for further -development. +The program provides an alternative partial implementation of the PulseAudio +API. It consists of a loader script and a number of shared libraries with the +same names as from original PulseAudio, so applications could dynamically load +them and think they are talking to PulseAudio. Internally, no separate sound +mixing daemon is used. Instead, apulse relies on ALSA's `dmix`, `dsnoop`, and +`plug` plugins to handle multiple sound sources and capture streams running at +the same time. `dmix` plugin muxes multiple playback streams; `dsnoop` plugin +allow multiple applications to capture from a single microphone; and `plug` +plugin transparently converts audio between various sample formats, sample rates +and channel numbers. For more than a decade now, ALSA comes with these plugins +enabled and configured by default. + +`apulse` wasn't designed to be a drop-in replacement of PulseAudio. It's +pointless, since that will be just reimplementation of original PulseAudio, with +the same client-daemon architecture, required by the complete feature +set. Instead, only parts of the API that are crucial to specific applications +are implemented. That's why there is a loader script, named `apulse`. It updates +value of `LD_LIBRARY_PATH` environment variable to point also to the directory +where apulse's libraries are installed, making them available to the +application. + +Name comes from names of both ALSA and PulseAudio. As `aoss` was a compatibility +layer between OSS programs and ALSA, `apulse` was designed to be compatibility +layer between PulseAudio applications and ALSA. + Install ======= @@ -35,20 +57,40 @@ If libraries are installed to a regular library path, you don't need run applications through `apulse` wrapper. -Note you need to select build type to be `Release`, otherwise it will output enourmous -amount of debug text to the stdout. Usage ===== ``` -$ apulse <program-name> [parameters] +$ apulse <program-name> [program-parameters] ``` Environment variables `APULSE_CAPTURE_DEVICE` and `APULSE_PLAYBACK_DEVICE` can be used to configure capture and playback devices. Try `hw:0,0`, `plughw:0,0` and the like. Refer to the ALSA user guide for a full list of device names. +System directory versus separate directory installations +-------------------------------------------------------- + +By default, libraries from `apulse` are installed into a separate directory, in +order to hide them from all applications. + +Most applications in the wild, that support both PulseAudio and ALSA, try to +autodetect which sound system is used. First, applications try to start with +PulseAudio. Original client libraries fail early if no PulseAudio daemon is +running or can be started. Then they switch to ALSA. Decision is made once, at +the beginning. It works fine with PulseAudio, but doesn't work with +`apulse`. Latter has no daemons, it happily says that everything is fine, and +it's capable of playing audio. Applications then try to call more functions, and +eventually touch unimplemented parts, often with crashes. So, libraries are +hidden, and become visible only when a program is called through `apulse` +wrapper script. + +It's possible to install apulse libraries to `/usr/lib`. Wrapper script won't +be required, but then all applications will try to use PulseAudio API, despite +they can use ALSA. + + License ======= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apulse-0.1.8/src/apulse-context.c new/apulse-0.1.10/src/apulse-context.c --- old/apulse-0.1.8/src/apulse-context.c 2017-03-08 17:38:10.000000000 +0100 +++ new/apulse-0.1.10/src/apulse-context.c 2017-04-18 17:55:19.000000000 +0200 @@ -132,9 +132,27 @@ return 8; // PA headers say "8" is the protocol version used in PulseAudio 0.9 } -static void -pa_context_get_sink_info_by_name_impl(pa_operation *op) +static pa_sink_info +pai_fill_default_sink_info(void) { + static pa_proplist *proplist = NULL; + + if (!proplist) { + // TODO: free memory + proplist = pa_proplist_new(); + } + + static pa_sink_port_info sink_port = { + .name = "ALSA sink", + .description = "ALSA sink", + .priority = 1, + .available = PA_PORT_AVAILABLE_YES, + }; + + static pa_sink_port_info *sink_ports[] = { + &sink_port, + }; + // TODO: real data pa_sink_info info = { .name = "default_sink_name", @@ -162,23 +180,32 @@ }, .mute = 0, .monitor_source = 0, - .monitor_source_name = "monitor_source_name", + .monitor_source_name = NULL, .latency = 100000, .driver = "apulse", .flags = 0, - .proplist = NULL, + .proplist = proplist, .configured_latency = 100000, .base_volume = PA_VOLUME_NORM, .state = PA_SINK_RUNNING, .n_volume_steps = 0, - .card = PA_INVALID_INDEX, - .n_ports = 0, - .ports = NULL, - .active_port = NULL, + .card = 0, + .n_ports = 1, + .ports = sink_ports, + .active_port = &sink_port, }; + return info; +} + +static void +pa_context_get_sink_info_by_name_impl(pa_operation *op) +{ + pa_sink_info info = pai_fill_default_sink_info(); + if (op->sink_info_cb) { - op->sink_info_cb(op->c, &info, 0, op->cb_userdata); + if (strcmp(op->char_ptr_arg_1, info.name) == 0) + op->sink_info_cb(op->c, &info, 0, op->cb_userdata); op->sink_info_cb(op->c, NULL, 1, op->cb_userdata); } @@ -205,6 +232,192 @@ } static void +pa_context_get_sink_info_list_impl(pa_operation *op) +{ + pa_sink_info info = pai_fill_default_sink_info(); + + if (op->sink_info_cb) { + op->sink_info_cb(op->c, &info, 0, op->cb_userdata); + op->sink_info_cb(op->c, NULL, 1, op->cb_userdata); + } + + pa_operation_done(op); +} + +APULSE_EXPORT +pa_operation * +pa_context_get_sink_info_list(pa_context *c, pa_sink_info_cb_t cb, void *userdata) +{ + trace_info_f("F %s c=%p, cb=%p, userdata=%p\n", __func__, c, cb, userdata); + + pa_operation *op = pa_operation_new(c->mainloop_api, pa_context_get_sink_info_list_impl); + op->c = c; + op->sink_info_cb = cb; + op->cb_userdata = userdata; + + pa_operation_launch(op); + return op; +} + +static pa_source_info +pai_fill_default_source_info(void) +{ + static pa_proplist *proplist = NULL; + + if (!proplist) { + // TODO: free memory + proplist = pa_proplist_new(); + } + + static pa_source_port_info source_port = { + .name = "ALSA source", + .description = "ALSA source", + .priority = 1, + .available = PA_PORT_AVAILABLE_YES, + }; + + static pa_source_port_info *source_ports[] = { + &source_port, + }; + + // TODO: real data + pa_source_info info = { + .name = "default_source_name", + .index = 0, + .description = "default_source_name", + .sample_spec = { + .format = PA_SAMPLE_S16LE, + .rate = 44100, + .channels = 2, + }, + .channel_map = { + .channels = 2, + .map = { + PA_CHANNEL_POSITION_FRONT_LEFT, + PA_CHANNEL_POSITION_FRONT_RIGHT, + }, + }, + .owner_module = PA_INVALID_INDEX, + .volume = { + .channels = 2, + .values = { + PA_VOLUME_NORM, + PA_VOLUME_NORM, + }, + }, + .mute = 0, + .monitor_of_sink = 0, + .monitor_of_sink_name = NULL, + .latency = 100000, // TODO: where to get latency figures? + .driver = "apulse", + .flags = 0, + .proplist = proplist, + .configured_latency = 100000, + .base_volume = PA_VOLUME_NORM, + .state = PA_SOURCE_RUNNING, + .n_volume_steps = 0, + .card = PA_INVALID_INDEX, + .n_ports = 1, + .ports = source_ports, + .active_port = &source_port, + .n_formats = 0, + .formats = NULL, + }; + + return info; +} + +static void +pa_context_get_source_info_list_impl(pa_operation *op) +{ + pa_source_info info = pai_fill_default_source_info(); + + if (op->source_info_cb) { + op->source_info_cb(op->c, &info, 0, op->cb_userdata); + op->source_info_cb(op->c, NULL, 1, op->cb_userdata); + } + + pa_operation_done(op); +} + +APULSE_EXPORT +pa_operation * +pa_context_get_source_info_list(pa_context *c, pa_source_info_cb_t cb, void *userdata) +{ + trace_info_f("F %s c=%p, cb=%p, userdata=%p\n", __func__, c, cb, userdata); + + pa_operation *op = pa_operation_new(c->mainloop_api, pa_context_get_source_info_list_impl); + op->c = c; + op->source_info_cb = cb; + op->cb_userdata = userdata; + + pa_operation_launch(op); + return op; +} + +static void +pa_context_get_source_info_by_name_impl(pa_operation *op) +{ + pa_source_info info = pai_fill_default_source_info(); + + if (op->source_info_cb) { + if (strcmp(op->char_ptr_arg_1, info.name) == 0) + op->source_info_cb(op->c, &info, 0, op->cb_userdata); + op->source_info_cb(op->c, NULL, 1, op->cb_userdata); + } + + free(op->char_ptr_arg_1); + pa_operation_done(op); +} + +APULSE_EXPORT +pa_operation * +pa_context_get_source_info_by_name(pa_context *c, const char *name, pa_source_info_cb_t cb, + void *userdata) +{ + trace_info_f("F %s c=%p, name=%s, cb=%p, userdata=%p\n", __func__, c, name, cb, userdata); + + pa_operation *op = pa_operation_new(c->mainloop_api, pa_context_get_source_info_by_name_impl); + op->c = c; + op->char_ptr_arg_1 = strdup(name ? name : ""); + op->source_info_cb = cb; + op->cb_userdata = userdata; + + pa_operation_launch(op); + return op; +} + +static void +pa_context_get_sink_info_by_index_impl(pa_operation *op) +{ + pa_sink_info info = pai_fill_default_sink_info(); + + if (op->sink_info_cb) { + if (op->int_arg_1 == info.index) + op->sink_info_cb(op->c, &info, 0, op->cb_userdata); + op->sink_info_cb(op->c, NULL, 1, op->cb_userdata); + } + + pa_operation_done(op); +} + +APULSE_EXPORT +pa_operation * +pa_context_get_sink_info_by_index(pa_context *c, uint32_t idx, pa_sink_info_cb_t cb, void *userdata) +{ + trace_info_f("F %s c=%p, idx=%u, cb=%p, userdata=%p\n", __func__, c, idx, cb, userdata); + + pa_operation *op = pa_operation_new(c->mainloop_api, pa_context_get_sink_info_by_index_impl); + op->c = c; + op->int_arg_1 = idx; + op->sink_info_cb = cb; + op->cb_userdata = userdata; + + pa_operation_launch(op); + return op; +} + +static void pa_context_get_sink_input_info_impl(pa_operation *op) { uint32_t idx = op->int_arg_1; @@ -300,7 +513,6 @@ c->streams_ht = g_hash_table_new(g_direct_hash, g_direct_equal); for (uint32_t k = 0; k < PA_CHANNELS_MAX; k++) { - c->sink_volume[k] = PA_VOLUME_NORM; c->source_volume[k] = PA_VOLUME_NORM; } @@ -323,9 +535,15 @@ pa_time_event * pa_context_rttime_new(pa_context *c, pa_usec_t usec, pa_time_event_cb_t cb, void *userdata) { - trace_info_z("Z %s c=%p, usec=%" PRIu64 " cb=%p, userdata=%p\n", __func__, c, (uint64_t)usec, + trace_info_f("F %s c=%p, usec=%" PRIu64 " cb=%p, userdata=%p\n", __func__, c, (uint64_t)usec, cb, userdata); - return NULL; + + struct timeval when = { + .tv_sec = usec / PA_USEC_PER_SEC, + .tv_usec = usec % PA_USEC_PER_SEC, + }; + + return c->mainloop_api->time_new(c->mainloop_api, &when, cb, userdata); } APULSE_EXPORT @@ -342,12 +560,23 @@ static void pa_context_set_sink_input_volume_impl(pa_operation *op) { - memset(&op->c->sink_volume, 0, sizeof(op->c->sink_volume)); + uint32_t idx = op->int_arg_1; + pa_stream *s = g_hash_table_lookup(op->c->streams_ht, GINT_TO_POINTER(idx)); + + if (!s) { + // Can't find a stream with selected index. + if (op->context_success_cb) + op->context_success_cb(op->c, 0, op->cb_userdata); + + return; + } + + memset(s->volume, 0, sizeof(s->volume)); const uint32_t channels = MIN(op->pa_cvolume_arg_1.channels, PA_CHANNELS_MAX); for (uint32_t k = 0; k < channels; k++) - op->c->sink_volume[k] = op->pa_cvolume_arg_1.values[k]; + s->volume[k] = op->pa_cvolume_arg_1.values[k]; if (op->context_success_cb) op->context_success_cb(op->c, 1, op->cb_userdata); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apulse-0.1.8/src/apulse-mainloop.c new/apulse-0.1.10/src/apulse-mainloop.c --- old/apulse-0.1.8/src/apulse-mainloop.c 2017-03-08 17:38:10.000000000 +0100 +++ new/apulse-0.1.10/src/apulse-mainloop.c 2017-04-18 17:55:19.000000000 +0200 @@ -65,7 +65,7 @@ trace_info_f("F %s\n", __func__); pa_mainloop *ml = e->mainloop; - g_queue_remove(ml->queue, e); + g_queue_remove(ml->deferred_events_queue, e); g_slice_free(pa_defer_event, e); pa_mainloop_wakeup(ml); } @@ -82,7 +82,7 @@ de->cb = cb; de->userdata = userdata; de->mainloop = ml; - g_queue_push_tail(ml->queue, de); + g_queue_push_tail(ml->deferred_events_queue, de); pa_mainloop_wakeup(ml); return de; @@ -154,34 +154,92 @@ trace_info_z("Z %s\n", __func__); } -static -void -ml_api_time_free(pa_time_event* e) +static void +ml_api_time_free(pa_time_event *e) { - trace_info_z("Z %s\n", __func__); + trace_info_f("F %s e=%p\n", __func__, e); + + pa_mainloop *ml = e->mainloop; + g_queue_remove(ml->timed_events_queue, e); + + if (e->destroy_cb) + e->destroy_cb(&ml->api, e, e->userdata); + + g_slice_free(pa_time_event, e); + pa_mainloop_wakeup(ml); } -static -pa_time_event * +/// Comparator function for |timed_events_queue|. Orders events by value of |when| parameter. +static gint +time_event_comparator(gconstpointer a, gconstpointer b, gpointer user_data) +{ + const pa_time_event *te_a = a; + const pa_time_event *te_b = b; + + // First, try to compare seconds. + if (te_a->when.tv_sec < te_b->when.tv_sec) + return -1; + + if (te_a->when.tv_sec > te_b->when.tv_sec) + return 1; + + // If we got here, tv_sec fields are equal. + + // Then, compare microseconds. + if (te_a->when.tv_usec < te_b->when.tv_usec) + return -1; + + if (te_a->when.tv_usec > te_b->when.tv_usec) + return 1; + + // Timestamps are equal. + return 0; +} + +static pa_time_event * ml_api_time_new(pa_mainloop_api *a, const struct timeval *tv, pa_time_event_cb_t cb, void *userdata) { - trace_info_z("Z %s\n", __func__); + trace_info_f("F %s a=%p, tv=%p {%ld, %ld}, cb=%p, userdata=%p\n", __func__, a, tv, + tv ? tv->tv_sec : 0, tv ? tv->tv_usec : 0, cb, userdata); + + pa_mainloop *ml = a->userdata; + pa_time_event *te = g_slice_new0(pa_time_event); + te->enabled = 1; + te->when = tv ? *tv : (struct timeval){}; + te->cb = cb; + te->userdata = userdata; + te->mainloop = ml; - return NULL; + g_queue_insert_sorted(ml->timed_events_queue, te, time_event_comparator, NULL); + + pa_mainloop_wakeup(ml); + return te; } -static -void -ml_api_time_restart(pa_time_event* e, const struct timeval *tv) +static void +ml_api_time_restart(pa_time_event *e, const struct timeval *tv) { - trace_info_z("Z %s\n", __func__); + trace_info_f("F %s e=%p, tv=%p {%ld, %ld}\n", __func__, e, tv, tv ? tv->tv_sec : 0, + tv ? tv->tv_usec : 0); + + pa_mainloop *ml = e->mainloop; + + g_queue_remove(ml->timed_events_queue, e); + + e->enabled = 1; + e->when = tv ? *tv : (struct timeval){}; + + g_queue_insert_sorted(ml->timed_events_queue, e, time_event_comparator, NULL); + + pa_mainloop_wakeup(ml); } -static -void +static void ml_api_time_set_destroy(pa_time_event *e, pa_time_event_destroy_cb_t cb) { - trace_info_z("Z %s\n", __func__); + trace_info_f("F %s e=%p, cb=%p\n", __func__, e, cb); + + e->destroy_cb = cb; } static void @@ -224,6 +282,12 @@ } } +static long +microseconds_till_event(pa_usec_t now, const struct timeval *event_when) +{ + return (uint64_t)event_when->tv_sec * 1000 * 1000 + event_when->tv_usec - now; +} + APULSE_EXPORT int pa_mainloop_dispatch(pa_mainloop *m) @@ -271,11 +335,20 @@ m->fds[0].revents = 0; } - pa_defer_event *de = g_queue_pop_head(m->queue); + pa_usec_t now = pa_rtclock_now(); + pa_time_event *te = g_queue_peek_head(m->timed_events_queue); + while (te && microseconds_till_event(now, &te->when) <= 0) { + if (te->cb && te->enabled) + te->cb(&m->api, te, &te->when, te->userdata); + g_queue_pop_head(m->timed_events_queue); + te = g_queue_peek_head(m->timed_events_queue); + } + + pa_defer_event *de = g_queue_pop_head(m->deferred_events_queue); while (de) { if (de->cb) de->cb(&m->api, de, de->userdata); - de = g_queue_pop_head(m->queue); + de = g_queue_pop_head(m->deferred_events_queue); } return cnt; @@ -287,7 +360,8 @@ { trace_info_f("F %s m=%p\n", __func__, m); - g_queue_free(m->queue); + g_queue_free(m->deferred_events_queue); + g_queue_free(m->timed_events_queue); g_hash_table_unref(m->events_ht); close(m->wakeup_pipe[0]); close(m->wakeup_pipe[1]); @@ -365,7 +439,8 @@ m->api.defer_set_destroy = ml_api_defer_set_destroy; m->api.quit = ml_api_quit; - m->queue = g_queue_new(); + m->deferred_events_queue = g_queue_new(); + m->timed_events_queue = g_queue_new(); m->events_ht = g_hash_table_new(g_direct_hash, g_direct_equal); m->recreate_fds = 1; @@ -382,11 +457,33 @@ { trace_info_f("F %s m=%p\n", __func__, m); + long int timeout = m->timeout; + + pa_time_event *te = g_queue_peek_head(m->timed_events_queue); + if (te) { + pa_usec_t now = pa_rtclock_now(); + long int msecs_till_next_event = microseconds_till_event(now, &te->when) / PA_USEC_PER_MSEC; + + // Ensure delay is non-negative, even if event is already expired. + msecs_till_next_event = MAX(msecs_till_next_event, 0); + + if (timeout < 0) { + // poll() call was supposed to wait for indefinite period of time. + timeout = msecs_till_next_event; + + } else { + timeout = MIN(timeout, msecs_till_next_event); + } + + // |timeout| value should fit int limits. + timeout = MIN(timeout, INT32_MAX); + } + int ret; if (m->poll_func) { - ret = m->poll_func(m->fds, m->nfds, m->timeout, m->poll_func_userdata); + ret = m->poll_func(m->fds, m->nfds, timeout, m->poll_func_userdata); } else { - ret = poll(m->fds, m->nfds, m->timeout); + ret = poll(m->fds, m->nfds, timeout); } return ret; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apulse-0.1.8/src/apulse-stream.c new/apulse-0.1.10/src/apulse-stream.c --- old/apulse-0.1.8/src/apulse-stream.c 2017-03-08 17:38:10.000000000 +0100 +++ new/apulse-0.1.10/src/apulse-stream.c 2017-04-18 17:55:19.000000000 +0200 @@ -30,17 +30,6 @@ #define HAVE_SND_PCM_AVAIL SND_LIB_VERSION >= MAKE_SND_LIB_VERSION(1, 0, 18) -#define CHECK_A(funcname, params) \ - do { \ - int errcode___ = funcname params; \ - if (errcode___ < 0) { \ - trace_error("%s, " #funcname ", %s\n", __func__, \ - snd_strerror(errcode___)); \ - goto err; \ - } \ - } while (0) - - static void deh_stream_state_changed(pa_mainloop_api *api, pa_defer_event *de, void *userdata) @@ -78,7 +67,7 @@ snd_pcm_sframes_t frame_count; size_t frame_size = pa_frame_size(&s->ss); char buf[16 * 1024]; - const size_t buf_size = pa_find_multiple_of(sizeof(buf), frame_size); + const size_t buf_size = pa_find_multiple_of(sizeof(buf), frame_size, 0); int paused = g_atomic_int_get(&s->paused); if (events & (PA_IO_EVENT_INPUT | PA_IO_EVENT_OUTPUT)) { @@ -102,6 +91,64 @@ ret = snd_pcm_recover(s->ph, frame_count, 1); } while (ret == -1 && errno == EINTR && cnt < 5); + switch (snd_pcm_state(s->ph)) { + case SND_PCM_STATE_OPEN: + // Highly unlikely device will be here in this state. But if it is, there is nothing + // can be done. + trace_error( + "Stream '%s' of context '%s' have its associated PCM device in " + "SND_PCM_STATE_OPEN state. Reconfiguration is required, but is not possible at " + "the moment. Giving up.", + s->name ? s->name : "", s->c->name ? s->c->name : ""); + break; + + case SND_PCM_STATE_SETUP: + // There is configuration, but device is not prepared and not started. + snd_pcm_prepare(s->ph); + snd_pcm_start(s->ph); + break; + + case SND_PCM_STATE_PREPARED: + // Device prepared, but not started. + snd_pcm_start(s->ph); + break; + + case SND_PCM_STATE_RUNNING: + // That's the expected state. + break; + + case SND_PCM_STATE_XRUN: + trace_error( + "Stream '%s' of context '%s' have its associated device in SND_PCM_STATE_XRUN " + "state even after xrun recovery.", + s->name ? s->name : "", s->c->name ? s->c->name : ""); + break; + + case SND_PCM_STATE_DRAINING: + trace_error( + "Stream '%s' of context '%s' have its associated device in " + "SND_PCM_STATE_DRAINING state, which is highly unusual.", + s->name ? s->name : "", s->c->name ? s->c->name : ""); + break; + + case SND_PCM_STATE_PAUSED: + // Resume from paused state. + snd_pcm_pause(s->ph, 0); + break; + + case SND_PCM_STATE_SUSPENDED: + // Resume from suspended state. + snd_pcm_resume(s->ph); + break; + + case SND_PCM_STATE_DISCONNECTED: + trace_error( + "Stream '%s' of context '%s' have its associated device in " + "SND_PCM_STATE_DISCONNECTED state. Giving up.", + s->name ? s->name : "", s->c->name ? s->c->name : ""); + break; + } + #if HAVE_SND_PCM_AVAIL frame_count = snd_pcm_avail(s->ph); #else @@ -128,13 +175,14 @@ } else { size_t writable_size = pa_stream_writable_size(s); - if (s->write_cb && writable_size > 0) - s->write_cb(s, writable_size, s->write_cb_userdata); + // Ask client for data, but only if we are ready for at least |minreq| bytes. + if (s->write_cb && writable_size >= s->buffer_attr.minreq) + s->write_cb(s, s->buffer_attr.minreq, s->write_cb_userdata); size_t bytecnt = MIN(buf_size, frame_count * frame_size); bytecnt = ringbuffer_read(s->rb, buf, bytecnt); - pa_apply_volume_multiplier(buf, bytecnt, s->c->sink_volume, &s->ss); + pa_apply_volume_multiplier(buf, bytecnt, s->volume, &s->ss); if (bytecnt == 0) { // application is not ready yet, play silence @@ -182,52 +230,174 @@ { snd_pcm_hw_params_t *hw_params; snd_pcm_sw_params_t *sw_params; - int dir; - unsigned int rate; - const char *dev_name; + int errcode; + const char *device_name; + const char *direction_name; switch (stream_direction) { default: case SND_PCM_STREAM_PLAYBACK: - dev_name = getenv("APULSE_PLAYBACK_DEVICE"); - CHECK_A(snd_pcm_open, (&s->ph, dev_name ? dev_name : "default", stream_direction, 0)); + device_name = getenv("APULSE_PLAYBACK_DEVICE"); + direction_name = "playback"; break; case SND_PCM_STREAM_CAPTURE: - dev_name = getenv("APULSE_CAPTURE_DEVICE"); - CHECK_A(snd_pcm_open, (&s->ph, dev_name ? dev_name : "default", stream_direction, 0)); + device_name = getenv("APULSE_CAPTURE_DEVICE"); + direction_name = "capture"; break; } - CHECK_A(snd_pcm_hw_params_malloc, (&hw_params)); - CHECK_A(snd_pcm_hw_params_any, (s->ph, hw_params)); - CHECK_A(snd_pcm_hw_params_set_access, (s->ph, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)); - CHECK_A(snd_pcm_hw_params_set_format, (s->ph, hw_params, pa_format_to_alsa(s->ss.format))); - CHECK_A(snd_pcm_hw_params_set_rate_resample, (s->ph, hw_params, 1)); - rate = s->ss.rate; - dir = 0; - CHECK_A(snd_pcm_hw_params_set_rate_near, (s->ph, hw_params, &rate, &dir)); - CHECK_A(snd_pcm_hw_params_set_channels, (s->ph, hw_params, s->ss.channels)); - - unsigned int period_time = 20 * 1000; - dir = 1; - CHECK_A(snd_pcm_hw_params_set_period_time_near, (s->ph, hw_params, &period_time, &dir)); - - unsigned int buffer_time = 4 * period_time; - dir = 1; - CHECK_A(snd_pcm_hw_params_set_buffer_time_near, (s->ph, hw_params, &buffer_time, &dir)); - CHECK_A(snd_pcm_hw_params, (s->ph, hw_params)); + if (device_name == NULL) + device_name = "default"; + + char *device_description = g_strdup_printf("%s device \"%s\"", direction_name, device_name); + if (!device_description) { + trace_error("%s: can't allocate memory for device description string\n", __func__); + goto fatal_error; + } + + errcode = snd_pcm_open(&s->ph, device_name, stream_direction, 0); + if (errcode != 0) { + trace_error("%s: can't open %s. Error code %d (%s)\n", __func__, device_description, + errcode, snd_strerror(errcode)); + goto fatal_error; + } + + errcode = snd_pcm_hw_params_malloc(&hw_params); + if (errcode != 0) { + trace_error("%s: can't allocate memory for hw parameters for %s. Error code %d (%s)\n", + __func__, device_description, errcode, snd_strerror(errcode)); + goto fatal_error; + } + + errcode = snd_pcm_hw_params_any(s->ph, hw_params); + if (errcode != 0) { + trace_error("%s: can't get initial hw parameters for %s. Error code %d (%s)\n", __func__, + device_description, errcode, snd_strerror(errcode)); + goto fatal_error; + } + + errcode = snd_pcm_hw_params_set_access(s->ph, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED); + if (errcode != 0) { + trace_error("%s: can't select interleaved mode for %s. Error code %d (%s)\n", __func__, + device_description, errcode, snd_strerror(errcode)); + // TODO: is it worth to support non-interleaved mode? + goto fatal_error; + } + + errcode = snd_pcm_hw_params_set_format(s->ph, hw_params, pa_format_to_alsa(s->ss.format)); + if (errcode != 0) { + snd_pcm_format_t alsa_format = pa_format_to_alsa(s->ss.format); + trace_error("%s: can't set sample format %d (\"%s\") for %s. Error code %d (%s)\n", + __func__, alsa_format, snd_pcm_format_name(alsa_format), device_description, + errcode, snd_strerror(errcode)); + goto fatal_error; + } + + errcode = snd_pcm_hw_params_set_rate_resample(s->ph, hw_params, 1); + if (errcode != 0) { + trace_error("%s: can't enable rate resample for %s. Error code %d (%s)\n", __func__, + device_description, errcode, snd_strerror(errcode)); + // This is not a fatal error. Audio speed will be wrong, but there will be something. + // And it sounds funny. + } + + unsigned int rate = s->ss.rate; + int dir = 0; + + errcode = snd_pcm_hw_params_set_rate_near(s->ph, hw_params, &rate, &dir); + if (errcode != 0) { + trace_error("%s: can't set sample rate for %s. Error code %d (%s)\n", __func__, + device_description, errcode, snd_strerror(errcode)); + goto fatal_error; + } + + trace_info_f("%s: demanded %d Hz sample rate, got %d Hz for %s, dir = %d\n", __func__, + (int)s->ss.rate, (int)rate, device_description, dir); + + if (rate != s->ss.rate) + trace_error("%s: actual sample rate, %d Hz, differs from required %d Hz\n", __func__, + (int)rate, (int)s->ss.rate); + + errcode = snd_pcm_hw_params_set_channels(s->ph, hw_params, s->ss.channels); + if (errcode != 0) { + trace_error("%s: can't set channel count to %d for %s. Error code %d (%s)\n", __func__, + (int)s->ss.channels, device_description, errcode, snd_strerror(errcode)); + // TODO: channel count handling? + goto fatal_error; + } + + const size_t frame_size = pa_frame_size(&s->ss); + snd_pcm_uframes_t requested_period_size = s->buffer_attr.minreq / frame_size; + snd_pcm_uframes_t period_size = requested_period_size; + dir = 1; // Prefer larger period sizes, if exact is not possible. + errcode = snd_pcm_hw_params_set_period_size_near(s->ph, hw_params, &period_size, &dir); + if (errcode != 0) { + trace_error("%s: can't set period size to %d frames for %s. Error code %d (%s)\n", __func__, + (int)requested_period_size, device_description, errcode, snd_strerror(errcode)); + goto fatal_error; + } + + trace_info_f("%s: requested period size of %d frames, got %d frames for %s\n", __func__, + (int)requested_period_size, (int)period_size, device_description); + + // Set up buffer size. Ensure it's at least four times larger than a period size. + snd_pcm_uframes_t requested_buffer_size = s->buffer_attr.tlength / frame_size; + snd_pcm_uframes_t buffer_size = MAX(requested_buffer_size, 4 * period_size); + errcode = snd_pcm_hw_params_set_buffer_size_near(s->ph, hw_params, &buffer_size); + if (errcode != 0) { + trace_error( + "%s: can't set buffer size to %d frames for %s. Error code %d (%s)\n", + __func__, (int)buffer_size, device_description, errcode, snd_strerror(errcode)); + goto fatal_error; + } + + trace_info_f("%s: requested buffer size of %d frames, got %d frames for %s\n", __func__, + (int)requested_buffer_size, (int)buffer_size, device_description); + + errcode = snd_pcm_hw_params(s->ph, hw_params); + if (errcode != 0) { + trace_error("%s: can't apply configured hw parameter block for %s\n", __func__, + device_description); + goto fatal_error; + } + snd_pcm_hw_params_free(hw_params); - CHECK_A(snd_pcm_sw_params_malloc, (&sw_params)); - CHECK_A(snd_pcm_sw_params_current, (s->ph, sw_params)); + errcode = snd_pcm_sw_params_malloc(&sw_params); + if (errcode != 0) { + trace_error("%s: can't allocate memory for sw parameters for %s\n", __func__, + device_description); + goto fatal_error; + } + + errcode = snd_pcm_sw_params_current(s->ph, sw_params); + if (errcode != 0) { + trace_error("%s: can't acquire current sw parameters for %s\n", __func__, + device_description); + goto fatal_error; + } + + errcode = snd_pcm_sw_params_set_avail_min(s->ph, sw_params, period_size); + if (errcode != 0) { + trace_error("%s: can't set avail min for %s\n", __func__, device_description); + goto fatal_error; + } - const snd_pcm_uframes_t period_size = (uint64_t)period_time * rate / (1000 * 1000); - CHECK_A(snd_pcm_sw_params_set_avail_min, (s->ph, sw_params, period_size)); // no period event requested - CHECK_A(snd_pcm_sw_params, (s->ph, sw_params)); + + errcode = snd_pcm_sw_params(s->ph, sw_params); + if (errcode != 0) { + trace_error("%s: can't apply sw parameters for %s\n", __func__, device_description); + goto fatal_error; + } + snd_pcm_sw_params_free(sw_params); - CHECK_A(snd_pcm_prepare, (s->ph)); + errcode = snd_pcm_prepare(s->ph); + if (errcode != 0) { + trace_error("%s: can't prepare PCM device to use for %s\n", __func__, device_description); + goto fatal_error; + } int nfds = snd_pcm_poll_descriptors_count(s->ph); struct pollfd *fds = calloc(nfds, sizeof(struct pollfd)); @@ -248,8 +418,19 @@ pa_stream_ref(s); s->c->mainloop_api->defer_new(s->c->mainloop_api, deh_stream_first_readwrite_callback, s); + g_free(device_description); return 0; -err: + +fatal_error: + trace_error( + "%s: failed to open ALSA device. Apulse does no resampling or format conversion, leaving " + "that task to ALSA plugins. Ensure that selected device is capable of playing a particular " + "sample format at a particular rate. They have to be supported by either hardware " + "directly, or by \"plug\" and \"dmix\" ALSA plugins which will perform required " + "conversions on CPU.\n", + __func__); + + g_free(device_description); return -1; } @@ -257,13 +438,16 @@ int pa_stream_begin_write(pa_stream *p, void **data, size_t *nbytes) { - trace_info_f("F %s p=%p\n", __func__, p); + trace_info_f("F %s p=%p nbytes=%p(%" PRIu64 ")\n", __func__, p, nbytes, + (uint64_t)(nbytes ? *nbytes : 0)); free(p->write_buffer); if (*nbytes == (size_t)-1) *nbytes = 8192; + *nbytes = pa_find_multiple_of(*nbytes, pa_frame_size(&p->ss), 0); + p->write_buffer = malloc(*nbytes); if (!p->write_buffer) @@ -286,6 +470,72 @@ return 0; } +static void +stream_adjust_buffer_attrs(pa_stream *s, const pa_buffer_attr *attr) +{ + pa_buffer_attr *ba = &s->buffer_attr; + const size_t frame_size = pa_frame_size(&s->ss); + + if (attr) { + *ba = *attr; + } else { + // If client passed NULL, all parameters have default values. + ba->maxlength = (uint32_t)-1; + ba->tlength = (uint32_t)-1; + ba->prebuf = (uint32_t)-1; + ba->minreq = (uint32_t)-1; + ba->fragsize = (uint32_t)-1; + } + + // Adjust default values. + // Overall buffer length. + if (ba->maxlength == (uint32_t)-1) + ba->maxlength = 4 * 1024 * 1024; + + if (ba->maxlength == 0) + ba->maxlength = frame_size; + + // Target length of a buffer. + if (ba->tlength == (uint32_t)-1) + ba->tlength = pa_usec_to_bytes(2 * 1000 * 1000, &s->ss); + + if (ba->tlength == 0) + ba->tlength = frame_size; + + ba->tlength = MIN(ba->tlength, ba->maxlength); + + // Minimum request (playback). + if (ba->minreq == (uint32_t)-1) { + ba->minreq = pa_usec_to_bytes(20 * 1000, &s->ss); + ba->minreq = MIN(ba->minreq, ba->tlength / 4); + } + + if (ba->minreq == 0) + ba->minreq = frame_size; + + // Fragment size (recording). + if (ba->fragsize == (uint32_t)-1) { + ba->fragsize = pa_usec_to_bytes(20 * 1000, &s->ss); + } + + if (ba->fragsize == 0) + ba->fragsize = frame_size; + + // Pre-buffering. + if (ba->prebuf == (uint32_t)-1) + ba->prebuf = ba->tlength - ba->minreq; + + if (ba->prebuf > ba->tlength - ba->minreq) + ba->prebuf = ba->tlength - ba->minreq; + + // Ensure values are all multiple of |frame_size|. + ba->maxlength = pa_find_multiple_of(ba->maxlength, frame_size, 1); + ba->tlength = pa_find_multiple_of(ba->tlength, frame_size, 1); + ba->prebuf = pa_find_multiple_of(ba->prebuf, frame_size, 1); + ba->minreq = pa_find_multiple_of(ba->minreq, frame_size, 1); + ba->fragsize = pa_find_multiple_of(ba->fragsize, frame_size, 1); +} + APULSE_EXPORT int pa_stream_connect_playback(pa_stream *s, const char *dev, const pa_buffer_attr *attr, @@ -298,15 +548,7 @@ g_free(s_attr); s->direction = PA_STREAM_PLAYBACK; - if (attr) { - s->buffer_attr = *attr; - } else { - s->buffer_attr.maxlength = (uint32_t)-1; - s->buffer_attr.tlength = (uint32_t)-1; - s->buffer_attr.prebuf = (uint32_t)-1; - s->buffer_attr.minreq = (uint32_t)-1; - s->buffer_attr.fragsize = (uint32_t)-1; - } + stream_adjust_buffer_attrs(s, attr); if (do_connect_pcm(s, SND_PCM_STREAM_PLAYBACK) != 0) goto err; @@ -596,6 +838,9 @@ s->rb = ringbuffer_new(72 * 1024); // TODO: figure out size s->peek_buffer = malloc(s->rb->end - s->rb->start); + for (uint32_t k = 0; k < PA_CHANNELS_MAX; k++) + s->volume[k] = PA_VOLUME_NORM; + return s; } @@ -759,7 +1004,7 @@ if (writable_size < limit) writable_size = 0; - return pa_find_multiple_of(writable_size, pa_frame_size(&s->ss)); + return pa_find_multiple_of(writable_size, pa_frame_size(&s->ss), 0); } APULSE_EXPORT @@ -769,7 +1014,7 @@ trace_info_f("F %s s=%p\n", __func__, s); size_t readable_size = ringbuffer_readable_size(s->rb); - return pa_find_multiple_of(readable_size, pa_frame_size(&s->ss)); + return pa_find_multiple_of(readable_size, pa_frame_size(&s->ss), 0); } APULSE_EXPORT @@ -810,15 +1055,7 @@ g_free(s_attr); s->direction = PA_STREAM_RECORD; - if (attr) { - s->buffer_attr = *attr; - } else { - s->buffer_attr.maxlength = (uint32_t)-1; - s->buffer_attr.tlength = (uint32_t)-1; - s->buffer_attr.prebuf = (uint32_t)-1; - s->buffer_attr.minreq = (uint32_t)-1; - s->buffer_attr.fragsize = (uint32_t)-1; - } + stream_adjust_buffer_attrs(s, attr); if (do_connect_pcm(s, SND_PCM_STREAM_CAPTURE) != 0) goto err; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apulse-0.1.8/src/apulse.h new/apulse-0.1.10/src/apulse.h --- old/apulse-0.1.8/src/apulse.h 2017-03-08 17:38:10.000000000 +0100 +++ new/apulse-0.1.10/src/apulse.h 2017-04-18 17:55:19.000000000 +0200 @@ -46,7 +46,6 @@ int next_stream_idx; GHashTable *streams_ht; pa_volume_t source_volume[PA_CHANNELS_MAX]; - pa_volume_t sink_volume[PA_CHANNELS_MAX]; }; struct pa_io_event { @@ -62,8 +61,9 @@ struct pa_mainloop { pa_mainloop_api api; - GQueue *queue; - GHashTable *events_ht; + GQueue *deferred_events_queue; + GQueue *timed_events_queue; + GHashTable *events_ht; ///< a set of (pa_io_event *) struct pollfd *fds; nfds_t nfds; int recreate_fds; ///< 1 if fds array needs to be recreated from events_ht @@ -114,6 +114,7 @@ size_t peek_buffer_data_len; void *write_buffer; volatile int paused; + pa_volume_t volume[PA_CHANNELS_MAX]; }; struct pa_operation { @@ -123,6 +124,7 @@ pa_sink_info_cb_t sink_info_cb; pa_context_success_cb_t context_success_cb; pa_server_info_cb_t server_info_cb; + pa_source_info_cb_t source_info_cb; void *cb_userdata; pa_mainloop_api *api; @@ -148,6 +150,14 @@ pa_mainloop *mainloop; }; +struct pa_time_event { + int enabled; + struct timeval when; + pa_time_event_cb_t cb; + void *userdata; + pa_mainloop *mainloop; + pa_time_event_destroy_cb_t destroy_cb; +}; pa_operation * pa_operation_new(pa_mainloop_api *api, void (*handler)(pa_operation *op)); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apulse-0.1.8/src/notimplemented.c new/apulse-0.1.10/src/notimplemented.c --- old/apulse-0.1.8/src/notimplemented.c 2017-03-08 17:38:10.000000000 +0100 +++ new/apulse-0.1.10/src/notimplemented.c 2017-04-18 17:55:19.000000000 +0200 @@ -860,20 +860,6 @@ } APULSE_EXPORT -pa_operation* pa_context_get_sink_info_by_index(pa_context *c, uint32_t idx, pa_sink_info_cb_t cb, void *userdata) -{ - trace_info_z("Z %s\n", __func__); - return NULL; -} - -APULSE_EXPORT -pa_operation* pa_context_get_sink_info_list(pa_context *c, pa_sink_info_cb_t cb, void *userdata) -{ - trace_info_z("Z %s\n", __func__); - return NULL; -} - -APULSE_EXPORT pa_operation* pa_context_set_sink_volume_by_index(pa_context *c, uint32_t idx, const pa_cvolume *volume, pa_context_success_cb_t cb, void *userdata) { trace_info_z("Z %s\n", __func__); @@ -927,20 +913,6 @@ { trace_info_z("Z %s\n", __func__); return NULL; -} - -APULSE_EXPORT -pa_operation* pa_context_get_source_info_by_name(pa_context *c, const char *name, pa_source_info_cb_t cb, void *userdata) -{ - trace_info_z("Z %s\n", __func__); - return NULL; -} - -APULSE_EXPORT -pa_operation* pa_context_get_source_info_list(pa_context *c, pa_source_info_cb_t cb, void *userdata) -{ - trace_info_z("Z %s\n", __func__); - return NULL; } APULSE_EXPORT diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apulse-0.1.8/src/trace.c new/apulse-0.1.10/src/trace.c --- old/apulse-0.1.8/src/trace.c 2017-03-08 17:38:10.000000000 +0100 +++ new/apulse-0.1.10/src/trace.c 2017-04-18 17:55:19.000000000 +0200 @@ -62,17 +62,31 @@ void trace_error(const char *fmt, ...) { + static int stdout_tested = 0; + static int stdout_is_a_tty = 0; + pthread_mutex_lock(&lock); + + if (!stdout_tested) { + stdout_is_a_tty = isatty(1); + stdout_tested = 1; + } + va_list args; fprintf(stderr, "[apulse] [error] "); va_start(args, fmt); vfprintf(stderr, fmt, args); va_end(args); - fprintf(stdout, "[apulse] [error] "); - va_start(args, fmt); - vfprintf(stdout, fmt, args); - va_end(args); + if (!stdout_is_a_tty) { + // If stdout is redirected to a file, make sure it also gets error messages. + // That helps to figure out where errors were. + fprintf(stdout, "[apulse] [error] "); + va_start(args, fmt); + vfprintf(stdout, fmt, args); + va_end(args); + } + pthread_mutex_unlock(&lock); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apulse-0.1.8/src/util.c new/apulse-0.1.10/src/util.c --- old/apulse-0.1.8/src/util.c 2017-03-08 17:38:10.000000000 +0100 +++ new/apulse-0.1.10/src/util.c 2017-04-18 17:55:19.000000000 +0200 @@ -84,9 +84,13 @@ } size_t -pa_find_multiple_of(size_t number, size_t multiple_of) +pa_find_multiple_of(size_t number, size_t multiple_of, int towards_larger_numbers) { - return number - (number % multiple_of); + if (multiple_of == 0) + return number; + + size_t n = towards_larger_numbers ? (number + multiple_of - 1) : number; + return n - (n % multiple_of); } void @@ -131,10 +135,10 @@ case PA_SAMPLE_S16NE: while (p < last) { for (uint32_t k = 0; k < channels && p < last; k++) { - uint16_t sample; + int16_t sample; memcpy(&sample, p, sizeof(sample)); float sample_scaled = sample * fvol[k]; - sample = CLAMP(sample_scaled, 0.0, 65535.0); + sample = CLAMP(sample_scaled, -32768.0, 32767.0); memcpy(p, &sample, sizeof(sample)); p += sizeof(sample); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apulse-0.1.8/src/util.h new/apulse-0.1.10/src/util.h --- old/apulse-0.1.8/src/util.h 2017-03-08 17:38:10.000000000 +0100 +++ new/apulse-0.1.10/src/util.h 2017-04-18 17:55:19.000000000 +0200 @@ -37,7 +37,7 @@ /// finds largest number that is less or equal than |number| and multiple of |multiple_of|. size_t -pa_find_multiple_of(size_t number, size_t multiple_of); +pa_find_multiple_of(size_t number, size_t multiple_of, int towards_larger_numbers); void pa_apply_volume_multiplier(void *buf, size_t sz, const pa_volume_t volume[PA_CHANNELS_MAX],