Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Watch for volume changes in ALSA/Pulseaudio

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.

like image 262
Michael Eden Avatar asked Jan 22 '16 00:01

Michael Eden


People also ask

Does PulseAudio use ALSA?

PulseAudio is a general purpose sound server intended to run as a middleware between your applications and your hardware devices, either using ALSA or OSS. It also offers easy network streaming across local devices using Avahi if enabled.

How do I change the output in PulseAudio?

In Pulseaudio Volume Control (pavucontrol), under the "Playback" tab, change the output of an application to <name>, and in the recording tab change the input of an application to "Monitor of <name>". Audio will now be outputted from one application into the other.

Does Aplay use PulseAudio?

It states that alsa (aplay) is configured to send its output to pulseaudio (which is your sound server) by default. When your sound server is not running you will not be able to hear a sound as you have observed.


3 Answers

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

pactl subscribe | grep --line-buffered "sink" | xargs -n1 ./volume_notify.sh
like image 161
Hendrik Schro Avatar answered Oct 20 '22 11:10

Hendrik Schro


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]);
    }
}
like image 28
sealj553 Avatar answered Oct 20 '22 11:10

sealj553


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.

like image 7
CL. Avatar answered Oct 20 '22 10:10

CL.