• Welcome to ASR. There are many reviews of audio hardware and expert members to help answer your questions. Click here to have your audio equipment measured for free!

Bit-Perfect Audio on Linux with PipeWire

alpha_logic

Active Member
Joined
Aug 16, 2021
Messages
136
Likes
251

Bit-Perfect Audio on Linux with PipeWire​


A technically rigorous guide to eliminating unnecessary audio processing on Linux.

Target: PipeWire 1.0+ on Ubuntu 24.04+ (principles apply to any PipeWire-based distro)
Scope: USB DAC playback only. Not recording, not JACK production, not Bluetooth.
Based on: A working, verified setup — confirmed bit-perfect via graph inspection and rate-matching verification.

1. Why This Guide Exists​


Most Linux audio setups silently apply processing you didn't ask for:

  • Resampling all audio to a fixed rate (typically 48kHz)
  • Applying software volume in the digital domain
  • Channel mixing (upmix, downmix, normalization)
  • Extra buffering stages that add latency

This guide shows you how to configure PipeWire so that none of that happens — the exact samples from your source reach the DAC unchanged in value, format, and sample rate.

Important caveat: Whether eliminating these processing stages produces an audible difference is a separate question. PipeWire's SoXR-based resampler at quality 15, and 32-bit float internal processing with dithered output, are arguably transparent in controlled ABX testing. The goal here is engineering correctness — if the DAC can accept the source format natively, there is no reason to resample or modify it in software.

2. What "Bit-Perfect" Actually Means​


Definition: The digital samples written to the USB endpoint are identical to the samples in the source stream. No resampling, no volume scaling, no channel mixing, no dithering applied by the audio server.

What it does NOT mean: It does not guarantee the analog output of your DAC is "perfect." DAC reconstruction filters, analog output stage, clock jitter, and power supply all affect the analog domain. Bit-perfect is a digital transport property only.

Verification Challenge​


USB Audio Class uses isochronous transfers — there is no application-layer ACK or checksum. You cannot prove bit-perfection purely from the host side.

The closest practical verification methods:

  1. Loopback null-test: If your DAC has a digital output (S/PDIF), capture it back into the system, subtract from the source, and confirm the residual is null.
  2. Graph inspection: Confirm via pw-top and pw-dump that no SRC or volume nodes exist in the active signal path. This is necessary but not sufficient proof.
  3. Rate verification: Play files at different sample rates and confirm the DAC's reported rate changes to match (visible in pactl list sinks).

Where Quality Gets Lost in a Typical Setup​


Processing StageWhat It DoesEffect
ResamplerConverts sample rate (e.g., 44100→48000)Introduces filter artifacts, rounding
Volume nodeScales sample valuesReduces effective bit depth (unless done in float)
Channel mixerUpmix, downmix, normalizeModifies channel content

3. The PipeWire Audio Stack​


Understanding the layers is critical to understanding what we're configuring:

Code:
Application (Firefox, mpd, etc.)
    ↓
PipeWire graph (format negotiation, routing, optional SRC/volume)
    ↓
ALSA backend (opens hardware device)
    ↓
USB Audio Class driver (kernel, isochronous transfers)
    ↓
DAC hardware

PipeWire's ALSA backend is where the hw:X vs plughw:X distinction matters:

ALSA PathDescriptionBit-Perfect?
hw:XDirect hardware access — no ALSA plugin processing (no dmix, no softvol, no format conversion)Yes
plughw:XALSA plugin layer — auto-converts format, rate, and channelsNo
front:X / defaultHigher-level ALSA devices that may route through dmix, softvol, etc.No

When you create a PipeWire sink node with api.alsa.path = "hw:USB", PipeWire opens the ALSA hw device directly. Combined with resample.disable = true, this eliminates PipeWire's internal SRC. Samples pass from the PipeWire graph to ALSA hw to USB without rate conversion.

Note: You are not "choosing hw:" in the traditional ALSA sense (as you would with aplay -D hw:1). PipeWire is the audio server — it manages the ALSA backend internally. You are telling PipeWire's ALSA backend which device path to open.

4. Identifying Your DAC​


Step 1: Check ALSA Card List​


Bash:
cat /proc/asound/cards

