~schnouki/pustule

b306719228ff0077780358eb911b9303da683deb — Thomas Jost 4 years ago ffbb3a1
Replace "sink" by "input"
6 files changed, 106 insertions(+), 106 deletions(-)

M README.md
M events.c
M guile.c
M pulse.c
M pustule.h
M pustule.scm
M README.md => README.md +8 -8
@@ 58,23 58,23 @@ Configuration

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

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

    Called when a sink is added to PulseAudio (for instance when a new
    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 sink index (an integer) and an alist of the properties of the sink.
    the input index (an integer) and an alist of the properties of the input.

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

    Called when a sink is removed from PulseAudio (playback stops, application
    closed…). Each function is called with a single argument: the sink index.
    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** *sink-idx* *volume*
-   **pustule-set-volume** *input-idx* *volume*

    Sets the volume of the sink whose index is *sink-idx* (an integer) to
    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

M events.c => events.c +9 -9
@@ 29,7 29,7 @@

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



@@ 46,7 46,7 @@ void pustule_run() {
        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;
            pustule_input_event_t* event = first->event;
            event_queue_first = first->next;
            if (!event)
                continue;


@@ 54,17 54,17 @@ void pustule_run() {
            pthread_mutex_unlock(&event_queue_mutex);

            SCM index = scm_from_uint32(event->index);
            if ((event->type == SINK_ADDED) && scm_is_false(scm_hook_empty_p(sink_added_hook))) {
                SCM info = scm_from_sink_event(event);
            if ((event->type == INPUT_ADDED) && scm_is_false(scm_hook_empty_p(input_added_hook))) {
                SCM info = scm_from_input_event(event);
                SCM args = scm_list_2(index, info);
                scm_run_hook(sink_added_hook, args);
                scm_run_hook(input_added_hook, args);
                if (event->proplist)
                    pa_proplist_free(event->proplist);
                free(event->name);
                free(event->driver);
            }
            else if ((event->type == SINK_REMOVED) && scm_is_false(scm_hook_empty_p(sink_removed_hook))) {
                scm_run_hook(sink_removed_hook, scm_list_1(index));
            else if ((event->type == INPUT_REMOVED) && scm_is_false(scm_hook_empty_p(input_removed_hook))) {
                scm_run_hook(input_removed_hook, scm_list_1(index));
            }
            free(event);
            pthread_mutex_lock(&event_queue_mutex);


@@ 72,8 72,8 @@ void pustule_run() {
    }
}

/* Handle sink events from PulseAudio */
void pustule_handle_sink_event(pustule_sink_event_t* event) {
/* Handle input events from PulseAudio */
void pustule_handle_input_event(pustule_input_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");

M guile.c => guile.c +13 -13
@@ 36,24 36,24 @@ struct pustule_exported_func {
    const char* doc;
} exported_funcs[] = {
    {"pustule-get-volume", 1, 0, 0, &pustule_get_volume,
     "Gets the volume of the sink whose index is SINK-IDX (an integer)."},
     "Gets the volume of the input whose index is INPUT-IDX (an integer)."},
    {"pustule-set-volume", 2, 0, 0, &pustule_set_volume,
     "Sets the volume of the sink whose index is SINK-IDX (an integer) to\n"
     "Sets the volume of the input whose index is INPUT-IDX (an integer) to\n"
     "VOLUME (a number between 0 and 1)."},
    {NULL, 0, 0, 0, NULL}
};

/* Exported hooks */
SCM sink_added_hook = NULL;
SCM sink_removed_hook = NULL;
SCM input_added_hook = NULL;
SCM input_removed_hook = NULL;

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

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

    /* Cleanup */


@@ 64,7 64,7 @@ SCM pustule_get_volume(SCM idx) {
    return scm_from_double(vol.volume);
}

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


@@ 78,8 78,8 @@ SCM pustule_set_volume(SCM idx, SCM volume) {
    return volume;
}

/* Convert a sink event to an association list */
SCM scm_from_sink_event(pustule_sink_event_t* event) {
/* Convert an input event to an association list */
SCM scm_from_input_event(pustule_input_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);


@@ 121,8 121,8 @@ void pustule_init_guile() {
    }

    /* Create hooks */
    sink_added_hook = scm_make_hook(scm_from_int(2));
    scm_c_define("pustule-sink-added-hook", sink_added_hook);
    sink_removed_hook = scm_make_hook(scm_from_int(1));
    scm_c_define("pustule-sink-removed-hook", sink_removed_hook);
    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);
}

M pulse.c => pulse.c +19 -19
@@ 35,40 35,40 @@ pa_threaded_mainloop* paloop = NULL;
void state_callback(pa_context* ctx, void* userdata) {
    pa_context_state_t state = pa_context_get_state(ctx);
    if (state == PA_CONTEXT_READY) {
        // Get infos about all current sinks
        pa_context_get_sink_input_info_list(ctx, sink_info_callback, NULL);
        // Get infos about all current inputs
        pa_context_get_sink_input_info_list(ctx, input_info_callback, NULL);

        // Subscribe to new sinks events
        pa_context_set_subscribe_callback(ctx, sink_event_callback, NULL);
        // Subscribe to new inputs events
        pa_context_set_subscribe_callback(ctx, input_event_callback, NULL);
        pa_context_subscribe(ctx, PA_SUBSCRIPTION_MASK_SINK_INPUT, NULL, NULL);
    }
}

/* Event handler -- dispatch Scheme events */
void sink_event_callback(pa_context* ctx, pa_subscription_event_type_t t, uint32_t idx, void* userdata) {
void input_event_callback(pa_context* ctx, pa_subscription_event_type_t t, uint32_t idx, void* userdata) {
    if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_NEW) {
        // Sink added: query info about it
        pa_context_get_sink_input_info(ctx, idx, sink_info_callback, NULL);
        // Input added: query info about it
        pa_context_get_sink_input_info(ctx, idx, input_info_callback, NULL);
    }
    else if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
        // Sink removed: directly call the corresponding callback
        pustule_sink_event_t* event = malloc(sizeof(pustule_sink_event_t));
        // Input removed: directly call the corresponding callback
        pustule_input_event_t* event = malloc(sizeof(pustule_input_event_t));
        if (!event) abort();

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

/* 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) {
/* New input available (or infos about an existing input): defer a call the added callback */
void input_info_callback(pa_context* ctx, const pa_sink_input_info* i, int eol, void* userdata) {
    if (!i) return;

    pustule_sink_event_t* event = malloc(sizeof(pustule_sink_event_t));
    pustule_input_event_t* event = malloc(sizeof(pustule_input_event_t));
    if (!event) abort();

    event->type = SINK_ADDED;
    event->type = INPUT_ADDED;
    event->index = i->index;
    event->name = strdup(i->name);
    event->driver = strdup(i->name);


@@ 91,14 91,14 @@ void sink_info_callback(pa_context* ctx, const pa_sink_input_info* i, int eol, v
    }
    else {
        /* Event callback */
        pustule_handle_sink_event(event);
        pustule_handle_input_event(event);
    }
}

/* Query info about a sink */
void sink_get_info(uint32_t idx, pustule_volume_t* vol) {
/* Query info about an input */
void input_get_info(uint32_t idx, pustule_volume_t* vol) {
    pa_threaded_mainloop_lock(paloop);
    pa_operation* op = pa_context_get_sink_input_info(pactx, idx, sink_info_callback, (void*) vol);
    pa_operation* op = pa_context_get_sink_input_info(pactx, idx, input_info_callback, (void*) vol);
    pa_operation_unref(op);
    pa_threaded_mainloop_unlock(paloop);
}

M pustule.h => pustule.h +9 -9
@@ 28,7 28,7 @@

/* Data structures */
typedef struct {
    enum {SINK_ADDED, SINK_REMOVED} type;
    enum {INPUT_ADDED, INPUT_REMOVED} type;
    uint32_t index;
    char* name;
    char* driver;


@@ 37,7 37,7 @@ typedef struct {
    int has_volume;
    pa_cvolume volume;
    pa_proplist* proplist;
} pustule_sink_event_t;
} pustule_input_event_t;

typedef struct {
    double volume;


@@ 46,12 46,12 @@ typedef struct {
} pustule_volume_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);
void input_event_callback(pa_context* ctx, pa_subscription_event_type_t t, uint32_t idx, void* userdata);
void input_info_callback(pa_context* ctx, const pa_sink_input_info* i, int eol, void* userdata);
void state_callback(pa_context* ctx, void* userdata);

/* PulseAudio functions */
void sink_get_info(uint32_t idx, pustule_volume_t* vol);
void input_get_info(uint32_t idx, pustule_volume_t* vol);

/* Guile functions */
SCM pustule_get_volume(SCM idx);


@@ 59,16 59,16 @@ SCM pustule_set_volume(SCM idx, SCM volume);

/* Guile helpers */
void pustule_init_guile();
SCM scm_from_sink_event(pustule_sink_event_t* event);
SCM scm_from_input_event(pustule_input_event_t* event);

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

/* Global variables */
extern pa_context* pactx;
extern pa_threaded_mainloop* paloop;
extern SCM sink_added_hook;
extern SCM sink_removed_hook;
extern SCM input_added_hook;
extern SCM input_removed_hook;

#endif

M pustule.scm => pustule.scm +48 -48
@@ 20,18 20,18 @@
;; explanation of how this works.
;;
;; Each application that plays sound using PulseAudio creates one or several
;; sinks. The volume of each sink can be set independently.
;; inputs. The volume of each input can be set independently.
;;
;; pustule provides with two hooks, named `pustule-sink-added-hook' and
;; `pustule-sink-removed-hook', which are called when a sink is added to or
;; pustule provides with two hooks, named `pustule-input-added-hook' and
;; `pustule-input-removed-hook', which are called when an input is added to or
;; removed from the PulseAudio daemon.
;;
;; The volume of each sink (a number between 0 and 1) can be set using the
;; The volume of each input (a number between 0 and 1) can be set using the
;; `pustule-set-volume' function.
;;
;; 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 sink. Some fun examples are shown, such as setting
;; 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.
;;


@@ 44,44 44,44 @@
(use-modules (ice-9 format))

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

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

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

(define (show-sink idx sink)
(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 sink)))
    (for-each show-prop input)))

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

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

(define (added/gajim idx sink)
(define (added/gajim idx input)
  "Set volume for Gajim notifications."
  (when (and (not (volume-set? idx))
             (prop= sink "application.name" "gajim"))
    (let* ((fn (assoc-ref sink "media.name"))
             (prop= input "application.name" "gajim"))
    (let* ((fn (assoc-ref input "media.name"))
           (event (basename fn ".wav")))
      (cond
       ((member event '("connected" "disconnected"))


@@ 90,56 90,56 @@
        (set-volume idx 0.75))
       ((string= event "message2")
        (set-volume idx 1))))))
(add-hook! pustule-sink-added-hook added/gajim)
(add-hook! pustule-input-added-hook added/gajim)
;; }}}

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

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

    ;; Add the sink to roles alist
    (let ((role (assoc-ref sink "media.role")))
    ;; 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= sink "media.role" "music") (set-volume idx 0.5))
     ((prop= sink "media.role" "video") (set-volume idx 0.8)))))
(add-hook! pustule-sink-added-hook added/media-role #t)
     ((prop= input "media.role" "music") (set-volume idx 0.5))
     ((prop= input "media.role" "video") (set-volume idx 0.8)))))
(add-hook! pustule-input-added-hook added/media-role #t)

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

;; {{{ Default rules
(define (added/all idx sink)
  "Base hook applied to all the added sinks."
  (format #t "[~d] Sink added.\n" idx)
  (show-sink idx sink)
  (set! active-sinks (assoc-set! active-sinks idx #f)))
(add-hook! pustule-sink-added-hook added/all)
(define (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 added/all)

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

(define (added/default-volume idx sink)
(define (added/default-volume idx input)
  "Set a default value for the volume if it wasn't set by the previous hooks."
  (when (not (volume-set? idx))
    (show-sink idx sink)
    (show-input idx input)
    (set-volume idx 0.6)))
(add-hook! pustule-sink-added-hook added/default-volume #t)
(add-hook! pustule-input-added-hook added/default-volume #t)
;; }}}