Routing and Mixing

Routing and mixing audio in Linux

Routing and mixing audio in Linux

Sound server: PipeWire with pipewire-pulse

Audio router (patchbay): qpwgraph

Audio mixer: PulseAudio Volume Control

media.class

  • Audio/Sink: Can be selected as output in PulseAudio applications
  • Audio/Source/Virtual: Can be selected as input in PulseAudio applications

Topography

                          Microphone -\
                                       -> Virtual sink -> Virtual source -> Voice software
Music player -> Virtual monitor sink -<
                                       -> Speaker

Create the virtual sink

Combines the audio from all the desired sources.

pactl load-module module-null-sink media.class=Audio/Sink sink_name=virtual-sink channel_map=stereo

Create the virtual source

Sends the combined audio to the desired applications.

pactl load-module module-null-sink media.class=Audio/Source/Virtual sink_name=virtual-mic channel_map=front-left,front-right

Create the virtual monitor sink

For audio applications to be mixed, sends the audio to the virtual sink as well as the speaker.

pactl load-module module-null-sink media.class=Audio/Sink sink_name=virtual-monitor-sink channel_map=stereo

Then, in PulseAudio Volume Control, go to the Playback tab and set virtual-monitor-sink Audio/Sink sink as the playback device of the applications to be mixed.

Connect the heck out of it in qpwgraph.

Alternative persistent configuration

Create ~/.config/pipewire/pipewire.conf.d/99-route-n-mix.conf with the following:

# Daemon config file for PipeWire version "1.2.7" #
#
# Copy and edit this file in /etc/pipewire for system-wide changes
# or in ~/.config/pipewire for local changes.
#
# It is also possible to place a file with an updated section in
# /etc/pipewire/pipewire.conf.d/ for system-wide changes or in
# ~/.config/pipewire/pipewire.conf.d/ for local changes.
#

context.objects = [
    #{ factory = <factory-name>
    #    ( args  = { <key> = <value> ... } )
    #    ( flags = [ ( nofail ) ] )
    #    ( condition = [ { <key> = <value> ... } ... ] )
    #}
    #
    # Creates an object from a PipeWire factory with the given parameters.
    # If nofail is given, errors are ignored (and no object is created).
    # If condition is given, the object is created only when the context properties
    # all match the match rules.
    #
    #{ factory = spa-node-factory   args = { factory.name = videotestsrc node.name = videotestsrc node.description = videotestsrc "Spa:Pod:Object:Param:Props:patternType" = 1 } }
    #{ factory = spa-device-factory args = { factory.name = api.jack.device foo=bar } flags = [ nofail ] }
    #{ factory = spa-device-factory args = { factory.name = api.alsa.enum.udev } }
    #{ factory = spa-node-factory   args = { factory.name = api.alsa.seq.bridge node.name = Internal-MIDI-Bridge } }
    #{ factory = adapter            args = { factory.name = audiotestsrc node.name = my-test node.description = audiotestsrc } }
    #{ factory = spa-node-factory   args = { factory.name = api.vulkan.compute.source node.name = my-compute-source } }

    # This creates a new Source node. It will have input ports
    # that you can link, to provide audio for this source.
    #{ factory = adapter
    #    args = {
    #        factory.name     = support.null-audio-sink
    #        node.name        = "my-mic"
    #        node.description = "Microphone"
    #        media.class      = "Audio/Source/Virtual"
    #        audio.position   = "FL,FR"
    #        monitor.passthrough = true
    #    }
    #}

    { factory = adapter
        args = {
            factory.name     = support.null-audio-sink
            node.name        = "mix-sink"
            node.description = "Sink"
            media.class      = "Audio/Sink"
            audio.position   = "FL,FR"
            monitor.channel-volumes = true
            monitor.passthrough = true
            adapter.auto-port-config = {
                mode = dsp
                monitor = true
                position = preserve
            }

        }
    }
    { factory = adapter
        args = {
            factory.name     = support.null-audio-sink
            node.name        = "mix-monitored-sink"
            node.description = "Monitored Sink"
            media.class      = "Audio/Sink"
            audio.position   = "FL,FR"
            monitor.channel-volumes = true
            monitor.passthrough = true
            adapter.auto-port-config = {
                mode = dsp
                monitor = true
                position = preserve
            }

        }
    }
    { factory = adapter
        args = {
            factory.name     = support.null-audio-sink
            node.name        = "b1"
            node.description = "B1"
            media.class      = "Audio/Source/Virtual"
            audio.position   = "FL,FR"
            monitor.channel-volumes = true
            monitor.passthrough = true
            adapter.auto-port-config = {
                mode = dsp
                monitor = true
                position = preserve
            }

        }
    }
]

In qpwgraph > Graph > General, check “Start minimized to system tray”. Then, activate the patchbay.

Add qpwgraph to startup applications.

Log out and log in again to take effect.

Reference

https://superuser.com/questions/1675877/how-to-create-a-new-pipewire-virtual-device-that-to-combines-an-real-input-and-o

https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Virtual-Devices

https://linuxmusicians.com/viewtopic.php?t=27623

ChatGPT

Content Licensed under CC BY-SA 4.0. Code licensed under the MIT License.
Last updated on Jul 25, 2025 07:50 UTC