Example output:
Code:
 0 [NVidia         ]: HDA-Intel - HDA NVidia
                      HDA NVidia at 0xf6080000 irq 93
 1 [USB            ]: USB-Audio - Schiit Bifrost 2 Unison USB
                      Schiit Audio Schiit Bifrost 2 Unison USB at usb-0000:0b:00.3-2, high speed
 2 [Generic        ]: HDA-Intel - HD-Audio Generic
                      HD-Audio Generic at 0xf6500000 irq 94

The ALSA ID is the value in brackets. Here, the DAC is card 1 with ID USB. Use this ID in your config — hw:USB is more stable than hw:1 because card numbers can change after suspend/resume or USB re-enumeration.

Step 2: Check Supported Formats and Rates​


Bash:
cat /proc/asound/card1/stream0

Example output (Schiit Bifrost 2):
Code:
Playback:
  Interface 1
    Altset 1
    Format: S16_LE
    Channels: 2
    Rates: 44100, 48000, 88200, 96000, 176400, 192000
    Bits: 16

  Interface 1
    Altset 2
    Format: S24_3LE
    Channels: 2
    Rates: 44100, 48000, 88200, 96000, 176400, 192000
    Bits: 24

  Interface 1
    Altset 3
    Format: S32_LE
    Channels: 2
    Rates: 44100, 48000, 88200, 96000, 176400, 192000
    Bits: 32

This tells you:
  • Your DAC supports S16_LE, S24_3LE, and S32_LE
  • Supported rates: 44100, 48000, 88200, 96000, 176400, 192000
  • You will need these values for the config

Step 3: Confirm in WirePlumber​


Bash:
wpctl status

Look for your DAC in the Sinks section. Note the node number and name.

5. Creating the Bit-Perfect Sink​


This is the core of the guide. We create a custom PipeWire sink node that opens your DAC via the hw: path with resampling disabled.

Create the file:
Bash:
mkdir -p ~/.config/pipewire/pipewire.conf.d/
nano ~/.config/pipewire/pipewire.conf.d/50-bitperfect-dac.conf

Minimal Bit-Perfect Config​


Code:
# Bit-perfect sink for USB DAC
# Replace "hw:USB" with your DAC's ALSA ID from /proc/asound/cards
# Replace audio.format with the widest format from /proc/asound/cardX/stream0

context.objects = [
    {
        factory = adapter
        args = {
            factory.name           = api.alsa.pcm.sink
            node.name              = "DAC_Bit_Perfect"
            node.description       = "USB DAC (Bit-Perfect Mode)"
            media.class            = "Audio/Sink"
            api.alsa.path          = "hw:USB"
            api.alsa.period-size   = 1024
            api.alsa.headroom      = 0
            audio.format           = S32LE
            audio.channels         = 2
            node.suspend-on-idle   = false
            priority.driver        = 9000
            priority.session       = 9000
            resample.disable       = true
            monitor.channel-volumes = false
        }
    }
]

Settings Explained​


SettingValueWhy
api.alsa.pathhw:USBDirect ALSA hardware access. Replace with your DAC's ALSA ID.
audio.formatS32LEWidest container your DAC accepts. See format guide below.
resample.disabletrueCritical. Disables PipeWire's internal sample rate converter for this node.
node.suspend-on-idlefalsePrevents PipeWire from closing the ALSA device when idle. Avoids clicks on DAC power-cycling.
priority.driver / priority.session9000High priority ensures this sink wins over auto-detected sinks as the default.
api.alsa.period-size1024DMA transfer size in frames. 1024 is a safe default for playback.
api.alsa.headroom0Extra buffer frames. 0 = no extra buffering. PipeWire may override to a safe minimum depending on the ALSA driver's timer model. Increase if you get clicks/pops.
monitor.channel-volumesfalsePrevents per-channel volume tracking in the monitor. Keeps signal path clean.

Choosing the Right audio.format​


