~schnouki/pustule

ab68932a49bb36f7c35e9ddba46426b82ca5055c — Thomas Jost 7 years ago d3a752c
Run the PulseAudio event loop in another thread

And avoir running Guile code from the PulseAudio thread. It tends to
break things.

It is therefore necessary to run the handlers functions from the main
thread (or any other Guile thread), so there's a very simple mutex-based
event loop in pustule_run() which does just that. So far it seems to
work :)

There are still some quirks regarding memory usage and the Guile GC;
hopefully more testing will reveal what the problems are.
6 files changed, 174 insertions(+), 39 deletions(-)

M Makefile
A events.c
M guile.c
M main.c
M pulse.c
M pustule.h
M Makefile => Makefile +2 -2
@@ 7,10 7,10 @@ LDFLAGS ?= -O2
DEPS_CFLAGS  = -Wall `pkg-config --cflags guile-2.0 libpulse`
DEPS_LDFLAGS = -Wall `pkg-config --libs guile-2.0 libpulse`

pustule: main.o pulse.o guile.o
pustule: events.o guile.o main.o pulse.o
	$(CC) -o $@ $^ $(LDFLAGS) $(DEPS_LDFLAGS)

%.o: %.c
%.o: %.c pustule.h
	$(CC) -c -o $@ $< $(CFLAGS) $(DEPS_CFLAGS)

main.o: docopt.c

A events.c => events.c +86 -0
@@ 0,0 1,86 @@
/**
 * pustule
 * Copyright 2014 Thomas Jost <schnouki@schnouki.net>
 *
 * This file is part of pustule.
 *
 * pustule 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 3 of the License, or (at your option) any later
 * version.
 *
 * pustule 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
 * pustule. If not, see <http://www.gnu.org/licenses/>.
 */

#define _GNU_SOURCE
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <libguile.h>

#include "pustule.h"

/* Event queue structure */
typedef struct _event_queue_entry_t {
    pustule_sink_event_t* event;
    struct _event_queue_entry_t* next;
} event_queue_entry_t;

/* The event queue itself, with its mutex and its condition variable */
static event_queue_entry_t* event_queue_first = NULL;
static event_queue_entry_t* event_queue_last = NULL;
static pthread_mutex_t event_queue_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t event_queue_cond = PTHREAD_COND_INITIALIZER;

/* Pseudo event loop */
void pustule_run() {
    pthread_mutex_lock(&event_queue_mutex);
    while (1) {
        pthread_cond_wait(&event_queue_cond, &event_queue_mutex);
        while (event_queue_first) {
            event_queue_entry_t* first = event_queue_first;
            pustule_sink_event_t* event = first->event;
            event_queue_first = first->next;
            pthread_mutex_unlock(&event_queue_mutex);

            SCM index = scm_from_uint32(event->index);
            if ((event->type == SINK_ADDED) && scm_is_true(sink_added_callback)) {
                SCM info = scm_from_sink_event(event);
                scm_call_2(sink_added_callback, index, info);
                if (event->proplist)
                    pa_proplist_free(event->proplist);
                free(event->name);
                free(event->driver);
            }
            else if ((event->type == SINK_REMOVED) && scm_is_true(sink_removed_callback)) {
                scm_call_1(sink_removed_callback, index);
            }
            free(event);
            pthread_mutex_lock(&event_queue_mutex);
        }
    }
}

/* Handle sink events from PulseAudio */
void pustule_handle_sink_event(pustule_sink_event_t* event) {
    pthread_mutex_lock(&event_queue_mutex);

    event_queue_entry_t* entry = scm_gc_malloc_pointerless(sizeof(event_queue_entry_t), "event queue entry");
    entry->event = event;
    entry->next = NULL;
    if (event_queue_last)
        event_queue_last->next = entry;
    event_queue_last = entry;
    if (!event_queue_first)
        event_queue_first = entry;

    pthread_cond_broadcast(&event_queue_cond);
    pthread_mutex_unlock(&event_queue_mutex);
}

