HiFi sound configuration for Pipewire

Hey!

I was playing with USB DAC and wanted to maximize sound quality. So I created script with configuration files, to maximize audio quality in Pipewire.

Enjoy!

2 Likes

Thanks for sharing.

I don’t see any benefits with these changes to the pipewire configuration.

The script simply does two things, changing the default clock rate to 384000 Hz.
And setting the resample quality to 14, which might be problematic. As it’s stated in the section of the Arch wiki

There is a very little quality difference between 10 and 14, but the CPU load difference is 2-3x. And the latency difference between 4, 10, 14 is yet to be investigated by anybody. resample.quality = 14 on 44100→48000 Hz on Ryzen 2600 causes pipewire or pipewire-pulse processes to cause 4.0% one CPU core load.

You’ld be better off by avoiding resampling when not needed and to change the allowed clockrates in pipewire to those your DAC (or integrated chipset) supports.

If you’ll upsample effectively all sources from 44100 Hz → 384000 Hz with the highest quality… you’re audio quality won’t improve at all.

For comparison, even highest quality online streams won’t exceed 96000 Hz at 24 bit.

You are wrong. Install script asks you for maximum clock rate of your card.

Tidal streaming service offers 192 kHz

There is very little quality difference between 10 and 14, but there is, and this script is about maximizing possible quality output. I didn’t notice any high CPU usage on 8 years old i7, so I would say Wiki may be wrong at this point.

I haven’t used the script, just looked at the changes to be made.

As a matter of fact, Tidals max quality streaming could provide up to 192kHz and 24 bit. But based on albums I’ve checked their maximum quality is at 96kHz at 24 bit in most of the cases. Which is also labeled as max quality on their side. Some sources may be available with higher sample rates, sure. But Tidals max quality isn’t strictly 192kHz at 24 bit.

That you don’t see higher CPU loads with the quality setting of 14, might be related to the fact that pipewire uses its own resampling algorithm called Spa. And the quality preset only addresses resampling via Pulse Audio.

In any case, resampling is not required. Just add the supported sample rates of your DAC to the allowed sample rates without setting a default sample rate at all and pipewire will manage it on its own, without the need to enforce defaults.

My friend, information you try to provide does not contribute in any positive way as it is based only on Wiki and is not accurate in light of real world usage.

  1. Tidal has 192kHz tracks. It is enough for me to want to play Tidal in highest quality possible.

  2. Setting allowed sample rates, is not sufficient. I have USB DAC. If I use only allowed sample rates, it will never switch above 48kHz. I suspect this is because USB DAC does not report allowed sample rates properly. Anyway, only way to make it work is to explicitly set clock.rate to desired value. And this is also simplest and most straightforward way to do it. And apparently bullet proof, as there are strange devices out there.

  3. I don’t know how you got impression that resample.quality is for Pulse Audio. From what I understand it is for Pipewire, as replacement of PulseAudio samplers. Even Wiki mentions this parameter when using example of standalone Spa version:

$ spa-resample -q 14

Also you can check that Pipewire config file where sample quality parameter lives is called client.conf ( not pipewire-pulse.conf )

Next time I recommend less wiki & less confidence and more reading & real life testing.

And here you’re mistaken. My current conf adapts to the highest audio quality provided automatically, based on the available sink.

pw-top with a 96kHz 24bit playback via strawberry

S   ID  QUANT   RATE    WAIT    BUSY   W/Q   B/Q  ERR FORMAT           NAME 
S   30      0      0    ---     ---   ---   ---     0                  Dummy-Driver
S   31      0      0    ---     ---   ---   ---     0                  Freewheel-Driver
S   51      0      0    ---     ---   ---   ---     0                  Midi-Bridge
S   54      0      0    ---     ---   ---   ---     0                  bluez_midi.server
R   57   8192  96000  24.6us  11.8us  0.00  0.00    0    S32LE 2 96000 alsa_output.usb-SMSL_SMSL_USB_AUDIO-00.analog-stereo
R   65   8192  96000   8.9us  10.9us  0.00  0.00    0    S24LE 2 96000  + Strawberry

