diff --git a/include/lol/private/audio/stream.h b/include/lol/private/audio/stream.h index 1b1d8666..bfe79f25 100644 --- a/include/lol/private/audio/stream.h +++ b/include/lol/private/audio/stream.h @@ -35,9 +35,7 @@ public: static inline TO convert(FROM x) { constexpr auto from_fp = std::is_floating_point_v; - constexpr auto from_signed = !from_fp && std::is_signed_v; constexpr auto to_fp = std::is_floating_point_v; - constexpr auto to_signed = !to_fp && std::is_signed_v; if constexpr (std::is_same_v || (from_fp && to_fp)) { @@ -47,7 +45,7 @@ public: else if constexpr (from_fp) { // From floating point to integer: - // - renormalise to 0…1 + // - change range from -1…1 to 0…1 // - multiply by the size of the integer range // - add min, round down, and clamp to min…max FROM constexpr min(std::numeric_limits::min()); @@ -57,14 +55,38 @@ public: } else if constexpr (to_fp) { + // From integer to floating point: + // - compute (x - min) / (max - min) + // - change range from 0…1 to -1…1 TO constexpr min(std::numeric_limits::min()); TO constexpr max(std::numeric_limits::max()); return (TO(x) - min) * 2 / (max - min) - 1; } else { - // FIXME: this is better than nothing but we need a better implementation - return convert(convert(x)); + using UFROM = std::make_unsigned_t; + using UTO = std::make_unsigned_t; + + if constexpr (sizeof(FROM) > sizeof(TO)) + { + // From a larger integer type to a smaller integer type: + // - convert to unsigned + // - shift right + // - convert back to signed if necessary + UFROM constexpr m = UFROM(1) << (8 * (sizeof(FROM) - sizeof(TO))); + UFROM tmp = UFROM(UFROM(x) - UFROM(std::numeric_limits::min())) / m; + return TO(UTO(tmp) + UTO(std::numeric_limits::min())); + } + else + { + // From a smaller integer type to a larger integer type: + // - convert to unsigned + // - multiply by a magic constant such as 0x01010101 to propagate bytes + // - convert back to signed if necessary + UTO constexpr m = std::numeric_limits::max() / std::numeric_limits::max(); + UTO tmp = UFROM(UFROM(x) - UFROM(std::numeric_limits::min())) * m; + return TO(UTO(tmp) + UTO(std::numeric_limits::min())); + } } } }; diff --git a/t/audio.cpp b/t/audio.cpp index 5dc32714..5abd31cd 100644 --- a/t/audio.cpp +++ b/t/audio.cpp @@ -27,81 +27,123 @@ TEST_CASE("sample conversion between floating point types") TEST_CASE("sample conversion from float to 8-bit") { auto cv1 = lol::audio::sample::convert; - CHECK(cv1(-1.5f) == -128); - CHECK(cv1(-1.0f) == -128); - CHECK(cv1(-0.5f) == -64); - CHECK(cv1( 0.0f) == 0); - CHECK(cv1( 0.5f) == 64); - CHECK(cv1( 1.0f) == 127); - CHECK(cv1( 1.5f) == 127); + CHECK(cv1(-1.5f) == -0x80); + CHECK(cv1(-1.0f) == -0x80); + CHECK(cv1(-0.5f) == -0x40); + CHECK(cv1( 0.0f) == 0x00); + CHECK(cv1( 0.5f) == 0x40); + CHECK(cv1( 1.0f) == 0x7f); + CHECK(cv1( 1.5f) == 0x7f); auto cv2 = lol::audio::sample::convert; - CHECK(cv2(-1.5f) == 0); - CHECK(cv2(-1.0f) == 0); - CHECK(cv2(-0.5f) == 64); - CHECK(cv2( 0.0f) == 128); - CHECK(cv2( 0.5f) == 192); - CHECK(cv2( 1.0f) == 255); - CHECK(cv2( 1.5f) == 255); + CHECK(cv2(-1.5f) == 0x00); + CHECK(cv2(-1.0f) == 0x00); + CHECK(cv2(-0.5f) == 0x40); + CHECK(cv2( 0.0f) == 0x80); + CHECK(cv2( 0.5f) == 0xc0); + CHECK(cv2( 1.0f) == 0xff); + CHECK(cv2( 1.5f) == 0xff); } TEST_CASE("sample conversion from 8-bit to float") { auto cv1 = lol::audio::sample::convert; - CHECK(cv1(-128) == -1.0f); - CHECK(cv1( 127) == 1.0f); + CHECK(cv1(-0x80) == -1.0f); + CHECK(cv1( 0x7f) == 1.0f); auto cv2 = lol::audio::sample::convert; - CHECK(cv2( 0) == -1.0f); - CHECK(cv2(255) == 1.0f); + CHECK(cv2(0x00) == -1.0f); + CHECK(cv2(0xff) == 1.0f); } TEST_CASE("sample conversion from float to 16-bit") { auto cv1 = lol::audio::sample::convert; - CHECK(cv1(-1.5f) == -32768); - CHECK(cv1(-1.0f) == -32768); - CHECK(cv1(-0.5f) == -16384); - CHECK(cv1( 0.0f) == 0); - CHECK(cv1( 0.5f) == 16384); - CHECK(cv1( 1.0f) == 32767); - CHECK(cv1( 1.5f) == 32767); + CHECK(cv1(-1.5f) == -0x8000); + CHECK(cv1(-1.0f) == -0x8000); + CHECK(cv1(-0.5f) == -0x4000); + CHECK(cv1( 0.0f) == 0x0000); + CHECK(cv1( 0.5f) == 0x4000); + CHECK(cv1( 1.0f) == 0x7fff); + CHECK(cv1( 1.5f) == 0x7fff); auto cv2 = lol::audio::sample::convert; - CHECK(cv2(-1.5f) == 0); - CHECK(cv2(-1.0f) == 0); - CHECK(cv2(-0.5f) == 16384); - CHECK(cv2( 0.0f) == 32768); - CHECK(cv2( 0.5f) == 49152); - CHECK(cv2( 1.0f) == 65535); - CHECK(cv2( 1.5f) == 65535); + CHECK(cv2(-1.5f) == 0x0000); + CHECK(cv2(-1.0f) == 0x0000); + CHECK(cv2(-0.5f) == 0x4000); + CHECK(cv2( 0.0f) == 0x8000); + CHECK(cv2( 0.5f) == 0xc000); + CHECK(cv2( 1.0f) == 0xffff); + CHECK(cv2( 1.5f) == 0xffff); } TEST_CASE("sample conversion from 16-bit to float") { auto cv1 = lol::audio::sample::convert; - CHECK(cv1(-32768) == -1.0f); - CHECK(cv1( 32767) == 1.0f); + CHECK(cv1(-0x8000) == -1.0f); + CHECK(cv1( 0x7fff) == 1.0f); auto cv2 = lol::audio::sample::convert; - CHECK(cv2( 0) == -1.0f); - CHECK(cv2(65535) == 1.0f); + CHECK(cv2(0x0000) == -1.0f); + CHECK(cv2(0xffff) == 1.0f); +} + +TEST_CASE("sample conversion between signed and unsigned 8-bit") +{ + 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); + + 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); +} + +TEST_CASE("sample conversion from 16-bit to 8-bit") +{ + auto cv1 = lol::audio::sample::convert; + CHECK(cv1(-0x8000) == -0x80); + CHECK(cv1(-0x4000) == -0x40); + CHECK(cv1(-0x0080) == -0x01); + CHECK(cv1(-0x0001) == -0x01); + CHECK(cv1( 0x0000) == 0x00); + CHECK(cv1( 0x00ff) == 0x00); + CHECK(cv1( 0x3fff) == 0x3f); + CHECK(cv1( 0x7fff) == 0x7f); + + auto cv2 = lol::audio::sample::convert; + CHECK(cv2(0x0000) == -0x80); + CHECK(cv2(0x4000) == -0x40); + CHECK(cv2(0x7f80) == -0x01); + CHECK(cv2(0x7fff) == -0x01); + CHECK(cv2(0x8000) == 0x00); + CHECK(cv2(0x80ff) == 0x00); + CHECK(cv2(0xbfff) == 0x3f); + CHECK(cv2(0xffff) == 0x7f); } TEST_CASE("round-trip conversion from 8-bit to 8-bit") { auto cv1 = lol::audio::sample::convert; auto cv2 = lol::audio::sample::convert; - CHECK(cv2(cv1(-128)) == -128); - CHECK(cv2(cv1(-127)) == -127); - CHECK(cv2(cv1( -64)) == -64); - CHECK(cv2(cv1( -32)) == -32); - CHECK(cv2(cv1( -2)) == -2); - CHECK(cv2(cv1( -1)) == -1); - CHECK(cv2(cv1( 0)) == 0); - CHECK(cv2(cv1( 1)) == 1); - CHECK(cv2(cv1( 2)) == 2); - CHECK(cv2(cv1( 32)) == 32); - CHECK(cv2(cv1( 64)) == 64); - CHECK(cv2(cv1( 127)) == 127); + 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); }