Search code examples
shellunixposixpipewire

Swallow input from a monitoring callback while processing in a while loop


I'm trying to create a script to monitor a pipewire audio source, and provide outputs relating to the "percentage" of the volume. Since I'm using pipewire and wireplumber, I decided to use pw-mon, provided by pipewire for monitoring, and wpctl, provided by wireplumber to accomplish this.

pw-mon | while read; do
    # wpctl returns output as Volume: 0.00 [MUTED] where 0.00 is the volume, and [MUTED] is irrelevant for our case.
    # I process via cut to get the second field.
    awk '{print $1 * 100}' <<< $(wpctl get-volume @DEFAULT_AUDIO_SINK@ | cut -d" " -f2)
done

The main issue with this is that pw-mon returns many stdout responses about their device state, and as a side-effect, awk has to process tens of times because while read gets a lot of lines from pw-mon stdout. I want to prevent this and only print out once, so essentially squash all the stdout from pw-mon, have the while run only once because of that, and then in-turn process my awk only once as well. Problem is I'm not sure how to accomplish this.

I tried to use tr -d "\r\n" from https://stackoverflow.com/a/68091645/9091276, but while read no longer runs (I made sure of this by doing while read l; do echo $l; done). I thought to add a sleep in the while loop, but that doesn't make sense because it'll just delay the reading of the pw-mon stdout lines rather than hold back on reading all of them.

Any thoughts or ideas of what I can do? For clarity, here's some output from pw-mon so you can see what happens per event callback from it: https://hastebin.com/share/agajayosoq.rust (its not rust idk why it says that).

EDIT: Let me clarify what I'm trying to do

pw-mon fires some audio device data into the stdout every time it detects a change to the audio device (volume up/down/mute). Otherwise, pw-mon does NOT output any data. The problem is every time a change occurs, pw-mon dumps a ton of lines of data to stdout, which means, with my current script, itll fire the while read multiple times. That is not what I want, I want to only run the "awk" portion once every time a change occurs. My idea was to squash all my stdout into one line, so while read would only fire once, but doing pw-mon | tr -d "\r\n" | while read did not work, for some reason the while read would not fire.

EDIT 2: sample output of pw-mon:

    type: PipeWire:Interface:Core
    cookie: 2956247259
    user-name: "frontear"
    host-name: "frontear-net"
    version: "0.3.81"
    name: "pipewire-0"
*   properties:
*       config.name = "pipewire.conf"
*       link.max-buffers = "16"
*       core.daemon = "true"
*       core.name = "pipewire-0"
*       module.jackdbus-detect = "true"
*       module.x11.bell = "true"
*       module.access = "true"
*       cpu.max-align = "64"
*       default.clock.rate = "48000"
*       default.clock.quantum = "1024"
*       default.clock.min-quantum = "32"
*       default.clock.max-quantum = "2048"
*       default.clock.quantum-limit = "8192"
*       default.video.width = "640"
*       default.video.height = "480"
*       default.video.rate.num = "25"
*       default.video.rate.denom = "1"
*       log.level = "2"
*       clock.power-of-two-quantum = "true"
*       mem.warn-mlock = "false"
*       mem.allow-mlock = "true"
*       settings.check-quantum = "false"
*       settings.check-rate = "false"
*       object.id = "0"
*       object.serial = "0"
added:
    id: 0
    permissions: r-xm-
    type: PipeWire:Interface:Core (version 4)
    properties:
        object.serial = "0"
        core.name = "pipewire-0"
added:
    ... (omitted for brevity)

*     id:15 (Spa:Enum:ParamId:Latency)
          Object: size 176, type Spa:Pod:Object:Param:Latency (262155), id Spa:Enum:ParamId:Latency (15)
            Prop: key Spa:Pod:Object:Param:Latency:direction (1), flags 00000000
              Id 0        (Spa:Enum:Direction:Input)
            Prop: key Spa:Pod:Object:Param:Latency:minQuantum (2), flags 00000000
              Float 0.000000
            Prop: key Spa:Pod:Object:Param:Latency:maxQuantum (3), flags 00000000
              Float 0.000000
            Prop: key Spa:Pod:Object:Param:Latency:minRate (4), flags 00000000
              Int 0
            Prop: key Spa:Pod:Object:Param:Latency:maxRate (5), flags 00000000
              Int 0
            Prop: key Spa:Pod:Object:Param:Latency:minNs (6), flags 00000000
              Long 0
            Prop: key Spa:Pod:Object:Param:Latency:maxNs (7), flags 00000000
              Long 0
    properties:
        port.id = "0"
        port.direction = "out"
        object.path = "xdg-desktop-portal-hyprland:capture_0"
        port.name = "capture_1"
        port.alias = "xdg-desktop-portal-hyprland:capture_1"
        node.id = "74"
        object.id = "75"
        object.serial = "377"

Solution

  • Had some ideas from the questions, such as finding data specific to both initial call and each callbacks, and I found it. Since pipewire is going to print the device id for my device that changes, I can simply match that line.

    I first used wpctl inspect @DEFAULT_AUDIO_SINK@, then piped it through awk via awk -F"\"" "/device.id/{print \$2}" to find the device.id output and pull the number. Then, using this, I run pw-mon -oa to remove way extra output (just learned about this command), leaving device.id as one of the few props it prints out since that's the device that changes. Finally, I pipe it via awk once again to find the line with my id, reducing while invocations.

    The full code looks like this:

    DEVICE_ID=$(wpctl inspect @DEFAULT_AUDIO_SINK@ | awk -F"\"" "/device.id/{print \$2}")
    pw-mon -oa | awk "/id: $DEVICE_ID/{print; fflush()}" | while read; do
        wpctl get-volume @DEFAULT_AUDIO_SINK@ | awk "{print \$2 * 100}"
    done