Pipewire natively supports a filter to create a 7.1 virtual surround sound device that will work with any headphones or earphones. It’s not well documented, so I decided to write a step-by-step guide for enabling it in Pop!
How it Works
Sound is distorted by your head and shoulders relative to your ears in slightly different ways based on the direction the sound is coming from. The distortion, which is known as HRIR (head-related impulse response), is how our brains are able to interpret sound spatially, despite ours ears only being capable of receiving stereo audio.
Pipewire is able to achieve a convincing 7.1 surround sound effect either by using either a SOFA (spatially oriented acoustic data) spatializer, or a HRTF (head-related transfer function) convolver to interpolate a replicated 7.1 HRIR input onto a 7.1 surround input, mimicking the natural process by which we hear sound spatially.
A replicated 7.1 HRIR input is created by placing microphones in the ears of an artificial dummy, and measuring the differences in sound it experienced while listening to a 7.1 surround sound system. Which is why we perceive surround sound in headphones.
SOFA takes this technology to the next level with a more advanced algorithm that can process many additional forms of data inputs to improve the surround sound effect.
Option 1: SOFA Spatializer
Step 1: Copy the following 7.1 SOFA spatializer filter-chain config locally. This creates a virtual output sink with 7.1 surround sound channels.
mkdir -p ~/.config/pipewire/filter-chain.conf.d/
curl -o ~/.config/pipewire/filter-chain.conf.d/spatializer.conf \
https://gist.githubusercontent.com/mmstick/039422a63c73a09e998d08608abaee43/raw/9c4dfef5a447fe25a47e3492e518e134e57ee9d4/7.1-spatializer.conf
Step 2: Download a SOFA DTF for the filter to utilize as its input.
sudo mkdir -p /usr/share/pipewire/sofa/
sudo curl -o /usr/share/pipewire/sofa/dtf.sofa \
https://sofacoustics.org/data/database_sofa_0.6/ari/dtf%20b_nh724.sofa
Then go to Step 4 below
Option 2: HRIR Convolver
Step 1: Copy the 7.1 filter-chain config locally. This creates a virtual output sink with 7.1 surround sound channels.
mkdir -p ~/.config/pipewire/filter-chain.conf.d/
cp /usr/share/pipewire/filter-chain/sink-virtual-surround-7.1-hesuvi.conf \
~/.config/pipewire/filter-chain.conf.d/virtual-surround.conf
Step 2: Download a 7.1 HRIR wav file from the HRTF Database, such as Atmos or CMSS-3D. Then move it into your local pipewire configuration.
mkdir -p ~/.config/pipewire/hrir/
mv ~/Downloads/atmos.wav ~/.config/pipewire/hrir/atmos.wav
Step 3: Edit the copied virtual-surround config to use this wav file.
sed -i "s#hrir_hesuvi/hrir.wav#${HOME}/.config/pipewire/hrir/atmos.wav#g" \
~/.config/pipewire/filter-chain.conf.d/virtual-surround.conf
Start the filter and test it
Step 4: Start pipewire with the filter-chain config. The virtual surround device will now exist as long as this is running in the background.
pipewire -c filter-chain.conf
Step 5: Select the virtual surround sink output device and try it out.
- https://www2.iis.fraunhofer.de/AAC/7.1auditionOutLeader_v2_rtb.mp4
- https://www.youtube.com/watch?v=ClpEj1ayNSs
Side Effects
There’s a slight audio latency increase from using virtual surround sound, depending on how fast the CPU is. It is a simple process though so the performance cost is slight.
Stereo audio sources should have the same sound before and after. Surround sound content will sound as they were intended to be heard, and it could help with dialogue in some movies being difficult to hear. Especially if you are able to configure the volume of the center speaker channel where dialogue is usually played.
Help
Any help with finding a way to automate this when plugging in headphones would be great.
Great guide, It’s worth noting that you can also use SOFA filters too! HRTF is a very “Per-person” thing, Before downloading a filter, Highly reccomended to play the demo tracks first (may need to scroll right). IF none of them really do it for you, you can also try using SOFA filters instead, clubfritz is a very common sofa filter. though sadly I dont think there is a way to preview them before setting up a filter then swapping out the names
Do you have a guide for using SOFA filters?
it’s not 100% the same, but it is mostly similar, you can see an example conf in the PR that added sofa https://gitlab.freedesktop.org/pipewire/pipewire/-/merge_requests/1497
I’ve added instructions for setting up surround sound with SOFA.
Thank you so much for sharing this. Excellent post.
Cheers for this guide!
i have been having difficulty trying to understand the naming conventions for the sofa files as posted in the ari databases, such that i could somehow choose one that i think would best suit. The documentation doesn’t really help, and i haven’t found a write up on it that addresses this in a mere human readable way, i am anything but a coder and don’t use matlab for anything. Any suggestions?
The following should work to automate turning on 7.1 when headphones plugged in. I would def suggest using at least two terminal instances to help with the copy/pasta goodness.
Use systemd to automate:
sudo nano /etc/systemd/system/pipewire-headphones.service
in said file use the following:
######################################
[Unit] Description=Run Pipewire with filter-chain when headphones are plugged in Wants=sys-devices-pci0000:40-0000:40:01.1-0000:41:00.0-0000:42:08.0-0000:45:00.3-usb9-9\x2d2-9\x2d2:1.0-sound-card1-controlC1.device BindsTo=sys-devices-pci0000:40-0000:40:01.1-0000:41:00.0-0000:42:08.0-0000:45:00.3-usb9-9\x2d2-9\x2d2:1.0-sound-card1-controlC1.device
[Service] ExecStart=/bin/pipewire -c /home/USER/.config/pipewire/filter-chain.conf.d/filter-chain.conf User=your_username Group=your_groupname [Install] WantedBy=default.target
######################################
Replace /path/to/pipewire with the actual path to the pipewire binary and /path/to/filter-chain.conf with the path to your filter-chain.conf configuration file. Also, replace your_username and your_groupname with your actual username and group name.
To obtain Wants, and BindsTo= path
Plug in desired device
ls /dev/input/by-id/
copy name of your device
eg: usb-Logitech_PRO_X_000000000000-event-if03
then:
sudo udevadm info --attribute-walk --path=$(udevadm info --query=path --name=/dev/input/by-id/usb-Logitech_PRO_X_000000000000-event-if03)
locate “looking at parent device” that matches to your device, add to service file.
save file.
the udevadm command will also allow you to obtain info needed for the udev rule.
Now Create udev rule.
sudo nano /etc/udev/rules.d/99-headphones.rules
enter text:
################################################
ACTION==“add”, SUBSYSTEM==“input”, ENV{ID_INPUT}==“1”, ENV{ID_VENDOR_ID}==“046d”, ENV{ID_PRODUCT}==“0aaa”, ENV{PHYS}==“usb-0000:45:00.3-2/input3”, ENV{SYSTEMD_WANTS}=“pipewire-headphones.service”
################################################
ID_VENDOR_ID =ATTRS{id/vendor}==“046d” etc
save file.
sudo udevadm control --reload sudo systemctl daemon-reload
for manual start/stop:
sudo systemctl start pipewire-headphones.service sudo systemctl stop pipewire-headphones.service
To have service started and enabled at boot:
sudo systemctl enable - -now pipewire-headphones.service
why sudo systemctl daemon-reload
Have tested with logi headphones using the usb dac, and then the standard jack seems to work ok, in ubuntu appears as virtual-surround-sink,though I did have to choose this as my device manually. this should also stop the service if the device is unplugged.
To automate that you could edit the service Execstart entry :
ExecStart=/bin/sh -c ‘pipewire -c /path/to/filter-chain.conf && pactl set-default-sink your_virtual_surround_sink_name’
Hope this is useful :)
Hi, thanks for this! However, I am struggling a bit to understand some part, especially the one about Wants and Bindto, using the udevadm command. Maybe you could explain a bit more details what you are doing? This is the output of the udevadm cmd. I am using the same headset (Logitech G Pro X), and I am trying to have the same audio experience as the one I get one Windows using Logitech G Hub, but it is really hard to understand everything.
looking at device '/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.3/0003:046D:0AAA.0006/input/input35/event28': KERNEL=="event28" SUBSYSTEM=="input" DRIVER=="" ATTR{power/async}=="disabled" ATTR{power/control}=="auto" ATTR{power/runtime_active_kids}=="0" ATTR{power/runtime_active_time}=="0" ATTR{power/runtime_enabled}=="disabled" ATTR{power/runtime_status}=="unsupported" ATTR{power/runtime_suspended_time}=="0" ATTR{power/runtime_usage}=="0" looking at parent device '/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.3/0003:046D:0AAA.0006/input/input35': KERNELS=="input35" SUBSYSTEMS=="input" DRIVERS=="" ATTRS{capabilities/abs}=="0" ATTRS{capabilities/ev}=="20013" ATTRS{capabilities/ff}=="0" ATTRS{capabilities/key}=="100000000000000 0 0 0" ATTRS{capabilities/led}=="80" ATTRS{capabilities/msc}=="10" ATTRS{capabilities/rel}=="0" ATTRS{capabilities/snd}=="0" ATTRS{capabilities/sw}=="0" ATTRS{id/bustype}=="0003" ATTRS{id/product}=="0aaa" ATTRS{id/vendor}=="046d" ATTRS{id/version}=="0111" ATTRS{inhibited}=="0" ATTRS{name}=="Logitech PRO X" ATTRS{phys}=="usb-0000:00:14.0-1/input3" ATTRS{power/async}=="disabled" ATTRS{power/control}=="auto" ATTRS{power/runtime_active_kids}=="0" ATTRS{power/runtime_active_time}=="0" ATTRS{power/runtime_enabled}=="disabled" ATTRS{power/runtime_status}=="unsupported" ATTRS{power/runtime_suspended_time}=="0" ATTRS{power/runtime_usage}=="0" ATTRS{properties}=="0" ATTRS{uniq}=="000000000000" looking at parent device '/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.3/0003:046D:0AAA.0006': KERNELS=="0003:046D:0AAA.0006" SUBSYSTEMS=="hid" DRIVERS=="hid-generic" ATTRS{country}=="00" ATTRS{power/async}=="enabled" ATTRS{power/control}=="auto" ATTRS{power/runtime_active_kids}=="0" ATTRS{power/runtime_active_time}=="0" ATTRS{power/runtime_enabled}=="disabled" ATTRS{power/runtime_status}=="unsupported" ATTRS{power/runtime_suspended_time}=="0" ATTRS{power/runtime_usage}=="0" looking at parent device '/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.3': KERNELS=="1-1:1.3" SUBSYSTEMS=="usb" DRIVERS=="usbhid" ATTRS{authorized}=="1" ATTRS{bAlternateSetting}==" 0" ATTRS{bInterfaceClass}=="03" ATTRS{bInterfaceNumber}=="03" ATTRS{bInterfaceProtocol}=="00" ATTRS{bInterfaceSubClass}=="00" ATTRS{bNumEndpoints}=="01" ATTRS{physical_location/dock}=="no" ATTRS{physical_location/horizontal_position}=="left" ATTRS{physical_location/lid}=="no" ATTRS{physical_location/panel}=="top" ATTRS{physical_location/vertical_position}=="upper" ATTRS{power/async}=="enabled" ATTRS{power/runtime_active_kids}=="0" ATTRS{power/runtime_enabled}=="enabled" ATTRS{power/runtime_status}=="suspended" ATTRS{power/runtime_usage}=="0" ATTRS{supports_autosuspend}=="1" looking at parent device '/devices/pci0000:00/0000:00:14.0/usb1/1-1': KERNELS=="1-1" SUBSYSTEMS=="usb" DRIVERS=="usb" ATTRS{authorized}=="1" ATTRS{avoid_reset_quirk}=="0" ATTRS{bConfigurationValue}=="1" ATTRS{bDeviceClass}=="00" ATTRS{bDeviceProtocol}=="00" ATTRS{bDeviceSubClass}=="00" ATTRS{bMaxPacketSize0}=="64" ATTRS{bMaxPower}=="100mA" ATTRS{bNumConfigurations}=="1" ATTRS{bNumInterfaces}==" 4" ATTRS{bcdDevice}=="0031" ATTRS{bmAttributes}=="80" ATTRS{busnum}=="1" ATTRS{configuration}=="" ATTRS{devnum}=="8" ATTRS{devpath}=="1" ATTRS{idProduct}=="0aaa" ATTRS{idVendor}=="046d" ATTRS{ltm_capable}=="no" ATTRS{manufacturer}=="Logitech" ATTRS{maxchild}=="0" ATTRS{physical_location/dock}=="no" ATTRS{physical_location/horizontal_position}=="left" ATTRS{physical_location/lid}=="no" ATTRS{physical_location/panel}=="top" ATTRS{physical_location/vertical_position}=="upper" ATTRS{power/active_duration}=="28449032" ATTRS{power/async}=="enabled" ATTRS{power/autosuspend}=="2" ATTRS{power/autosuspend_delay_ms}=="2000" ATTRS{power/connected_duration}=="28449032" ATTRS{power/control}=="on" ATTRS{power/level}=="on" ATTRS{power/persist}=="1" ATTRS{power/runtime_active_kids}=="1" ATTRS{power/runtime_active_time}=="28448749" ATTRS{power/runtime_enabled}=="forbidden" ATTRS{power/runtime_status}=="active" ATTRS{power/runtime_suspended_time}=="0" ATTRS{power/runtime_usage}=="1" ATTRS{product}=="PRO X" ATTRS{quirks}=="0x0" ATTRS{removable}=="removable" ATTRS{rx_lanes}=="1" ATTRS{serial}=="000000000000" ATTRS{speed}=="12" ATTRS{tx_lanes}=="1" ATTRS{urbnum}=="11392870" ATTRS{version}==" 2.00" looking at parent device '/devices/pci0000:00/0000:00:14.0/usb1': KERNELS=="usb1" SUBSYSTEMS=="usb" DRIVERS=="usb" ATTRS{authorized}=="1" ATTRS{authorized_default}=="1" ATTRS{avoid_reset_quirk}=="0" ATTRS{bConfigurationValue}=="1" ATTRS{bDeviceClass}=="09" ATTRS{bDeviceProtocol}=="01" ATTRS{bDeviceSubClass}=="00" ATTRS{bMaxPacketSize0}=="64" ATTRS{bMaxPower}=="0mA" ATTRS{bNumConfigurations}=="1" ATTRS{bNumInterfaces}==" 1" ATTRS{bcdDevice}=="0606" ATTRS{bmAttributes}=="e0" ATTRS{busnum}=="1" ATTRS{configuration}=="" ATTRS{devnum}=="1" ATTRS{devpath}=="0" ATTRS{idProduct}=="0002" ATTRS{idVendor}=="1d6b" ATTRS{interface_authorized_default}=="1" ATTRS{ltm_capable}=="no" ATTRS{manufacturer}=="Linux 6.6.6-76060606-generic xhci-hcd" ATTRS{maxchild}=="16" ATTRS{power/active_duration}=="40312872" ATTRS{power/async}=="enabled" ATTRS{power/autosuspend}=="0" ATTRS{power/autosuspend_delay_ms}=="0" ATTRS{power/connected_duration}=="40312872" ATTRS{power/control}=="auto" ATTRS{power/level}=="auto" ATTRS{power/runtime_active_kids}=="4" ATTRS{power/runtime_active_time}=="40312872" ATTRS{power/runtime_enabled}=="enabled" ATTRS{power/runtime_status}=="active" ATTRS{power/runtime_suspended_time}=="0" ATTRS{power/runtime_usage}=="0" ATTRS{power/wakeup}=="disabled" ATTRS{power/wakeup_abort_count}=="" ATTRS{power/wakeup_active}=="" ATTRS{power/wakeup_active_count}=="" ATTRS{power/wakeup_count}=="" ATTRS{power/wakeup_expire_count}=="" ATTRS{power/wakeup_last_time_ms}=="" ATTRS{power/wakeup_max_time_ms}=="" ATTRS{power/wakeup_total_time_ms}=="" ATTRS{product}=="xHCI Host Controller" ATTRS{quirks}=="0x0" ATTRS{removable}=="unknown" ATTRS{rx_lanes}=="1" ATTRS{serial}=="0000:00:14.0" ATTRS{speed}=="480" ATTRS{tx_lanes}=="1" ATTRS{urbnum}=="39733" ATTRS{version}==" 2.00" looking at parent device '/devices/pci0000:00/0000:00:14.0': KERNELS=="0000:00:14.0" SUBSYSTEMS=="pci" DRIVERS=="xhci_hcd" ATTRS{ari_enabled}=="0" ATTRS{broken_parity_status}=="0" ATTRS{class}=="0x0c0330" ATTRS{consistent_dma_mask_bits}=="64" ATTRS{d3cold_allowed}=="1" ATTRS{dbc}=="disabled" ATTRS{dbc_bInterfaceProtocol}=="01" ATTRS{dbc_bcdDevice}=="0010" ATTRS{dbc_idProduct}=="0010" ATTRS{dbc_idVendor}=="1d6b" ATTRS{device}=="0xa36d" ATTRS{dma_mask_bits}=="64" ATTRS{driver_override}=="(null)" ATTRS{enable}=="1" ATTRS{index}=="5" ATTRS{irq}=="145" ATTRS{label}=="Onboard - Other" ATTRS{local_cpulist}=="0-11" ATTRS{local_cpus}=="fff" ATTRS{msi_bus}=="1" ATTRS{msi_irqs/145}=="msi" ATTRS{numa_node}=="-1" ATTRS{power/async}=="enabled" ATTRS{power/control}=="on" ATTRS{power/runtime_active_kids}=="1" ATTRS{power/runtime_active_time}=="40315796" ATTRS{power/runtime_enabled}=="forbidden" ATTRS{power/runtime_status}=="active" ATTRS{power/runtime_suspended_time}=="0" ATTRS{power/runtime_usage}=="1" ATTRS{power/wakeup}=="enabled" ATTRS{power/wakeup_abort_count}=="0" ATTRS{power/wakeup_active}=="0" ATTRS{power/wakeup_active_count}=="0" ATTRS{power/wakeup_count}=="0" ATTRS{power/wakeup_expire_count}=="0" ATTRS{power/wakeup_last_time_ms}=="0" ATTRS{power/wakeup_max_time_ms}=="0" ATTRS{power/wakeup_total_time_ms}=="0" ATTRS{power_state}=="D0" ATTRS{revision}=="0x10" ATTRS{subsystem_device}=="0x08ea" ATTRS{subsystem_vendor}=="0x1028" ATTRS{vendor}=="0x8086" looking at parent device '/devices/pci0000:00': KERNELS=="pci0000:00" SUBSYSTEMS=="" DRIVERS=="" ATTRS{power/async}=="enabled" ATTRS{power/control}=="auto" ATTRS{power/runtime_active_kids}=="14" ATTRS{power/runtime_active_time}=="0" ATTRS{power/runtime_enabled}=="disabled" ATTRS{power/runtime_status}=="unsupported" ATTRS{power/runtime_suspended_time}=="0" ATTRS{power/runtime_usage}=="0" ATTRS{waiting_for_supplier}=="0"
Looking at the documentation for Pipewire it seems like it’s a case of “work it out yourself”. Getting this integrated/functional is no small task. Nice job!
pipewire in a nutshell
@[email protected] Since Pop_Os still has GNOME as default DE, the simplest way to automate the service start (which is the main thing to do, since it’s just a copy-paste of command now, right?) is like that:
Removed by mod
deleted by creator
Hey, thank you so much for this!
I seem to be misunderstanding some step though: I have copy-pasted the steps above and put the
atmos.wav
file in thehrir
directory, but I don’t get any sound in my headphones when I select the Virtual Surround sink with connected headphones? I get sound when I select the regular Headphone device.Is this meant to work out of the box when I connect my headphones in the regular jack on my laptop, or does it only work with a USB DAC?
I’m using it with onboard audio at the moment. You may need to log out and log back in, and then start the filter-chain config before launching anything else. If not, running it with
PIPEWIRE_DEBUG=3
might help.Thanks, the
PIPEWIRE_DEBUG=3
thing showed what’s wrong: it uses a relative path to find theatmos.wav
, so when I changed to use an absolute path it worked.It might have something to do with my
.config/pipewire
directory being a symlink to my dotfiles, but now I know that it works! Thanks again :)I’ve updated instructions to insert by absolute path rather than relative.
The config file doesn’t seem to support environment variables: for me it throws an error in the logs when i use the
${HOME}
variable.I can’t find any docs on if it supports any environment vars - does it work for you?
The above sed command will automatically convert it before writing the replacement.
(I don’t see comments below the post - only saw this in my inbox right now?)
Aha, I did the change manually, forgetting that sed would substitute the var. Would be great if Pipewire was aware of environment variables, but that’s not an issue to complain about here I guess :)
Check out the new guide for a SOFA spatializer
deleted by creator