pw-top and with a 192kHz 24bit playback via strawberry

S   ID  QUANT   RATE    WAIT    BUSY   W/Q   B/Q  ERR FORMAT           NAME 
S   30      0      0    ---     ---   ---   ---     0                  Dummy-Driver
S   31      0      0    ---     ---   ---   ---     0                  Freewheel-Driver
S   51      0      0    ---     ---   ---   ---     0                  Midi-Bridge
S   54      0      0    ---     ---   ---   ---     0                  bluez_midi.server
R   57   8192 192000  43.2us  13.6us  0.00  0.00    0   S32LE 2 192000 alsa_output.usb-SMSL_SMSL_USB_AUDIO-00.analog-stereo
R   65   8192 192000  19.4us  16.7us  0.00  0.00    0   S24LE 2 192000  + Strawberry

As I said, no default clock rate.
Limiting pipewire to the sample rates my USB DAC supports natively.
Everything routed directly to ALSA via pipewire, avoiding any post-processing or such.

This is great your’s work. Mine don’t work like this unfortunately. So maybe your’s can report supported rates. Mine didn’t expose this info, when I looked for it.

I’ve only been able to make a few small changes to my pipewire.conf that I picked up somewhere years ago. I also use an external USB-DAC that can do 192kHz 24bit. But that’s not needed too often here. But it’s nice to have.

I don’t know where to insert the optimizations you suggested.

My pipewire.conf:

# Daemon config file for PipeWire version "0.3.70" #
#
# 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.properties = {
    ## Configure properties in the system.
    #library.name.system                   = support/libspa-support
    #context.data-loop.library.name.system = support/libspa-support
    #support.dbus                          = true
    #link.max-buffers                      = 64
    link.max-buffers                       = 16                       # version < 3 clients can't handle more
    #mem.warn-mlock                        = false
    #mem.allow-mlock                       = true
    #mem.mlock-all                         = false
    #clock.power-of-two-quantum            = true
    #log.level                             = 2
    #cpu.zero.denormals                    = false

    core.daemon = true              # listening for socket connections
    core.name   = pipewire-0        # core name and socket name

    ## Properties for the DSP configuration.
    default.clock.rate           = 48000
    default.clock.allowed-rates  = [ 48000 44100 88200 96000 192000 ]
    #default.clock.quantum       = 1024
    #default.clock.min-quantum   = 32
    #default.clock.max-quantum   = 2048
    #default.clock.quantum-limit = 8192
    #default.video.width         = 640
    #default.video.height        = 480
    #default.video.rate.num      = 25
    #default.video.rate.denom    = 1
    #
    #settings.check-quantum      = false
    #settings.check-rate         = false
    #
    # These overrides are only applied when running in a vm.
    vm.overrides = {
        default.clock.min-quantum = 1024
    }

    # keys checked below to disable module loading
    module.x11.bell = true
    # enables autoloading of access module, when disabled an alternative
    # access module needs to be loaded.
    module.access = true
}

context.spa-libs = {
    #<factory-name regex> = <library-name>
    #
    # Used to find spa factory names. It maps an spa factory name
    # regular expression to a library name that should contain
    # that factory.
    #
    audio.convert.* = audioconvert/libspa-audioconvert
    avb.*           = avb/libspa-avb
    api.alsa.*      = alsa/libspa-alsa
    api.v4l2.*      = v4l2/libspa-v4l2
    api.libcamera.* = libcamera/libspa-libcamera
    api.bluez5.*    = bluez5/libspa-bluez5
    api.vulkan.*    = vulkan/libspa-vulkan
    api.jack.*      = jack/libspa-jack
    support.*       = support/libspa-support
    #videotestsrc   = videotestsrc/libspa-videotestsrc
    #audiotestsrc   = audiotestsrc/libspa-audiotestsrc
}