M guile.c => guile.c +28 -0
@@ 51,3 51,31 @@ SCM pustule_set_volume(SCM idx, SCM volume) {
    pa_operation_unref(op);
    return volume;
}

/* Convert a sink event to an association list */
SCM scm_from_sink_event(pustule_sink_event_t* event) {
    SCM info = SCM_EOL;
    info = scm_acons(scm_from_utf8_string("name"),            scm_from_utf8_string(event->name),                info);
    info = scm_acons(scm_from_utf8_string("driver"),          scm_from_utf8_string(event->driver),              info);
    info = scm_acons(scm_from_utf8_string("muted"),           event->mute ? SCM_BOOL_T : SCM_BOOL_F,            info);
    info = scm_acons(scm_from_utf8_string("volume_writable"), event->volume_writable ? SCM_BOOL_T : SCM_BOOL_F, info);
    if (event->has_volume) {
        double vol = ((double) pa_cvolume_avg(&(event->volume))) / PA_VOLUME_NORM;
        info = scm_acons(scm_from_utf8_string("volume"), scm_from_double(vol), info);

        vol = pa_sw_volume_to_dB(pa_cvolume_avg(&(event->volume)));
        info = scm_acons(scm_from_utf8_string("volume_db"), scm_from_double(vol), info);
    }
    else {
        info = scm_acons(scm_from_utf8_string("volume"), SCM_BOOL_F, info);
    }
    if (event->proplist && !pa_proplist_isempty(event->proplist)) {
        void* state = NULL;
        const char* key;
        while ((key = pa_proplist_iterate(event->proplist, &state)) != NULL) {
            const char* value = pa_proplist_gets(event->proplist, key);
            info = scm_acons(scm_from_utf8_string(key), scm_from_utf8_string(value), info);
        }
    }
    return info;
}

M main.c => main.c +10 -5
@@ 107,14 107,15 @@ static void* inner_main(void* data) {
        free(spawn_server);
    }

    /* Start the libpulse main loop */
    pa_mainloop* loop = pa_mainloop_new();
    /* Create the libpulse threaded main loop */
    pa_threaded_mainloop* loop = pa_threaded_mainloop_new();
    if (!loop) {
        fprintf(stderr, "Can't create main loop\n");
        return NULL;
    }
    paloop = loop;

    pa_mainloop_api* api = pa_mainloop_get_api(loop);
    pa_mainloop_api* api = pa_threaded_mainloop_get_api(loop);
    if (!api) {
        fprintf(stderr, "Can't get main loop API\n");
        return NULL;


@@ 129,8 130,11 @@ static void* inner_main(void* data) {

    /* Start the libpulse main loop */
    pa_context_set_state_callback(ctx, state_callback, NULL);
    pa_context_connect(ctx, NULL, PA_CONTEXT_NOFLAGS, NULL);
    pa_mainloop_run(loop, NULL);
    pa_context_connect(pactx, NULL, PA_CONTEXT_NOFLAGS, NULL);
    pa_threaded_mainloop_start(loop);

    /* Run the pseudo event loop */
    pustule_run();
    return NULL;
}



@@ 143,5 147,6 @@ int main(int argc, char** argv) {

    /* Initialize Guile */
    scm_with_guile(inner_main, &args);

    return 0;
}

M pulse.c => pulse.c +27 -32
@@ 27,8 27,9 @@

#include "pustule.h"

/* Global PulseAudio context */
/* Global PulseAudio context and main loop */
pa_context* pactx = NULL;
pa_threaded_mainloop* paloop = NULL;

/* PulseAudio state changed -- query for infos and subscribe to new events */
void state_callback(pa_context* ctx, void* userdata) {


@@ 51,40 52,34 @@ void sink_event_callback(pa_context* ctx, pa_subscription_event_type_t t, uint32
    }
    else if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
        // Sink removed: directly call the corresponding callback
        if (scm_is_true(sink_removed_callback)) {
            SCM my_idx = scm_from_uint32(idx);
            scm_call_1(sink_removed_callback, my_idx);
        }
        pustule_sink_event_t* event = malloc(sizeof(pustule_sink_event_t));
        if (!event) abort();

        event->type = SINK_REMOVED;
        event->index = idx;
        pustule_handle_sink_event(event);
    }
}

