diff --git a/include/lol/private/audio/stream.h b/include/lol/private/audio/stream.h index 2b754944..581f12e8 100644 --- a/include/lol/private/audio/stream.h +++ b/include/lol/private/audio/stream.h @@ -64,21 +64,23 @@ public: } else { - // Conversion between integer types: - // - convert to unsigned - // - shift right or multiply by a magic constant such as 0x0101 to propagate bytes - // - convert back to signed if necessary - // Most operations are done using the UBIG type, which is an unsigned integer type - // at least as large as FROM and TO. + // 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 + // 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. using UFROM = std::make_unsigned_t; using UTO = std::make_unsigned_t; using UBIG = std::conditional_t<(sizeof(FROM) > sizeof(TO)), UFROM, UTO>; - UBIG constexpr div = UBIG(1) << 8 * (sizeof(UBIG) - sizeof(UTO)); UBIG constexpr mul = std::numeric_limits::max() / std::numeric_limits::max(); + UBIG constexpr div = UBIG(1) << 8 * (sizeof(UBIG) - sizeof(UTO)); auto tmp = UFROM(UFROM(x) - UFROM(std::numeric_limits::min())) * mul / div; - return TO(UTO(tmp) + UTO(std::numeric_limits::min())); + return TO(tmp + UTO(std::numeric_limits::min())); } } }; diff --git a/t/audio.cpp b/t/audio.cpp index 376aa517..40b19db1 100644 --- a/t/audio.cpp +++ b/t/audio.cpp @@ -5,22 +5,30 @@ TEST_CASE("sample conversion: float|double ←→ float|double") { auto cv1 = lol::audio::sample::convert; CHECK(cv1(-1.0f) == -1.0f); + CHECK(cv1(-0.5f) == -0.5f); CHECK(cv1( 0.0f) == 0.0f); + CHECK(cv1( 0.5f) == 0.5f); CHECK(cv1( 1.0f) == 1.0f); auto cv2 = lol::audio::sample::convert; CHECK(cv2(-1.0f) == -1.0); + CHECK(cv2(-0.5f) == -0.5); CHECK(cv2( 0.0f) == 0.0); + CHECK(cv2( 0.5f) == 0.5); CHECK(cv2( 1.0f) == 1.0); auto cv3 = lol::audio::sample::convert; CHECK(cv3(-1.0) == -1.0f); + CHECK(cv3(-0.5) == -0.5f); CHECK(cv3( 0.0) == 0.0f); + CHECK(cv3( 0.5) == 0.5f); CHECK(cv3( 1.0) == 1.0f); auto cv4 = lol::audio::sample::convert; CHECK(cv4(-1.0) == -1.0); + CHECK(cv4(-0.5) == -0.5); CHECK(cv4( 0.0) == 0.0); + CHECK(cv4( 0.5) == 0.5); CHECK(cv4( 1.0) == 1.0); } @@ -94,7 +102,6 @@ TEST_CASE("sample conversion: int16|uint16 → float") auto cv1 = lol::audio::sample::convert; CHECK(cv1(-0x8000) == -1.0f); CHECK(cv1( 0x7fff) == 1.0f); - for (int n = -0x8000; n < 0x7fff; ++n) { CAPTURE(n); @@ -104,7 +111,6 @@ TEST_CASE("sample conversion: int16|uint16 → float") auto cv2 = lol::audio::sample::convert; CHECK(cv2(0x0000) == -1.0f); CHECK(cv2(0xffff) == 1.0f); - for (int n = 0x0000; n < 0xffff; ++n) { CAPTURE(n); @@ -115,23 +121,38 @@ TEST_CASE("sample conversion: int16|uint16 → float") TEST_CASE("sample conversion: int8 ←→ uint8") { auto cv1 = lol::audio::sample::convert; - CHECK(cv1(-0x80) == 0x00); - CHECK(cv1(-0x40) == 0x40); - CHECK(cv1(-0x01) == 0x7f); - CHECK(cv1( 0x00) == 0x80); - CHECK(cv1( 0x3f) == 0xbf); - CHECK(cv1( 0x7f) == 0xff); + for (int n = -0x80; n <= 0x7f; ++n) + { + CAPTURE(n); + CHECK(cv1(n) == n + 0x80); + } auto cv2 = lol::audio::sample::convert; - CHECK(cv2(0x00) == -0x80); - CHECK(cv2(0x40) == -0x40); - CHECK(cv2(0x7f) == -0x01); - CHECK(cv2(0x80) == 0x00); - CHECK(cv2(0xbf) == 0x3f); - CHECK(cv2(0xff) == 0x7f); + for (int n = 0x00; n <= 0xff; ++n) + { + CAPTURE(n); + CHECK(cv2(n) == n - 0x80); + } +} + +TEST_CASE("sample conversion: int16 ←→ uint16") +{ + auto cv1 = lol::audio::sample::convert; + for (int n = -0x8000; n <= 0x7fff; ++n) + { + CAPTURE(n); + CHECK(cv1(n) == n + 0x8000); + } + + auto cv2 = lol::audio::sample::convert; + for (int n = 0x0000; n <= 0xffff; ++n) + { + CAPTURE(n); + CHECK(cv2(n) == n - 0x8000); + } } -TEST_CASE("sample conversion: int16|uint16 → int8") +TEST_CASE("sample conversion: int16|uint16 → int8|uint8") { auto cv1 = lol::audio::sample::convert; CHECK(cv1(-0x8000) == -0x80); @@ -152,22 +173,63 @@ TEST_CASE("sample conversion: int16|uint16 → int8") CHECK(cv2(0x80ff) == 0x00); CHECK(cv2(0xbfff) == 0x3f); CHECK(cv2(0xffff) == 0x7f); + + auto cv3 = lol::audio::sample::convert; + CHECK(cv3(-0x8000) == 0x00); + CHECK(cv3(-0x4000) == 0x40); + CHECK(cv3(-0x0080) == 0x7f); + CHECK(cv3(-0x0001) == 0x7f); + CHECK(cv3( 0x0000) == 0x80); + CHECK(cv3( 0x00ff) == 0x80); + CHECK(cv3( 0x3fff) == 0xbf); + CHECK(cv3( 0x7fff) == 0xff); + + auto cv4 = lol::audio::sample::convert; + CHECK(cv4(0x0000) == 0x00); + CHECK(cv4(0x4000) == 0x40); + CHECK(cv4(0x7f80) == 0x7f); + CHECK(cv4(0x7fff) == 0x7f); + CHECK(cv4(0x8000) == 0x80); + CHECK(cv4(0x80ff) == 0x80); + CHECK(cv4(0xbfff) == 0xbf); + CHECK(cv4(0xffff) == 0xff); +} + +TEST_CASE("sample conversion: uint8 → int16|uint16") +{ + auto cv1 = lol::audio::sample::convert; + for (int n = 0x00; n <= 0xff; ++n) + { + CAPTURE(n); + CHECK(cv1(n) == n * 0x101 - 0x8000); + } + + auto cv2 = lol::audio::sample::convert; + for (int n = 0x00; n <= 0xff; ++n) + { + CAPTURE(n); + CHECK(cv2(n) == n * 0x101); + } } TEST_CASE("sample conversion: int8 → float → int8") { auto cv1 = lol::audio::sample::convert; auto cv2 = lol::audio::sample::convert; - CHECK(cv2(cv1(-0x80)) == -0x80); - CHECK(cv2(cv1(-0x7f)) == -0x7f); - CHECK(cv2(cv1(-0x40)) == -0x40); - CHECK(cv2(cv1(-0x20)) == -0x20); - CHECK(cv2(cv1(-0x02)) == -0x02); - CHECK(cv2(cv1(-0x01)) == -0x01); - CHECK(cv2(cv1( 0x00)) == 0x00); - CHECK(cv2(cv1( 0x01)) == 0x01); - CHECK(cv2(cv1( 0x02)) == 0x02); - CHECK(cv2(cv1( 0x20)) == 0x20); - CHECK(cv2(cv1( 0x40)) == 0x40); - CHECK(cv2(cv1( 0x7f)) == 0x7f); + for (int n = -0x80; n <= 0x7f; ++n) + { + CAPTURE(n); + CHECK(cv2(cv1(n)) == n); + } +} + +TEST_CASE("sample conversion: uint8 → float → uint8") +{ + auto cv1 = lol::audio::sample::convert; + auto cv2 = lol::audio::sample::convert; + for (int n = 0x00; n <= 0xff; ++n) + { + CAPTURE(n); + CHECK(cv2(cv1(n)) == n); + } }