pcm_meter: give the s16 scope a level for DSD streams#517
Open
Gjuju wants to merge 2 commits into
Open
Conversation
s16_enable() returns -EINVAL for any format it cannot convert to S16 - DSD, the 3-byte packed formats, float. snd_pcm_scope_enable() records that as "not enabled" and carries on, so the buffer is never allocated. Any other scope stays enabled, though, and scopes reach the s16 buffer through snd_pcm_scope_s16_get_channel_buffer(), which asserts on exactly that never-allocated pointer. The application dies. Our own level scope does this, so alsa-lib kills its own caller; the same happens to libpeppyalsa, where a DSD track aborts the music player mid-playback. Allocate the buffer anyway and leave it zero, so a scope reads silence on a format the s16 conversion cannot see. Callers need no change and none can be made to abort. The condition is reported once through SNDERR rather than being swallowed. The neighbouring S16/MMAP_NONINTERLEAVED branch returns -EINVAL after assigning s16->buf and reaches the same dead end; it is left alone here because its intent is unclear. Signed-off-by: Julien Gainza <gainza.julien@gmail.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The s16 scope cannot convert DSD, so scopes see nothing on a DSD source - needles sit still on a format that is increasingly common on USB DACs. DSD carries no sample values, only a bit density: over a short window the proportion of ones is the amplitude, one half being silence. One S16 sample per frame can therefore be recovered by counting the bits of that frame and smoothing the result, which is what a scope needs - a level, not a reconstruction. Counting bits is invariant to their order, so the LE and BE variants share one code path and only the width of a frame matters. Two things set the quality, and both follow the format's own definition rather than a tuning choice. The count is noisy - 32 bits give a standard deviation of 0.5/sqrt(32) - hence a per-channel exponential average whose time constant comes from the rate, so it is a duration and behaves the same from DSD64 to DSD512. And the scale: 0 dBFS in DSD is a 50% modulation index, so a full-scale signal swings the density between 25% and 75% only; referring to the whole range would read 6 dB low. Measured on x86-64 with a DSD-capable USB DAC, playing the same master in both formats: native DSD64 reads min 2 / max 21 / mean 7.1 where the FLAC 96/24 of the same track reads min 2 / max 22 / mean 7.6. The DAC keeps receiving DSD_U32_BE bit-perfect; this only feeds the scope. Signed-off-by: Julien Gainza <gainza.julien@gmail.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gjuju
marked this pull request as draft
July 22, 2026 16:43
Gjuju
marked this pull request as ready for review
July 22, 2026 22:39
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
The s16 scope has no conversion for DSD, so
s16_enable()rejects it and the s16 buffercarries nothing for a format that is increasingly common on USB DACs. (Before the parent
PR the outcome was worse than an empty buffer — the application aborted.)
The approach
DSD carries no sample values, only a bit density: over a short window the proportion
of ones is the amplitude, one half being silence. One S16 sample per frame can be
recovered by counting the bits of that frame and smoothing the result — which is what a
scope needs, a level rather than a reconstruction. No filter design, no resampling, no
new dependency.
Counting bits is invariant to their order, so the LE and BE variants share one code path
and only the width of a frame matters.
Two parameters set the quality, and both follow the format's own definition rather than
being tuning choices:
keeps an exponential average whose time constant is derived from the rate. It is
therefore a duration, and behaves identically from DSD64 to DSD512;
the density between 25% and 75% only. Referring to the whole 0–100% range would read
6 dB low.
Testing
x86-64, Debian 13, DSD-capable USB DAC, playing the same master in both formats and
sampling the level exposed on the s16 buffer:
DSD_U32_BE@ 88200)S32_LE@ 96000)The same master reads the same level in either format — the point being that the scale is
right, not merely that the value moves. The DAC keeps receiving
DSD_U32_BEbit-perfectthroughout;
type meteris a passive tap and this patch only feeds the scope.