• 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

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?

Good question. "Bit-perfect" strictly means the bits are unchanged from source to DAC. Since F32 and S24/S32 are different encodings, the term doesn't directly apply to a format conversion — the bits will be different by definition.

What you can say is that the conversion is precision-preserving — no information is lost:

  • F32 → S24: F32 has a 24-bit mantissa. S24 has 24 bits of integer range. The conversion captures all the precision F32 carries. Nothing is lost.
  • F32 → S32: Same precision as above. F32 still only carries 24 bits of meaningful precision — the extra 8 bits in the S32 container add no information from the F32 source.

So for game audio originating in F32: the format conversion to your DAC's native integer format is lossless in terms of precision. The more important factors for audio quality are whether PipeWire is resampling the stream (e.g., 48kHz game output to a different system rate) and whether software volume is being applied — those are where actual signal modification occurs.
 
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.
Wow, that is a bit sloppy there, from Pipewire. Is the limit 14 or 15? And what would resampler.quality=0 mean, exactly. They are still experiencing some growing pains, I guess...

@alpha_logic I would am concerned that providing what could possibly be an out-of-range value (e.g. 15) would result in the default value of '4' being used. Better to be safe than sorry?
 
Wow, that is a bit sloppy there, from Pipewire. Is the limit 14 or 15? And what would resampler.quality=0 mean, exactly. They are still experiencing some growing pains, I guess...

@alpha_logic I would am concerned that providing what could possibly be an out-of-range value (e.g. 15) would result in the default value of '4' being used. Better to be safe than sorry?

Went to the source. Literally — cloned the PipeWire repo and read the resampler code.

The valid range is 0–14. Quality 15 is silently clamped to 14.

The resampler quality maps to filter parameter arrays with 15 entries (indices 0–14). The clamp logic is explicit:

Code:
r->quality = SPA_CLAMP(r->quality, 0, (int)(window_info[c->window].n_qualities - 1));

n_qualities = 15, so max index = 14. Setting 15 gives you 14 — silently, no error, no fallback to default. It does NOT reset to 4.

pw-cat --help saying "0 - 15" is a bug in the help text. The property documentation saying 0–14 is correct.

For reference, the quality levels map to filter tap counts:

QualityTapsCutoff
080.620
4 (default)480.895
14 (max)10240.993

So to answer both concerns: setting 15 does not fall back to the default of 4 — it clamps to 14, which is the maximum. The guide's config is functionally correct (you get max quality), but the value should be 14, not 15. Noted for future reference.

Source: spa/plugins/audioconvert/resample-native.c, PipeWire HEAD (5075f27).
 
Question; Apart from the need to share an audio device, why not use the ALSA engine directly?
 
Question; Apart from the need to share an audio device, why not use the ALSA engine directly?

You can, and some people do. If you run a dedicated music player (like mpd) with exclusive ALSA output and nothing else needs the sound device, you bypass PipeWire entirely and get the shortest possible path to the DAC.

The reasons most people don't:

  • Exclusive access: ALSA hw: is one client at a time. No system sounds, no notifications, no second application. Alt-tab to a YouTube video? Silence, or it blocks.
  • No session management: No automatic routing, no default device handling, no hotplug. Unplug your DAC and replug — you're restarting the player.
  • No rate switching negotiation: PipeWire with resample.disable = true and allowed-rates automatically renegotiates the DAC's sample rate to match the source. With raw ALSA, the application has to handle this itself — and most don't.
  • Desktop integration: Volume keys, Bluetooth fallback, per-app routing, screen sharing audio — all gone.

PipeWire with the config in this guide gives you the same signal path — hw: device, no resampling, no volume processing — while keeping the rest of your desktop functional. The tradeoff is one extra layer in the stack, but with resample.disable = true, that layer is doing routing only, not processing.
 
Wow - amazing post.

Though I'm just gonna refer everyone to my sig - life is too short : :p
 
Went to the source. Literally — cloned the PipeWire repo and read the resampler code.

The valid range is 0–14. Quality 15 is silently clamped to 14.

The resampler quality maps to filter parameter arrays with 15 entries (indices 0–14). The clamp logic is explicit:

Code:
r->quality = SPA_CLAMP(r->quality, 0, (int)(window_info[c->window].n_qualities - 1));

n_qualities = 15, so max index = 14. Setting 15 gives you 14 — silently, no error, no fallback to default. It does NOT reset to 4.

pw-cat --help saying "0 - 15" is a bug in the help text. The property documentation saying 0–14 is correct.

For reference, the quality levels map to filter tap counts:

QualityTapsCutoff
080.620
4 (default)480.895
14 (max)10240.993

So to answer both concerns: setting 15 does not fall back to the default of 4 — it clamps to 14, which is the maximum. The guide's config is functionally correct (you get max quality), but the value should be 14, not 15. Noted for future reference.

Source: spa/plugins/audioconvert/resample-native.c, PipeWire HEAD (5075f27).
upstream code here: https://gitlab.freedesktop.org/pipe...er/spa/plugins/audioconvert/resample-native.c
 