context.modules = [
    #{ name = <module-name>
    #    ( args  = { <key> = <value> ... } )
    #    ( flags = [ ( ifexists ) ( nofail ) ] )
    #    ( condition = [ { <key> = <value> ... } ... ] )
    #}
    #
    # Loads a module with the given parameters.
    # If ifexists is given, the module is ignored when it is not found.
    # If nofail is given, module initialization failures are ignored.
    # If condition is given, the module is loaded only when the context
    # properties all match the match rules.
    #

    # Uses realtime scheduling to boost the audio thread priorities. This uses
    # RTKit if the user doesn't have permission to use regular realtime
    # scheduling.
    { name = libpipewire-module-rt
        args = {
            nice.level    = -11
            #rt.prio      = 88
            #rt.time.soft = -1
            #rt.time.hard = -1
        }
        flags = [ ifexists nofail ]
    }

    # The native communication protocol.
    { name = libpipewire-module-protocol-native }

    # The profile module. Allows application to access profiler
    # and performance data. It provides an interface that is used
    # by pw-top and pw-profiler.
    { name = libpipewire-module-profiler }

    # Allows applications to create metadata objects. It creates
    # a factory for Metadata objects.
    { name = libpipewire-module-metadata }

    # Creates a factory for making devices that run in the
    # context of the PipeWire server.
    { name = libpipewire-module-spa-device-factory }

    # Creates a factory for making nodes that run in the
    # context of the PipeWire server.
    { name = libpipewire-module-spa-node-factory }

    # Allows creating nodes that run in the context of the
    # client. Is used by all clients that want to provide
    # data to PipeWire.
    { name = libpipewire-module-client-node }

    # Allows creating devices that run in the context of the
    # client. Is used by the session manager.
    { name = libpipewire-module-client-device }

    # The portal module monitors the PID of the portal process
    # and tags connections with the same PID as portal
    # connections.
    { name = libpipewire-module-portal
        flags = [ ifexists nofail ]
    }

    # The access module can perform access checks and block
    # new clients.
    { name = libpipewire-module-access
        args = {
            # access.allowed to list an array of paths of allowed
            # apps.
            #access.allowed = [
            #    /usr/bin/pipewire-media-session
            #]

            # An array of rejected paths.
            #access.rejected = [ ]

            # An array of paths with restricted access.
            #access.restricted = [ ]

            # Anything not in the above lists gets assigned the
            # access.force permission.
            #access.force = flatpak
        }
        condition = [ { module.access = true } ]
    }

    # Makes a factory for wrapping nodes in an adapter with a
    # converter and resampler.
    { name = libpipewire-module-adapter }

    # Makes a factory for creating links between ports.
    { name = libpipewire-module-link-factory }

    # Provides factories to make session manager objects.
    { name = libpipewire-module-session-manager }

    # Use libcanberra to play X11 Bell
    { name = libpipewire-module-x11-bell
        args = {
            #sink.name = ""
            #sample.name = "bell-window-system"
            #x11.display = null
            #x11.xauthority = null
        }
        flags = [ ifexists nofail ]
        condition = [ { module.x11.bell = true } ]
    }
]

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 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 } }
    #{ factory = spa-node-factory   args = { factory.name = api.vulkan.compute.source node.name = my-compute-source } }

    # A default dummy driver. This handles nodes marked with the "node.always-driver"
    # property when no other driver is currently active. JACK clients need this.
    { factory = spa-node-factory
        args = {
            factory.name    = support.node.driver
            node.name       = Dummy-Driver
            node.group      = pipewire.dummy
            priority.driver = 20000
            #clock.id       = monotonic # realtime | tai | monotonic-raw | boottime
            #clock.name     = "clock.system.monotonic"
        }
    }
    { factory = spa-node-factory
        args = {
            factory.name    = support.node.driver
            node.name       = Freewheel-Driver
            priority.driver = 19000
            node.group      = pipewire.freewheel
            node.freewheel  = true
        }
    }
    # An example clock reading from /dev/ptp0. Another option is to sync the
    # ptp clock to CLOCK_TAI and then set clock.id = tai.
    #{ factory = spa-node-factory
    #    args = {
    #        factory.name    = support.node.driver
    #        node.name       = PTP0-Driver
    #        node.group      = pipewire.ptp0
    #        priority.driver = 30000
    #        clock.name      = "clock.system.ptp0"
    #        #clock.id       = tai
    #        clock.device    = "/dev/ptp0"
    #    }
    #}

    # 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"
    #    }
    #}

    # This creates a single PCM source device for the given
    # alsa device path hw:0. You can change source to sink
    # to make a sink in the same way.
    #{ factory = adapter
    #    args = {
    #        factory.name           = api.alsa.pcm.source
    #        node.name              = "alsa-source"
    #        node.description       = "PCM Source"
    #        media.class            = "Audio/Source"
    #        api.alsa.path          = "hw:0"
    #        api.alsa.period-size   = 1024
    #        api.alsa.headroom      = 0
    #        api.alsa.disable-mmap  = false
    #        api.alsa.disable-batch = false
    #        audio.format           = "S16LE"
    #        audio.rate             = 48000
    #        audio.channels         = 2
    #        audio.position         = "FL,FR"
    #    }
    #}

    # Use the metadata factory to create metadata and some default values.
    #{ factory = metadata
    #    args = {
    #        metadata.name = my-metadata
    #        metadata.values = [
    #            { key = default.audio.sink   value = { name = somesink } }
    #            { key = default.audio.source value = { name = somesource } }
    #        ]
    #    }
    #}
]

