@@ -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); | |||
} |