diff --git a/include/lol/private/audio/stream.h b/include/lol/private/audio/stream.h
index 2b754944..581f12e8 100644
--- a/include/lol/private/audio/stream.h
+++ b/include/lol/private/audio/stream.h
@@ -64,21 +64,23 @@ public:
         }
         else
         {
-            // Conversion between integer types:
-            //  - convert to unsigned
-            //  - shift right or multiply by a magic constant such as 0x0101 to propagate bytes
-            //  - convert back to signed if necessary
-            // Most operations are done using the UBIG type, which is an unsigned integer type
-            // at least as large as FROM and TO.
+            // When converting between integer types, we first convert to an unsigned
+            // type of same size as source (e.g. int16_t → uint16_t) to ensure that all
+            // operations will happen modulo n (not guaranteed with signed types).
+            // The next step is to shift right (drop bits) or promote left (multiplying
+            // by a magic constant such as 0x1010101 or 0x10001). This happens using the
+            // UBIG type, which is an unsigned integer type at least as large as FROM
+            // and TO.
+            // Finally, we convert back to signed (e.g. uint16_t → int16_t) if necessary.
             using UFROM = std::make_unsigned_t<FROM>;
             using UTO = std::make_unsigned_t<TO>;
             using UBIG = std::conditional_t<(sizeof(FROM) > sizeof(TO)), UFROM, UTO>;
 
-            UBIG constexpr div = UBIG(1) << 8 * (sizeof(UBIG) - sizeof(UTO));
             UBIG constexpr mul = std::numeric_limits<UBIG>::max() / std::numeric_limits<UFROM>::max();
+            UBIG constexpr div = UBIG(1) << 8 * (sizeof(UBIG) - sizeof(UTO));
             auto tmp = UFROM(UFROM(x) - UFROM(std::numeric_limits<FROM>::min())) * mul / div;
 
-            return TO(UTO(tmp) + UTO(std::numeric_limits<TO>::min()));
+            return TO(tmp + UTO(std::numeric_limits<TO>::min()));
         }
     }
 };
diff --git a/t/audio.cpp b/t/audio.cpp
index 376aa517..40b19db1 100644
--- a/t/audio.cpp
+++ b/t/audio.cpp
@@ -5,22 +5,30 @@ TEST_CASE("sample conversion: float|double ←→ float|double")
 {
     auto cv1 = lol::audio::sample::convert<float, float>;
     CHECK(cv1(-1.0f) == -1.0f);
+    CHECK(cv1(-0.5f) == -0.5f);
     CHECK(cv1( 0.0f) ==  0.0f);
+    CHECK(cv1( 0.5f) ==  0.5f);
     CHECK(cv1( 1.0f) ==  1.0f);
 
     auto cv2 = lol::audio::sample::convert<float, double>;
     CHECK(cv2(-1.0f) == -1.0);
+    CHECK(cv2(-0.5f) == -0.5);
     CHECK(cv2( 0.0f) ==  0.0);
+    CHECK(cv2( 0.5f) ==  0.5);
     CHECK(cv2( 1.0f) ==  1.0);
 
     auto cv3 = lol::audio::sample::convert<double, float>;
     CHECK(cv3(-1.0) == -1.0f);
+    CHECK(cv3(-0.5) == -0.5f);
     CHECK(cv3( 0.0) ==  0.0f);
+    CHECK(cv3( 0.5) ==  0.5f);
     CHECK(cv3( 1.0) ==  1.0f);
 
     auto cv4 = lol::audio::sample::convert<double, double>;
     CHECK(cv4(-1.0) == -1.0);
+    CHECK(cv4(-0.5) == -0.5);
     CHECK(cv4( 0.0) ==  0.0);
+    CHECK(cv4( 0.5) ==  0.5);
     CHECK(cv4( 1.0) ==  1.0);
 }
 
@@ -94,7 +102,6 @@ TEST_CASE("sample conversion: int16|uint16 → float")
     auto cv1 = lol::audio::sample::convert<int16_t, float>;
     CHECK(cv1(-0x8000) == -1.0f);
     CHECK(cv1( 0x7fff) ==  1.0f);
-
     for (int n = -0x8000; n < 0x7fff; ++n)
     {
         CAPTURE(n);
@@ -104,7 +111,6 @@ TEST_CASE("sample conversion: int16|uint16 → float")
     auto cv2 = lol::audio::sample::convert<uint16_t, float>;
     CHECK(cv2(0x0000) == -1.0f);
     CHECK(cv2(0xffff) ==  1.0f);
-
     for (int n = 0x0000; n < 0xffff; ++n)
     {
         CAPTURE(n);
@@ -115,23 +121,38 @@ TEST_CASE("sample conversion: int16|uint16 → float")
 TEST_CASE("sample conversion: int8 ←→ uint8")
 {
     auto cv1 = lol::audio::sample::convert<int8_t, uint8_t>;
-    CHECK(cv1(-0x80) == 0x00);
-    CHECK(cv1(-0x40) == 0x40);
-    CHECK(cv1(-0x01) == 0x7f);
-    CHECK(cv1( 0x00) == 0x80);
-    CHECK(cv1( 0x3f) == 0xbf);
-    CHECK(cv1( 0x7f) == 0xff);
+    for (int n = -0x80; n <= 0x7f; ++n)
+    {
+        CAPTURE(n);
+        CHECK(cv1(n) == n + 0x80);
+    }
 
     auto cv2 = lol::audio::sample::convert<uint8_t, int8_t>;
-    CHECK(cv2(0x00) == -0x80);
-    CHECK(cv2(0x40) == -0x40);
-    CHECK(cv2(0x7f) == -0x01);
-    CHECK(cv2(0x80) ==  0x00);
-    CHECK(cv2(0xbf) ==  0x3f);
-    CHECK(cv2(0xff) ==  0x7f);
+    for (int n = 0x00; n <= 0xff; ++n)
+    {
+        CAPTURE(n);
+        CHECK(cv2(n) == n - 0x80);
+    }
+}
+
+TEST_CASE("sample conversion: int16 ←→ uint16")
+{
+    auto cv1 = lol::audio::sample::convert<int16_t, uint16_t>;
+    for (int n = -0x8000; n <= 0x7fff; ++n)
+    {
+        CAPTURE(n);
+        CHECK(cv1(n) == n + 0x8000);
+    }
+
+    auto cv2 = lol::audio::sample::convert<uint16_t, int16_t>;
+    for (int n = 0x0000; n <= 0xffff; ++n)
+    {
+        CAPTURE(n);
+        CHECK(cv2(n) == n - 0x8000);
+    }
 }
 
-TEST_CASE("sample conversion: int16|uint16 → int8")
+TEST_CASE("sample conversion: int16|uint16 → int8|uint8")
 {
     auto cv1 = lol::audio::sample::convert<int16_t, int8_t>;
     CHECK(cv1(-0x8000) == -0x80);
@@ -152,22 +173,63 @@ TEST_CASE("sample conversion: int16|uint16 → int8")
     CHECK(cv2(0x80ff) ==  0x00);
     CHECK(cv2(0xbfff) ==  0x3f);
     CHECK(cv2(0xffff) ==  0x7f);
+
+    auto cv3 = lol::audio::sample::convert<int16_t, uint8_t>;
+    CHECK(cv3(-0x8000) == 0x00);
+    CHECK(cv3(-0x4000) == 0x40);
+    CHECK(cv3(-0x0080) == 0x7f);
+    CHECK(cv3(-0x0001) == 0x7f);
+    CHECK(cv3( 0x0000) == 0x80);
+    CHECK(cv3( 0x00ff) == 0x80);
+    CHECK(cv3( 0x3fff) == 0xbf);
+    CHECK(cv3( 0x7fff) == 0xff);
+
+    auto cv4 = lol::audio::sample::convert<uint16_t, uint8_t>;
+    CHECK(cv4(0x0000) == 0x00);
+    CHECK(cv4(0x4000) == 0x40);
+    CHECK(cv4(0x7f80) == 0x7f);
+    CHECK(cv4(0x7fff) == 0x7f);
+    CHECK(cv4(0x8000) == 0x80);
+    CHECK(cv4(0x80ff) == 0x80);
+    CHECK(cv4(0xbfff) == 0xbf);
+    CHECK(cv4(0xffff) == 0xff);
+}
+
+TEST_CASE("sample conversion: uint8 → int16|uint16")
+{
+    auto cv1 = lol::audio::sample::convert<uint8_t, int16_t>;
+    for (int n = 0x00; n <= 0xff; ++n)
+    {
+        CAPTURE(n);
+        CHECK(cv1(n) == n * 0x101 - 0x8000);
+    }
+
+    auto cv2 = lol::audio::sample::convert<uint8_t, uint16_t>;
+    for (int n = 0x00; n <= 0xff; ++n)
+    {
+        CAPTURE(n);
+        CHECK(cv2(n) == n * 0x101);
+    }
 }
 
 TEST_CASE("sample conversion: int8 → float → int8")
 {
     auto cv1 = lol::audio::sample::convert<int8_t, float>;
     auto cv2 = lol::audio::sample::convert<float, int8_t>;
-    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);
+    for (int n = -0x80; n <= 0x7f; ++n)
+    {
+        CAPTURE(n);
+        CHECK(cv2(cv1(n)) == n);
+    }
+}
+
+TEST_CASE("sample conversion: uint8 → float → uint8")
+{
+    auto cv1 = lol::audio::sample::convert<uint8_t, float>;
+    auto cv2 = lol::audio::sample::convert<float, uint8_t>;
+    for (int n = 0x00; n <= 0xff; ++n)
+    {
+        CAPTURE(n);
+        CHECK(cv2(cv1(n)) == n);
+    }
 }