| @@ -1,3 +1,6 @@ | |||
| # Editor cruft | |||
| .*.swp | |||
| *~ | |||
| # Binaries | |||
| *.exe | |||
| t/test | |||
| @@ -27,6 +27,48 @@ | |||
| namespace lol::audio | |||
| { | |||
| class sample | |||
| { | |||
| public: | |||
| // Convert samples between different types (float, int16_t, uint8_t, …) | |||
| template<typename FROM, typename TO> | |||
| static inline TO convert(FROM x) | |||
| { | |||
| constexpr auto from_fp = std::is_floating_point_v<FROM>; | |||
| constexpr auto from_signed = !from_fp && std::is_signed_v<FROM>; | |||
| constexpr auto to_fp = std::is_floating_point_v<TO>; | |||
| constexpr auto to_signed = !to_fp && std::is_signed_v<TO>; | |||
| if constexpr (std::is_same_v<FROM, TO> || (from_fp && to_fp)) | |||
| { | |||
| // If types are the same, or both floating-point, no conversion is needed | |||
| return TO(x); | |||
| } | |||
| else if constexpr (from_fp) | |||
| { | |||
| // From floating point to integer: | |||
| // - renormalise 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<TO>::min()); | |||
| FROM constexpr max(std::numeric_limits<TO>::max()); | |||
| x = (x + 1) * ((max - min + 1) / 2); | |||
| return TO(std::max(min, std::min(max, std::floor(x + min)))); | |||
| } | |||
| else if constexpr (to_fp) | |||
| { | |||
| TO constexpr min(std::numeric_limits<FROM>::min()); | |||
| TO constexpr max(std::numeric_limits<FROM>::max()); | |||
| return (TO(x) - min) * 2 / (max - min) - 1; | |||
| } | |||
| else | |||
| { | |||
| // FIXME: this is better than nothing but we need a better implementation | |||
| return convert<double, TO>(convert<FROM, double>(x)); | |||
| } | |||
| } | |||
| }; | |||
| template<typename T> | |||
| class stream | |||
| { | |||
| @@ -124,42 +166,6 @@ protected: | |||
| std::unordered_set<std::shared_ptr<stream<T>>> m_streams; | |||
| }; | |||
| // Convert samples from and to different types (float, int16_t, uint8_t, …) | |||
| template<typename T0, typename T> | |||
| static inline T convert_sample(T0 x) | |||
| { | |||
| constexpr auto from_fp = std::is_floating_point_v<T0>; | |||
| constexpr auto from_signed = !from_fp && std::is_signed_v<T0>; | |||
| constexpr auto to_fp = std::is_floating_point_v<T>; | |||
| constexpr auto to_signed = !to_fp && std::is_signed_v<T>; | |||
| if constexpr (std::is_same_v<T0, T> || (from_fp && to_fp)) | |||
| { | |||
| // If types are the same, or both floating-point, no conversion is needed | |||
| return T(x); | |||
| } | |||
| else if constexpr (from_fp) | |||
| { | |||
| // From floating point to integer | |||
| if constexpr (to_signed) | |||
| x = (x + T0(1.0)) * T0(0.5); | |||
| return T(x * std::numeric_limits<T>::max()); | |||
| } | |||
| else if constexpr (to_fp) | |||
| { | |||
| // From integer to floating point | |||
| if constexpr (from_signed) | |||
| return x / -T(std::numeric_limits<T0>::min()); | |||
| else | |||
| return x / (T(std::numeric_limits<T0>::max()) * T(0.5)) - T(1.0); | |||
| } | |||
| else | |||
| { | |||
| // FIXME: this is better than nothing but we need a better implementation | |||
| return convert_sample<double, T>(convert_sample<T0, double>(x)); | |||
| } | |||
| } | |||
| template<typename T, typename T0> | |||
| class converter : public stream<T> | |||
| { | |||
| @@ -179,7 +185,7 @@ public: | |||
| std::vector<T0> tmp(samples); | |||
| m_in->get(tmp.data(), frames); | |||
| for (size_t n = 0; n < samples; ++n) | |||
| buf[n] = convert_sample<T0, T>(tmp[n]); | |||
| buf[n] = sample::convert<T0, T>(tmp[n]); | |||
| return frames; | |||
| } | |||
| @@ -0,0 +1,13 @@ | |||
| SRC = audio.cpp | |||
| all: test | |||
| clean: | |||
| rm -f test test.exe | |||
| check: test | |||
| ./test | |||
| test: $(SRC) | |||
| $(CXX) -I../include $^ -o $@ | |||
| @@ -0,0 +1,107 @@ | |||
| #include <lol/lib/doctest_main> | |||
| #include <lol/audio/stream> | |||
| TEST_CASE("sample conversion between floating point types") | |||
| { | |||
| auto cv1 = lol::audio::sample::convert<float, float>; | |||
| CHECK(cv1(-1.0f) == -1.0f); | |||
| CHECK(cv1( 0.0f) == 0.0f); | |||
| CHECK(cv1( 1.0f) == 1.0f); | |||
| auto cv2 = lol::audio::sample::convert<float, double>; | |||
| CHECK(cv2(-1.0f) == -1.0); | |||
| CHECK(cv2( 0.0f) == 0.0); | |||
| CHECK(cv2( 1.0f) == 1.0); | |||
| auto cv3 = lol::audio::sample::convert<double, float>; | |||
| CHECK(cv3(-1.0) == -1.0f); | |||
| CHECK(cv3( 0.0) == 0.0f); | |||
| CHECK(cv3( 1.0) == 1.0f); | |||
| auto cv4 = lol::audio::sample::convert<double, double>; | |||
| CHECK(cv4(-1.0) == -1.0); | |||
| CHECK(cv4( 0.0) == 0.0); | |||
| CHECK(cv4( 1.0) == 1.0); | |||
| } | |||
| TEST_CASE("sample conversion from float to 8-bit") | |||
| { | |||
| auto cv1 = lol::audio::sample::convert<float, int8_t>; | |||
| 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); | |||
| auto cv2 = lol::audio::sample::convert<float, uint8_t>; | |||
| 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); | |||
| } | |||
| TEST_CASE("sample conversion from 8-bit to float") | |||
| { | |||
| auto cv1 = lol::audio::sample::convert<int8_t, float>; | |||
| CHECK(cv1(-128) == -1.0f); | |||
| CHECK(cv1( 127) == 1.0f); | |||
| auto cv2 = lol::audio::sample::convert<uint8_t, float>; | |||
| CHECK(cv2( 0) == -1.0f); | |||
| CHECK(cv2(255) == 1.0f); | |||
| } | |||
| TEST_CASE("sample conversion from float to 16-bit") | |||
| { | |||
| auto cv1 = lol::audio::sample::convert<float, int16_t>; | |||
| 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); | |||
| auto cv2 = lol::audio::sample::convert<float, uint16_t>; | |||
| 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); | |||
| } | |||
| TEST_CASE("sample conversion from 16-bit to float") | |||
| { | |||
| auto cv1 = lol::audio::sample::convert<int16_t, float>; | |||
| CHECK(cv1(-32768) == -1.0f); | |||
| CHECK(cv1( 32767) == 1.0f); | |||
| auto cv2 = lol::audio::sample::convert<uint16_t, float>; | |||
| CHECK(cv2( 0) == -1.0f); | |||
| CHECK(cv2(65535) == 1.0f); | |||
| } | |||
| TEST_CASE("round-trip conversion from 8-bit to 8-bit") | |||
| { | |||
| auto cv1 = lol::audio::sample::convert<int8_t, float>; | |||
| auto cv2 = lol::audio::sample::convert<float, int8_t>; | |||
| 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); | |||
| } | |||