My use case is splitting audio into separate channels in OBS for Twitch Streams so I can play music live without getting my VoDs struck. If my approach is entirely wrong for the use case, I’m happy to scrap the whole thing and sign it off as learning experience.

My solution is to use virtual sinks that I record through Audio Sources in OBS. I’ve got two loopback-devices (config at the end) with media.class = Audio/Sink, assign my playback streams to the relevant output capture.
The loopback of each is then passed on to the common default (physical) output device, namely my headphones.
So far, this has been working great for me, aside from minor inconveniences:

The first is that I want certain apps or playback streams to automatically be assigned to the capture sinks upon starting the app.
I had a working pulseaudio¹ setup on Ubuntu where I used pavucontrol to set the output once per app and it remembered that setting. Every time I opened that app, it would direct its playback streams to that sink.
I migrated to Nobara and opted to try configuring pipewire (directly)² instead. The devices are created correctly but every time I (re-)start a relevant app I have to go set its capture device again.

The second is that occasionaly upon logging in, one loopback stream will initially be passed to the other sink instead of the default output, which resolves upon restarting pipewire³. Is something wrong with my config?
Both have the same target.object and restarting it fixes it, so I’m guessing it may be some race condition thing where the alsa_output isn’t initialised at startup yet, but I don’t know how to diagnose or fix that


1: I have since learned that apparently it’s actually still pipewire parsing that config, but the point is I configured it through ~/.config/pulse/default.pa

2: ~/config/pipewire/pipewire.conf.d/default-devices.conf

3: Trying to set it in pavucontrol doesn’t work and keeps resetting that playback’s output to the given sink if I try to select the correct capture device. Repatching them in Helvum does the job, but then pavucontrol just shows blank for the device (doesn’t interfere with controlling the volume, but maybe it’s relevant for diagnosing)


My current ~/.config/pipewire/pipewire.conf.d/default-devices.conf:

context.modules = [
    {   name = libpipewire-module-loopback
        args = {
            audio.position = [ FL FR ]
            capture.props = {
                media.class = Audio/Sink
                node.name = vod_sink
                node.description = "Sink for VoD Audio"
            }
            playback.props = {
                node.name = "vod_sink.output"
                node.description = "VoD Audio"
                node.passive = true
                target.object = "alsa_output.pci-0000_00_1b.0.analog-stereo"
            }
        }
    }
    {   name = libpipewire-module-loopback
        args = {
            audio.position = [ FL FR ]
            capture.props = {
                media.class = Audio/Sink
                node.name = live_sink
                node.description = "Sink for Live-Only Audio"
            }
            playback.props = {
                node.name = "live_sink.output"
                node.description = "Live-Only Audio"
                node.passive = true
                target.object = "alsa_output.pci-0000_00_1b.0.analog-stereo"
            }
        }
    }    
]
  • Communist@lemmy.ml
    link
    fedilink
    English
    arrow-up
    2
    ·
    12 days ago

    I’ve found the alternative to PULSE_SINK in a pipewire context, which is PIPEWIRE_NODE=

    that might help.

  • Communist@lemmy.ml
    link
    fedilink
    English
    arrow-up
    1
    ·
    edit-2
    24 days ago

    pactl load-module module-combine-sink sink_name='(namehere)' slaves='(put the sink you want to duplicate here)'

    Then there will be another sink that’s the exact same as the one you set as a slave that you can play audio to and that OBS can record separately, as for getting something to automatically go to that stream, generally you can use this environment variable:

    PULSE_SINK='(sinknamehere)'

    and make a .desktop file or keyboard shortcut that launches the program with that

    you can also make the sink names consistent with something like this but adapted to your audio devices in your wireplumber conf:

    monitor.alsa.rules = [
    	{
    		matches = [
    		{
    			device.name = "~alsa_card.pci-0000_06_00.*"
    		}
    		]
    		actions = {
    			update-props = {
    				device.name = "alsa_card.pci-0000_06_00"
    				node.nick = "Speakers"
    			}
    		}
    	}
    		{
    		matches = [
    		{
    			device.name = "~alsa_card.usb-Creative_Technology_Ltd_Sound_Blaster_X4*"
    		}
    		]
    		actions = {
    			update-props = {
    				device.name = "alsa_card.usb-Creative_Technology_Ltd_Sound_Blaster_X4"
    				node.nick = "Headphones"
    			}
    		}
    	}
    ]
    
    
    • luciferofastoraOP
      link
      fedilink
      English
      arrow-up
      1
      ·
      24 days ago

      What would the combine sink be for? Joining the two loopback sinks into a third? I don’t see where I would need another sink.

      Or are you saying to replace the loopbacks with combines, each being recorded by OBS while also forwarding the audio to my headphones? I can try that. I will have to see if it still allows me to change the volumes separately for OBS and my headphones.

      If I wanted to persist that setup, I would have to add it to my config. Given I’m trying to migrate to pipewire, what would be the equivalent pw config setup? I could just put the command(s) into context.exec, but is that really the “proper” way instead of using pipewire modules?

      • Communist@lemmy.ml
        link
        fedilink
        English
        arrow-up
        1
        ·
        23 days ago

        Adding the second sink lets you easily route the audio through one, if I run that command I have it setup like this

        pactl load-module module-combine-sink sink_name=‘Game’ slaves=‘easyeffects_sink’

        and then I have a sink named “game” that I can record from OBS that isolates my audio from the one called game, but also passes that to my speakers, just seems like an easier way to do what you’re trying to accomplish with the wireplumber thing. I don’t know about if it’s the “proper” way but it works for me!

    • luciferofastoraOP
      link
      fedilink
      English
      arrow-up
      1
      ·
      24 days ago

      The pulse sink environment variable worked perfectly. My apps now launch with the correct sink set. That solves the bulk of my issue. The other thing is just an occasional nuisance. Thanks for that pointer!

  • luciferofastoraOP
    link
    fedilink
    English
    arrow-up
    1
    ·
    24 days ago

    For future readers looking to set separate default pulseaudio or pipewire sinks for individual apps, this his how I accomplished it.
    If you’re using pipewire config, sink_name will be called node.name in the capture.props of the module.

    For flatpak apps, I used this per-user override only for my current user:
    flatpak override --user --env=PULSE_SINK=(sink_name) (full application name)
    For example:
    flatpak override --user --env=PULSE_SINK=live_sink com.spotify.Client

    For steam games, insert the respective environment variable into the launch options if you already have some, or otherwise put PULSE_SINK=(sink_name) %command% in there.

    Steam Tinker Launch maintains a gamecfgs/customvars/(Game ID).conf config file for each game to set custom environment variables in, which you can most conveniently find through from the launcher’s Main Menu > Editor > find the customvars entry. In there, just put the line PULSE_SINK=(sink_name) and you’ll be good to go.