FormatDescriptionWhen to Use
S32LE32-bit signed, little-endianSafest default. Most modern USB DACs are natively 24-bit but accept S32LE — the DAC ignores the lowest 8 bits. Using S32LE avoids truncation of 24-bit content and works as a superset container.
S24LE24-bit in 32-bit containerEquivalent to S32LE for 24-bit DACs. More explicit about intent.
S24_3LE24-bit packed (3 bytes/sample)Some DACs prefer this. Check your stream0.
S16LE16-bit signed, little-endianFor 16-bit-only DACs (rare in modern USB DACs).

Rule of thumb: Check /proc/asound/cardX/stream0. Use the widest format listed. If it shows S32_LE, use S32LE.

6. Configuring Clock and Rates​


Add to the same file, or create a separate drop-in:

Code:
# Clock and rate settings for bit-perfect operation
context.properties = {
    # Fallback rate when no stream is active.
    # NOT the playback rate — with resample.disable and allowed-rates,
    # the DAC will switch to match the source stream's native rate.
    # Set to 44100 if most content is CD-quality, 48000 for video/gaming.
    default.clock.rate          = 44100

    # Rates PipeWire may negotiate with the DAC.
    # MUST match your DAC's supported rates from stream0.
    # Do NOT include rates your DAC doesn't support.
    default.clock.allowed-rates = [ 44100 48000 88200 96000 176400 192000 ]

    # Buffer size in frames. 1024 = ~23ms at 44100Hz.
    # Lower = less latency, higher xrun risk.
    # Higher = safer, more latency.
    default.clock.quantum       = 1024

    # Bounds for dynamic quantum adjustment.
    default.clock.min-quantum   = 32
    default.clock.max-quantum   = 8192
}

Rate Switching Behavior​


When a new stream starts at a different rate (e.g., switching from a 44.1kHz track to a 96kHz track), PipeWire renegotiates with the DAC if:

  1. The new rate is in allowed-rates
  2. resample.disable = true on the sink
  3. The DAC supports the rate

There may be a brief click or silence during rate switches. This is normal — the USB Audio Class endpoint is being reconfigured.

7. PipeWire-Pulse Tuning​


Most desktop applications (Firefox, Spotify, etc.) use the PulseAudio API, which PipeWire serves via pipewire-pulse. These settings tune that compatibility layer.

Bash:
mkdir -p ~/.config/pipewire/pipewire-pulse.conf.d/
nano ~/.config/pipewire/pipewire-pulse.conf.d/99-bitperfect.conf

Code:
# PipeWire-Pulse tuning for bit-perfect playback

pulse.properties = {
    # Internal processing format — F32 gives maximum headroom
    pulse.default.format = F32
}

stream.properties = {
    # Maximum SoXR quality for when resampling DOES occur.
    # In a bit-perfect setup this should rarely fire — only when
    # two streams at different rates must be mixed, or an app
    # outputs a rate not in allowed-rates.
    # This is a safety net, not a primary control.
    resample.quality = 15

    # Disable channel mixing modifications
    channelmix.normalize    = false
    channelmix.upmix        = false
    channelmix.upmix-method = none
    channelmix.mix-lfe      = false
}

8. Volume Control — The Right Way​


The Engineering Argument​


Modifying sample values in software — even for volume — means the output is no longer bit-identical to the source. By definition, this breaks bit-perfect playback.

The Practical Nuance​


PipeWire processes audio internally in 32-bit float. Applying volume in float with dithered output to 24-bit yields >140dB of dynamic range, which exceeds the noise floor of any real DAC or listening environment. In controlled ABX testing, this is generally considered transparent.

Whether you care about this distinction is a personal engineering choice.

Recommendation​


For bit-perfect operation:

  • Set digital volume to 100% (0.00 dB): wpctl set-volume @DEFAULT_AUDIO_SINK@ 1.0
  • Control volume on your amplifier or DAC (analog domain)
  • Do not use desktop volume sliders, media key volume, or per-app volume controls

If you must use software volume — understand that bit-perfect is broken by definition. The audible impact at 32-bit float precision with proper dithering is negligible. Just know what you're trading.

9. Apply and Verify​


Restart PipeWire​


Bash:
systemctl --user restart pipewire pipewire-pulse wireplumber

Verify Your Sink Is Active​


Bash:
wpctl status

Your custom sink should appear in the Sinks list with an asterisk (*) indicating it's the default. If it's not the default:

