@@ -1,3 +1,6 @@ | |||||
# Editor cruft | # Editor cruft | ||||
.*.swp | .*.swp | ||||
*~ | *~ | ||||
# Binaries | |||||
*.exe | |||||
t/test |
@@ -27,6 +27,48 @@ | |||||
namespace lol::audio | 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> | template<typename T> | ||||
class stream | class stream | ||||
{ | { | ||||
@@ -124,42 +166,6 @@ protected: | |||||
std::unordered_set<std::shared_ptr<stream<T>>> m_streams; | 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> | template<typename T, typename T0> | ||||
class converter : public stream<T> | class converter : public stream<T> | ||||
{ | { | ||||
@@ -179,7 +185,7 @@ public: | |||||
std::vector<T0> tmp(samples); | std::vector<T0> tmp(samples); | ||||
m_in->get(tmp.data(), frames); | m_in->get(tmp.data(), frames); | ||||
for (size_t n = 0; n < samples; ++n) | 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; | 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); | |||||
} |