Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions packages/alsa-lib/alsa_lib_scope_dsd_levels.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
diff --git a/src/pcm/pcm_meter.c b/src/pcm/pcm_meter.c
index dd57f5ca..4cd1e60d 100644
--- a/src/pcm/pcm_meter.c
+++ b/src/pcm/pcm_meter.c
@@ -1004,8 +1004,100 @@ typedef struct _snd_pcm_scope_s16 {
int16_t *buf;
snd_pcm_channel_area_t *buf_areas;
int silent;
+ unsigned int dsd_bits; /* DSD bits per frame and channel */
+ unsigned int dsd_shift; /* smoothing time constant, in frames */
+ int32_t *dsd_states;
} snd_pcm_scope_s16_t;

+/* DSD carries no sample values, only a bit density: over a short window the
+ * proportion of ones IS the amplitude, one half being silence. So one S16 sample
+ * per frame can be recovered by counting the bits of that frame and smoothing the
+ * result, which is what the scopes need - 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. The count itself is noisy - 32 bits give a standard
+ * deviation of 0.5/sqrt(32) - hence the smoothing, whose time constant is taken
+ * from the rate so that it is a duration rather than a number of frames, and so
+ * behaves the same from DSD64 to DSD512. And the scale: 0 dBFS in DSD is a 50%
+ * modulation index, so a full-scale signal only swings the density between 25% and
+ * 75%; referring to the whole range would read 6 dB low.
+ */
+static unsigned int dsd_frame_bits(snd_pcm_format_t format)
+{
+ switch (format) {
+ case SND_PCM_FORMAT_DSD_U8:
+ return 8;
+ case SND_PCM_FORMAT_DSD_U16_LE:
+ case SND_PCM_FORMAT_DSD_U16_BE:
+ return 16;
+ case SND_PCM_FORMAT_DSD_U32_LE:
+ case SND_PCM_FORMAT_DSD_U32_BE:
+ return 32;
+ default:
+ return 0;
+ }
+}
+
+static void s16_dsd_decode(const snd_pcm_channel_area_t *dst_areas,
+ snd_pcm_uframes_t dst_offset,
+ const snd_pcm_channel_area_t *src_areas,
+ snd_pcm_uframes_t src_offset,
+ unsigned int channels, snd_pcm_uframes_t frames,
+ unsigned int bits, unsigned int shift,
+ int32_t *states)
+{
+ static const unsigned char popcount[256] = {
+#define P2(n) n, n + 1, n + 1, n + 2
+#define P4(n) P2(n), P2(n + 1), P2(n + 1), P2(n + 2)
+#define P6(n) P4(n), P4(n + 1), P4(n + 1), P4(n + 2)
+ P6(0), P6(1), P6(1), P6(2)
+#undef P6
+#undef P4
+#undef P2
+ };
+ unsigned int channel, bytes = bits / 8;
+
+ for (channel = 0; channel < channels; ++channel, ++states) {
+ const snd_pcm_channel_area_t *src_area = &src_areas[channel];
+ const snd_pcm_channel_area_t *dst_area = &dst_areas[channel];
+ const unsigned char *src;
+ char *dst;
+ int src_step, dst_step;
+ snd_pcm_uframes_t frames1 = frames;
+ int32_t acc = *states;
+
+ src = snd_pcm_channel_area_addr(src_area, src_offset);
+ src_step = snd_pcm_channel_area_step(src_area);
+ dst = snd_pcm_channel_area_addr(dst_area, dst_offset);
+ dst_step = snd_pcm_channel_area_step(dst_area);
+
+ while (frames1-- > 0) {
+ unsigned int i, ones = 0;
+ int sample;
+
+ for (i = 0; i < bytes; i++)
+ ones += popcount[src[i]];
+ /* density deviation, doubled because full scale is a
+ * 50% modulation index, in Q15 */
+ sample = ((int)(2 * ones) - (int)bits) * (2 * 32767) /
+ (int)bits;
+ /* exponential average: acc holds the sum, not the mean */
+ acc += sample - (acc >> shift);
+ sample = acc >> shift;
+ if (sample > 32767)
+ sample = 32767;
+ else if (sample < -32768)
+ sample = -32768;
+ *(int16_t *)dst = sample;
+ dst += dst_step;
+ src += src_step;
+ }
+ *states = acc;
+ }
+}
+
static int s16_enable(snd_pcm_scope_t *scope)
{
snd_pcm_scope_s16_t *s16 = scope->private_data;
@@ -1041,6 +1133,20 @@ static int s16_enable(snd_pcm_scope_t *scope)
case SND_PCM_FORMAT_U32_BE:
idx = snd_pcm_linear_convert_index(spcm->format, SND_PCM_FORMAT_S16);
break;
+ case SND_PCM_FORMAT_DSD_U8:
+ case SND_PCM_FORMAT_DSD_U16_LE:
+ case SND_PCM_FORMAT_DSD_U16_BE:
+ case SND_PCM_FORMAT_DSD_U32_LE:
+ case SND_PCM_FORMAT_DSD_U32_BE:
+ s16->dsd_bits = dsd_frame_bits(spcm->format);
+ /* Smooth over about a quarter of a millisecond, taken from the
+ * rate so the behaviour is the same at every DSD rate. */
+ s16->dsd_shift = 1;
+ while ((1U << (s16->dsd_shift + 1)) <= spcm->rate / 4000 &&
+ s16->dsd_shift < 8)
+ s16->dsd_shift++;
+ idx = 0;
+ break;
default:
/* No S16 conversion exists for this format - DSD, the 3-byte
* packed formats, float. Leaving the scope disabled is worse
@@ -1063,6 +1169,11 @@ static int s16_enable(snd_pcm_scope_t *scope)
if (!s16->adpcm_states)
return -ENOMEM;
}
+ if (s16->dsd_bits) {
+ s16->dsd_states = calloc(spcm->channels, sizeof(*s16->dsd_states));
+ if (!s16->dsd_states)
+ return -ENOMEM;
+ }
/* calloc, not malloc: when s16->silent is set nothing ever writes to
* this buffer and scopes must read silence, not uninitialised memory.
*/
@@ -1091,6 +1202,8 @@ static void s16_disable(snd_pcm_scope_t *scope)
snd_pcm_scope_s16_t *s16 = scope->private_data;
free(s16->adpcm_states);
s16->adpcm_states = NULL;
+ free(s16->dsd_states);
+ s16->dsd_states = NULL;
free(s16->buf);
s16->buf = NULL;
free(s16->buf_areas);
@@ -1154,6 +1267,17 @@ static void s16_update(snd_pcm_scope_t *scope)
s16->index,
s16->adpcm_states);
break;
+ case SND_PCM_FORMAT_DSD_U8:
+ case SND_PCM_FORMAT_DSD_U16_LE:
+ case SND_PCM_FORMAT_DSD_U16_BE:
+ case SND_PCM_FORMAT_DSD_U32_LE:
+ case SND_PCM_FORMAT_DSD_U32_BE:
+ s16_dsd_decode(s16->buf_areas, offset,
+ meter->buf_areas, offset,
+ spcm->channels, frames,
+ s16->dsd_bits, s16->dsd_shift,
+ s16->dsd_states);
+ break;
default:
snd_pcm_linear_convert(s16->buf_areas, offset,
meter->buf_areas, offset,
58 changes: 58 additions & 0 deletions packages/alsa-lib/alsa_lib_scope_no_abort.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
diff --git a/src/pcm/pcm_meter.c b/src/pcm/pcm_meter.c
index 68c369de..dd57f5ca 100644
--- a/src/pcm/pcm_meter.c
+++ b/src/pcm/pcm_meter.c
@@ -1003,6 +1003,7 @@ typedef struct _snd_pcm_scope_s16 {
snd_pcm_uframes_t old;
int16_t *buf;
snd_pcm_channel_area_t *buf_areas;
+ int silent;
} snd_pcm_scope_s16_t;

static int s16_enable(snd_pcm_scope_t *scope)
@@ -1041,7 +1042,20 @@ static int s16_enable(snd_pcm_scope_t *scope)
idx = snd_pcm_linear_convert_index(spcm->format, SND_PCM_FORMAT_S16);
break;
default:
- return -EINVAL;
+ /* No S16 conversion exists for this format - DSD, the 3-byte
+ * packed formats, float. Leaving the scope disabled is worse
+ * than useless: scopes that depend on it, including our own
+ * level scope, call snd_pcm_scope_s16_get_channel_buffer()
+ * from their update callback and that assert()s on the buffer
+ * this function never allocated, killing the application.
+ * Hand out a silent buffer instead, so a scope reads zero on a
+ * format it cannot see rather than aborting the process.
+ */
+ SNDERR("s16 scope: no S16 conversion for format %s, scopes will read silence",
+ snd_pcm_format_name(spcm->format));
+ s16->silent = 1;
+ idx = 0;
+ break;
}
s16->index = idx;
if (spcm->format == SND_PCM_FORMAT_IMA_ADPCM) {
@@ -1049,7 +1063,10 @@ static int s16_enable(snd_pcm_scope_t *scope)
if (!s16->adpcm_states)
return -ENOMEM;
}
- s16->buf = malloc(meter->buf_size * 2 * spcm->channels);
+ /* calloc, not malloc: when s16->silent is set nothing ever writes to
+ * this buffer and scopes must read silence, not uninitialised memory.
+ */
+ s16->buf = calloc(meter->buf_size * 2 * spcm->channels, 1);
if (!s16->buf) {
free(s16->adpcm_states);
return -ENOMEM;
@@ -1101,6 +1118,11 @@ static void s16_update(snd_pcm_scope_t *scope)
snd_pcm_t *spcm = meter->gen.slave;
snd_pcm_sframes_t size;
snd_pcm_uframes_t offset;
+ if (s16->silent) {
+ /* Nothing to convert; the buffer stays zero. */
+ s16->old = meter->now;
+ return;
+ }
size = meter->now - s16->old;
if (size < 0)
size += spcm->boundary;
106 changes: 106 additions & 0 deletions packages/alsa-lib/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/bin/bash
#########################################################################
#
# Build recipe for alsa-lib debian package with the PCM meter scope fixes
#
# The stock library is used as shipped by the distro. These two patches
# only touch the s16 scope of the `type meter` plugin, which is what
# libpeppyalsa taps for the VU meter:
#
# - scope_no_abort: the scope aborts the calling application on any format
# it cannot convert to S16 (DSD, the 3-byte packed formats, float). MPD
# dies mid-playback on a DSD track. It now reads silence instead.
# - scope_dsd_levels: recover a level from a DSD stream by bit density, so
# the needles work on native DSD instead of sitting still.
#
# Both are submitted upstream (alsa-project/alsa-lib #516 and #517), but merging
# there is only the first of four steps: alsa-project must cut a release, Debian
# package it, and the base OS pick it up. Trixie is stable and keeps alsa-lib
# 1.2.14 for its whole life, so the fix reaches users on the NEXT base OS - years,
# not months.
#
# The recipe therefore follows the base OS instead of pinning one version: it
# builds whatever alsa-lib the build host's distro offers, refuses versions it
# has not been validated against, and asks the source itself whether it is still
# needed, so nothing here has to track an upstream release or pull request number.
#
# (C) bitkeeper 2026 http://moodeaudio.org
# License: GPLv3
#
#########################################################################
. ../../scripts/rebuilder.lib.sh

# Version to rebuild. Defaults to the distro candidate, so a point release that
# drops the old .dsc from the pool does not break the build. Override both when
# the base OS carries its own alsa-lib revision.
ALSA_MIRROR=${ALSA_MIRROR:-http://deb.debian.org/debian}
ALSA_VERSION=${ALSA_VERSION:-`LC_ALL=C apt-cache policy libasound2t64 libasound2 2>/dev/null | awk '/Candidate:/ && $2 != "(none)" {print $2; exit}'`}

if [ -z "$ALSA_VERSION" ]
then
echo "${RED}Error: could not determine the alsa-lib version to build, set ALSA_VERSION${NORMAL}"
exit 1
fi

ALSA_UPSTREAM=${ALSA_VERSION%-*}

# Oldest upstream release the patches were validated against
PATCH_FLOOR="1.2.14"

if dpkg --compare-versions "$ALSA_UPSTREAM" lt "$PATCH_FLOOR"
then
echo "${RED}Error: alsa-lib $ALSA_UPSTREAM is older than $PATCH_FLOOR, the patches are untested there${NORMAL}"
exit 1
fi

# One patch set covers 1.2.14 up to 1.2.16.1: pcm_meter.c is unchanged from
# 1.2.15 onwards, and the 1.2.14 -> 1.2.15 churn (whitespace, SNDERR replaced by
# the new log macros) misses the patched hunks. Branch here if a later version
# moves them.
PATCHES="alsa_lib_scope_no_abort.patch alsa_lib_scope_dsd_levels.patch"

PKG_DSC_URL="$ALSA_MIRROR/pool/main/a/alsa-lib/alsa-lib_${ALSA_VERSION}.dsc"

echo "building alsa-lib $ALSA_VERSION from $PKG_DSC_URL"

rbl_prepare_from_dsc_url $PKG_DSC_URL

#------------------------------------------------------------
# Custom part of the packing

# This package only exists while the scope still bails out on a format it cannot
# convert. Ask the source, not a version number: any accepted form of the fix
# drops that bailout, whatever release or pull request it arrives by, and
# upstream numbers are not a reliable key - alsa-project reviews on alsa-devel
# and its maintainer commits most patches himself.
if ! awk '/^static int s16_enable/,/^}/' src/pcm/pcm_meter.c | grep -A2 "^ default:" | grep -q -- "return -EINVAL"
then
echo "alsa-lib $ALSA_UPSTREAM already handles unconvertible formats, this package is obsolete - nothing to build"
exit 0
fi

# patch and add patches to debian
for patch in $PATCHES
do
rbl_patch $BASE_DIR/$patch
EDITOR=/bin/true dpkg-source --commit . $patch
done

# patch(1) applies with offset and fuzz, so check the result instead of trusting
# the exit code alone
for marker in "s16->silent" "dsd_frame_bits"
do
if ! grep -q "$marker" src/pcm/pcm_meter.c
then
echo "${RED}Error: $marker missing after patching, the patches did not apply as expected${NORMAL}"
exit 1
fi
done

# set debian local suffix flag
DEBFULLNAME=$DEBFULLNAME DEBEMAIL=$DEBEMAIL dch --local $DEBSUFFIX "Added PCM meter scope patches: no abort on unconvertible formats, DSD levels"

#------------------------------------------------------------

rbl_build
echo "done"
Loading