9

My problem is this: I'm talking to some friends via voice chat and at some point I'd also like to mix some music into it. So my current setup is this:

Microphone (Input device) -> Voice software
Music player -> Headphones (Output device)

I'd like to have this:

Music player -> Headphones

Music player -
-> ? -> Voice software Microphone -/

I've had a look at the Pipewire wiki quite a bit and especially the part about Virtual-Devices seemed to be very relevant but after playing around with a lot of pw-loopback I eventually gave up.

In the end, I think I'll need to end up with a virtual input device so that it's even selectable in my recording applications.

Is this even the right approach?

svenstaro
  • 535

5 Answers5

17

If you want to mix your voice input (mic) and the output of music player, then the setup would be like this:

Music player -\
               -> Combined Sink/Source -> Virtual Microphone -> Voice software
Microphone   -/

Here is the walktrough:

1. Create the combined-sink interface

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

2. Create a Virtual Microphone

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

3. Link your microphone & the music player output into the combined sink

Note: The microphone interface name & the music player output interface name may be different. Run pw-link -o to show list of the outputs.

pw-link spotify:output_FL my-combined-sink:playback_FL
pw-link spotify:output_FR my-combined-sink:playback_FR

pw-link alsa_input.pci-0000_00_1f.3.analog-stereo:capture_FL my-combined-sink:playback_FL pw-link alsa_input.pci-0000_00_1f.3.analog-stereo:capture_FR my-combined-sink:playback_FR

4. Link your combined sink to the virtual microphone

pw-link my-combined-sink:monitor_FL my-virtualmic:input_FL
pw-link my-combined-sink:monitor_FR my-virtualmic:input_FR

Now you can tell the voice software to make use this virtual microphone or set the virtual microphone as the default voice input.

Pujianto
  • 286
2

Here's a scripted version of the answer from Pujianto. This will create a combined virtual mic with another application (which is assumed to be putting out stereo, uniquely named, and greppable).

e.g. combine_audio.sh spotify

The script will also remove any null sinks that are active, so be warned, but I think that isn't so common in normal use? I use this with Skype and OBS so I can chat with people and stream application/game audio as well as my mic, without looping their audio in. You may need to restart stuff to have the mic show up, but once it's there you can hot-swap applications without any problem.

You also need to check your mic's name, e.g. the script below uses my webcam.

#!/bin/bash

SINK_NAME=combined-app-sink VIRTUAL_MIC_NAME=my-virtualmic MIC_SOURCE=alsa_input.usb-046d_Logitech_StreamCam_901DCE45-02.analog-stereo

results=$(pw-link -o | grep ${1}) IFS=$'\n' read -ra ADDR -d $'\0' <<< "$results"

Unload if exists

pactl unload-module module-null-sink

Make new sinks

pactl load-module module-null-sink media.class=Audio/Sink sink_name=$SINK_NAME channel_map=stereo >> /dev/null pactl load-module module-null-sink media.class=Audio/Source/Virtual sink_name=$VIRTUAL_MIC_NAME channel_map=front-left,front-right >> /dev/null

Extract app name

IFS=':' read -a APP_STR <<< ${ADDR[0]} echo "Linking app ${APP_STR} to ${SINK_NAME}" pw-link "${APP_STR}":output_FL $SINK_NAME:playback_FL pw-link "${APP_STR}":output_FR $SINK_NAME:playback_FR

echo "Linking $MIC_SOURCE to ${SINK_NAME}" pw-link $MIC_SOURCE:capture_FL $SINK_NAME:playback_FL pw-link $MIC_SOURCE:capture_FR $SINK_NAME:playback_FR

echo "Creating virtual mic: $VIRTUAL_MIC_NAME" pw-link $SINK_NAME:monitor_FL $VIRTUAL_MIC_NAME:input_FL pw-link $SINK_NAME:monitor_FR $VIRTUAL_MIC_NAME:input_FR

Could combine with https://askubuntu.com/questions/355082/pulseaudio-loopback-unload-audio-output-devices to make unloading more intelligent.

Josh
  • 142
1

use pw-link to conect stuff (check out "pw-link -h"). an alternative GUI way would be to use Carla. there is also the Helvum GUI created especificly for Pipewire

Joe Doe
  • 11
1

To answer my own question some years down the road: There's now a convenient graphical tool called sonusmix that has this feature built in.

svenstaro
  • 535
0

