apply_volume() corrupts 16- and 32-bit PCM on big-endian hosts, against its own little-endian contract
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
- Domain
- audio-video-rtc
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
lenbytes of signed little-endian PCM in place by a Q32 gain of at mostQ32_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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from Sendspin/sendspin-cpp-cli
-
Recovering from a device outage reports a sync error and plays silence for the swallowed lookahead Openenhancement
Difficulty 5/5 Over a week Newbie friendliness 25/100
Sendspin/sendspin-cpp-cli#63 · 1 comment ·
-
bug
Difficulty 4/5 3-5 days Newbie friendliness 45/100
Sendspin/sendspin-cpp-cli#32 · 1 comment ·
-
bug
Difficulty 4/5 3-5 days Newbie friendliness 38/100
Sendspin/sendspin-cpp-cli#30 · 3 comments ·
All issues in Sendspin/sendspin-cpp-cli
Similar issues
-
Difficulty 1/5 1-3 hours Newbie friendliness 92/100
autowarefoundation/autoware_universe#13413 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
automated-analysis bug memory-safety
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
-
Sensor initialization takes very long when `--initial-sim-time` is set to current UNIX timestamp Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
gazebosim/gz-sensors#662 · 1 comment ·