~schnouki/pustule

1ac5d35054f9c29419833edd84a766a7cb761417 — Thomas Jost 1 year, 4 months ago ffcebe1
Rewrite the whole thing using Lua

No more Guile, no more threads. Pustule is now single-threaded and uses Lua.
11 files changed, 879 insertions(+), 707 deletions(-)

M Makefile
M README.md
A dyn_array.c
M events.c
D guile.c
A lua_mod.c
M main.c
M pulse.c
M pustule.h
A pustule.lua
D pustule.scm
M Makefile => Makefile +3 -3
@@ 3,10 3,10 @@ all: pustule
CFLAGS ?= -O2
LDFLAGS ?= -O2

DEPS_CFLAGS  = -Wall `pkg-config --cflags guile-2.0 libpulse` -DLOG_USE_COLOR
DEPS_LDFLAGS = -Wall `pkg-config --libs guile-2.0 libpulse`
DEPS_CFLAGS  = -Wall `pkg-config --cflags lua libpulse` -DLOG_USE_COLOR
DEPS_LDFLAGS = -Wall `pkg-config --libs lua libpulse`

pustule: events.o guile.o main.o pulse.o argparse/argparse.o log.o
pustule: dyn_array.o events.o lua_mod.o main.o pulse.o argparse/argparse.o log.o
	$(CC) -o $@ $^ $(LDFLAGS) $(DEPS_LDFLAGS)

%.o: %.c pustule.h

M README.md => README.md +10 -40
@@ 3,7 3,7 @@ pustule

pustule is a rule-based volume control for [PulseAudio][].

By writing some simple rules in [Scheme][], it is possible to easily control the
By writing some simple rules in [Lua][], it is possible to easily control the
volume of all the applications that use PulseAudio to play sound on a computer.

The default configuration file, provided with pustule, shows some of these


@@ 11,9 11,6 @@ possibilities:

- music players have their volume set to 50%
- video players and web browsers have their volume set to 80%
- notifications from the Gajim IM client are played at 50% for
  connection/disconnections, 75% for sent messages, and 100% for received
  messages
- other sounds are played at 60%




@@ 29,58 26,34 @@ Tech
pustule is written in C. It's very lightweight and has few dependencies:

- [PulseAudio][] (specifically libpulse), as it acts as a PulseAudio client
- [Guile][], an interpreter and compiler for the [Scheme][] programming language
  that is part of the [GNU project][].
- [Lua][], a lightweight, extensible programming language.


Installation
--------------

```sh
git clone git://github.com/Schnouki/pustule.git
git clone https://git.sr.ht/~schnouki/pustule
cd pustule
git submodule update --init
make
```

The default configuration file is `pustule.scm`. It's actually not a simple
configuration file, but a Scheme script, as documented below.
The default configuration file is `pustule.lua`. It's actually not a simple
configuration file, but a Lua script, as documented below.

```sh
./pustule --config pustule.scm
./pustule --config pustule.lua
```

If you don't add the `--config` option, pustule will try to load
`~/.config/pustule.scm`.
`~/.config/pustule.lua`.


Configuration
-------------

pustule defines two hooks that you can use from the configuration file:

-   **pustule-input-added-hook**

    Called when an input is added to PulseAudio (for instance when a new
    application starts). Each function in the hook is called with two arguments:
    the input index (an integer) and an alist of the properties of the input.

-   **pustule-input-removed-hook**

    Called when an input is removed from PulseAudio (playback stops, application
    closed…). Each function is called with a single argument: the input index.


The following Scheme functions are available from the configuration file:

-   **pustule-set-volume** *input-idx* *volume*

    Sets the volume of the input whose index is *input-idx* (an integer) to
    *volume* (a number between 0 and 1).

