Users browsing this thread: 1 Guest(s)
venam
Administrators
Hello nixers,
I searched the forums and couldn't actually find anything related to audio.

I recently wrote a summary of the audio stack on open source Unix-like systems.
But I'd like to know your opinion and your actual local setup.

Do you prefer the audio processing to happen in user-land or kernel?
Do you have a use for audio pipeline?
Or for volume per-application?
What do you think of PipeWire, are you using it?
What's your preferred audio stack currently?
Do you generate music on your machine?

Otherwise, let's talk anything audio.

The thing I'm most amazed is that it's the first time that I actually understand ALSA's configuration format. Now it just clicked: it's a big dictionary of different profiles/devices that get merged.
movq
Long time nixers
(08-02-2021, 03:37 AM)venam Wrote: I recently wrote a summary
A “summary”! 😂 That’s an awesome in-depth article that could show up in a book, my guy.


I use PulseAudio and Jack. PA has features that are really hard to do with plain ALSA, for example I can easily plug in an equalizer (or any LADSPA module!), I can set volume per-application (yes, I do that), I can easily move programs between sound cards (I have at least two of them, plus maybe HDMI). It’s also awesome to have those “Monitor” devices, where you can just record what’s playing on a certain device. They also allow me to use rtspeccy as a generic visualizer for everything, which is great. I admit that I skipped the first few years of PulseAudio where it might have been very buggy – no idea. It’s solid today and I actually like it.

pulsemixer is the frontend that I use. It’s a very tidy UI. Sometimes I get the impression that people are just confused by tools like pavucontrol and then blame PA for it, but that’s pure speculation, of course.

Jack is unavoidable for some software like Guitarix (which saved me a ton of money) or Ardour. My external audio interface (a Focusrite Scarlett Solo) is blacklisted in PulseAudio, so I can run Jack and PA at the same time, where Jack only uses the Scarlett. That said, Jack (or the applications using it) has more bugs than PA. For example, LMMS gets kicked from Jack all the time, which is very annoying. Ardour sometimes screws up everything, all the routing is gone. This is a major PITA, it really feels a bit fragile. Could be a bug in Ardour or Jack, I don’t know.

I used to have a hand-written ~/.asoundrc and used plain ALSA, but come on, this is so much work …

Haven’t used PipeWire yet. PA just became a really stable and solid platform for me, so I’m a bit reluctant to change again. (I have zero use for all that Flatpak stuff and containerized apps – yet.)

My desktop environment doesn’t use sound events. Well, GTK sometimes does, but not on purpose. Some of my scripts sometimes run beep, but that’s it. Sound events feel like a relic from the Windows 95 era to me. %)
venam
Administrators
I'm also currently using pulseaudio.

(09-02-2021, 01:06 PM)vain Wrote: pulsemixer is the frontend that I use
Similarly, I use pulsemixer, it's simple and neat.

I got paprefs installed in case I want to set simultaneous output, when I have multiple headsets connected like when watching a movie with friends each can have their own audio without bothering the rest of the house. I use the GUI instead of loading the module manually - It's just so much easier.

One thing I'm testing is having different virtual sinks for different cases: voip, media, notification.
So far, I have these in my default.pa
Code:
load-sample-lazy x11-bell /usr/share/sounds/freedesktop/stereo/bell.oga
#load-sample-lazy x11-bell  /home/vnm/media/musics/yeah_yeah.ogg
load-module module-x11-bell sample=x11-bell sink=notification

load-module module-virtual-sink use_volume_sharing=no force_flat_volume=yes sink_name=media master=alsa_output.usb-C-Media_Electronics_Inc._Microsoft_LifeChat_LX-3000-00.iec958-stereo sink_properties="device.description='media'"
load-module module-virtual-sink use_volume_sharing=no force_flat_volume=yes sink_name=voip master=alsa_output.usb-C-Media_Electronics_Inc._Microsoft_LifeChat_LX-3000-00.iec958-stereo sink_properties="device.description='voip'"
load-module module-virtual-sink use_volume_sharing=no force_flat_volume=yes sink_name=notification master=alsa_output.usb-C-Media_Electronics_Inc._Microsoft_LifeChat_LX-3000-00.iec958-stereo sink_properties="device.description='notification'"
set-default-sink alsa_output.usb-C-Media_Electronics_Inc._Microsoft_LifeChat_LX-3000-00.iec958-stereo
alsa_output.usb-C-Media_Electronics_Inc._Microsoft_LifeChat_LX-3000-00.iec958-stereo is the name of my headset (hardware sink).

