From 159f422c233e6d4f99d751305b621ff3134d4462 Mon Sep 17 00:00:00 2001 From: Sam Hocevar Date: Tue, 20 Feb 2024 22:22:58 +0100 Subject: [PATCH] audio: add saturated addition for samples --- include/lol/private/audio/stream.h | 81 +++++++++---- t/Makefile | 2 +- t/{audio.cpp => audio-convert.cpp} | 2 +- t/audio-sadd.cpp | 182 +++++++++++++++++++++++++++++ t/test.cpp | 1 + 5 files changed, 241 insertions(+), 27 deletions(-) rename t/{audio.cpp => audio-convert.cpp} (99%) create mode 100644 t/audio-sadd.cpp create mode 100644 t/test.cpp diff --git a/include/lol/private/audio/stream.h b/include/lol/private/audio/stream.h index fd6cc984..ff823f44 100644 --- a/include/lol/private/audio/stream.h +++ b/include/lol/private/audio/stream.h @@ -40,7 +40,7 @@ public: if constexpr (std::is_same_v || (from_fp && to_fp)) { - // If types are the same, or both floating-point, no conversion is needed + // If types are the same, or both floating point, no conversion is needed return TO(x); } else if constexpr (from_fp) @@ -68,8 +68,8 @@ public: // When converting between integer types, we first convert to an unsigned // type of same size as source (e.g. int16_t → uint16_t) to ensure that all // operations will happen modulo n (not guaranteed with signed types). - // The next step is to shift right (drop bits) or promote left (multiplying - // by a magic constant such as 0x1010101 or 0x10001). This happens using the + // The next step is to shift right (drop bits) or promote left (multiply by + // a magic constant such as 0x1010101 or 0x10001). This happens using the // UBIG type, which is an unsigned integer type at least as large as FROM // and TO. // Finally, we convert back to signed (e.g. uint16_t → int16_t) if necessary. @@ -84,6 +84,49 @@ public: return TO(tmp + UTO(std::numeric_limits::min())); } } + + // Saturated addition for samples + template + static inline T sadd(T x, T y) + { + if constexpr (std::is_floating_point_v) + { + // No saturation for floating point types + return x + y; + } + else if constexpr (sizeof(T) <= 4) + { + // For integer types up to 32-bit, do the computation with a larger type + using BIG = std::conditional_t>>; + BIG constexpr min = std::numeric_limits::min(); + BIG constexpr max = std::numeric_limits::max(); + BIG constexpr zero = (min + max + 1) >> 1; + + return T(std::max(min, std::min(max, BIG(BIG(x) + BIG(y) - zero)))); + } + else if constexpr (std::is_unsigned_v) + { + // Unsigned saturated add for 64-bit and larger: clamp according to overflow + T constexpr zero = T(1) << 8 * sizeof(T) - 1; + T constexpr minus_one = zero - T(1); + T ret = x + y; + return ret >= x ? std::max(zero, ret) - zero : std::min(minus_one, ret) + zero; + } + else + { + // Signed saturated add for 64-bit and larger: if signs differ, no overflow + // occurred, just return the sum of the arguments; otherwise, clamp according + // to the arguments sign. + using U = std::make_unsigned_t; + U constexpr umax = U(std::numeric_limits::max()); + U constexpr umin = U(std::numeric_limits::min()); + U ret = U(x) + U(y); + + return T(x ^ y) < 0 ? T(ret) : x >= 0 ? T(std::min(ret, umax)) : T(std::max(ret, umin)); + } + } }; template @@ -162,18 +205,12 @@ public: for (size_t n = 0; n < samples; ++n) { - T sample = T(0); + T x = T(0); + for (auto const &b : buffers) - sample += b[n]; - - if constexpr (std::is_same_v) - buf[n] = std::min(1.0f, std::max(-1.0f, sample)); - else if constexpr (std::is_same_v) - buf[n] = std::min(int16_t(32767), std::max(int16_t(-32768), sample)); - else if constexpr (std::is_same_v) - buf[n] = std::min(uint16_t(65535), std::max(uint16_t(0), sample)); - else - buf[n] = sample; + x = sample::sadd(x, b[n]); + + buf[n] = x; } return frames; @@ -236,18 +273,12 @@ public: { for (size_t out_ch = 0; out_ch < this->channels(); ++out_ch) { - T sample(0); + T x(0); + for (size_t in_ch = 0; in_ch < m_in->channels(); ++in_ch) - sample += tmp[f * m_in->channels() + in_ch] * matrix[out_ch * m_in->channels() + in_ch]; - - if constexpr (std::is_same_v) - buf[f * this->channels() + out_ch] = std::min(1.0f, std::max(-1.0f, sample)); - else if constexpr (std::is_same_v) - buf[f * this->channels() + out_ch] = std::min(int16_t(32767), std::max(int16_t(-32768), sample)); - else if constexpr (std::is_same_v) - buf[f * this->channels() + out_ch] = std::min(uint16_t(65535), std::max(uint16_t(0), sample)); - else - buf[f * this->channels() + out_ch] = sample; + x = sample::sadd(x, tmp[f * m_in->channels() + in_ch] * matrix[out_ch * m_in->channels() + in_ch]); + + buf[f * this->channels() + out_ch] = x; } } diff --git a/t/Makefile b/t/Makefile index 1a89510e..ced36fda 100644 --- a/t/Makefile +++ b/t/Makefile @@ -1,5 +1,5 @@ -SRC = audio.cpp +SRC = test.cpp audio-convert.cpp audio-sadd.cpp all: test diff --git a/t/audio.cpp b/t/audio-convert.cpp similarity index 99% rename from t/audio.cpp rename to t/audio-convert.cpp index 40b19db1..55d496b7 100644 --- a/t/audio.cpp +++ b/t/audio-convert.cpp @@ -1,4 +1,4 @@ -#include +#include #include TEST_CASE("sample conversion: float|double ←→ float|double") diff --git a/t/audio-sadd.cpp b/t/audio-sadd.cpp new file mode 100644 index 00000000..05dd30e9 --- /dev/null +++ b/t/audio-sadd.cpp @@ -0,0 +1,182 @@ +#include +#include + +TEST_CASE("sample saturated add: int8_t") +{ + // Underflow + CHECK(lol::audio::sample::sadd(-0x80, -0x80) == -0x80); + CHECK(lol::audio::sample::sadd(-0x41, -0x41) == -0x80); + CHECK(lol::audio::sample::sadd(-0x40, -0x41) == -0x80); + + // Standard operating mode + CHECK(lol::audio::sample::sadd(-0x40, -0x40) == -0x80); + CHECK(lol::audio::sample::sadd(-0x3f, -0x3f) == -0x7e); + CHECK(lol::audio::sample::sadd(-0x01, -0x01) == -0x02); + CHECK(lol::audio::sample::sadd(-0x01, 0x00) == -0x01); + CHECK(lol::audio::sample::sadd( 0x00, 0x00) == 0x00); + CHECK(lol::audio::sample::sadd( 0x00, 0x01) == 0x01); + CHECK(lol::audio::sample::sadd( 0x01, 0x01) == 0x02); + CHECK(lol::audio::sample::sadd( 0x3f, 0x3f) == 0x7e); + CHECK(lol::audio::sample::sadd( 0x3f, 0x40) == 0x7f); + + // Overflow + CHECK(lol::audio::sample::sadd( 0x40, 0x40) == 0x7f); + CHECK(lol::audio::sample::sadd( 0x7f, 0x7f) == 0x7f); +} + +TEST_CASE("sample saturated add: uint8_t") +{ + // Underflow + CHECK(lol::audio::sample::sadd(0x00, 0x00) == 0x00); + CHECK(lol::audio::sample::sadd(0x3f, 0x3f) == 0x00); + + // Standard operating mode + CHECK(lol::audio::sample::sadd(0x40, 0x40) == 0x00); + CHECK(lol::audio::sample::sadd(0x41, 0x41) == 0x02); + CHECK(lol::audio::sample::sadd(0x7f, 0x7f) == 0x7e); + CHECK(lol::audio::sample::sadd(0x7f, 0x80) == 0x7f); + CHECK(lol::audio::sample::sadd(0x80, 0x80) == 0x80); + CHECK(lol::audio::sample::sadd(0x80, 0x81) == 0x81); + CHECK(lol::audio::sample::sadd(0x81, 0x81) == 0x82); + CHECK(lol::audio::sample::sadd(0xbf, 0xbf) == 0xfe); + CHECK(lol::audio::sample::sadd(0xbf, 0xc0) == 0xff); + + // Overflow + CHECK(lol::audio::sample::sadd(0xc0, 0xc0) == 0xff); + CHECK(lol::audio::sample::sadd(0xff, 0xff) == 0xff); +} + +TEST_CASE("sample saturated add: int16_t") +{ + // Underflow + CHECK(lol::audio::sample::sadd(-0x8000, -0x8000) == -0x8000); + CHECK(lol::audio::sample::sadd(-0x4001, -0x4001) == -0x8000); + CHECK(lol::audio::sample::sadd(-0x4000, -0x4001) == -0x8000); + + // Standard operating mode + CHECK(lol::audio::sample::sadd(-0x4000, -0x4000) == -0x8000); + CHECK(lol::audio::sample::sadd(-0x3fff, -0x3fff) == -0x7ffe); + CHECK(lol::audio::sample::sadd(-0x0001, -0x0001) == -0x0002); + CHECK(lol::audio::sample::sadd(-0x0001, 0x0000) == -0x0001); + CHECK(lol::audio::sample::sadd( 0x0000, 0x0000) == 0x0000); + CHECK(lol::audio::sample::sadd( 0x0000, 0x0001) == 0x0001); + CHECK(lol::audio::sample::sadd( 0x0001, 0x0001) == 0x0002); + CHECK(lol::audio::sample::sadd( 0x3fff, 0x3fff) == 0x7ffe); + CHECK(lol::audio::sample::sadd( 0x3fff, 0x4000) == 0x7fff); + + // Overflow + CHECK(lol::audio::sample::sadd( 0x4000, 0x4000) == 0x7fff); + CHECK(lol::audio::sample::sadd( 0x7fff, 0x7fff) == 0x7fff); +} + +TEST_CASE("sample saturated add: uint16_t") +{ + // Underflow + CHECK(lol::audio::sample::sadd(0x0000, 0x0000) == 0x0000); + CHECK(lol::audio::sample::sadd(0x3fff, 0x3fff) == 0x0000); + + // Standard operating mode + CHECK(lol::audio::sample::sadd(0x4000, 0x4000) == 0x0000); + CHECK(lol::audio::sample::sadd(0x4001, 0x4001) == 0x0002); + CHECK(lol::audio::sample::sadd(0x7fff, 0x7fff) == 0x7ffe); + CHECK(lol::audio::sample::sadd(0x7fff, 0x8000) == 0x7fff); + CHECK(lol::audio::sample::sadd(0x8000, 0x8000) == 0x8000); + CHECK(lol::audio::sample::sadd(0x8000, 0x8001) == 0x8001); + CHECK(lol::audio::sample::sadd(0x8001, 0x8001) == 0x8002); + CHECK(lol::audio::sample::sadd(0xbfff, 0xbfff) == 0xfffe); + CHECK(lol::audio::sample::sadd(0xbfff, 0xc000) == 0xffff); + + // Overflow + CHECK(lol::audio::sample::sadd(0xc000, 0xc000) == 0xffff); + CHECK(lol::audio::sample::sadd(0xffff, 0xffff) == 0xffff); +} + +TEST_CASE("sample saturated add: uint32_t") +{ + // Underflow + CHECK(lol::audio::sample::sadd(0x00000000, 0x00000000) == 0x00000000); + CHECK(lol::audio::sample::sadd(0x3fffffff, 0x3fffffff) == 0x00000000); + + // Standard operating mode + CHECK(lol::audio::sample::sadd(0x40000000, 0x40000000) == 0x00000000); + CHECK(lol::audio::sample::sadd(0x40000001, 0x40000001) == 0x00000002); + CHECK(lol::audio::sample::sadd(0x7fffffff, 0x7fffffff) == 0x7ffffffe); + CHECK(lol::audio::sample::sadd(0x7fffffff, 0x80000000) == 0x7fffffff); + CHECK(lol::audio::sample::sadd(0x80000000, 0x80000000) == 0x80000000); + CHECK(lol::audio::sample::sadd(0x80000000, 0x80000001) == 0x80000001); + CHECK(lol::audio::sample::sadd(0x80000001, 0x80000001) == 0x80000002); + CHECK(lol::audio::sample::sadd(0xbfffffff, 0xbfffffff) == 0xfffffffe); + CHECK(lol::audio::sample::sadd(0xbfffffff, 0xc0000000) == 0xffffffff); + + // Overflow + CHECK(lol::audio::sample::sadd(0xc0000000, 0xc0000000) == 0xffffffff); + CHECK(lol::audio::sample::sadd(0xffffffff, 0xffffffff) == 0xffffffff); +} + +TEST_CASE("sample saturated add: int32_t") +{ + // Underflow + CHECK(lol::audio::sample::sadd(-0x80000000, -0x80000000) == -0x80000000); + CHECK(lol::audio::sample::sadd(-0x40000001, -0x40000001) == -0x80000000); + CHECK(lol::audio::sample::sadd(-0x40000000, -0x40000001) == -0x80000000); + + // Standard operating mode + CHECK(lol::audio::sample::sadd(-0x40000000, -0x40000000) == -0x80000000); + CHECK(lol::audio::sample::sadd(-0x3fffffff, -0x3fffffff) == -0x7ffffffe); + CHECK(lol::audio::sample::sadd(-0x00000001, -0x00000001) == -0x00000002); + CHECK(lol::audio::sample::sadd(-0x00000001, 0x00000000) == -0x00000001); + CHECK(lol::audio::sample::sadd( 0x00000000, 0x00000000) == 0x00000000); + CHECK(lol::audio::sample::sadd( 0x00000000, 0x00000001) == 0x00000001); + CHECK(lol::audio::sample::sadd( 0x00000001, 0x00000001) == 0x00000002); + CHECK(lol::audio::sample::sadd( 0x3fffffff, 0x3fffffff) == 0x7ffffffe); + CHECK(lol::audio::sample::sadd( 0x3fffffff, 0x40000000) == 0x7fffffff); + + // Overflow + CHECK(lol::audio::sample::sadd( 0x40000000, 0x40000000) == 0x7fffffff); + CHECK(lol::audio::sample::sadd( 0x7fffffff, 0x7fffffff) == 0x7fffffff); +} + +TEST_CASE("sample saturated add: uint64_t") +{ + // Underflow + CHECK(lol::audio::sample::sadd(0x0000000000000000, 0x0000000000000000) == 0x0000000000000000); + CHECK(lol::audio::sample::sadd(0x3fffffffffffffff, 0x3fffffffffffffff) == 0x0000000000000000); + + // Standard operating mode + CHECK(lol::audio::sample::sadd(0x4000000000000000, 0x4000000000000000) == 0x0000000000000000); + CHECK(lol::audio::sample::sadd(0x4000000000000001, 0x4000000000000001) == 0x0000000000000002); + CHECK(lol::audio::sample::sadd(0x7fffffffffffffff, 0x7fffffffffffffff) == 0x7ffffffffffffffe); + CHECK(lol::audio::sample::sadd(0x7fffffffffffffff, 0x8000000000000000) == 0x7fffffffffffffff); + CHECK(lol::audio::sample::sadd(0x8000000000000000, 0x8000000000000000) == 0x8000000000000000); + CHECK(lol::audio::sample::sadd(0x8000000000000000, 0x8000000000000001) == 0x8000000000000001); + CHECK(lol::audio::sample::sadd(0x8000000000000001, 0x8000000000000001) == 0x8000000000000002); + CHECK(lol::audio::sample::sadd(0xbfffffffffffffff, 0xbfffffffffffffff) == 0xfffffffffffffffe); + CHECK(lol::audio::sample::sadd(0xbfffffffffffffff, 0xc000000000000000) == 0xffffffffffffffff); + + // Overflow + CHECK(lol::audio::sample::sadd(0xc000000000000000, 0xc000000000000000) == 0xffffffffffffffff); + CHECK(lol::audio::sample::sadd(0xffffffffffffffff, 0xffffffffffffffff) == 0xffffffffffffffff); +} + +TEST_CASE("sample saturated add: int64_t") +{ + // Underflow + CHECK(lol::audio::sample::sadd(-0x8000000000000000, -0x8000000000000000) == -0x8000000000000000); + CHECK(lol::audio::sample::sadd(-0x4000000000000001, -0x4000000000000001) == -0x8000000000000000); + CHECK(lol::audio::sample::sadd(-0x4000000000000000, -0x4000000000000001) == -0x8000000000000000); + + // Standard operating mode + CHECK(lol::audio::sample::sadd(-0x4000000000000000, -0x4000000000000000) == -0x8000000000000000); + CHECK(lol::audio::sample::sadd(-0x3fffffffffffffff, -0x3fffffffffffffff) == -0x7ffffffffffffffe); + CHECK(lol::audio::sample::sadd(-0x0000000000000001, -0x0000000000000001) == -0x0000000000000002); + CHECK(lol::audio::sample::sadd(-0x0000000000000001, 0x0000000000000000) == -0x0000000000000001); + CHECK(lol::audio::sample::sadd( 0x0000000000000000, 0x0000000000000000) == 0x0000000000000000); + CHECK(lol::audio::sample::sadd( 0x0000000000000000, 0x0000000000000001) == 0x0000000000000001); + CHECK(lol::audio::sample::sadd( 0x0000000000000001, 0x0000000000000001) == 0x0000000000000002); + CHECK(lol::audio::sample::sadd( 0x3fffffffffffffff, 0x3fffffffffffffff) == 0x7ffffffffffffffe); + CHECK(lol::audio::sample::sadd( 0x3fffffffffffffff, 0x4000000000000000) == 0x7fffffffffffffff); + + // Overflow + CHECK(lol::audio::sample::sadd( 0x4000000000000000, 0x4000000000000000) == 0x7fffffffffffffff); + CHECK(lol::audio::sample::sadd( 0x7fffffffffffffff, 0x7fffffffffffffff) == 0x7fffffffffffffff); +} diff --git a/t/test.cpp b/t/test.cpp new file mode 100644 index 00000000..0df28674 --- /dev/null +++ b/t/test.cpp @@ -0,0 +1 @@ +#include