If you like to hack Scheme with a running REPL (but who doesn't? :wink:), you
can start one listening on a local TCP port by starting pustule with the
`--listen PORT` option. (Emacs users: use this with [Geiser][], you won't regret
it!)
TODO.


License


@@ 88,8 61,5 @@ License

GPLv3+

[GNU Project]: http://www.gnu.org/
[Geiser]: http://www.nongnu.org/geiser/
[Guile]: http://www.gnu.org/software/guile/
[Lua]: https://www.lua.org/
[PulseAudio]: http://www.freedesktop.org/wiki/Software/PulseAudio/
[Scheme]: http://en.wikipedia.org/wiki/Scheme_(programming_language)

A dyn_array.c => dyn_array.c +79 -0
@@ 0,0 1,79 @@
/**
 * pustule
 * Copyright 2014-2020 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 <stdbool.h>
#include <stdlib.h>

#include "log.h"
#include "pustule.h"

#define DEFAULT_SIZE 4
#define DEFAULT_SIZE_INCR 4


void array_init(array_t* arr) {
    intptr_t* ptr = malloc(sizeof(intptr_t) * DEFAULT_SIZE);
    if (!ptr) {
        log_fatal("Could not allocate memory");
        exit(1);
    }

    arr->capacity = DEFAULT_SIZE;
    arr->size = 0;
    arr->array = ptr;
}


void array_push(array_t* arr, intptr_t value) {
    if (arr->size >= arr->capacity) {
        intptr_t* ptr = realloc(arr->array, sizeof(intptr_t) * (arr->capacity + DEFAULT_SIZE_INCR));
        if (!ptr) {
            log_fatal("Could not reallocate memory");
            exit(1);
        }
        arr->array = ptr;
        arr->capacity += DEFAULT_SIZE_INCR;
    }

    arr->array[arr->size++] = value;
}

bool array_find(array_t* arr, intptr_t value, size_t* idx) {
    for (size_t i = 0; i < arr->size; i++) {
        if (arr->array[i] == value) {
            if (idx)
                *idx = i;
            return true;
        }
    }
    return false;
}

void array_remove(array_t* arr, intptr_t value) {
    size_t idx;
    bool found = array_find(arr, value, &idx);
    if (!found)
        return;

    for (size_t i = idx; i < arr->size - 1; i++) {
        arr->array[i] = arr->array[i+1];
    }
    arr->size -= 1;
}

M events.c => events.c +90 -84
@@ 1,6 1,6 @@
/**
 * pustule
 * Copyright 2014, 2016 Thomas Jost <schnouki@schnouki.net>
 * Copyright 2014-2020 Thomas Jost <schnouki@schnouki.net>
 *
 * This file is part of pustule.
 *


@@ 23,94 23,100 @@
#include <stdlib.h>
#include <string.h>

#include <libguile.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

#include "log.h"
#include "pustule.h"

/* Event queue structure */
typedef struct _event_queue_entry_t {
    pustule_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_event_t* event = first->event;
            event_queue_first = first->next;
            if (!event)
                continue;

            pustule_input_event_t* iev = &event->input_event;
            pustule_device_event_t* dev = &event->device_event;

            pthread_mutex_unlock(&event_queue_mutex);

            // Input events
            if (event->type == INPUT_ADDED) {
                if (scm_is_false(scm_hook_empty_p(input_added_hook))) {
                    SCM index = scm_from_uint32(iev->index);
                    SCM info = scm_from_input_event(iev);
                    SCM args = scm_list_2(index, info);
                    scm_run_hook(input_added_hook, args);
                }
                if (iev->proplist)
                    pa_proplist_free(iev->proplist);
                free(iev->name);
                free(iev->driver);
            }
            else if ((event->type == INPUT_REMOVED) && scm_is_false(scm_hook_empty_p(input_removed_hook))) {
                SCM index = scm_from_uint32(iev->index);
                scm_run_hook(input_removed_hook, scm_list_1(index));
            }

            // Device events
            else if (event->type == DEVICE_ADDED) {
                if (scm_is_false(scm_hook_empty_p(device_added_hook))) {
                    SCM index = scm_from_uint32(dev->index);
                    SCM info = scm_from_device_event(dev);
                    SCM args = scm_list_2(index, info);
                    scm_run_hook(device_added_hook, args);
                }
                if (dev->proplist)
                    pa_proplist_free(dev->proplist);
                free(dev->name);
                free(dev->description);
            }
            else if ((event->type == DEVICE_REMOVED) && scm_is_false(scm_hook_empty_p(device_removed_hook))) {
                SCM index = scm_from_uint32(dev->index);
                scm_run_hook(device_removed_hook, scm_list_1(index));
            }

            free(event);
            pthread_mutex_lock(&event_queue_mutex);
        }

/* Call Lua callbacks */
static void handle_input_added(pustule_input_event_t* iev) {
    log_debug("Handling input_added event with %d listeners", input_added_listeners.size);
    if (input_added_listeners.size == 0)
        return;

    for (size_t cb_idx = 0; cb_idx < input_added_listeners.size; cb_idx++) {
        lua_Integer cb_ref = input_added_listeners.array[cb_idx];

        lua_rawgeti(g_L, LUA_REGISTRYINDEX, cb_ref);  // Push callback to the stack
        lua_pushinteger(g_L, iev->index);  // Push 1st argument (index) to the stack
        pustule_push_input_event(g_L, iev);  // Push 2nd argument (event table) to the stack
        lua_call(g_L, 2, 0);  // Call the function with 2 arguments and no result
    }
}
static void handle_input_removed(pustule_input_event_t* iev) {
    log_debug("Handling input_removed event with %d listeners", input_removed_listeners.size);
    if (input_removed_listeners.size == 0)
        return;

    for (size_t cb_idx = 0; cb_idx < input_removed_listeners.size; cb_idx++) {
        lua_Integer cb_ref = input_removed_listeners.array[cb_idx];

        lua_rawgeti(g_L, LUA_REGISTRYINDEX, cb_ref);  // Push callback to the stack
        lua_pushinteger(g_L, iev->index);  // Push 1st argument (index) to the stack
        lua_call(g_L, 1, 0);  // Call the function with 1 argument and no result
    }
}

static void handle_device_added(pustule_device_event_t* dev) {
    log_debug("Handling device_added event with %d listeners", device_added_listeners.size);
    if (device_added_listeners.size == 0)
        return;

    for (size_t cb_idx = 0; cb_idx < device_added_listeners.size; cb_idx++) {
        lua_Integer cb_ref = device_added_listeners.array[cb_idx];

        lua_rawgeti(g_L, LUA_REGISTRYINDEX, cb_ref);  // Push callback to the stack
        lua_pushinteger(g_L, dev->index);  // Push 1st argument (index) to the stack
        pustule_push_device_event(g_L, dev);  // Push 2nd argument (event table) to the stack
        lua_call(g_L, 2, 0);  // Call the function with 2 arguments and no result
    }
}
static void handle_device_removed(pustule_device_event_t* dev) {
    log_debug("Handling device_removed event with %d listeners", device_removed_listeners.size);
    if (device_removed_listeners.size == 0)
        return;

/* Handle input events from PulseAudio */
    for (size_t cb_idx = 0; cb_idx < device_removed_listeners.size; cb_idx++) {
        lua_Integer cb_ref = device_removed_listeners.array[cb_idx];

        lua_rawgeti(g_L, LUA_REGISTRYINDEX, cb_ref);  // Push callback to the stack
        lua_pushinteger(g_L, dev->index);  // Push 1st argument (index) to the stack
        lua_call(g_L, 1, 0);  // Call the function with 1 argument and no result
    }
}


/* Handle events from PulseAudio */
void pustule_handle_event(pustule_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);
    pustule_input_event_t* iev = &event->input_event;
    pustule_device_event_t* dev = &event->device_event;

    // Input events
    if (event->type == INPUT_ADDED) {
        handle_input_added(iev);
        if (iev->proplist)
            pa_proplist_free(iev->proplist);
        free(iev->name);
        free(iev->driver);
    }
    else if (event->type == INPUT_REMOVED) {
        handle_input_removed(iev);
    }

    // Device events
    else if (event->type == DEVICE_ADDED) {
        handle_device_added(dev);
        if (dev->proplist)
            pa_proplist_free(dev->proplist);
        free(dev->name);
        free(dev->description);
    }
    else if (event->type == DEVICE_REMOVED) {
        handle_device_removed(dev);
    }

    free(event);
}

D guile.c => guile.c +0 -250
@@ 1,250 0,0 @@
/**
 * pustule
 * Copyright 2014, 2016 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 <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <libguile.h>
#include <pthread.h>
#include <pulse/pulseaudio.h>

#include "pustule.h"

/* Exported functions */
struct pustule_exported_func {
    const char* name;
    int req, opt, rst;
    scm_t_subr func;
    const char* doc;
} exported_funcs[] = {
    {"pustule-get-input-volume", 1, 0, 0, &pustule_get_input_volume,
     "Gets the volume of the input whose index is INPUT-IDX (an integer)."},
    {"pustule-set-input-volume", 2, 0, 0, &pustule_set_input_volume,
     "Sets the volume of the input whose index is INPUT-IDX (an integer) to\n"
     "VOLUME (a number between 0 and 1)."},
    {"pustule-get-device-volume", 1, 0, 0, &pustule_get_device_volume,
     "Gets the volume of the device whose index is DEVICE-IDX (an integer)."},
    {"pustule-set-device-volume", 2, 0, 0, &pustule_set_device_volume,
     "Sets the volume of the device whose index is DEVICE-IDX (an integer) to\n"
     "VOLUME (a number between 0 and 1)."},
    {"pustule-get-default-device", 0, 0, 0, &pustule_get_default_device,
     "Gets the name of the default device."},
    {"pustule-set-default-device", 1, 0, 0, &pustule_set_default_device,
     "Sets the default device to NAME."},
    {"pustule-suspend-device", 2, 0, 0, &pustule_suspend_device,
     "Suspend or resume device DEVICE-IDX."},
    {NULL, 0, 0, 0, NULL}
};

/* Exported hooks */
SCM input_added_hook = NULL;
SCM input_removed_hook = NULL;
SCM device_added_hook = NULL;
SCM device_removed_hook = NULL;

/* Get the volume for the specified input */
SCM pustule_get_input_volume(SCM idx) {
    pustule_volume_t vol = {0, PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER};

    /* Query the volume with PulseAudio */
    pthread_mutex_lock(&(vol.mutex));
    input_get_volume(scm_to_uint32(idx), &vol);
    pthread_cond_wait(&(vol.cond), &(vol.mutex));

    /* Cleanup */
    pthread_mutex_unlock(&(vol.mutex));
    pthread_mutex_destroy(&(vol.mutex));
    pthread_cond_destroy(&(vol.cond));

    return scm_from_double(vol.volume);
}

/* Set the volume for the specified input */
SCM pustule_set_input_volume(SCM idx, SCM volume) {
    uint32_t idx_ = scm_to_uint32(idx);
    double volume_ = scm_to_double(volume);

    pa_cvolume cvol;
    pa_cvolume_init(&cvol);
    pa_volume_t new_vol = (pa_volume_t) (volume_ * PA_VOLUME_NORM);
    pa_cvolume_set(&cvol, 1, new_vol);
    pa_operation* op = pa_context_set_sink_input_volume(pactx, idx_, &cvol, NULL, NULL);
    pa_operation_unref(op);
    return volume;
}