/* New sink available (or infos about an existing sink): call the added callback */
/* New sink available (or infos about an existing sink): defer a call the added callback */
void sink_info_callback(pa_context* ctx, const pa_sink_input_info* i, int eol, void* userdata) {
    if (!i || !scm_is_true(sink_added_callback)) {
        return;
    }
    SCM info = SCM_EOL;
    info = scm_acons(scm_from_utf8_string("name"),            scm_from_utf8_string(i->name),                info);
    info = scm_acons(scm_from_utf8_string("driver"),          scm_from_utf8_string(i->driver),              info);
    info = scm_acons(scm_from_utf8_string("muted"),           i->mute ? SCM_BOOL_T : SCM_BOOL_F,           info);
    info = scm_acons(scm_from_utf8_string("volume_writable"), i->volume_writable ? SCM_BOOL_T : SCM_BOOL_F, info);
    if (i->has_volume) {
        double vol = ((double) pa_cvolume_avg(&(i->volume))) / PA_VOLUME_NORM;
        info = scm_acons(scm_from_utf8_string("volume"), scm_from_double(vol), info);
    if (!i) return;

        vol = pa_sw_volume_to_dB(pa_cvolume_avg(&(i->volume)));
        info = scm_acons(scm_from_utf8_string("volume_db"), scm_from_double(vol), info);
    }
    else {
        info = scm_acons(scm_from_utf8_string("volume"), SCM_BOOL_F, info);
    }
    if (i->proplist && !pa_proplist_isempty(i->proplist)) {
        void* state = NULL;
        const char* key;
        while ((key = pa_proplist_iterate(i->proplist, &state)) != NULL) {
            const char* value = pa_proplist_gets(i->proplist, key);
            info = scm_acons(scm_from_utf8_string(key), scm_from_utf8_string(value), info);
        }
    }
    scm_call_2(sink_added_callback, scm_from_uint32(i->index), info);
    pustule_sink_event_t* event = malloc(sizeof(pustule_sink_event_t));
    if (!event) abort();

    event->type = SINK_ADDED;
    event->index = i->index;
    event->name = strdup(i->name);
    event->driver = strdup(i->name);
    event->mute = i->mute;
    event->volume_writable = i->volume_writable;
    event->has_volume = i->has_volume;
    event->volume = i->volume;

    event->proplist = NULL;
    if (i->proplist)
        event->proplist = pa_proplist_copy(i->proplist);

    pustule_handle_sink_event(event);
}

M pustule.h => pustule.h +21 -0
@@ 25,6 25,19 @@
#include <libguile.h>
#include <pulse/pulseaudio.h>

/* Data structures */
typedef struct _pustule_sink_event_t {
    enum {SINK_ADDED, SINK_REMOVED} type;
    uint32_t index;
    char* name;
    char* driver;
    int mute;
    int volume_writable;
    int has_volume;
    pa_cvolume volume;
    pa_proplist* proplist;
} pustule_sink_event_t;

/* PulseAudio callbacks */
void sink_event_callback(pa_context* ctx, pa_subscription_event_type_t t, uint32_t idx, void* userdata);
void sink_info_callback(pa_context* ctx, const pa_sink_input_info* i, int eol, void* userdata);


@@ 34,8 47,16 @@ void state_callback(pa_context* ctx, void* userdata);
SCM pustule_set_handlers(SCM sink_added, SCM sink_removed);
SCM pustule_set_volume(SCM idx, SCM volume);

/* Guile helpers */
SCM scm_from_sink_event(pustule_sink_event_t* event);

/* Pustule event loop */
void pustule_run();
void pustule_handle_sink_event(pustule_sink_event_t* event);

/* Global variables */
extern pa_context* pactx;
extern pa_threaded_mainloop* paloop;
extern SCM sink_added_callback;
extern SCM sink_removed_callback;