apply_volume() corrupts 16- and 32-bit PCM on big-endian hosts, against its own little-endian contract

Open
#70 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
cpp

Research direction

Start in src/pcm_volume.cpp at apply_volume() and read the little-endian contract in src/pcm_volume.h, then run the standalone harness described against the 16- and 32-bit paths on a big-endian target. Done means S16_LE and S32_LE produce the same scaled little-endian samples as the reference while the existing 24-bit behavior remains correct.

Written by the indexing model from the issue text.

Description

src/pcm_volume.h states the contract in its own words:

Software volume for interleaved signed little-endian PCM, in Q32 fixed point.

and again on the function:

Scales len bytes of signed little-endian PCM in place by a Q32 gain of at most Q32_ONE.

Two of the three sample widths do not honour that on a big-endian host.

The code

src/pcm_volume.cpp, in apply_volume(). The 24-bit path reads and writes byte by byte, which is correct anywhere:

case 3: {
    int32_t sample = static_cast<int32_t>(p[0] | (p[1] << 8) | (p[2] << 16));
    ...
    p[0] = static_cast<uint8_t>(out & 0xFF);
    p[1] = static_cast<uint8_t>((out >> 8) & 0xFF);
    p[2] = static_cast<uint8_t>((out >> 16) & 0xFF);
}

The 16- and 32-bit paths reinterpret the buffer at native width instead:

case 2: {
    auto* samples = reinterpret_cast<int16_t*>(data);
    ...
}
case 4: {
    auto* samples = reinterpret_cast<int32_t*>(data);
    ...
}

On a little-endian host those agree. On a big-endian one they read each sample with its bytes reversed, scale the wrong number, and store it reversed again.

Reproducing it

pcm_volume.{h,cpp} are self-contained — <cmath>, <cstddef>, <cstdint> and nothing else — so they build standalone. Compiled unchanged from 0.3.0 for MIPS big-endian and run under qemu-mips-static, against a little-endian sine scaled by Q32_ONE / 2 (-6 dB) and read back as little-endian:

──────── x86_64 (little-endian) ────────
  16-bit (S16_LE)        ok (0/64 samples wrong)
  24-bit (S24_3LE)       ok (0/64 samples wrong)
  32-bit (S32_LE)        ok (0/64 samples wrong)
  => contract holds

──────── MIPS big-endian under QEMU ────────
  16-bit (S16_LE)        FAIL (57/64 samples wrong)
  24-bit (S24_3LE)       ok  (0/64 samples wrong)
  32-bit (S32_LE)        FAIL (60/64 samples wrong)
  => contract violated

The harness builds a sine in little-endian PCM, calls apply_volume(buf, len, bytes_per_sample, Q32_ONE/2), reads the result back as little-endian and compares against sample * gain >> 32. Glad to attach it, or to open a pull request with it as a test.

That the 24-bit path is right while the other two are not is why this reads as an oversight rather than a design choice.

Why it matters in practice

The ALSA sink opens SND_PCM_FORMAT_S16_LE / S24_3LE / S32_LE, so the bytes handed to the device are little-endian by construction and the 16- and 32-bit volume paths corrupt them on a big-endian build. Anything other than unity gain plays as noise.

I hit this packaging sendspin-cli for OpenWrt, where ath79 — MIPS 24Kc, big-endian, still on kernel 6.18 — is a common "old router with a USB port" host. My package carries @!BIG_ENDIAN for now so it is not offered there.

A related one, read-confirmed but not reproduced: opus_decode() in sendspin-cpp src/decoder.cpp fills its output through an (int16_t*) cast, and libopus writes native-endian opus_int16, so the Opus path looks like it has the same problem one layer down. The same goes for micro-flac's write_samples() fast paths. I am reporting those separately.

Environment

Source sendspin-cli 0.3.0, src/pcm_volume.{h,cpp} unchanged
Toolchain mips-linux-gnu-g++ (Debian), -O2 -static
Runner qemu-mips-static
Reference same harness on x86_64, which passes all three
Dominant language
C++
Stars
3
Forks
3
Avg merge
5h 26m
Merged PRs (30d)
35

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from Sendspin/sendspin-cpp-cli

All issues in Sendspin/sendspin-cpp-cli

Similar issues

More C++ issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.