/* Get the volume for the specified device */
SCM pustule_get_device_volume(SCM idx) {
    pustule_volume_t vol = {0, PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER};

    /* Query the volume with PulseAudio */
    pthread_mutex_lock(&(vol.mutex));
    device_get_volume(scm_to_uint32(idx), &vol);
    pthread_cond_wait(&(vol.cond), &(vol.mutex));

    /* Cleanup */
    pthread_mutex_unlock(&(vol.mutex));
    pthread_mutex_destroy(&(vol.mutex));
    pthread_cond_destroy(&(vol.cond));

    return scm_from_double(vol.volume);
}

/* Set the volume for the specified device */
SCM pustule_set_device_volume(SCM idx, SCM volume) {
    uint32_t idx_ = scm_to_uint32(idx);
    double volume_ = scm_to_double(volume);

    pa_cvolume cvol;
    pa_cvolume_init(&cvol);
    pa_volume_t new_vol = (pa_volume_t) (volume_ * PA_VOLUME_NORM);
    pa_cvolume_set(&cvol, 1, new_vol);
    pa_operation* op = pa_context_set_sink_volume_by_index(pactx, idx_, &cvol, NULL, NULL);
    pa_operation_unref(op);
    return volume;
}

/* Get the default device (by name) */
SCM pustule_get_default_device() {
    pustule_server_info_t info = {NULL, NULL, NULL, NULL, NULL,
                                  PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER};

    /* Query the server info with PulseAudio */
    pthread_mutex_lock(&(info.mutex));
    server_get_info(&info);
    pthread_cond_wait(&(info.cond), &(info.mutex));

    /* Result! */
    SCM res = scm_from_utf8_string(info.default_sink_name);

    /* Cleanup */
    free(info.user_name);
    free(info.host_name);
    free(info.server_version);
    free(info.server_name);
    free(info.default_sink_name);
    pthread_mutex_unlock(&(info.mutex));
    pthread_mutex_destroy(&(info.mutex));
    pthread_cond_destroy(&(info.cond));

    return res;
}

/* Set a device as the default device */
SCM pustule_set_default_device(SCM name) {
      scm_dynwind_begin(0);
      char* device_name = scm_to_utf8_stringn(name, NULL);
      scm_dynwind_free(device_name);

      pa_operation* op = pa_context_set_default_sink(pactx, device_name, NULL, NULL);
      pa_operation_unref(op);
      scm_dynwind_end();
      return SCM_BOOL_T;
}

/* Suspend/resume the specified device */
SCM pustule_suspend_device(SCM idx, SCM suspend) {
    uint32_t idx_ = scm_to_uint32(idx);
    uint32_t suspend_ = suspend == SCM_BOOL_T ? 1 : 0;

    pa_operation* op = pa_context_suspend_sink_by_index(pactx, idx_, suspend_, NULL, NULL);
    pa_operation_unref(op);
    return suspend;
}

/* Helpers */
static SCM _alist_add_str(SCM info, const char* name, const char* value) {
    return scm_acons(scm_from_utf8_string(name), scm_from_utf8_string(value), info);
}
static SCM _alist_add_bool(SCM info, const char* name, int value) {
    return scm_acons(scm_from_utf8_string(name), value ? SCM_BOOL_T : SCM_BOOL_F, info);
}
static SCM _alist_add_double(SCM info, const char* name, double value) {
    return scm_acons(scm_from_utf8_string(name), scm_from_double(value), info);
}
static SCM _alist_add_volume(SCM info, const pa_cvolume* vol) {
    double v = ((double) pa_cvolume_avg(vol)) / PA_VOLUME_NORM;
    double vdb = pa_sw_volume_to_dB(pa_cvolume_avg(vol));
    info = _alist_add_double(info, "volume", v);
    return _alist_add_double(info, "volume_db", vdb);
}
static SCM _alist_add_proplist(SCM info, pa_proplist* props) {
    if (props && !pa_proplist_isempty(props)) {
        void* state = NULL;
        const char* key;
        while ((key = pa_proplist_iterate(props, &state)) != NULL) {
            const char* value = pa_proplist_gets(props, key);
            info = _alist_add_str(info, key, value);
        }
    }
    return info;
}

/* Convert an input event to an association list */
SCM scm_from_input_event(pustule_input_event_t* iev) {
    SCM info = SCM_EOL;
    info = _alist_add_str(info,  "name",            iev->name);
    info = _alist_add_str(info,  "driver",          iev->driver);
    info = _alist_add_bool(info, "muted",           iev->mute);
    info = _alist_add_bool(info, "volume_writable", iev->volume_writable);
    if (iev->has_volume)
        info = _alist_add_volume(info, &(iev->volume));
    else
        info = _alist_add_bool(info, "volume", 0);
    info = _alist_add_proplist(info, iev->proplist);
    return info;
}

/* Convert a device event to an association list */
SCM scm_from_device_event(pustule_device_event_t* dev) {
    SCM info = SCM_EOL;
    info = _alist_add_str(info,  "name",        dev->name);
    info = _alist_add_str(info,  "description", dev->description);
    info = _alist_add_bool(info, "muted",       dev->mute);
    info = _alist_add_volume(info, &(dev->volume));
    info = _alist_add_proplist(info, dev->proplist);
    return info;
}

/* Register functions and other things with Guile */
void pustule_init_guile() {
    /* Register functions (and their docs) */
    SCM key = scm_from_utf8_symbol("documentation");
    struct pustule_exported_func* func;
    for (func = exported_funcs; func->name != NULL; func++) {
        SCM proc = scm_c_define_gsubr(func->name, func->req, func->opt,
                                      func->rst, func->func);
        if (func->doc) {
            SCM doc = scm_from_utf8_string(func->doc);
            scm_set_procedure_property_x(proc, key, doc);
        }
    }

    /* Create hooks */
    input_added_hook = scm_make_hook(scm_from_int(2));
    scm_c_define("pustule-input-added-hook", input_added_hook);
    input_removed_hook = scm_make_hook(scm_from_int(1));
    scm_c_define("pustule-input-removed-hook", input_removed_hook);
    device_added_hook = scm_make_hook(scm_from_int(2));
    scm_c_define("pustule-device-added-hook", device_added_hook);
    device_removed_hook = scm_make_hook(scm_from_int(1));
    scm_c_define("pustule-device-removed-hook", device_removed_hook);
}

A lua_mod.c => lua_mod.c +325 -0
@@ 0,0 1,325 @@
/**
 * pustule
 * Copyright 2014-2020 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 <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <pulse/pulseaudio.h>

#include "log.h"
#include "pustule.h"

static void stackDump (lua_State *L) {
    int i;
    int top = lua_gettop(L);  /* depth of the stack */
    printf("LUA STACK: ");
    for (i = 1; i <= top; i++) {  /* repeat for each level */
        printf("%d:", i);
        int t = lua_type(L, i);
        switch (t) {
        case LUA_TSTRING: {  /* strings */
            printf("'%s'", lua_tostring(L, i));
            break;
        }
        case LUA_TBOOLEAN: {  /* Booleans */
            printf(lua_toboolean(L, i) ? "true" : "false");
            break;
        }
        case LUA_TNUMBER: {  /* numbers */
            if (lua_isinteger(L, i))  /* integer? */
                printf("%lld", lua_tointeger(L, i));
            else  /* float */
                printf("%g", lua_tonumber(L, i));
            break;
        }
        default: {  /* other values */
            printf("%s", lua_typename(L, t));
            break;
        }
        }
        printf("  ");  /* put a separator */
    }
    printf("\n");  /* end the listing */
}