context.exec = [
    #{   path = <program-name>
    #    ( args = "<arguments>" )
    #    ( condition = [ { <key> = <value> ... } ... ] )
    #}
    #
    # Execute the given program with arguments.
    # If condition is given, the program is executed only when the context
    # properties all match the match rules.
    #
    # You can optionally start the session manager here,
    # but it is better to start it as a systemd service.
    # Run the session manager with -h for options.
    #
    #{ path = "/usr/bin/pipewire-media-session" args = ""
    #  condition = [ { exec.session-manager = null } { exec.session-manager = true } ] }
    #
    # You can optionally start the pulseaudio-server here as well
    # but it is better to start it as a systemd service.
    # It can be interesting to start another daemon here that listens
    # on another address with the -a option (eg. -a tcp:4713).
    #
    #{ path = "/usr/bin/pipewire" args = "-c pipewire-pulse.conf"
    #  condition = [ { exec.pipewire-pulse = null } { exec.pipewire-pulse = true } ] }

Check this post in the arch forums Get bit-perfect audio with PipeWire

For the supported clock rates of your DAC, you’ll need to identify it’s card ID with aplay -l and with the associated card ID cat /proc/asound/cardX/streamX will list all supported clockrates for the DAC.

That’s the same approach the entry in Discovery is suggesting.

Seems to be fine, as the most common clock rates are included within the allowed list. Only difference would be that I’ve commented out the default.clock.rate as I don’t want any resampling.

1 Like

thanks for your answer

1 Like

Use install.sh script from the repo. Othwerwise you may end up using default 48000 sample rate.

Also do not edit /usr/share/pipewire/ as this seems to be the case, as this file will be overwritten during Pipewire update and you will loose optimization.

Don’t worry, the pipewire.conf is in my home folder :wink: , but thanks for the tip.

1 Like

Using script from the repo was designed to help newbies do it faster, than editing files manually.

  1. As already explained, setting default.clock.allowed-rates will not ensure system will pick up maximum allowed rate - you choose to ignore this info.

  2. Setting default.clock.rate does not do any extra resampling, as you imply this. This fixes resmpling in a same way as default.clock.allowed-rates on your PC

  3. Rates reported by DAC from cat /proc/asound/cardX/streamX may not be achievable in normal setup, ex. my card will report 768000 as maximum sampling rate, but this can be only achieved when using USB to I2S mode. Trying to enforce such sample rate brakes sound. So this info is not as reliable as it should be.

I understand your passion to follow Wiki and Guides, but real life scenario differs from them. Later this week will take some time to update WIki with my findings.

You can check using pw-top if they worked properly

1 Like