Question; Apart from the need to share an audio device, why not use the ALSA engine directly?

PipeWire as a multimedia server is now standard on typical Linux desktop systems. Therefore, it's usually more practical to configure it on those systems than to avoid it altogether. I interpret the OP the same way: it's about achieving bit-perfect playback in such a system, not about whether to bypass PipeWire.
 
F32 → S24: F32 has a 24-bit mantissa. S24 has 24 bits of integer range. The conversion captures all the precision F32 carries. Nothing is lost.
F32 has a separate bit for sign, so SNR of full scale signal in F32 is 1 bit more than in S24.
 
I did some research on this as I found this subject very cool.

So pipewire will convert all input to F32 no matter what.

This means that all type of audio samples (16-bit, 24-bit, 32-bit) will be converted to a value between -1.0 and 1.0. If we send in a 24 bit audio stream, it will be converted, but since we do nothing with the audio it will map out perfectly back to a 24-bit stream. Super cool.

But this means that we can probably get the same effect if we send in a 24-bit audio stream and map it out to a 32 bit stream. With the right volume attenuation we could get the same 24-bits in a 32 bit container (the very same 24 bits with 8 bits of zero).

*edit*
Thinking further: isn't it strange that the dacs don't accept the F32 format? Then it could map it out to the lowest to the highest voltage it can produce over the noise...
 
Last edited:
isn't it strange that the dacs don't accept the F32 format?

Not really - when you consider that 24 bits is enough to deliver audio with quantisation noise at least 34dB lower than even the most sensitive human ears can detect in a soundproofed room. With the 25 bits in F32 you can increase that to 40dB.

And add to that the fact that no real world home audio DAC can do better than 22bits SNR no matter how many bits they are fed.
 
My thinking is along the lines of the F32 format being the perfect intermediate format. Most modern audio software uses a F32 to do mixing and audio control and DAC have started coming with DSP, which will use also use F32. Why not just supply the DAC with F32 we already have, so that there will be less chance of conversion/rounding error?
 
There is no conversion, if there were any possibility of rounding error the system is so broken it would be obvious not subtle
 
My thinking is along the lines of the F32 format being the perfect intermediate format. Most modern audio software uses a F32 to do mixing and audio control and DAC have started coming with DSP, which will use also use F32. Why not just supply the DAC with F32 we already have, so that there will be less chance of conversion/rounding error?
Why dont you first try to reply to my question?
 
Thinking further: isn't it strange that the dacs don't accept the F32 format?
It's an option in the UAC2 standard - see section "2.3.1.7.3 IEEE_FLOAT Format" but it probably only makes sense for DACs that will be using it for internal DSP, and those are few and far between. I've no idea what OS support for it is like.
 
It's an option in the UAC2 standard - see section "2.3.1.7.3 IEEE_FLOAT Format" but it probably only makes sense for DACs that will be using it for internal DSP, and those are few and far between. I've no idea what OS support for it is like.
Supercool! I will look into it. Maybe the XMOS chips also support it.
 
Last edited:
but some certainly put the lower bits into the DA conversion too.
That prompts the question how useful they are. I generated a stepped level sweep signal from -136 to -146 dBFS in 32 bits/44.1k:

Input signal

The hex values for peaks are:
Code:
-136 dBFS, peak ±341: 0x…0155 0x…feab
-137 dBFS, peak ±304: 0x…0130 0x…fed0
-138 dBFS, peak ±271: 0x…010f 0x…fef1
-139 dBFS, peak ±242: 0x…00f2 0x…ff0e
-140 dBFS, peak ±216: 0x…00d8 0x…ff28
-141 dBFS, peak ±192: 0x…00c0 0x…ff40
-142 dBFS, peak ±172: 0x…00ac 0x…ff54
-143 dBFS, peak ±153: 0x…0099 0x…ff67
-144 dBFS, peak ±136: 0x…0088 0x…ff78
-145 dBFS, peak ±122: 0x…007a 0x…ff86
-146 dBFS, peak ±109: 0x…006d 0x…ff93

I played it on RME Adi-2 Pro, Fiio K3 (ESS) (it also has ES9038Q2M chip) and Tanchjim Space, through E1DA Scaler at max amplification (+26.7 dB) and recorded on E1DA ADCiso +13 dBu at 96 kHz sampling rate:

RME Adi-2 Pro
FiiO K3 (ESS)
Tanchjim Space

So the lowest bits still generate some signal but not much linearity is left.

JCally JM20 seems to actually truncate to 24 bits because starting from -138.46 dBFS there is no more output. JCally JM6 also does some truncation although there the output is gone starting from -124.01 dBFS.

EDIT
See update:
 
Last edited:
So the lowest bits still generate some signal but not much linearity is left.

And none of it audible unless you want to kill your speakers and shred your eardrums when the real music starts playing. :p
 
Back
Top Bottom