/* Signal listeners */
lua_State* g_L;
array_t input_added_listeners;
array_t input_removed_listeners;
array_t device_added_listeners;
array_t device_removed_listeners;

/* Query helpers */
static void _query_number_cb(lua_Number number, void* userdata) {
    intptr_t cb_ref = (intptr_t) userdata;
    lua_rawgeti(g_L, LUA_REGISTRYINDEX, cb_ref);
    lua_pushnumber(g_L, number);
    lua_call(g_L, 1, 0);
    luaL_unref(g_L, LUA_REGISTRYINDEX, cb_ref);
}
static void _query_device_name_cb(pustule_server_info_t* info, void* userdata) {
    intptr_t cb_ref = (intptr_t) userdata;
    lua_rawgeti(g_L, LUA_REGISTRYINDEX, cb_ref);
    lua_pushstring(g_L, info->default_sink_name);
    lua_call(g_L, 1, 0);
    luaL_unref(g_L, LUA_REGISTRYINDEX, cb_ref);
}

/* Lua query functions */
/**
 * Query the volume of an input.
 * @function get_input_volume
 * @param idx input index
 * @param callback function called with 1 parameter (volume as a number) when the volume is available
 */
int pustule_get_input_volume(lua_State* L) {
    uint32_t idx = luaL_checkinteger(L, 1);
    intptr_t cb_ref = luaL_ref(L, LUA_REGISTRYINDEX);
    input_get_volume(idx, _query_number_cb, (void*) cb_ref);
    return 0;
}

/**
 * Set the volume of an input.
 * @function set_input_volume
 * @param idx input index
 * @param vol new volume value (0-1)
 */
int pustule_set_input_volume(lua_State* L) {
    uint32_t idx = luaL_checkinteger(L, 1);
    double volume = luaL_checknumber(L, 2);

    pa_cvolume cvol;
    pa_cvolume_init(&cvol);
    pa_volume_t new_vol = (pa_volume_t) (volume * PA_VOLUME_NORM);
    pa_cvolume_set(&cvol, 1, new_vol);
    pa_operation* op = pa_context_set_sink_input_volume(pactx, idx, &cvol, NULL, NULL);
    pa_operation_unref(op);

    return 0;
}

/**
 * Query the volume of a device.
 * @function get_input_volume
 * @param idx input index
 * @param callback function called with 1 parameter (volume as a number) when the volume is available
 */
int pustule_get_device_volume(lua_State* L) {
    uint32_t idx = luaL_checkinteger(L, 1);
    intptr_t cb_ref = luaL_ref(L, LUA_REGISTRYINDEX);
    device_get_volume(idx, _query_number_cb, (void*) cb_ref);
    return 0;
}

/**
 * Set the volume for a device.
 * @function set_device_volume
 * @param idx device index
 * @param vol new volume value (0-1)
 */
int pustule_set_device_volume(lua_State* L) {
    uint32_t idx = luaL_checkinteger(L, 1);
    double volume = luaL_checknumber(L, 2);

    pa_cvolume cvol;
    pa_cvolume_init(&cvol);
    pa_volume_t new_vol = (pa_volume_t) (volume * PA_VOLUME_NORM);
    pa_cvolume_set(&cvol, 1, new_vol);
    pa_operation* op = pa_context_set_sink_volume_by_index(pactx, idx, &cvol, NULL, NULL);
    pa_operation_unref(op);

    return 0;
}

/**
 * Get the default device (by name).
 * @function get_default_device
 * @param callback function called with the default device name
 */
int pustule_get_default_device(lua_State* L) {
    intptr_t cb_ref = luaL_ref(L, LUA_REGISTRYINDEX);
    server_get_info(_query_device_name_cb, (void*) cb_ref);
    return 0;
}

/**
 * Set a device as the default device.
 * @function set_default_device
 * @param name name of the new default device
 */
int pustule_set_default_device(lua_State* L) {
    const char* device_name = luaL_checkstring(L, 1);
    pa_operation* op = pa_context_set_default_sink(pactx, device_name, NULL, NULL);
    pa_operation_unref(op);
    return 0;
}

/**
 * Suspend/resume the specified device.
 * @function suspend_device
 * @param idx device index
 * @param suspend boolean indicating whether to suspend (true) or resume (false)
 */
int pustule_suspend_device(lua_State* L) {
    uint32_t idx = luaL_checkinteger(L, 1);
    bool suspend = lua_toboolean(L, 2);

    pa_operation* op = pa_context_suspend_sink_by_index(pactx, idx, suspend, NULL, NULL);
    pa_operation_unref(op);
    return 0;
}

/* Helpers */
static void _table_add_str(lua_State* L, const char* name, const char* value) {
    lua_pushstring(L, name);
    lua_pushstring(L, value);
    lua_settable(L, -3);
}
static void _table_add_bool(lua_State* L, const char* name, bool value) {
    lua_pushstring(L, name);
    lua_pushboolean(L, value ? 1 : 0);
    lua_settable(L, -3);
}
static void _table_add_double(lua_State* L, const char* name, double value) {
    lua_pushstring(L, name);
    lua_pushnumber(L, value);
    lua_settable(L, -3);
}
static void _table_add_volume(lua_State* L, const pa_cvolume* vol) {
    double v = ((double) pa_cvolume_avg(vol)) / PA_VOLUME_NORM;
    double vdb = pa_sw_volume_to_dB(pa_cvolume_avg(vol));
    _table_add_double(L, "volume", v);
    _table_add_double(L, "volume_db", vdb);
}
static void _table_add_proplist(lua_State* L, pa_proplist* props) {
    if (props && !pa_proplist_isempty(props)) {
        void* state = NULL;
        const char* key;
        while ((key = pa_proplist_iterate(props, &state)) != NULL) {
            const char* value = pa_proplist_gets(props, key);
            _table_add_str(L, key, value);
        }
    }
}

/* Convert an input event to a Lua table */
void pustule_push_input_event(lua_State* L, pustule_input_event_t* iev) {
    lua_newtable(L);
    _table_add_str(L,  "name",            iev->name);
    _table_add_str(L,  "driver",          iev->driver);
    _table_add_bool(L, "muted",           iev->mute);
    _table_add_bool(L, "volume_writable", iev->volume_writable);
    if (iev->has_volume)
        _table_add_volume(L, &(iev->volume));
    else
        _table_add_bool(L, "volume", false);
    _table_add_proplist(L, iev->proplist);
}

/* Convert a device event to a Lua table */
void pustule_push_device_event(lua_State* L, pustule_device_event_t* dev) {
    lua_newtable(L);
    _table_add_str(L,  "name",        dev->name);
    _table_add_str(L,  "description", dev->description);
    _table_add_bool(L, "muted",       dev->mute);
    _table_add_volume(L, &(dev->volume));
    _table_add_proplist(L, dev->proplist);
}

/* Handle listeners */
array_t* _listeners_by_name(const char* name) {
    if (strcmp("input_added", name) == 0)
        return &input_added_listeners;
    if (strcmp("input_removed", name) == 0)
        return &input_removed_listeners;
    if (strcmp("device_added", name) == 0)
        return &device_added_listeners;
    if (strcmp("device_removed", name) == 0)
        return &device_removed_listeners;
    return NULL;
}

int pustule_connect_signal(lua_State* L) {
    // type: (string signal_name, function callback) -> int
    const char* signal_name = luaL_checkstring(L, 1);
    log_debug("Connecting listener for signal '%s'", signal_name);
    array_t* listeners = _listeners_by_name(signal_name);
    if (!listeners) {
        return luaL_error(L, "Invalid signal name: %s", signal_name);
    }

    int callback_ref = luaL_ref(L, LUA_REGISTRYINDEX);
    array_push(listeners, callback_ref);

    lua_pushinteger(L, callback_ref);
    return 1;
}