NB: The module-x11-bell can't be rerouted using pulseaudio restoration database so you have to specify the sink at module load time.
Other than that this gives me 3 virtual sinks called "voip", "notification", and "media". Then I need pulseaudio to remember the routing of streams. It does that automatically normally when you manually move streams (according to different criteria) and saves it in the restoration database.
So far I haven't found any user-interface that allows editing it easily, so I started writing one today.
movq
Long time nixers
(09-02-2021, 01:30 PM)venam Wrote: load-module module-virtual-sink
Do you know if there’s a comprehensive documentation on this module? Or the standard PA modules in general?

I know of this page:

https://www.freedesktop.org/wiki/Softwar...r/Modules/

But either I’m blind or module-virtual-sink is not listed here. Of course, I can try to deduce stuff from reading the source code, but meh.
venam
Administrators
(10-02-2021, 11:33 AM)vain Wrote: Do you know if there’s a comprehensive documentation on this module? Or the standard PA modules in general?

You can always do this:
Code:
pacmd describe-module module-virtual-sink
Name: module-virtual-sink
Version: 13.99.1
Description: Virtual sink
Author: Pierre-Louis Bossart
Usage: sink_name=<name for the sink> sink_properties=<properties for the sink> master=<name of sink to filter> rate=<sample rate> channels=<number of channels> channel_map=<channel map> use_volume_sharing=<yes or no> force_flat_volume=<yes or no>
Load Once: no

There are a bunch of default modules that comes with PulseAudio in the source directory you mentioned. Not all of them are documented on that page you sent. You can find a bigger list here. Another way would be to look in your local lib folder, somewhere like /usr/lib/pulse-<version>/modules.
That's really one of the drawback of PulseAudio and even more with PipeWire, the documentation is lacking, and the toolset could be much more advanced and easy if people were aware of the features.

EDIT: You can dump all installed modules like this:
Code:
pulseaudio --dump-modules --verbose
venam
Administrators
Let's update this thread with my new setup as I recently dived bit more into PipeWire.

I was able to reproduce my previous setup with PipeWire since my last issue with the restoration stream was fixed.
I even tested if the target-node, which is what PipeWire called the node a stream remembers to attach to when seen, keeps the right target in edge-cases like when it fallsback to another node when the current node the stream is attached to disappears. It still keeps the right expected target node in that case, which is what we want.


My setup consists of 3 nodes that are virtual path connected to my headset by default:

- Multimedia Node
- Voip Node
- Notification Node

[Image: qjackctl.png]

The idea is that I rely on the restoration mechanism to have streams automatically remember which type of path they should attach. The use-case is that if I am in a Voip call, I can listen to it through the Voip Node path, and meanwhile, I can redirect the Multimedia Node audio towards the call if I need to without the person hearing a loopback of their own voice.

Additionally, as a bonus, I should be able to plug and unplug my headset without loosing the streams that are currently being played as they are attached to these virtual nodes. I've had issues in the past with this on PulseAudio with this specific setup that I mentioned above.

Now to achieve this there are two steps that need to be done:

1. Create the virtual nodes
2. Have a mechanism to attach them automatically to my headset

The first step is pretty simple, you can rely on the `context.objects` section of one of the service you're starting, be it pipewire, pipewire-pulse, or pipewire-media-session, they all allow this. It doesn't make much difference which one, as I noticed, so I just added them in my ~/.config/pipewire/pipewire.conf
It looks like this:

Code:
{   factory = adapter
        args = {
            factory.name     = support.null-audio-sink
            node.name        = "Multimedia"
            node.description = "Multimedia Node"
            media.class      = "Audio/Duplex"
            audio.position   = "FL,FR"
            monitor.channel-volumes = true
        }
    }

    {   factory = adapter
        args = {
            factory.name     = support.null-audio-sink
            node.name        = "Notification"
            node.description = "Notification Node"
            media.class      = "Audio/Duplex"
            audio.position   = "FL,FR"
            monitor.channel-volumes = true
        }
    }

    {   factory = adapter
        args = {
            factory.name     = support.null-audio-sink
            node.name        = "Voip"
            node.description = "Voip Node"
            media.class      = "Audio/Duplex"
            audio.position   = "FL,FR"
            monitor.channel-volumes = true
        }
    }

The node.description is what appears in pulseaudio tools, media.class dictates how ports are created, adding "Virtual" to it hides it from most tools, Duplex makes the output port name be called "capture_*" instead of "monitor_*". Monitor would the equivalent of the automated capture streams created in PulseAudio language.
The part that was the hardest to discover was the monitor.channel-volumes, which if set to false would mean that the audio is pass-through, so if you change the volume in one of the virtual stream it wouldn't affect it. That's not what I want so I opted to set this to true.

The second step is the one that gave me issues.
I initially wanted to rely on WirePlumber implementation of the concept of Endpoints, which is similar to what I want to implement, but I wasn't able to rely on the restoration mechanism and nor did the Endpoint appear in PulseAudio tools, so it wasn't really what I wanted.

