Watch for volume changes in ALSA/Pulseaudio
Asked Answered
C

4

10

How do you listen to changes in volume on the Master channel on the default sound card? I'd like to be notified through dbus or a callback or something that the volume has changed.

I have tried looking and the ALSA and PulseAudio APIs and they only seem to allow you to set and get the volume, but not listen for changes in the volume.

Any programming language is fine.

Cointon answered 22/1, 2016 at 0:10 Comment(0)
S
9

This is possible with the ALSA API.

When you have a control device, call snd_ctl_subscribe_events() to enable events. Then use snd_ctl_read() to read events; to wait for them, use blocking mode or poll(). If the event is of type SND_CTL_EVENT_ELEM, and if its event bit mask contains SND_CTL_EVENT_MASK_VALUE, that element's value has changed.

See the implementation of amixer monitor for an example.

Spigot answered 22/1, 2016 at 9:46 Comment(1)
If anyone is wondering this turned into a node.js plugin here.Cointon
S
10

Edit: In the second example, an event isn't generated for me when volume is below 5% or above 100%. The first example works perfectly as far as I know.

pactl subscribe will print out data about the sinks when the volume changes. What I'm doing now is piping the output to a small C program that will run a script.

run.sh:

pactl subscribe | grep --line-buffered "sink" | ./prog

or for a specific sink, e.g. 3:

pactl subscribe | grep --line-buffered "sink #3" | ./prog

prog.c:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[]){
    while(1){
        while(getchar() != '\n');
        system("./volume_notify.sh");
    }
}

When the volume of the sink is changed, pactl will print a line, which will cause the program to run the script.

-or-

Here's an example based on the amixer monitor, as referenced by CL. The while loop will iterate each time the volume changes, so put your callback in there.

#include <stdio.h>
#include <alsa/asoundlib.h>

#define MAX_CARDS 256

int monitor_native(char const *name);
int open_ctl(const char *name, snd_ctl_t **ctlp);
void close_all(snd_ctl_t* ctls[], int ncards);

int main(int argc, char* argv[]){

    const char *ctl_name = "hw:0";

    while(monitor_native(ctl_name) == 1){
        //volume has been changed, do something
        system("~/.volume_notify.sh");
    }

    return 0;
}

int monitor_native(char const *name) {
    snd_ctl_t *ctls[MAX_CARDS];
    int ncards = 0;
    int i, err = 0;

    if (!name) {
        int card = -1;
        while (snd_card_next(&card) >= 0 && card >= 0) {
            char cardname[16];
            if (ncards >= MAX_CARDS) {
                fprintf(stderr, "alsactl: too many cards\n");
                close_all(ctls, ncards);
                return -E2BIG;
            }
            sprintf(cardname, "hw:%d", card);
            err = open_ctl(cardname, &ctls[ncards]);
            if (err < 0) {
                close_all(ctls, ncards);
                return err;
            }
            ncards++;
        }
    } else {
        err = open_ctl(name, &ctls[0]);
        if (err < 0) {
            close_all(ctls, ncards);
            return err;
        }
        ncards++;
    }

    for (;ncards > 0;) {
        pollfd* fds = new pollfd[ncards];

        for (i = 0; i < ncards; i++) {
            snd_ctl_poll_descriptors(ctls[i], &fds[i], 1);
        }

        err = poll(fds, ncards, -1);
        if (err <= 0) {
            err = 0;
            break;
        }

        for (i = 0; i < ncards; i++) {
            unsigned short revents;
            snd_ctl_poll_descriptors_revents(ctls[i], &fds[i], 1, &revents);
            if (revents & POLLIN) {
                snd_ctl_event_t *event;
                snd_ctl_event_alloca(&event);

                if (snd_ctl_read(ctls[i], event) < 0) {
                    continue;
                }
                if (snd_ctl_event_get_type(event) != SND_CTL_EVENT_ELEM) {
                    continue;
                }

                unsigned int mask = snd_ctl_event_elem_get_mask(event);
                if (mask & SND_CTL_EVENT_MASK_VALUE) {
                    close_all(ctls, ncards);
                    return 1;
                }
            }
        }
    }

    close_all(ctls, ncards);
    return 0;
}

int open_ctl(const char *name, snd_ctl_t **ctlp) {
    snd_ctl_t *ctl;
    int err;

    err = snd_ctl_open(&ctl, name, SND_CTL_READONLY);
    if (err < 0) {
        fprintf(stderr, "Cannot open ctl %s\n", name);
        return err;
    }
    err = snd_ctl_subscribe_events(ctl, 1);
    if (err < 0) {
        fprintf(stderr, "Cannot open subscribe events to ctl %s\n", name);
        snd_ctl_close(ctl);
        return err;
    }
    *ctlp = ctl;
    return 0;
}

void close_all(snd_ctl_t* ctls[], int ncards) {
    for (ncards -= 1; ncards >= 0; --ncards) {
        snd_ctl_close(ctls[ncards]);
    }
}
Sibbie answered 26/4, 2017 at 2:7 Comment(0)
S
9

This is possible with the ALSA API.

When you have a control device, call snd_ctl_subscribe_events() to enable events. Then use snd_ctl_read() to read events; to wait for them, use blocking mode or poll(). If the event is of type SND_CTL_EVENT_ELEM, and if its event bit mask contains SND_CTL_EVENT_MASK_VALUE, that element's value has changed.

See the implementation of amixer monitor for an example.

Spigot answered 22/1, 2016 at 9:46 Comment(1)
If anyone is wondering this turned into a node.js plugin here.Cointon
S
4

Similar to @sealj553's answer, but does not need a C-program:

pactl subscribe | grep --line-buffered "sink" | xargs -n1 ./volume_notify.sh
Sulfanilamide answered 30/8, 2019 at 7:51 Comment(1)
This one-liner almost works great, but it calls volume_notify.sh multiple times on each event. I suggest using this instead: shell pactl subscribe | grep --line-buffered "sink" | while read -r UNUSED_LINE; do ./volume_notify.sh; done Helbon
S
0

The Linkplay A31 Amplifier Control App monitors ALSA pcm/mixer devices for sound activity and volume changes and adjusts the (external) DAC/AMP accordingly. It also (un)mutes the amplifier if there is (no) sound activity for some (adjustable) time period.

You will probably need to modify the serial control logic for your use case (e.g. dbus, callback, system() cmd), but that should be fairly easy to accomplish.

Stringency answered 30/3 at 9:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.