Bash:
# Find the node ID from wpctl status, then:
wpctl set-default <node-id>

Verify Sample Rate Matches Source​


Bash:
pactl list sinks | grep -A 5 "DAC_Bit_Perfect"

The "Sample Specification" line shows the current rate. Play a known 44.1kHz file — it should show 44100Hz. Play a 96kHz file — it should switch to 96000Hz.

Verify No Resampling in the Graph​


Bash:
pw-top

Watch for your sink node. The QUANT column shows the current quantum. There should be no SRC (sample rate converter) node between your application and the sink.

Check Runtime Settings​


Bash:
pw-metadata -n settings

Confirms the active clock.rate, clock.allowed-rates, and clock.quantum.

10. Common Pitfalls​


Browser Audio​


  • Firefox: Resamples internally. Check about:configmedia.cubeb.output_rate and media.resampling.rate. By default, Firefox outputs at the system rate set by PipeWire. WebAudio API content is predominantly 48kHz.
  • Chromium/Chrome: Follows the system default rate set by PipeWire. If default.clock.rate is 44100, Chromium outputs 44100.
  • Bottom line: Browser audio is almost never bit-perfect regardless of PipeWire config. Web content is lossy/compressed and typically 48kHz. Optimize for your dedicated music player (mpd, Strawberry, Audacious, cmus, DeaDBeeF), not the browser.

Duplicate Sinks​


PipeWire's ALSA monitor auto-detects your DAC and creates a default sink node. Your custom bit-perfect sink is a second node on the same hardware. The auto-detected one will be SUSPENDED while yours is active (they can't both hold the hw: device). This is harmless but untidy.

To suppress it, see Section 11.

Multi-Stream Mixing​


If two applications play simultaneously at different sample rates, PipeWire must resample one stream to match the other — the DAC can only run at one rate at a time. This is unavoidable. The resample.quality = 15 setting ensures this uses the highest quality SoXR mode.

Suspend/Resume​


Some DACs disconnect from USB on system suspend. On resume, ALSA re-enumerates and card numbers may change. Using hw:USB (ALSA ID) instead of hw:1 (card number) helps, but isn't bulletproof. If your DAC's ALSA ID changes after resume, restart PipeWire.

Bluetooth​


Completely separate codec and transport path (SBC, AAC, aptX, LDAC). Bit-perfect does not apply. Not covered in this guide.

11. Suppressing the Auto-Detected Duplicate Sink (Optional)​


This is cosmetic cleanup — the duplicate sink is harmless while suspended.

WirePlumber 0.5+ (Arch, Fedora 40+, Ubuntu 24.10+)​


Bash:
mkdir -p ~/.config/wireplumber/wireplumber.conf.d/
nano ~/.config/wireplumber/wireplumber.conf.d/50-disable-auto-dac.conf

Code:
monitor.alsa.rules = [
    {
        matches = [
            {
                # Match the auto-detected node for your DAC.
                # Adjust the pattern to match your device.
                node.name = "~alsa_output.usb-*YOUR_DAC_NAME*"
            }
        ]
        actions = {
            update-props = {
                node.disabled = true
            }
        }
    }
]

WirePlumber 0.4.x (Ubuntu 24.04 LTS)​


Bash:
mkdir -p ~/.config/wireplumber/main.lua.d/
nano ~/.config/wireplumber/main.lua.d/50-disable-auto-dac.lua

Code:
rule = {
  matches = {
    {
      { "node.name", "matches", "alsa_output.usb-*YOUR_DAC_NAME*" },
    },
  },
  apply_properties = {
    ["node.disabled"] = true,
  },
}

table.insert(alsa_monitor.rules, rule)

Replace *YOUR_DAC_NAME* with a pattern matching your DAC's auto-detected node name. Find it with:

Bash:
wpctl status | grep -i "your dac name"
# or
pw-dump | grep node.name | grep alsa_output

12. Troubleshooting​


No Sound​


  • wpctl status — is your sink listed? Is it the default (*)?
  • journalctl --user -u pipewire -n 50 — PipeWire errors
  • journalctl --user -u wireplumber -n 50 — session manager errors
  • Test ALSA directly: aplay -D hw:USB -f S32_LE -r 44100 -c 2 /dev/zero — should produce silence, not an error. If this fails, the issue is ALSA/hardware, not PipeWire.

