Skip to content

pcm_meter: give the s16 scope a level for DSD streams#517

Open
Gjuju wants to merge 2 commits into
alsa-project:masterfrom
Gjuju:scope-dsd-levels
Open

pcm_meter: give the s16 scope a level for DSD streams#517
Gjuju wants to merge 2 commits into
alsa-project:masterfrom
Gjuju:scope-dsd-levels

Conversation

@Gjuju

@Gjuju Gjuju commented Jul 21, 2026

Copy link
Copy Markdown

Depends on #516 (pcm_meter: let the s16 scope degrade instead of killing the caller) — both touch the same switch, so this is stacked on it to avoid a textual
conflict. Only the second commit belongs to this PR.

The problem

The s16 scope has no conversion for DSD, so s16_enable() rejects it and the s16 buffer
carries 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:

  • the smoothing — a bit count is noisy (32 bits give σ = 0.5/√32), so each channel
    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 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 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:

source min max mean
native DSD64 (DSD_U32_BE @ 88200) 2 21 7.1
FLAC 96/24 (S32_LE @ 96000) 2 22 7.6

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_BE bit-perfect
throughout; type meter is a passive tap and this patch only feeds the scope.

Gjuju and others added 2 commits July 22, 2026 01:11
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
Gjuju force-pushed the scope-dsd-levels branch from 0263a1e to cc6449b Compare July 21, 2026 23:14
@Gjuju
Gjuju marked this pull request as draft July 22, 2026 16:43
@Gjuju
Gjuju marked this pull request as ready for review July 22, 2026 22:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant