瀏覽代碼

audio: fix bugs in the audio sample conversion, and add unit tests

wip/image-kernel
Sam Hocevar 9 月之前
父節點
當前提交
01f80d344e
共有 4 個檔案被更改,包括 166 行新增37 行删除
  1. +3
    -0
      .gitignore
  2. +43
    -37
      include/lol/private/audio/stream.h
  3. +13
    -0
      t/Makefile
  4. +107
    -0
      t/audio.cpp

+ 3
- 0
.gitignore 查看文件

@@ -1,3 +1,6 @@
# Editor cruft
.*.swp
*~
# Binaries
*.exe
t/test

+ 43
- 37
include/lol/private/audio/stream.h 查看文件

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


+ 13
- 0
t/Makefile 查看文件

@@ -0,0 +1,13 @@

SRC = audio.cpp

all: test

clean:
rm -f test test.exe

check: test
./test

test: $(SRC)
$(CXX) -I../include $^ -o $@

+ 107
- 0
t/audio.cpp 查看文件

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

Loading…
取消
儲存