Wrong Sample Rate​


  • pactl list sinks — check "Sample Specification" line
  • Is the source rate in allowed-rates?
  • Is resample.disable = true actually set? Check: pw-dump | grep -A 2 resample

Clicks and Pops (Xruns)​


  • Increase api.alsa.headroom — try 256, 512, 1024
  • Increase default.clock.quantum — try 2048
  • Check pw-top for xrun counts
  • Check system load — PipeWire uses SCHED_FIFO by default if rtkit is installed. Verify: chrt -p $(pidof pipewire)

DAC Not Detected​


  • dmesg | grep -i usb — does the kernel see it?
  • aplay -l — does ALSA see it?
  • lsusb — is the USB device enumerated?
  • Try a different USB port — avoid hubs for audio devices

Rate Switching Not Working​


  • Confirm the target rate is in allowed-rates
  • Confirm your DAC supports it: cat /proc/asound/cardX/stream0
  • Some DACs require the stream to fully stop and restart for rate changes. PipeWire handles this automatically but there may be a longer gap.

13. Full Reference Config​


Code:
# =============================================================
# Bit-Perfect USB DAC Configuration for PipeWire 1.0+
# =============================================================
#
# Instructions:
#   1. Replace "hw:USB" with your DAC's ALSA ID
#      (from: cat /proc/asound/cards)
#   2. Replace S32LE with the widest format your DAC supports
#      (from: cat /proc/asound/cardX/stream0)
#   3. Replace allowed-rates with your DAC's supported rates
#      (from: cat /proc/asound/cardX/stream0)
#   4. Save this file and run:
#      systemctl --user restart pipewire pipewire-pulse wireplumber
#
# Verify with:
#   wpctl status
#   pactl list sinks
#   pw-metadata -n settings

# --- Bit-perfect sink node ---
context.objects = [
    {
        factory = adapter
        args = {
            factory.name           = api.alsa.pcm.sink
            node.name              = "DAC_Bit_Perfect"
            node.description       = "USB DAC (Bit-Perfect Mode)"
            media.class            = "Audio/Sink"

            # ALSA hardware path — direct access, no plugins
            api.alsa.path          = "hw:USB"

            # Buffer tuning
            api.alsa.period-size   = 1024
            api.alsa.headroom      = 0

            # Sample format — use widest your DAC accepts
            audio.format           = S32LE
            audio.channels         = 2

            # Prevent idle suspend (avoids DAC power-cycling clicks)
            node.suspend-on-idle   = false

            # High priority — wins over auto-detected sinks
            priority.driver        = 9000
            priority.session       = 9000

            # THE critical setting — disable PipeWire's resampler
            resample.disable       = true

            # Keep signal path clean
            monitor.channel-volumes = false
        }
    }
]

# --- Clock and rate settings ---
context.properties = {
    # Fallback rate when idle — NOT the playback rate.
    # With resample.disable + allowed-rates, DAC follows source rate.
    default.clock.rate          = 44100

    # Rates PipeWire may negotiate with the DAC.
    # MUST match your DAC's supported rates.
    default.clock.allowed-rates = [ 44100 48000 88200 96000 176400 192000 ]

    # Buffer size (frames). 1024 ~= 23ms at 44100Hz.
    default.clock.quantum       = 1024
    default.clock.min-quantum   = 32
    default.clock.max-quantum   = 8192
}

Code:
# =============================================================
# PipeWire-Pulse Tuning for Bit-Perfect Playback
# =============================================================
# Most desktop apps use PulseAudio API → PipeWire serves it
# via pipewire-pulse. These settings tune that layer.

pulse.properties = {
    pulse.default.format = F32
}

stream.properties = {
    # Max quality SoXR — safety net for when resampling occurs
    # (e.g., mixing two streams at different rates)
    resample.quality = 15

    # No channel mixing modifications
    channelmix.normalize    = false
    channelmix.upmix        = false
    channelmix.upmix-method = none
    channelmix.mix-lfe      = false
}

After Applying​


Bash:
# Restart
systemctl --user restart pipewire pipewire-pulse wireplumber

