Skip to content

AudioStreamConverter: add a multiplier to convert_from - #1604

Draft
Gin890 wants to merge 6 commits into
mainfrom
audio-stream-converter-from-multiplier
Draft

Gin890 wants to merge 6 commits into
mainfrom
audio-stream-converter-from-multiplier

Conversation

@Gin890

@Gin890 Gin890 commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator

What

convert_to (float -> integer) already had a multiplier volume control; this adds the matching gain to convert_from (integer -> float) in every ISA — Default, x64 SSE41, AVX2, AVX512, ARM64 NEON — plus the dispatcher, the header, and the explicit instantiations:

template <typename ExternalType>
void convert_from(r_ptr<float> native, c_ptr<ExternalType> ext, size_t samples, float multiplier);

The gain scales the de-normalized sample: native = (ext - bias) * scale * multiplier. The SIMD from_u8 paths now scale by multiplier / 128 and subtract multiplier (the old constant -1 bias was only correct for a gain of 1).

Latent u8 convert_to bugs found while testing

Testing the multiplier exposed two bugs in the SIMD u8 convert_to paths (SSE41/AVX2/AVX512/NEON) that were masked because the tests only ever used a multiplier of 1.0:

  • Clamp/scale order: the gain was applied after the clamp, so a loud input with gain > 1 scaled past the uint8 range (values like 384 truncating modulo 256 when packed) instead of clipping to it. Default applies the gain first, then clamps to [-1, 127/128]. The SIMD paths now match.
  • Rounding offset: the offset added was the scaled SCALE (multiplier * 128) instead of the constant 128.5 that Default uses.

For in-range inputs at any multiplier the new SIMD convert_to_u8 is bit-exact with Default (verified natively across out-of-domain inputs and multipliers up to 20x).

Pipeline

RawAudio2WorkingAudio (the convert_from consumer) now takes the multiplier in its constructor and exposes set_multiplier(), mirroring WorkingAudio2RawAudio; the converter lambda passes the atomic value on every block. WorkingAudio2RawAudio already plumbed its multiplier into convert_to and is unchanged.

Tests

  • Kernel round-trip tests (TestFrom/TestTo per ISA) now run with reciprocal multiplier pairs (1.0, 1.0), (0.5, 2.0), (0.25, 4.0) — exact in binary and kept in the native range so the round trip stays lossless (the pairs also keep the uint8 to input below its clamp). Test names carry the pair, e.g. u8_from_ARM64_NEON(120) [from 0.5 / to 2].
  • The pipeline Raw -> Working -> Raw round-trip tests now run with 0.5 / 2.0 in addition to 1.0 / 1.0, exercising the new constructor parameter and the atomic load path end to end.
  • Native (aarch64, Default + NEON): 3073 tests, 0 failures, ASan clean.

Gin890 and others added 6 commits October 17, 2026 12:58
The float->integer convert_to already had a volume multiplier; add the
matching integer->float gain to convert_from in every ISA (Default,
SSE41, AVX2, AVX512, NEON), the dispatcher, and the explicit
instantiations.

This work also fixed two latent bugs in the SIMD u8 convert_to paths,
exposed by testing the multiplier: the gain was applied after the
clamp (so a loud signal with gain could scale past the uint8 range
instead of clipping to it, matching Default), and the rounding offset
was the scaled SCALE instead of the constant 128.5.

The u8 convert_from paths scale by (multiplier / 128) and subtract
the multiplier - the old constant -1 bias was only correct for a
gain of 1.

RawAudio2WorkingAudio now takes the multiplier in its constructor and
exposes set_multiplier(), mirroring WorkingAudio2RawAudio; it passes
the atomic value into convert_from on every block.

Tests: the kernel round-trip tests now run with reciprocal
multiplier pairs (1.0/1.0, 0.5/2.0, 0.25/4.0) - exact in binary and
in-range for the uint8 clamp - and the pipeline Raw->Working->Raw
round-trip tests run with 0.5/2.0.
Use tostr_default() instead of a local snprintf buffer, and move the
name construction out of a lambda in the member initializer into a
private static make_name() method on the test classes.
_xmm_cvtps_epi32 / _mm256_cvtps_epi32 / _mm512_cvtps_epi32 are the
rounding (CVTPS2DQ) conversions, not the truncating ones; the scalar
Default kernels use C casts (truncation). With the new
scale-then-clamp u8 path the final values land on k+0.5, where the
rounding conversion diverges from the scalar (round(95.5)=96). Use the
_cvttps_ (CVTTPS2DQ) truncating intrinsics in all nine x64 to-helpers.

Also:
- AVX512 u8 to: _mm512_cvtepi32_epi8 (VPMOVQB) saturates int32 lane
  values above 127, clamping the upper half of the uint8 range. Pack
  via a 128 bias into signed byte range, then add it back in byte lanes
  (mod 256).
- SSE41 u8/s16 from: the SIMD iterations read a full 128-bit line while
  only consuming 4 samples, reading past the end of short buffers.
  Restructure to full-width iterations (16 u8 / 8 s16 samples) with the
  scalar tail handling the rest.
- Remove the temporary DIAG_U8 test.
GCC declares the argument as char; 128 overflows (-Werror=overflow).
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.

2 participants