Extending @pujianto answer, I had a script that makes sure it keeps the link all the time. ./pw-easylink.py -ao 'Google *' -ai 'my-combined-sink' --continuous

I needed to pipe Chrome audio to my meetings- Like playing the audio from a Youtube video and making sure the audio was piping to the Meeting (Google Meet/Teams)

This is the code I grabbed from this gisthttps://gist.github.com/nidefawl/75ca46ba9979062290b066a7c1ee08fd

With a small modification for reliability

#!/usr/bin/python
# coding=utf-8

"""Configures and maintain PipeWire links between sets of queries for input/output ports"""

The following example configures links between output ports whose application

name starts with "Firefox" and channel is "Front-Right", and input ports whose

application name is "ardour" and port name starts with "Track name" and ends

with "2". Links are automatically created whenver a new port appears for

either of these queries.

Only the first two channels are linked. And links are created in stereo pairs

pw-easylink -ao 'Firefox.' -co FR -ai 'ardour' -ni 'Track name.2' --continuous

import re import sys import time import json import subprocess

def pipewire_dump(max_retries=3, delay=1): """ Attempts to dump PipeWire data and returns it as a JSON object.

Args:
    max_retries (int): The maximum number of times to retry the operation.
    delay (int): The delay in seconds between retries.

Returns:
    dict: The PipeWire data as a JSON object.
&quot;&quot;&quot;
for attempt in range(max_retries + 1):
    try:
        return json.loads(subprocess.check_output(&quot;pw-dump&quot;))
    except json.JSONDecodeError:
        if attempt &lt; max_retries:
            print(f&quot;Invalid JSON on attempt {attempt + 1}. Retrying in {delay} seconds.&quot;)
            time.sleep(delay)
        else:
            print(f&quot;Failed to parse JSON after {max_retries} attempts.&quot;)
            break

def pipewire_link(port_id_out, port_id_in): subprocess.check_call(["pw-link", str(port_id_out), str(port_id_in)])

def pipewire_ports(dump=None): dump = dump if dump is not None else pipewire_dump() ports = [data for data in dump if data["type"] == "PipeWire:Interface:Port"] return ports

def pipewire_links(dump=None): dump = dump if dump is not None else pipewire_dump() links = [data for data in dump if data["type"] == "PipeWire:Interface:Link"] return links

def pipewire_nodes(dump=None): dump = dump if dump is not None else pipewire_dump() ports = [data for data in dump if data["type"] == "PipeWire:Interface:Node"] return ports

def pipewire_find_node_by_id(id, dump=None): for node in pipewire_nodes(dump=dump): if node["id"] == id: return node

def pipewire_find_port(pid=None, name_regex=None, application_regex=None, channel=None, direction=None, dump=None):

if application_regex is not None:
    application_regex = re.compile(application_regex)
if name_regex is not None:
    name_regex = re.compile(name_regex)

for port in pipewire_ports(dump=dump):
    try:
        props = port[&quot;info&quot;][&quot;props&quot;]
        if any(v is not None for v in (pid, application_regex)):
            nodeid = props.get(&quot;node.id&quot;)
            node = pipewire_find_node_by_id(nodeid, dump=dump)
            nprops = node[&quot;info&quot;][&quot;props&quot;]
            application_name = nprops.get(&quot;application.name&quot;)
            application_name = application_name or nprops.get(&quot;node.name&quot;)
            application_name = application_name or nprops.get(&quot;client.name&quot;)
            if pid is not None and nprops.get(&quot;application.process.id&quot;) != pid:
                continue
            if application_regex is not None and not application_regex.match(application_name):
                continue
        if name_regex is not None:
            if name_regex.match(props.get(&quot;object.path&quot;, &quot;&quot;)):
                pass
            elif name_regex.match(props.get(&quot;port.alias&quot;, &quot;&quot;)):
                pass
            elif name_regex.match(props.get(&quot;port.name&quot;, &quot;&quot;)):
                pass
            else:
                continue
        if channel is not None and props.get(&quot;audio.channel&quot;) != channel:
            continue
        if direction is not None and port[&quot;info&quot;][&quot;direction&quot;] != direction:
            continue
    except KeyError:
        pass
    except ValueError:
        pass
    yield port


if name == 'main': import argparse parser = argparse.ArgumentParser(description=doc) parser.add_argument("--pid-out", "-po", help="Client pid for output part of the link") parser.add_argument("--pid-in", "-pi", help="Client pid for input part of the link") parser.add_argument("--name-regex-out", "-no", help="Name regex for output part of the link") parser.add_argument("--name-regex-in", "-ni", help="Name regex for input part of the link") parser.add_argument("--application-regex-out", "-ao", help="Application name regex for output part of the link") parser.add_argument("--application-regex-in", "-ai", help="Application name regex for input part of the link") parser.add_argument("--channel-out", "-co", help="Channel for output part of the link") parser.add_argument("--channel-in", "-ci", help="Channel for input part of the link") parser.add_argument("--list-only", "-l", help="Only list ports without connecting", action="store_true") parser.add_argument("--continuous", "-c", help="Run continuously", action="store_true") parser.add_argument("--interval", "-i", help="Interval between continuous checks (in s)", type=int, default=1) args = parser.parse_args()

if args.pid_out is None and args.name_regex_out is None and args.application_regex_out is None and args.channel_out is None:
    sys.stderr.write(f&quot;Please specify at least one output filter\n&quot;)
    sys.stderr.flush()
    sys.exit(1)

if args.pid_in is None and args.name_regex_in is None and args.application_regex_in is None and args.channel_in is None:
    sys.stderr.write(f&quot;Please specify at least one input filter\n&quot;)
    sys.stderr.flush()
    sys.exit(1)
printStatusOnce = False
while True:
    dump = pipewire_dump()
    ports_out = list(pipewire_find_port(pid=args.pid_out, name_regex=args.name_regex_out, application_regex=args.application_regex_out, channel=args.channel_out, direction=&quot;output&quot;, dump=dump))
    if not len(ports_out) and not printStatusOnce:
        sys.stderr.write(f&quot;No output ports found for pid {args.pid_out} name regex {args.name_regex_out} application regex {args.application_regex_out} channel {args.channel_out}\n&quot;)
        sys.stderr.flush()
        if not args.continuous:
            sys.exit(1)
    ports_in = list(pipewire_find_port(pid=args.pid_in, name_regex=args.name_regex_in, application_regex=args.application_regex_in, channel=args.channel_in, direction=&quot;input&quot;, dump=dump))
    if not len(ports_in) and not printStatusOnce:
        sys.stderr.write(f&quot;No input ports found for pid {args.pid_in} name regex {args.name_regex_in} application regex {args.application_regex_in} channel {args.channel_in}\n&quot;)
        sys.stderr.flush()
        if not args.continuous:
            sys.exit(1)
    links = pipewire_links(dump=dump)
    if args.list_only:
        print(&quot;Outputs&quot;)
        print(&quot;-------&quot;)
        print(ports_out)
        print(&quot;&quot;)
        print(&quot;&quot;)
        print(&quot;Inputs&quot;)
        print(&quot;------&quot;)
        print(ports_in)
    if not printStatusOnce:
        print(&quot;Ports out size =&quot;, len(ports_out))
        print(&quot;Ports in size =&quot;, len(ports_in))
    for i in range(2):
        for port_out in ports_out:
            for port_in in ports_in:
                port_id_out = port_out[&quot;id&quot;]
                port_id_in = port_in[&quot;id&quot;]
                port_name_out = port_out[&quot;info&quot;][&quot;props&quot;].get(&quot;object.path&quot;) or port_out[&quot;info&quot;][&quot;props&quot;].get(&quot;port.name&quot;)
                port_name_in = port_in[&quot;info&quot;][&quot;props&quot;].get(&quot;object.path&quot;) or port_in[&quot;info&quot;][&quot;props&quot;].get(&quot;port.name&quot;)
                if not port_name_out.endswith(str(i)):
                    continue
                if not port_name_in.endswith(str(i)):
                    continue
                if any(link[&quot;info&quot;][&quot;output-port-id&quot;] == port_id_out and link[&quot;info&quot;][&quot;input-port-id&quot;] == port_id_in for link in links):
                    if not printStatusOnce:
                        print(&quot;Skipping already linked ports:&quot;, port_name_out, &quot;-&gt;&quot;, port_name_in)
                    continue
                print(f&quot;Creating new link between ports {port_id_out}:{port_name_out} -&gt; {port_id_in}:{port_name_in}&quot;)
                if not args.list_only:
                    pipewire_link(port_id_out, port_id_in)
    if not args.continuous:
        break
    time.sleep(max(0, args.interval))
    printStatusOnce = True

mrbarletta
  • 101
  • 1