# Set volume to 0dB
wpctl set-volume @DEFAULT_AUDIO_SINK@ 1.0

# Verify
wpctl status
pactl list sinks | grep -A 5 "DAC_Bit_Perfect"
pw-metadata -n settings

14. Summary​


WhatSettingEffect
Direct hardware accessapi.alsa.path = "hw:USB"Bypasses ALSA plugins
Disable resamplingresample.disable = trueDAC receives source-native rate
Source-native ratesdefault.clock.allowed-ratesDAC switches rate to match content
No software volumeVolume at 100% / 0dBNo sample value modification
Widest formataudio.format = S32LENo truncation of 24-bit content

The goal is not audiophile magic — it's engineering correctness. If the DAC can accept the source format natively, let it.

---

Based on a verified working setup: PipeWire 1.0.5, WirePlumber 0.4.17, Ubuntu, USB DAC. Tested with 44.1kHz, 48kHz, and 96kHz content with confirmed rate switching and graph inspection showing zero SRC nodes in the active path.
 
A GUI for this would be a dream, but even without, great work, great thanks
 
audio.format = S32LE
pulse.properties = { # Internal processing format — F32 gives maximum headroom pulse.default.format = F32 }
Certainly not audible, but int32 cannot be represented by F32 at full resolution, as mantissa of F32 is only 23 bits long (+ 1 sign bit + 8 exponent bits). That means for S32 input/output the chain with pipewire by principle cannot be bit perfect.
 
Certainly not audible, but int32 cannot be represented by F32 at full resolution, as mantissa of F32 is only 23 bits long (+ 1 sign bit + 8 exponent bits). That means for S32 input/output the chain with pipewire by principle cannot be bit perfect.

You're correct that F32 has a 24-bit mantissa (23 stored + 1 implicit), so an S32→F32→S32 round-trip cannot preserve all 32 bits — the bottom 8 bits of the S32 container are lost. However, this doesn't affect bit-perfection in practice: every DAC chip on the market (ESS, AKM, TI, Cirrus, etc) is a 24-bit converter. They accept S32LE as a transport container, but only the top 24 bits reach the actual converter core. F32 represents all 2^24 integer values exactly. The chain is bit-perfect at the DAC's native resolution — the bits that F32 can't represent are bits no converter ever sees.
 
Going through floating-point math (for scaling volume) is always more accurate than doing it from integers (especially if you need a few back-and-forth ops).
The main point of F32 (or what it really is, in terms of bit count...) only guarantees that you will never overflow by rounding-up. F32 perfectly covers all that's needed for 24 bits integer values.

This said, in the float representation you NEVER HAVE a true value - but more granularity when scaling back to integer. bit-perfectness is maintained (well, with a float value you do not intrinsically have it, but you are guaranteed integer reconstruction extremely close to what it was BEFORE being converted to float, by simply multiplying every value by the maximum allowed by the bit-depth you are scaling to: +/- 32768 for 16 bits, and some 24-million - IIRC - for 24 bits).

And yes. I am a software engineer... ;-)
 
For clarification:

IEEE 754 single-precision (F32) layout:
  • Sign: 1 bit
  • Exponent: 8 bits
  • Mantissa: 23 bits stored + 1 implicit = 24 bits of significand

An integer N is exactly representable in F32 if and only if |N| ≤ 2²⁴ = 16,777,216.

A 24-bit signed audio sample has range [−2²³, 2²³ − 1] = [−8,388,608, 8,388,607].

Since 2²³ < 2²⁴: every 24-bit integer is exactly representable in F32. The conversion is lossless. The reconstruction is bit-identical, not approximate.

For S32LE (32-bit signed, range [−2³¹, 2³¹ − 1]): 2³¹ > 2²⁴, so F32 cannot represent all 32-bit integers — the bottom 8 bits are lost. But since all DAC converter chips (ESS, AKM, TI, Cirrus) are ≤24-bit, those bits are padding zeros that never reach the converter core.

Therefore: S32LE → F32 → S32LE → DAC(24-bit) is bit-perfect. ∎
 