int pustule_disconnect_signal(lua_State* L) {
    // type: (string signal_name, int callback_ref) -> void
    const char* signal_name = luaL_checkstring(L, 1);
    log_debug("Disconnecting listener for signal '%s'", signal_name);
    array_t* listeners = _listeners_by_name(signal_name);
    if (!listeners) {
        return luaL_error(L, "Invalid signal name: %s", signal_name);
    }

    int callback_ref = luaL_checkinteger(L, 2);
    array_remove(listeners, callback_ref);
    luaL_unref(L, LUA_REGISTRYINDEX, callback_ref);

    return 0;
}

/* Register Lua module */
void init_lua_module(lua_State* L) {
    g_L = L;

    array_init(&input_added_listeners);
    array_init(&input_removed_listeners);
    array_init(&device_added_listeners);
    array_init(&device_removed_listeners);

    luaL_requiref(L, "pustule", luaopen_pustule, 1);
    lua_pop(L, 1);
}

static const struct luaL_Reg pustule_lib[] = {
    {"connect_signal", pustule_connect_signal},
    {"disconnect_signal", pustule_disconnect_signal},
    {"get_input_volume", pustule_get_input_volume},
    {"set_input_volume", pustule_set_input_volume},
    {"get_device_volume", pustule_get_device_volume},
    {"set_device_volume", pustule_set_device_volume},
    {"get_default_device", pustule_get_default_device},
    {"set_default_device", pustule_set_default_device},
    {"suspend_device", pustule_suspend_device},
    {NULL, NULL},
};

int luaopen_pustule(lua_State* L) {
    luaL_newlib(L, pustule_lib);
    return 1;
}

M main.c => main.c +45 -67
@@ 1,6 1,6 @@
/**
 * pustule
 * Copyright 2014, 2016 Thomas Jost <schnouki@schnouki.net>
 * Copyright 2014-2020 Thomas Jost <schnouki@schnouki.net>
 *
 * This file is part of pustule.
 *


@@ 21,7 21,9 @@
#include <stdlib.h>
#include <string.h>

#include <libguile.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <pulse/pulseaudio.h>

#include "argparse/argparse.h"


@@ 33,10 35,6 @@ static const char* const usage[] = {
    NULL
};

struct _args {
    int listen_port;
    const char* config_path;
};

/* Get the name of the default configuration file, using environment
   variables */


