Old guy supposedly listening to some youtube video of "lofi beats to chill and study to" on his overpriced macbook connected (via overpriced cables) to overpriced auidophile audio equipment
audiophile.jpg meme on interent

Convenience audio setup (part 1): multiple streams

Premise

For a long time humanity had a big gripe with audio on pc gaming (and a console gaming as well) - volume balance between game audio and VoIP audio. If you're a multitasking freak or you played some game long enough that music in it annoys you, you might want to add music volume and video/stream/youtube/twitch volume to it as well. Evidently it's not really a niche problem as some systems/VoIP clients have feature to "duck" (reduce volume) of everything else while someone is talking, and there are "gamer" audio interfaces that have direct "game/voip balance" control, I think they even make headsets with such feature as well.

What if I told you that on desktop linux you can have this feature out of the box? Or at least as long as you have PulseAudio or PipeWire in the box. And I get what you might say - "but I can already adjust levels of each application separately on PC and i don't even need linux for that!", and that's true, but it has multiple caveats - at least on windows application might force its volume in the mixer back to 100% because it maintains its volume using some windows api that affects volume in the mixer (happens with multiple (mostly crappy) games), plus on windows (starting with vista and on linux if you use "flat volumes" option) adjusting volume of application gets progressively more difficult the lower your "main" volume - if your main volume is 25% to set volume of VLC to 50% of that you need to put it around 12.5% mark, and you're given like 32 pixels at best. Plus one day you might listen to music from your VLC and next day you'll be listening to it from MilkyTracker or MPD. Having multiple fixed devices is convenient, and in later post I will explain how to use hardware knobs to control audio volume ;).

First - let's determine what "streams" you want to have volume controls on - in my use case I have 4 + master: Game, Music, VoIP, Browser. The latter is more of a crutch than real usecase - it's painful to make different firefox/chromium tabs to use different audio devices so I just make it always use "Browser" stream instead. Next - we need to setup our virtual audio devices - if you use PulseAudio you could use something like module-loopback but it's probably better to use module-remap-sink since that requires less effort and probably gives less latency, the only caveat is that it's probably tied to the device you're using it with. On PipeWire side of things you'll need to use libpipewire-module-loopback which is used to create virtual devices. Don't worry it's essentially same as module-remap-sink, although some other options are possible, they require more effort and setup as of right now. PipeWire wiki does say that it adds overhead but it doesn't seem like it adds any latency at all.

Now generally module-remap-sink (and its brother, module-remap-source) is used to remap channels, i.e. swap stereo or in my case turn a "4.0 surround" device into two stereo pair device it was meant to be, and "source" version is very useful for turning "stereo" input device into two separate mono input devices, i.e. with same device on input side I have two XLR-combo ports, but only one of them is connected to the microphone, i occasionally use other one for guitar, this when downmixed to mono (which most VoIP clients use) ends up with quiter sound, or rarely with sound only coming from one channel. However remapping is completely optional - you can "remap" L - R stereo to L - R stereo! Creating such device also adds another layer of volume control: I have my Roland Rubix24 audio interface which looks like "4.0 Surround" device (because it has 4 outputs - 1L, 2R, 3L, 4R), but I remapped those into two stereo pairs, respectively named Rubix12 and Rubix34, now that original "4.0 Surround" device didn't go anywhere, and if I lower its volume - both Rubix12 and Rubix34 get quieter as a result, but their volume remains unchanged in the mixer. Same applies to "streams", i.e. I "remapped" Rubix12 into Rubix12-Music, so now Rubix12 serves as my "master" channel while the real hardware device technically always runs at full volume and I also get to adjust music volume separately.

Scheme that describes how my (virtual) audio devices are setup (for output)

So how to exactly accomplish this?

PulseAudio

For pulseaudio, you need to add lines like this into your default.pa (or system.pa or whatever you are actually using):

load-module module-remap-source sink_name=DEVICE_NAME sink_properties="device.description='DEVICE_DESCRIPTION'" remix=no master=UPSTREAM_DEVICE_HERE master_channel_map=front-left,front-    
left channel_map=front-left,front-right

Explanation:

  • load-module is essentially the pactl command, you could prepend whole thing with pactl, put it in bash and it will work.
  • module-remap-sink is module we're using
  • sink_name=DEVICE_NAME is the name of device you're creating, i.e. "rubix12music", it will be the identifier to use programmatically.
  • sink_properties="device.description='DEVICE_DESCRIPTION'" is how it will look like in the mixer, i.e. something human-readable like "Ruibx12-Music"
  • remix=no is some weird thing that PulseAudio docs tell you to set to "no"
  • master=UPSTREAM_DEVICE_HERE is what device to remap, in my case it would be "rubix12"
  • And finally, master_channel_map=front-left,front-   
    left channel_map=front-left,front-right
    tells PulseAudio to map channels 1:1 pretty much.

PipeWire (WIP, still testing it)

You'll need to setup pipewire configuration for your user, i.e.

mkdir ~/.config/pipewire
sudo cp /usr/share/pipewire/pipewire.conf $HOME/.config/pipewire

And then add modules to context.modules section, like so:

{   name = libpipewire-module-loopback # name of the module
  args = {
      node.name = "DEVICE_NAME" # PipeWire device name, i.e. Rubix12-Music
      node.description = "DEVICE_DESCRIPTION" # Human-readable label
      capture.props = {
          media.class = "Audio/Sink"
          audio.position = [ FL FR ]
          node.name = "PA_DEVICE_NAME" # PulseAudio device name, otherwise it will appear as loopback-XX in pactl
      }
      playback.props = {
          audio.position = [ FL FR ]
          node.target = "UPSTREAM_DEVICE_HERE" # PipeWire name of device you're remapping, i.e. Rubix12
          stream.dont-remix = true
          node.passive = true
      }
  }
}

It seems to be working exactly the same, but remember to set node.name within capture.props otherwise it will appear as "loopback-XX" from pulseaudio side (where XX are (random?) numbers) and existing PulseAudio tools won't recognize them.

Now what?

Now it's only a matter of moving applications to specific devices, most will remember last one used, some devices let you configure which audio device they're using from within. Very few devices are stubborn and will only use default one, although pipewire seems to have some countermeasures to that.

Next time I will plug tool that I wrote explain how to control those volumes with a real and cheap(ish) hardware.