Thanks for doing this! I nearly always have use for a mixer and I'm pretty firmly sold on Easy Effects for DSP so this is probably not something I'd use regularly, but I may set up a profile just to get a better understanding of how Pipewire works and how to configure it. Also really good to know if I ever get around to making my ultimate DAC ABX comparator! (0.1% chance of this ever happening)
 
Please note I explicitely talked about S32 as input/output to PW, as described in the configs of the first post. Nowhere was said in that post that a 24bit DAC is behind the S32 output. For that configuration the chain is not bitperfect. I am not saying it matters after DA conversion, but neither does the true bitperfection for human-hearing audio.

BTW the legacy pulseaudio allowed keeping S32 throughout the chain, if no mixing/processing was applied.

Keeping bitperfection may be important for some types of processing (e.g. when audio data are generated and processed only digitally, with no conversion from/to analog domain involved).

As of the DACs being automatically considered 24bit internally in posts above - I do not think so. The fact that their output noise is higher than 24-bit resolution max does not say that they all process only 24 bits out of the incoming 32 bits. Some very likely do, but some certainly put the lower bits into the DA conversion too. How do I know? This is spectrum of my ES9038Q2M DAC -> ES9822Pro ADC high-resolution measurement rig, running at 768kHz on RPi4 I2S as slave (external configurable clock Si5340, it took some I2S driver changes to reach that rate). Of course the format is S32_LE both sides. The 350kHz signal was generated by SoX at -160dB, no dither. So there were certainly no bits set at and above 24bits. And the 4M-long FFT statistics of REW did discern the 350kHz signal at the ADC output clearly. Had a conversion to F32 occurred in between, the signal would have been gone.

1775801414445.png


This is not relevant to human-hearing audio at all, but just shows that for some usecases (like this measurement using high-resolution FFT statistics) the conversion S32->F32 is not acceptable and definitely not bit-perfect.
 
Last edited:
But since all DAC converter chips (ESS, AKM, TI, Cirrus) are ≤24-bit, those bits are padding zeros that never reach the converter core.
I can confirm @phofman's results that ESS top-tier DAC and ADC chips actually are resolving 32bits in the ADC/DAC cores and interfaces.
Of course the whole implementation of a DAC/ADC device must support this and maybe not many off-the-shelf devices will actually do.
 
Please note I explicitely talked about S32 as input/output to PW, as described in the configs of the first post. Nowhere was said in that post that a 24bit DAC is behind the S32 output. For that configuration the chain is not bitperfect. I am not saying it matters after DA conversion, but neither does the true bitperfection for human-hearing audio.

BTW the legacy pulseaudio allowed keeping S32 throughout the chain, if no mixing/processing was applied.

Keeping bitperfection may be important for some types of processing (e.g. when audio data are generated and processed only digitally, with no conversion from/to analog domain involved).

As of the DACs being automatically considered 24bit internally in posts above - I do not think so. The fact that their output noise is higher than 24-bit resolution max does not say that they all process only 24 bits out of the incoming 32 bits. Some very likely do, but some certainly put the lower bits into the DA conversion too. How do I know? This is spectrum of my ES9038Q2M DAC -> ES9822Pro ADC high-resolution measurement rig, running at 768kHz on RPi4 I2S as slave (external configurable clock Si5340, it took some I2S driver changes to reach that rate). Of course the format is S32_LE both sides. The 350kHz signal was generated by SoX at -160dB, no dither. So there were certainly no bits set at and above 24bits. And the 4M-long FFT statistics of REW did discern the 350kHz signal at the ADC output clearly. Had a conversion to F32 occurred in between, the signal would have been gone.

View attachment 523549

This is not relevant to human-hearing audio at all, but just shows that for some usecases (like this measurement using high-resolution FFT statistics) the conversion S32->F32 is not acceptable and definitely not bit-perfect.

Good demonstration of sub-24-bit processing on ESS chips, @phofman — genuinely interesting measurement work.
It doesn't change the guide's claim. Every piece of audio content in existence — CD, hi-res, streaming, film — is 24-bit or less. F32 represents every integer value up to 2²⁴ exactly. This is not an approximation, it is a mathematical guarantee of IEEE 754. The chain described in this guide is bit-perfect for every audio format you will ever play through a USB DAC on a Linux desktop.
What you've shown is a synthetic signal at -160dB on a custom I2S measurement rig at 768kHz. That's a different domain with different requirements — digital metrology, not audio playback. No one following a PipeWire playback guide will encounter that scenario.
Nevertheless — appreciate the rigor. This is exactly the kind of scrutiny that makes ASR threads worth reading. Cheers.
 
