diff --git a/packages/alsa-lib/alsa_lib_scope_dsd_levels.patch b/packages/alsa-lib/alsa_lib_scope_dsd_levels.patch new file mode 100644 index 0000000..845efd6 --- /dev/null +++ b/packages/alsa-lib/alsa_lib_scope_dsd_levels.patch @@ -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, diff --git a/packages/alsa-lib/alsa_lib_scope_no_abort.patch b/packages/alsa-lib/alsa_lib_scope_no_abort.patch new file mode 100644 index 0000000..508545b --- /dev/null +++ b/packages/alsa-lib/alsa_lib_scope_no_abort.patch @@ -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; diff --git a/packages/alsa-lib/build.sh b/packages/alsa-lib/build.sh new file mode 100755 index 0000000..2daa5db --- /dev/null +++ b/packages/alsa-lib/build.sh @@ -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" diff --git a/packages/moode-player/moode-apt-mark b/packages/moode-player/moode-apt-mark index 39f27fe..6d8e279 100755 --- a/packages/moode-player/moode-apt-mark +++ b/packages/moode-player/moode-apt-mark @@ -19,16 +19,48 @@ fi cmd=$1 pkgstr="" +releasestr="" package_list="/etc/moode-apt-mark.conf" +# A package entry can carry an optional condition: " ". +# The hold then lasts only while the candidate version still satisfies it, so a +# package frozen while waiting for a fix to reach the distro releases itself once +# the distro ships that fix. Entries without a condition are held as before. +function hold_still_needed { + local package=$1 + local relation=$2 + local version=$3 + local candidate + + case "$relation" in + "<<"|"<="|"="|">="|">>") ;; + *) + echo "WARNING: $package has an unknown condition '$relation', keeping the hold" + return 0 + ;; + esac + + candidate=$(LC_ALL=C apt-cache policy "$package" | awk '/Candidate:/ {print $2; exit}') + if [[ -z "$candidate" ]] || [[ "$candidate" == "(none)" ]]; then + echo "WARNING: no candidate version for $package, keeping the hold" + return 0 + fi + + dpkg --compare-versions "$candidate" "$relation" "$version" +} + echo "Generating package list" while IFS= read -r line do - package=$(echo "$line" | sed -e 's/[[:space:]]*#.*// ; /^[[:space:]]*$/d') - if [[ -n "$package" ]]; then + entry=$(echo "$line" | sed -e 's/[[:space:]]*#.*// ; /^[[:space:]]*$/d') + if [[ -n "$entry" ]]; then + read -r package relation version <<< "$entry" result=$(dpkg-query --showformat='${Version}\n' --show $package) if [[ -z $result ]]; then echo "INFO: $package skipped, not installed" + elif [[ "$cmd" == "hold" ]] && [[ -n "$relation" ]] && ! hold_still_needed "$package" "$relation" "$version"; then + echo "INFO: $package no longer needs to be frozen, releasing it" + releasestr+="$package " else pkgstr+="$package " if [ $? -gt 1 ]; then @@ -40,8 +72,17 @@ do done < "$package_list" echo "Marking packages" -apt-mark "$cmd" $pkgstr -if [ $? -gt 1 ]; then - echo "ERROR: apt-mark $cmd failed, exiting script" - exit 1 +if [[ -n "$pkgstr" ]]; then + apt-mark "$cmd" $pkgstr + if [ $? -gt 1 ]; then + echo "ERROR: apt-mark $cmd failed, exiting script" + exit 1 + fi +fi +if [[ -n "$releasestr" ]]; then + apt-mark unhold $releasestr + if [ $? -gt 1 ]; then + echo "ERROR: apt-mark unhold failed, exiting script" + exit 1 + fi fi diff --git a/packages/moode-player/moode-apt-mark.conf b/packages/moode-player/moode-apt-mark.conf index 3589850..beaadff 100644 --- a/packages/moode-player/moode-apt-mark.conf +++ b/packages/moode-player/moode-apt-mark.conf @@ -19,6 +19,14 @@ chromium-codecs-ffmpeg-extra #chromium-sandbox #rpi-chromium-mods # +# Rebuilt with the PCM meter scope fixes (alsa-project/alsa-lib #516 and #517), +# so the distro's own package must not replace it - a trixie point update to +# 1.2.14-1+deb13u1 already outranks 1.2.14-1moode1. On a base OS whose alsa-lib +# carries the fixes the recipe builds nothing, the package is not installed and +# these two lines are skipped; on one that does not, append the release that +# fixes it as a condition ("libasound2t64 << 1.2.17") to lift the freeze. +libasound2-data +libasound2t64 libasound2-plugin-bluez librespot mediainfo