I couldn't also create the links between the virtual nodes and my headset directly from the config file, even when reproducing some configs I've seen online.
They set either "target.node" or "node.target", which I'm not sure what is the difference (it seems the first one is in the metadata and the other in the node properties), and that's suppose to give a hint to the session manager, similar to what is in the restoration db, to connect the nodes. Yet, it's not respected.
Maybe it's related to the fact that if I issue the following I don't see my nodes but only my headset:
Code:
pw-cat -p --list-targets

Available targets ("*" denotes default):
*    48: description="LifeChat LX-3000 Headset Digital Stereo (IEC958)" prio=880

I then tried to find another way to do this natively and stumbled upon this:
Code:
pw_metadata <id> target.node <target-id> Spa:Id
Which sets the target.node metadata on the node, so that it triggers the session manager to connect it to the specified target. However, it connects both ends of the node (capture/playback) to it, and it doesn't seem to keep respecting this value on different events. This wasn't a solid solution.

The best thing I've found when it comes to this was to rely on jack toolkits. I personally used something called jack-plumbing, but I've heard of similar tools such as jack_connect, and possibly others like aj-snapshot when running in daemon mode.
What these tools do is listen for events in the graph and decide how to connect nodes according to specific rules. I know that's supposed to be the role of the session manager, but in this case neither pipewire-media-session nor WirePlumber offers an easy way to do that.

Here are the rules I've set in my ~/.jack-plumbing:
Code:
(connect "Voip Node Monitor:capture_FL" "LifeChat LX-3000 Headset Digital Stereo \(IEC958\):playback_FL")
(connect "Voip Node Monitor:capture_FR" "LifeChat LX-3000 Headset Digital Stereo \(IEC958\):playback_FR")
(connect "Multimedia Node Monitor:capture_FL" "LifeChat LX-3000 Headset Digital Stereo \(IEC958\):playback_FL")
(connect "Multimedia Node Monitor:capture_FR" "LifeChat LX-3000 Headset Digital Stereo \(IEC958\):playback_FR")
(connect "Notification Node Monitor:capture_FL" "LifeChat LX-3000 Headset Digital Stereo \(IEC958\):playback_FL")
(connect "Notification Node Monitor:capture_FR" "LifeChat LX-3000 Headset Digital Stereo \(IEC958\):playback_FR")

What's nice with jack-plumbing (which you should run as pw-jack jack-plumbing), is that on any event, even when links change for a reason or another, it will go through the rules and re-create the links if needed. That's useful if the headset gets disconnected and reconnected.

The next issue is to run pipewire, pipewire-media-session, pipewire-pulse, and jack-plumbing as systemd services.
Fortunately, Arch comes with service and socket units for all of them, so I just had to enable them… except for my custom jack-plumbing.
Another thing to beware, is that socket units should be the first units started in the chain.

This is what I ended up having as a jack-plumbing.service in my ~/.config/systemd/user/jack-plumbing.service

Code:
[Unit]
Description=Jack Plumbing
Wants=pipewire-media-session.service
After=pipewire-media-session.service
PartOf=pipewire-media-session.service

[Service]
ExecStart=/usr/bin/pw-jack /home/vnm/docu/bin/jack-plumbing

[Install]
WantedBy=default.target

When testing you can stop everything by doing
systemctl --user stop pipewire.socket
systemctl --user stop pipewire-pulse.socket

Then restart everything in the following order

systemctl --user start pipewire.socket
systemctl --user start pipewire-pulse.socket
systemctl --user start pipewire
systemctl --user start pipewire-pulse
systemctl --user start jack-plumbing.service


The next thing I'll try to add, which I've tested but haven't really seen the benefits of yet, is having rnnoise ldaspa module filter for echo cancelation on the microphone.
I had to install a package called "noise-suppression-for-voice" on ArchLinux to get the ladspa library in /usr/lib/ladspa/librnnoise_ladspa.so, there wasn't so much documentation on that.

It could also be used to add another level of indirection, avoiding issues when real devices disconnect.

In the pipewire configuration you can then create a node that will use that filter for you.
Another way to test this is to create a separate pipewire configuration file, with the bare minimum in it and put only this module.

Code:
{   name = libpipewire-module-filter-chain
    args = {
        node.name = "effect_input.rnnoise"
        node.description = "Noise Canceling source"
        media.name = "Noise Canceling source"
        filter.graph = {
            nodes = [
                {
                    type = ladspa
                    name = rnnoise
                    plugin = librnnoise_ladspa
                    label = noise_suppressor_stereo
                    control = {
                        "VAD Threshold (%)" 50.0
                    }
                }
            ]
        }
        capture.props = {
            node.description = "Noise Canceling MicInput"
            node.passive = true
            node.pause-on-idle = true
        }
        playback.props = {
            node.description = "Noise Canceling Output"
            media.class = Audio/Source
        }
    }
}