@@ 57,7 55,7 @@ static char* get_default_config_file() {
            exit(1);
        }
    }
    if (asprintf(&config_file, "%s/pustule.scm", config_dir) < 0) {
    if (asprintf(&config_file, "%s/pustule.lua", config_dir) < 0) {
        log_fatal("Can't allocate memory for the path of the config directory");
        exit(1);
    }


@@ 66,91 64,71 @@ static char* get_default_config_file() {
}


/* Main function (once Guile is initialized) */
static void* inner_main(void* data) {
    struct _args* args = (struct _args*) data;
/* Initialization */
int main(int argc, const char** argv) {
    /* Parse command-line arguments */
    const char* config_path = get_default_config_file();
    int debug = 0;

    /* Register exported functions with Guile */
    pustule_init_guile();
    struct argparse_option options[] = {
        OPT_HELP(),
        OPT_STRING('c', "config", &config_path, "path to the configuration file; defaults to $XDG_CONFIG_HOME/pustule.lua"),
        OPT_BOOLEAN('d', "debug", &debug, "enable debug mode"),
        OPT_END(),
    };

    struct argparse argparse;
    argparse_init(&argparse, options, usage, 0);
    argparse_describe(&argparse, "Rule-based volume control for PulseAudio.", NULL);
    argc = argparse_parse(&argparse, argc, argv);

    log_set_level(debug ? LOG_DEBUG : LOG_INFO);

    /* Initialize Lua */
    log_debug("Initializing Lua");
    lua_State* L = luaL_newstate();
    luaL_openlibs(L);
    init_lua_module(L);

    /* Run the config file */
    log_debug("Loading config file");
    scm_c_primitive_load(args->config_path);

    /* Start a REPL server? */
    if (args->listen_port > 0) {
        log_debug("Starting REPL server on port %d", args->listen_port);
        scm_c_use_module("system repl server");
        char* spawn_server;
        if (asprintf(&spawn_server, "(spawn-server (make-tcp-server-socket #:port %d))", args->listen_port) < 0) {
            log_fatal("Can't allocate memory for the server spawn command");
            exit(1);
        }
        scm_c_eval_string(spawn_server);
        free(spawn_server);
    log_info("Loading config file");
    if (luaL_dofile(L, config_path)) {
        log_error("Error while loading config file: %s", lua_tostring(L, -1));
        return 1;
    }

    /* Create the libpulse threaded main loop */
    log_debug("Preparing to connect to PulseAudio");
    pa_threaded_mainloop* loop = pa_threaded_mainloop_new();
    pa_mainloop* loop = pa_mainloop_new();
    if (!loop) {
        log_error("Can't create main loop");
        return NULL;
        return 1;
    }
    paloop = loop;

    pa_mainloop_api* api = pa_threaded_mainloop_get_api(loop);
    pa_mainloop_api* api = pa_mainloop_get_api(loop);
    if (!api) {
        log_error("Can't get main loop API");
        return NULL;
        return 1;
    }

    pa_context* ctx = pa_context_new(api, "pustule");
    if (!ctx) {
        log_error("Can't get PulseAudio context");
        return NULL;
        return 1;
    }
    pactx = ctx;

    /* Start the libpulse main loop */
    /* Connect and run the event loop */
    log_debug("Connecting to PulseAudio");
    pa_context_set_state_callback(ctx, state_callback, NULL);
    pa_context_connect(pactx, NULL, PA_CONTEXT_NOFLAGS, NULL);
    pa_threaded_mainloop_start(loop);
    pa_context_connect(pactx, NULL, PA_CONTEXT_NOAUTOSPAWN | PA_CONTEXT_NOFAIL, NULL);

    /* Run the pseudo event loop */
    log_info("Running pustule");
    pustule_run();
    return NULL;
}

/* Initialization */
int main(int argc, const char** argv) {
    /* Parse command-line arguments */
    struct _args args;
    args.listen_port = -1;
    args.config_path = get_default_config_file();
    int debug = 0;

    struct argparse_option options[] = {
        OPT_HELP(),
        OPT_STRING('c', "config", &args.config_path, "path to the configuration file; defaults to $XDG_CONFIG_HOME/pustule.scm"),
        OPT_INTEGER('l', "listen", &args.listen_port, "start a REPL server listening on this TCP port"),
        OPT_BOOLEAN('d', "debug", &debug, "enable debug mode"),
        OPT_END(),
    };

    struct argparse argparse;
    argparse_init(&argparse, options, usage, 0);
    argparse_describe(&argparse, "Rule-based volume control for PulseAudio.", NULL);
    argc = argparse_parse(&argparse, argc, argv);

    if (debug)
        log_set_level(LOG_DEBUG);
    else
        log_set_level(LOG_INFO);
    int ret = pa_mainloop_run(loop, NULL);

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

    return 0;
    return ret;
}

M pulse.c => pulse.c +81 -40
@@ 1,6 1,6 @@
/**
 * pustule
 * Copyright 2014, 2016 Thomas Jost <schnouki@schnouki.net>
 * Copyright 2014-2020 Thomas Jost <schnouki@schnouki.net>
 *
 * This file is part of pustule.
 *


@@ 22,18 22,36 @@
#include <stdlib.h>
#include <string.h>

#include <libguile.h>
#include <pulse/pulseaudio.h>

#include "log.h"
#include "pustule.h"

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

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

    char* st = "unknown";
    if (state == PA_CONTEXT_UNCONNECTED)
        st = "unconnected";
    else if (state == PA_CONTEXT_CONNECTING)
        st = "connecting";
    else if (state == PA_CONTEXT_AUTHORIZING)
        st = "authorizing";
    else if (state == PA_CONTEXT_SETTING_NAME)
        st = "setting_name";
    else if (state == PA_CONTEXT_READY)
        st = "ready";
    else if (state == PA_CONTEXT_FAILED)
        st = "failed";
    else if (state == PA_CONTEXT_TERMINATED)
        st = "terminated";
    log_debug("State is now %s", st);

    if (state == PA_CONTEXT_READY) {
        // Get infos about all current inputs
        pa_context_get_sink_input_info_list(ctx, input_info_callback, NULL);


@@ 47,7 65,7 @@ void state_callback(pa_context* ctx, void* userdata) {
    }
}

/* Event handler -- dispatch Scheme events */
/* Event handler -- dispatch Lua events */
void event_callback(pa_context* ctx, pa_subscription_event_type_t t, uint32_t idx, void* userdata) {
    if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK_INPUT) {
        // This event is about an input!


@@ 134,56 152,79 @@ void device_info_callback(pa_context* ctx, const pa_sink_info* i, int eol, void*
    pustule_handle_event(event);
}

/* Query info about an input (for its volume) */
/* Query info about an input or device (for its volume) */
typedef struct {
    pa_operation* op;
    pustule_volume_callback_t callback;
    void* userdata;
} _volume_data_cb_t;

static void input_volume_info_callback(pa_context* ctx, const pa_sink_input_info* i, int eol, void* userdata) {
    if (!i) return;
    pustule_volume_t* vol = (pustule_volume_t*) userdata;
    pthread_mutex_lock(&(vol->mutex));
    vol->volume = ((double) pa_cvolume_avg(&(i->volume))) / PA_VOLUME_NORM;
    pthread_cond_broadcast(&(vol->cond));
    pthread_mutex_unlock(&(vol->mutex));
    _volume_data_cb_t* data = (_volume_data_cb_t*) userdata;
    double volume = ((double) pa_cvolume_avg(&(i->volume))) / PA_VOLUME_NORM;

    data->callback(volume, data->userdata);
    pa_operation_unref(data->op);
    free(data);
}
void input_get_volume(uint32_t idx, pustule_volume_t* vol) {
    pa_threaded_mainloop_lock(paloop);
    pa_operation* op = pa_context_get_sink_input_info(pactx, idx, input_volume_info_callback, (void*) vol);
    pa_operation_unref(op);
    pa_threaded_mainloop_unlock(paloop);
void input_get_volume(uint32_t idx, pustule_volume_callback_t callback, void* userdata) {
    _volume_data_cb_t* data = malloc(sizeof(_volume_data_cb_t));
    if (!data)
        abort();
    data->callback = callback;
    data->userdata = userdata;
    data->op = pa_context_get_sink_input_info(pactx, idx, input_volume_info_callback, (void*) data);
}

/* Query info about a device (for its volume) */
static void device_volume_info_callback(pa_context* ctx, const pa_sink_info* i, int eol, void* userdata) {
    if (!i) return;
    pustule_volume_t* vol = (pustule_volume_t*) userdata;
    pthread_mutex_lock(&(vol->mutex));
    vol->volume = ((double) pa_cvolume_avg(&(i->volume))) / PA_VOLUME_NORM;
    pthread_cond_broadcast(&(vol->cond));
    pthread_mutex_unlock(&(vol->mutex));
    _volume_data_cb_t* data = (_volume_data_cb_t*) userdata;
    double volume = ((double) pa_cvolume_avg(&(i->volume))) / PA_VOLUME_NORM;

    data->callback(volume, data->userdata);
    pa_operation_unref(data->op);
    free(data);
}
void device_get_volume(uint32_t idx, pustule_volume_t* vol) {
    pa_threaded_mainloop_lock(paloop);
    pa_operation* op = pa_context_get_sink_info_by_index(pactx, idx, device_volume_info_callback, (void*) vol);
    pa_operation_unref(op);
    pa_threaded_mainloop_unlock(paloop);
void device_get_volume(uint32_t idx, pustule_volume_callback_t callback, void* userdata) {
    _volume_data_cb_t* data = malloc(sizeof(_volume_data_cb_t));
    if (!data)
        abort();
    data->callback = callback;
    data->userdata = userdata;
    data->op = pa_context_get_sink_info_by_index(pactx, idx, device_volume_info_callback, (void*) data);
}

/* Query info about the server */
typedef struct {
    pa_operation* op;
    pustule_server_info_callback_t callback;
    void* userdata;
} _server_info_data_cb_t;

static void server_info_callback(pa_context* ctx, const pa_server_info *i, void* userdata) {
    if (!i) return;
    pustule_server_info_t* info = (pustule_server_info_t*) userdata;
    pthread_mutex_lock(&(info->mutex));
    _server_info_data_cb_t* data = (_server_info_data_cb_t*) userdata;

    pustule_server_info_t* info = malloc(sizeof(pustule_server_info_t));
    if (!info)
        abort();

    info->user_name = strdup(i->user_name);
    info->host_name = strdup(i->host_name);
    info->server_version = strdup(i->server_version);
    info->server_name = strdup(i->server_name);
    info->default_sink_name = strdup(i->default_sink_name);
    info->user_name = i->user_name;
    info->host_name = i->host_name;
    info->server_version = i->server_version;
    info->server_name = i->server_name;
    info->default_sink_name = i->default_sink_name;

    pthread_cond_broadcast(&(info->cond));
    pthread_mutex_unlock(&(info->mutex));
    data->callback(info, data->userdata);
    pa_operation_unref(data->op);
    free(info);
}
void server_get_info(pustule_server_info_t* info) {
    pa_threaded_mainloop_lock(paloop);
    pa_operation* op = pa_context_get_server_info(pactx, server_info_callback, (void*) info);
    pa_operation_unref(op);
    pa_threaded_mainloop_unlock(paloop);
void server_get_info(pustule_server_info_callback_t callback, void* userdata) {
    _server_info_data_cb_t* data = malloc(sizeof(_server_info_data_cb_t));
    if (!data)
        abort();
    data->callback = callback;
    data->userdata = userdata;
    data->op = pa_context_get_server_info(pactx, server_info_callback, (void*) data);
}

M pustule.h => pustule.h +60 -41
@@ 1,6 1,6 @@
/**
 * pustule
 * Copyright 2014, 2016 Thomas Jost <schnouki@schnouki.net>
 * Copyright 2014-2020 Thomas Jost <schnouki@schnouki.net>
 *
 * This file is part of pustule.
 *


@@ 22,8 22,11 @@

#define PUSTULE_VERSION "pustule 0.1"

#include <libguile.h>
#include <pthread.h>
#include <stdbool.h>

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <pulse/pulseaudio.h>

/* Data structures */


@@ 49,8 52,10 @@ typedef struct {

typedef struct {
    enum {
        INPUT_ADDED, INPUT_REMOVED,
        DEVICE_ADDED, DEVICE_REMOVED,
        INPUT_ADDED,
        INPUT_REMOVED,
        DEVICE_ADDED,
        DEVICE_REMOVED,
    } type;
    union {
        pustule_input_event_t input_event;


@@ 58,21 63,22 @@ typedef struct {
    };
} pustule_event_t;

typedef struct {
    double volume;
    pthread_mutex_t mutex;
    pthread_cond_t cond;
} pustule_volume_t;
typedef void (*pustule_volume_callback_t)(double volume, void* userdata);

typedef struct {
    char* user_name;
    char* host_name;
    char* server_name;
    char* server_version;
    char* default_sink_name;
    pthread_mutex_t mutex;
    pthread_cond_t cond;
    const char* user_name;
    const char* host_name;
    const char* server_name;
    const char* server_version;
    const char* default_sink_name;
} pustule_server_info_t;
typedef void (*pustule_server_info_callback_t)(pustule_server_info_t* info, void* userdata);

typedef struct {
    size_t capacity;
    size_t size;
    intptr_t* array;
} array_t;

/* PulseAudio callbacks */
void event_callback(pa_context* ctx, pa_subscription_event_type_t t, uint32_t idx, void* userdata);


@@ 80,35 86,48 @@ void input_info_callback(pa_context* ctx, const pa_sink_input_info* i, int eol, 
void device_info_callback(pa_context* ctx, const pa_sink_info* i, int eol, void* userdata);
void state_callback(pa_context* ctx, void* userdata);

/* PulseAudio functions */
void input_get_volume(uint32_t idx, pustule_volume_t* vol);
void device_get_volume(uint32_t idx, pustule_volume_t* vol);
void server_get_info(pustule_server_info_t* info);

/* Guile functions */
SCM pustule_get_input_volume(SCM idx);
SCM pustule_set_input_volume(SCM idx, SCM volume);
SCM pustule_get_device_volume(SCM idx);
SCM pustule_set_device_volume(SCM idx, SCM volume);
SCM pustule_get_default_device();
SCM pustule_set_default_device(SCM name);
SCM pustule_suspend_device(SCM idx, SCM suspend);

/* Guile helpers */
void pustule_init_guile();
SCM scm_from_input_event(pustule_input_event_t* event);
SCM scm_from_device_event(pustule_device_event_t* event);
/* PulseAudio query functions */
void input_get_volume(uint32_t idx, pustule_volume_callback_t callback, void* userdata);
void device_get_volume(uint32_t idx, pustule_volume_callback_t callback, void* userdata);
void server_get_info(pustule_server_info_callback_t callback, void* userdata);

/* Dynamic array functions */
void array_init(array_t* arr);
void array_push(array_t* arr, intptr_t value);
bool array_find(array_t* arr, intptr_t value, size_t* idx);
void array_remove(array_t* arr, intptr_t value);

/* Lua module */
void init_lua_module(lua_State* L);
int luaopen_pustule(lua_State* L);

/* Functions exported in the Lua module */
int pustule_connect_signal(lua_State* L);
int pustule_disconnect_signal(lua_State* L);

/* Lua query functions */
int pustule_get_input_volume(lua_State* L);
int pustule_set_input_volume(lua_State* L);
int pustule_get_device_volume(lua_State* L);
int pustule_set_device_volume(lua_State* L);
int pustule_get_default_device(lua_State* L);
int pustule_set_default_device(lua_State* L);
int pustule_suspend_device(lua_State* L);

/* Lua helpers */
void pustule_push_input_event(lua_State* L, pustule_input_event_t* event);
void pustule_push_device_event(lua_State* L, pustule_device_event_t* event);

/* Pustule event loop */
void pustule_run();
void pustule_handle_event(pustule_event_t* event);

/* Global variables */
extern pa_context* pactx;
extern pa_threaded_mainloop* paloop;
extern SCM input_added_hook;
extern SCM input_removed_hook;
extern SCM device_added_hook;
extern SCM device_removed_hook;
extern pa_mainloop* paloop;
extern lua_State* g_L;
extern array_t input_added_listeners;
extern array_t input_removed_listeners;
extern array_t device_added_listeners;
extern array_t device_removed_listeners;

#endif

A pustule.lua => pustule.lua +186 -0
@@ 0,0 1,186 @@
-- pustule
-- Copyright 2014-2020 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/>.


-- Welcome to the Pustule default configuration script! Here's a short
-- explanation of how this works.
--
-- Each application that plays sound using PulseAudio creates one or several
-- inputs (or "sink input" in PulseAudio). The volume of each input can be set
-- independently.
--
-- The sound is then played on one of several devices ("sinks" in PulseAudio).
-- Again, the volume of each of them can be set independently. An input can be
-- bound to a specific device, or use the global default device.
--
-- pustule sends a signal when an input or device is added to or removed from
-- the PulseAudio daemon. There are 4 signals: "input_added", "input_removed",
-- "device_added", "device_removed". Listeners can be added for any of these
-- signals using the `pustule.connect_signal()` and
-- `pustule.disconnect_signal()`.

--
-- The volume of each input and device (a number between 0 and 1) can be set
-- using the `pustule.set_input_volume()` and `pustule.set_device_volume()`
-- functions.
--
-- In the default script, there are a few helpers that provide sane defaults for
-- writing rich rules. For instance everything is logged, including all the
-- properties of each new input. Some fun examples are shown, such as setting
-- different volumes for music and video players and for web browsers.
--
-- In order for pustule to work, you must disable the flat volumes option in the
-- PulseAudio daemon configuration. To do that, add the following line to
-- /etc/pulse/daemon.conf:
--     flat-volumes = no
-- And restart PulseAudio.

local io, string = io, string

local pustule = require("pustule")

------------------------------------------------------------------------
-- Helpers
------------------------------------------------------------------------
function printf(s, ...)
   return io.write(s:format(...))
end

-- Table of all the active inputs and devices
active_inputs = {}
active_devices = {}

function set_input_volume(idx, vol)
   printf("[%d] Setting volume to %.2f.\n", idx, vol)
   pustule.set_input_volume(idx, vol)
   active_inputs[idx] = true
end

--- Check if a volume has already been set for an input
function is_input_volume_set(idx)
   return active_inputs[idx] == true
end

function set_device_volume(idx)
   printf("{%d} Setting volume to %.2f.\n", idx, vol)
   pustule.set_device_volume(idx, vol)
end

function show_input(idx, input)
   local k, v
   for k, v in pairs(input) do
      printf("  [%d] %s: %s\n", idx, k, v)
   end
end

function show_device(idx, device)
   local k, v
   for k, v in pairs(device) do
      printf("  {%d} %s: %s\n", idx, k, v)
   end
end


------------------------------------------------------------------------
-- Application-specific rules
------------------------------------------------------------------------
--- Set volume for web browsers
function input_added__browser(idx, input)
   if not is_input_volume_set(idx)
      and (input["application.process.binary"] == "firefox"
              or input["application.process.binary"] == "chromium")
   then
      set_input_volume(idx, 0.8)
   end
end
pustule.connect_signal("input_added", input_added__browser)


------------------------------------------------------------------------
-- Media roles
------------------------------------------------------------------------
-- Table mapping idx to role for inputs that have a media.role
roles = {}

--- Set volume for various applications based on their media role
function input_added__media_role(idx, input)
   if is_input_volume_set(idx) then return end

   -- Add role to inputs that should have one but don't
   if input["application.process.binary"] == "mpg123" or input["application.process.binary"] == "ogg123" then
      input["media.role"] = "music"
   end

   local role = input["media.role"]
   if not role then return end
   roles[idx] = role

   -- Set volume depending on the role
   if role == "music" then
      set_input_volume(idx, 0.5)
   elseif role == "video" then
      set_input_volume(idx, 0.8)
   end
end
pustule.connect_signal("input_added", input_added__media_role)

--- Cleanup the roles table
function input_removed__media_role(idx)
   if roles[idx] then roles[idx] = nil end
end
pustule.connect_signal("input_removed", input_removed__media_role)


------------------------------------------------------------------------
-- Default rules: inputs
------------------------------------------------------------------------
function input_added__all(idx, input)
   printf("[%d] Input added.\n", idx)
   show_input(idx, input)
end
pustule.connect_signal("input_added", input_added__all)

function input_removed__all(idx, input)
   printf("[%d] Input removed.\n", idx)
   active_inputs[idx] = nil
end
pustule.connect_signal("input_removed", input_removed__all)

--- Set a default value for the volume if it wasn't set by the previous listeners
function input_added__default_volume(idx, input)
   if not is_input_volume_set(idx) then
      set_input_volume(idx, 0.6)
   end
end
pustule.connect_signal("input_added", input_added__default_volume)


------------------------------------------------------------------------
-- Default rules: devices
------------------------------------------------------------------------
function device_added__all(idx, device)
   printf("{%d} Device added.\n", idx)
   show_device(idx, device)
   active_devices[idx] = device
end
pustule.connect_signal("device_added", device_added__all)

function device_removed__all(idx)
   printf("{%d} Device removed.\n", idx)
   active_devices[idx] = nil
end
pustule.connect_signal("device_removed", device_removed__all)

D pustule.scm => pustule.scm +0 -182
@@ 1,182 0,0 @@
;; pustule
;; Copyright 2014, 2016 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/>.


;; Welcome to the Pustule default configuration script! Here's a short
;; explanation of how this works.
;;
;; Each application that plays sound using PulseAudio creates one or several
;; inputs (or "sink input" in PulseAudio). The volume of each input can be set
;; independently.
;;
;; The sound is then played on one of several devices ("sinks" in PulseAudio).
;; Again, the volume of each of them can be set independently. An input can be
;; bound to a specific device, or use the global default device.
;;
;; pustule provides with four hooks: `pustule-input-added-hook',
;; `pustule-input-removed-hook' `pustule-device-added-hook' and
;; `pustule-device-removed-hook', which are called when an input or device is
;; added to or removed from the PulseAudio daemon.
;;
;; The volume of each input and device (a number between 0 and 1) can be set
;; using the `pustule-set-input-volume' and `pustule-set-device-volume'
;; functions.
;;
;; In the default script, there are a few helpers that provide sane defaults for
;; writing rich rules. For instance everything is logged, including all the
;; properties of each new input. Some fun examples are shown, such as setting
;; different volumes for music and video players, for web browsers (including
;; the Flash plugin), and for the Gajim IM client.
;;
;; In order for pustule to work, you must disable the flat volumes option in the
;; PulseAudio daemon configuration. To do that, add the following line to
;; /etc/pulse/daemon.conf:
;;     flat-volumes = no
;; And restart PulseAudio.

(use-modules (ice-9 format))

;; {{{ Helpers
;; Association list of all the active inputs and devices
(define active-inputs '())
(define active-devices '())

(define (set-input-volume idx vol)
  (format #t "[~d] Setting volume to ~,2f.\n" idx vol)
  (pustule-set-input-volume idx vol)
  (set! active-inputs (assoc-set! active-inputs idx #t)))

(define (input-volume-set? idx)
  "Check if a volume has already been set for an input."
  (assoc-ref active-inputs idx))

(define (set-device-volume idx vol)
  (format #t "{~d} Setting volume to ~,2f.\n" idx vol)
  (pustule-set-device-volume idx vol))

(define (show-input idx input)
  (let ((show-prop
         (lambda (cell)
           (format #t "  [~d] ~s: ~s\n" idx (car cell) (cdr cell)))))
    (for-each show-prop input)))

(define (show-device idx device)
  (let ((show-prop
         (lambda (cell)
           (format #t "  {~d} ~s: ~s\n" idx (car cell) (cdr cell)))))
    (for-each show-prop device)))

(define (prop= input name val)
  (let ((prop (assoc name input)))
    (and prop (string= (cdr prop) val))))
;; }}}

;; {{{ Application-specific hooks
(define (input-added/browser idx input)
  "Set volume for web browsers and Flash player."
  (when (and (not (input-volume-set? idx))
             (or (prop= input "application.process.binary" "firefox")
                 (prop= input "application.process.binary" "chromium")
                 (prop= input "application.process.binary" "plugin-container")))
    (set-input-volume idx 0.8)))
(add-hook! pustule-input-added-hook input-added/browser)

(define (input-added/gajim idx input)
  "Set volume for Gajim notifications."
  (when (and (not (input-volume-set? idx))
             (prop= input "application.name" "gajim"))
    (let* ((fn (assoc-ref input "media.name"))
           (event (basename fn ".wav")))
      (cond
       ((member event '("connected" "disconnected"))
        (set-input-volume idx 0.5))
       ((string= event "sent")
        (set-input-volume idx 0.75))
       ((string= event "message2")
        (set-input-volume idx 1))))))
(add-hook! pustule-input-added-hook input-added/gajim)
;; }}}

;; {{{ Media roles
;; Alist of (idx . role) for inputs that have a media.role
(define roles '())

(define (input-added/media-role idx input)
  "Set volume for various applications based on their media role."
  ;; Add some roles to inputs that should have one but don't
  (when (not (input-volume-set? idx))
    (cond
     ((or (prop= input "application.process.binary" "mpg123")
          (prop= input "application.process.binary" "ogg123"))
      (set! input (assoc-set! input "media.role" "music"))))

    ;; Add the input to roles alist
    (let ((role (assoc-ref input "media.role")))
      (when role
        (set! roles (assoc-set! roles idx role))))

    ;; Set volume depending on the role
    (cond
     ((prop= input "media.role" "music") (set-input-volume idx 0.5))
     ((prop= input "media.role" "video") (set-input-volume idx 0.8)))))
(add-hook! pustule-input-added-hook input-added/media-role #t)

(define (input-removed/media-roles idx)
  "Remove input for the roles alist."
  (set! roles (assoc-remove! roles idx)))
(add-hook! pustule-input-removed-hook input-removed/media-roles)
;; }}}

;; {{{ Default rules
;; {{{ Inputs
(define (input-added/all idx input)
  "Base hook applied to all the added inputs."
  (format #t "[~d] Input added.\n" idx)
  (show-input idx input)
  (set! active-inputs (assoc-set! active-inputs idx #f)))
(add-hook! pustule-input-added-hook input-added/all)

(define (input-removed/all idx)
  (format #t "[~d] Input removed.\n" idx)
  (set! active-inputs (assoc-remove! active-inputs idx)))
(add-hook! pustule-input-removed-hook input-removed/all #t)

(define (input-added/default-volume idx input)
  "Set a default value for the volume if it wasn't set by the previous hooks."
  (when (not (input-volume-set? idx))
    (show-input idx input)
    (set-input-volume idx 0.6)))
(add-hook! pustule-input-added-hook input-added/default-volume #t)
;; }}}
;; {{{ Devices
(define (device-added/all idx device)
  "Base hook applied to all added devices."
  (format #t "{~d} Device added.\n" idx)
  (show-device idx device)
  (set! active-devices (assoc-set! active-devices idx device)))
(add-hook! pustule-device-added-hook device-added/all)

(define (device-removed/all idx)
  (format #t "{~d} Device removed.\n" idx)
  (set! active-devices (assoc-remove! active-devices idx)))
(add-hook! pustule-device-removed-hook device-removed/all #t)
;; }}}
;; }}}

;; Local Variables:
;; geiser-scheme-implementation: guile
;; End: