Virtual Surround

7 minutes read | 1283 words

Aaruni Kaushik

7.1 Headphones!

You just got an new headset. Its expensive and brilliant and it says 7.1 surround sound on the box, and you swear you can hear the difference. Unfortunately for you, that’s not how this works. That’s not how any of this works!

The numbers in the surround sound standards refer to the number of physical channels there are for the sound output. 7.1 means you have 7 channels for sound, and a subwoofer for base. The channels in this setup are Front-Left (1), Front-Center (2), Front-Center (3), Surround Left (4), Surround Right (5), Rear Left (6), Rear Right(7), and a low frequency subwoofer (.1). Your fancy new headphones, on the other hand, only have two physical channels: left and right. But what about the stark difference you swear you can hear v/s your previous set of cans? All of that is software magic.

Binaural Setup

As a human being, you have at most two working ears. No matter how many sources sound reaches your head, you only hear it from two inputs : left and right. So, all spatial audio can be emulated with just a pair of high quality sound drivers and HRTF. So, you didn’t get duped. You paid for a pair of really high quality magnets, which can then correctly place the different channels of surround sound in the virtual audioscape they are able to immerse you in. Knowing this, you can setup ANY pair of headphones to offer their best approximation of surround sound, not just the ones which ship with magic software for it! All you need is a Linux computer running pipewire, and a text editor.

Setup on Pipewire/Linux

The setup to get this going is ridiculously simple. All we need is a HRIR wav file. This file contains data to modulate delays and intensity of audio from different tracks before sending it to the physical output, to make it sound like as if the sound is originating at a particular point in space. Then, we write a config file to tell pipewire how to modify sounds based on this HRIR file.

  1. First, create the pipewire conf folders if they don’t already exist

    mkdir -pv $HOME/.config/pipewire/pipewire.conf.d

  2. Download the WAV file for Dolby Atmos 7.1 from here, and place it in $HOME/.config/pipewire, and rename it to atmos.wav

  3. In any text editor, open the file $HOME/.config/pipewire/pipewire.conf.d/sink-virtual-surround-7.1-hesuvi.conf, and copy-paste the folowwing (which are taken from of this file) into your conf.

NOTE

In the following file, you must replace $HOME with the the path of your homefolder!

# Convolver sink
#

context.modules = [
    { name = libpipewire-module-filter-chain
        flags = [ nofail ]
        args = {
            node.description = "Dolby Atmos 7.1"
            media.name       = "Dolby Atmos 7.1"
            filter.graph = {
                nodes = [
                    # duplicate inputs
                    { type = builtin label = copy name = copyFL  }
                    { type = builtin label = copy name = copyFR  }
                    { type = builtin label = copy name = copyFC  }
                    { type = builtin label = copy name = copyRL  }
                    { type = builtin label = copy name = copyRR  }
                    { type = builtin label = copy name = copySL  }
                    { type = builtin label = copy name = copySR  }
                    { type = builtin label = copy name = copyLFE }

                    # apply hrir - HeSuVi 14-channel WAV (not the *-.wav variants) (note: */44/* in HeSuVi are the same, but resampled to 44100)
                    { type = builtin label = convolver name = convFL_L config = { filename = "$HOME/.config/pipewire/atmos.wav" channel =  0 } }
                    { type = builtin label = convolver name = convFL_R config = { filename = "$HOME/.config/pipewire/atmos.wav" channel =  1 } }
                    { type = builtin label = convolver name = convSL_L config = { filename = "$HOME/.config/pipewire/atmos.wav" channel =  2 } }
                    { type = builtin label = convolver name = convSL_R config = { filename = "$HOME/.config/pipewire/atmos.wav" channel =  3 } }
                    { type = builtin label = convolver name = convRL_L config = { filename = "$HOME/.config/pipewire/atmos.wav" channel =  4 } }
                    { type = builtin label = convolver name = convRL_R config = { filename = "$HOME/.config/pipewire/atmos.wav" channel =  5 } }
                    { type = builtin label = convolver name = convFC_L config = { filename = "$HOME/.config/pipewire/atmos.wav" channel =  6 } }
                    { type = builtin label = convolver name = convFR_R config = { filename = "$HOME/.config/pipewire/atmos.wav" channel =  7 } }
                    { type = builtin label = convolver name = convFR_L config = { filename = "$HOME/.config/pipewire/atmos.wav" channel =  8 } }
                    { type = builtin label = convolver name = convSR_R config = { filename = "$HOME/.config/pipewire/atmos.wav" channel =  9 } }
                    { type = builtin label = convolver name = convSR_L config = { filename = "$HOME/.config/pipewire/atmos.wav" channel = 10 } }
                    { type = builtin label = convolver name = convRR_R config = { filename = "$HOME/.config/pipewire/atmos.wav" channel = 11 } }
                    { type = builtin label = convolver name = convRR_L config = { filename = "$HOME/.config/pipewire/atmos.wav" channel = 12 } }
                    { type = builtin label = convolver name = convFC_R config = { filename = "$HOME/.config/pipewire/atmos.wav" channel = 13 } }

                    # treat LFE as FC
                    { type = builtin label = convolver name = convLFE_L config = { filename = "$HOME/.config/pipewire/atmos.wav" channel =  6 } }
                    { type = builtin label = convolver name = convLFE_R config = { filename = "$HOME/.config/pipewire/atmos.wav" channel = 13 } }

                    # stereo output
                    { type = builtin label = mixer name = mixL }
                    { type = builtin label = mixer name = mixR }
                ]
                links = [
                    # input
                    { output = "copyFL:Out"  input="convFL_L:In"  }
                    { output = "copyFL:Out"  input="convFL_R:In"  }
                    { output = "copySL:Out"  input="convSL_L:In"  }
                    { output = "copySL:Out"  input="convSL_R:In"  }
                    { output = "copyRL:Out"  input="convRL_L:In"  }
                    { output = "copyRL:Out"  input="convRL_R:In"  }
                    { output = "copyFC:Out"  input="convFC_L:In"  }
                    { output = "copyFR:Out"  input="convFR_R:In"  }
                    { output = "copyFR:Out"  input="convFR_L:In"  }
                    { output = "copySR:Out"  input="convSR_R:In"  }
                    { output = "copySR:Out"  input="convSR_L:In"  }
                    { output = "copyRR:Out"  input="convRR_R:In"  }
                    { output = "copyRR:Out"  input="convRR_L:In"  }
                    { output = "copyFC:Out"  input="convFC_R:In"  }
                    { output = "copyLFE:Out" input="convLFE_L:In" }
                    { output = "copyLFE:Out" input="convLFE_R:In" }

                    # output
                    { output = "convFL_L:Out"  input="mixL:In 1" }
                    { output = "convFL_R:Out"  input="mixR:In 1" }
                    { output = "convSL_L:Out"  input="mixL:In 2" }
                    { output = "convSL_R:Out"  input="mixR:In 2" }
                    { output = "convRL_L:Out"  input="mixL:In 3" }
                    { output = "convRL_R:Out"  input="mixR:In 3" }
                    { output = "convFC_L:Out"  input="mixL:In 4" }
                    { output = "convFC_R:Out"  input="mixR:In 4" }
                    { output = "convFR_R:Out"  input="mixR:In 5" }
                    { output = "convFR_L:Out"  input="mixL:In 5" }
                    { output = "convSR_R:Out"  input="mixR:In 6" }
                    { output = "convSR_L:Out"  input="mixL:In 6" }
                    { output = "convRR_R:Out"  input="mixR:In 7" }
                    { output = "convRR_L:Out"  input="mixL:In 7" }
                    { output = "convLFE_R:Out" input="mixR:In 8" }
                    { output = "convLFE_L:Out" input="mixL:In 8" }
                ]
                inputs  = [ "copyFL:In" "copyFR:In" "copyFC:In" "copyLFE:In" "copyRL:In" "copyRR:In", "copySL:In", "copySR:In" ]
                outputs = [ "mixL:Out" "mixR:Out" ]
            }
            capture.props = {
                node.name      = "effect_input.virtual-surround-7.1-hesuvi"
                media.class    = Audio/Sink
                audio.channels = 8
                audio.position = [ FL FR FC LFE RL RR SL SR ]
            }
            playback.props = {
                node.name      = "effect_output.virtual-surround-7.1-hesuvi"
                node.passive   = true
                audio.channels = 2
                audio.position = [ FL FR ]
            }
        }
    }
]

Then, simply restart the pipewire process (systemcl --user restart pipewire.service) (or, just your computer), and you’ll find new audio devices in your computer. Simply select them for a virtual surround sound experience, and try out your new (virtual) device on this video

Note

If the volume sounds too low, switch to normal headphones output, increase volume, and then switch back to the virtual device.