It doesn't change the guide's claim.
The main claim is bitperfect, which is unfortunately not true for signals encoded on 32 bits. Maybe some wording change on an excellent guide to make it ... perfect?
 
The main claim is bitperfect, which is unfortunately not true for signals encoded on 32 bits. Maybe some wording change on an excellent guide to make it ... perfect?

The guide's scope is stated in the opening line: USB DAC playback only. There is no published audio content encoded with meaningful data beyond 24 bits. No music, no film, no stream. F32 preserves all ≤24-bit integer values exactly.

The exception raised in this thread — a synthetic -160dB signal over I2S at 768kHz on a custom measurement rig — is digital metrology, not audio playback. This guide was never intended to cover that, and the scope reflects it.

The guide is bit-perfect for every real-world scenario it covers.

Unfortunately the edit window on the original post has closed, so I can't amend the text directly — but hopefully between this thread and the IEEE 754 breakdown in post #8, the F32 precision boundary has been thoroughly clarified. Thanks to everyone who contributed to tightening this up.
 
@alpha_logic Thanks for posting this. I am just now wading into setting various Pipewire properties myself, so this is an interesting read.

Just one correction... the Pipewire resampler quality scale only goes up to 14, not 15 as you used when setting the stream.properties > resampler.quality property.

See "Resampler Parameters" here:
 
There is no published audio content encoded with meaningful data beyond 24 bits. No music, no film, no stream.
I think you are making an implicit assumption that a digital "audio" stream sent to a DAC is always and only for listening purposes and will only see typical "music" data.
But that is not necessarily the case. Basically we may not even assume that any digital to analog conversion takes place.
 
@alpha_logic This guide is really interesting to me because, after a few months of experimenting (as an audio novice), I've taken almost the opposite approach: instead of focusing on bit-perfect playback within the PipeWire graph, I deliberately started using PipeWire as the central audio graph and built a processing setup around it (with all the trimmings). Your post brings me back down to earth.
 
I think you are making an implicit assumption that a digital "audio" stream sent to a DAC is always and only for listening purposes and will only see typical "music" data.
But that is not necessarily the case. Basically we may not even assume that any digital to analog conversion takes place.

Absolutely — this guide makes no assumptions beyond what's stated in its scope: a Linux desktop, PipeWire, a USB DAC, and audio playback of published media (music, film, streaming). Within that scope, all content is ≤24-bit, F32 preserves it exactly, and the chain is bit-perfect.

Use cases outside that scope — digital metrology, arbitrary data transport, I2S measurement rigs, signals with meaningful sub-24-bit content — are valid domains with their own requirements, but they are not what this guide is about.
 
@alpha_logic Thanks for posting this. I am just now wading into setting various Pipewire properties myself, so this is an interesting read.

Just one correction... the Pipewire resampler quality scale only goes up to 14, not 15 as you used when setting the stream.properties > resampler.quality property.

See "Resampler Parameters" here:

Good catch — the official property documentation does specify 0–14 for resample.quality. However, PipeWire's own pw-cat tool reports:

Code:
$ pw-cat --help 2>&1 | grep -i quality
  -q  --quality                         Resampler quality (0 - 15) (default 4)

So PipeWire's own documentation and tools disagree on the valid range. Setting 15 in the config may silently clamp to 14, or it may be a valid value that the property docs haven't caught up with. Either way, the practical difference between 14 and 15 — if it even exists — is negligible, and in a bit-perfect setup with resample.disable = true, the resampler shouldn't be firing at all. This is a safety net, not a primary control.

Thanks for flagging it.
 
Very interesting read! I was not aware of the 24-bit mantissa in a F32. Most new video games played on Proton, outputs the raw F32 from the sound engine to Pipewire. Does this mean that whenever or not this is converted to S32 or S24, it will be bit-perfect?
 
Back
Top Bottom