Now as for the utilities that can be used to interact with PipeWire stuff, it's not missing, you can rely on PulseAudio, on Jack, or on native tools.
I personally use the most pulsemixer for volume and qjackctl to move streams.

I was curious to see all the ways I coul set the volume for the default node.

The easiest way is to rely on the ALSA pcm that PipeWire creates:
Code:
amixer -D pipewire sset Master 5%- # decrease
amixer -D pipewire sset Master 5%+ # increase
However, I still use pulse as the default pcm with alsa (alsactl dump-cfg and search for ctl.default and pcm.default), so I would rely on:
Code:
amixer -D pulse sset Master 5%+
amixer -D pulse sset Master 5%-

We can rely on pactl
Code:
pactl set-sink-volume @DEFAULT_SINK@ +5%
pactl set-sink-volume @DEFAULT_SINK@ -5%

So far, so good, now the hardest way to set the volume is to do it natively, there's no direct tool to do that right now. So here's as a bonus how to do it.

1. Find the current default sink
2. Find the current volume
3. Set the volume

To find the current default sink, we have to rely on pw-metadata, as its an information that's only useful for the session manager.
Here's what I got (NB: I rely on the jq tool to parse json):

Code:
# the metadata only contains the name of the default sink
default_sink_name=$(pw-metadata 0 'default.audio.sink' | grep 'value' | sed "s/.* value:'//;s/' type:.*$//;" | jq .name)
default_sink_id=$(pw-dump Node | jq '.[].info.props|select(."node.name" == '" $default_sink_name "') | ."object.id"')

To find the current volume isn't easy either, you have to interrogate the params of the node and parse them, converting the volume using cube root function, here's what I got:

Code:
current_volume=$(pw-cli enum-params $default_sink_id 'Props' | grep -A 2 'Spa:Pod:Object:Param:Props:channelVolumes' | awk '/Float / {gsub(/.*Float\s/," "); print $1^(1/3) }')

Now the final step is to set the volume, we can do that using pw-cli, but we also need to calculate how much change we want.
Here we simulate an increment of 0.1.

Code:
change=0.1
new_volume=$(echo "$current_volume $change" | awk '{printf "%f", $1 + $2}')
# we need to reconvert to cubic root
new_volume_cube=$(echo $new_volume | awk '{ print $1^3 }')
pw-cli s $default_sink_id Props "{ mute: false, channelVolumes: [ $new_volume_cube , $new_volume_cube ] }"

This works fine, yet, the PulseAudio tools don't seem to show the volume as updated when you do this maneuver.
Here's the final script:

Code:
# the metadata only contains the name of the default sink
default_sink_name=$(pw-metadata 0 'default.audio.sink' | grep 'value' | sed "s/.* value:'//;s/' type:.*$//;" | jq .name)
default_sink_id=$(pw-dump Node | jq '.[].info.props|select(."node.name" == '" $default_sink_name "') | ."object.id"')
current_volume=$(pw-cli enum-params $default_sink_id 'Props' | grep -A 2 'Spa:Pod:Object:Param:Props:channelVolumes' | awk '/Float / {gsub(/.*Float\s/," "); print $1^(1/3) }')
change="${1:-0.1}" # defaults to increment of 0.1
new_volume=$(echo "$current_volume $change" | awk '{printf "%f", $1 + $2}')
# we need to reconvert to cubic root
new_volume_cube=$(echo $new_volume | awk '{ print $1^3 }')
pw-cli s $default_sink_id Props "{ mute: false, channelVolumes: [ $new_volume_cube , $new_volume_cube ] }"
# or use wpctl instead
gist

Another way to set the volume would be to rely on wpctl from WirePlumber tools (but doesn't actually need WirePlumber running), it also doesn't need all the juggling with cube transformation and updates PulseAudio tools:
Code:
wpctl set-volume $default_sink_id $new_volume

So to conclude, PipeWire is sort of the Perl of media, or the shell pipeline of media, mechanism not policy, somehwat the Tim Toady There's more than one way to do it.

I've had issues in the past with the PulseAudio setup I mentioned above but it seems to work fine on PipeWire. The native tooling is still missing things but you get around. Same for the stuff which I had to rely on jack-plumbing. But it's nice to have everything backward compatible.

NB: After further usage, I'm still hitting edge-cases and bugs, so be sure to know it's still not the most stable software yet.

Update: I'm testing with the following setup so that the media stream is automatically connected and merge through an intermediary node called "Record Node", that is the default source node for recording.
Here's what it looks like when the headset is in (using helvum instead of qjackctl):
[Image: helvum_headset_in.png]
And when I remove my headset:
[Image: helvum_headset_out.png]

I also forgot to set the rtkit permission for pipewire, I did that in polkit, it's helping it being more responsive.