Skip to content

Commit bb626f3

Browse files
committed
Core (LV::AudioConvert): Rewrite audio sample conversion, fixes a tonne of numerical errors.
1 parent 9c932a6 commit bb626f3

File tree

1 file changed

+127
-149
lines changed

1 file changed

+127
-149
lines changed

libvisual/libvisual/private/lv_audio_convert.cpp

Lines changed: 127 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -23,208 +23,186 @@
2323
#include "lv_audio_convert.hpp"
2424
#include "lv_audio.h"
2525
#include "lv_mem.h"
26+
#include <algorithm>
2627
#include <array>
2728
#include <concepts>
28-
#include <type_traits>
2929
#include <limits>
30+
#include <span>
31+
#include <type_traits>
3032

3133
using namespace LV;
3234

3335
namespace {
3436

35-
template <typename D, typename S>
36-
constexpr bool is_same_signedness_v =
37-
(std::is_signed_v<D> && std::is_signed_v<S>) ||
38-
(std::is_unsigned_v<D> && std::is_unsigned_v<S>);
37+
// Checks whether two integral types are both signed or unsigned.
38+
template <std::integral T, std::integral U>
39+
constexpr bool is_same_signedness_v = std::is_signed_v<T> == std::is_signed_v<U>;
3940

40-
template <std::signed_integral T>
41-
constexpr T half_range ()
42-
{
43-
return std::numeric_limits<T>::max ();
44-
}
41+
// Checks whether an integral type is promoted to unsigned or signed int in arithmetic operations.
42+
template <std::integral T>
43+
constexpr bool int_promotable_v = sizeof (T) <= sizeof (int);
4544

46-
template <std::unsigned_integral T>
47-
constexpr T half_range ()
48-
{
49-
return std::numeric_limits<T>::max () / 2 + 1;
50-
}
45+
// Checks if an integral type can be exactly representable with a float i.e. fits within the
46+
// mantissa without loss.
47+
template <std::integral T>
48+
constexpr bool is_float_representable_v = std::numeric_limits<T>::digits <= std::numeric_limits<float>::digits;
5149

52-
template <std::signed_integral T>
53-
constexpr T zero ()
54-
{
55-
return 0;
56-
}
57-
58-
template <std::unsigned_integral T>
59-
constexpr T zero ()
60-
{
61-
return std::numeric_limits<T>::max () / 2 + 1;
62-
}
50+
// Type constraint for integral samples.
51+
template <typename T>
52+
concept integral_sample = std::integral<T> && int_promotable_v<T>;
6353

64-
template <typename D, typename S>
65-
constexpr int shifter()
66-
{
67-
if constexpr (sizeof (S) > sizeof (D))
68-
return int (sizeof (S) - sizeof (D)) << 3;
69-
else
70-
return int (sizeof (D) - sizeof (S)) << 3;
71-
}
54+
// Type constraint for signed integral samples.
55+
template <typename T>
56+
concept signed_integral_sample = std::signed_integral<T> && int_promotable_v<T>;
7257

73-
// same format conversion
58+
// Type constraint for unsigned integral samples.
7459
template <typename T>
75-
inline void convert_sample_array (T* dst, T const* src, std::size_t count)
60+
concept unsigned_integral_sample = std::unsigned_integral<T> && int_promotable_v<T>;
61+
62+
// Returns the 'bias' of an unsigned integral type i.e. the offset that can be added to the minimum value of its
63+
// signed counterpart to get 0.
64+
// Example: The bias for uint8_t is 128 as the minimum value of an int8_t is -128.
65+
template <unsigned_integral_sample T>
66+
constexpr auto bias () -> unsigned int
7667
{
77-
visual_mem_copy (dst, src, sizeof (T) * count);
68+
return 1U << (std::numeric_limits<T>::digits - 1);
7869
}
7970

80-
// signed->unsigned int conversion (same width)
81-
template <std::unsigned_integral D, std::signed_integral S>
82-
requires (sizeof (D) == sizeof (S))
83-
inline void convert_sample_array (D* dst, S const* src, std::size_t count)
71+
// Converts an integral sample to an unsigned integral sample of the same width.
72+
// Reduces to an identity function if handed an unsigned integral sample.
73+
// Example: int16_t and uint16_t samples are both converted to uint16_t.
74+
template <integral_sample T>
75+
constexpr auto to_unsigned_sample (T x) -> std::make_unsigned_t<T>
8476
{
85-
constexpr auto a {zero<D> ()};
86-
87-
auto src_end {src + count};
88-
89-
while (src != src_end) {
90-
*dst = *src + a;
91-
dst++;
92-
src++;
77+
if constexpr (std::is_signed_v<T>) {
78+
using U = std::make_unsigned_t<T>;
79+
return x + bias<U> ();
80+
} else {
81+
return x;
9382
}
9483
}
9584

96-
// unsigned->signed int conversion (same width)
97-
template <std::signed_integral D, std::unsigned_integral S>
98-
requires (sizeof (D) == sizeof (S))
99-
inline void convert_sample_array (D* dst, S const* src, std::size_t count)
85+
// Converts an integral sample to an signed integral sample of the same width.
86+
// Reduces to an identity function if input is already unsigned.
87+
// Example: int16_t and uint16_t samples are both converted to uint16_t.
88+
template <integral_sample T>
89+
constexpr auto to_signed_sample (T x) -> std::make_signed_t<T>
10090
{
101-
constexpr auto a {zero<S> ()};
102-
103-
auto src_end {src + count};
104-
105-
while (src != src_end) {
106-
*dst = *src - a;
107-
dst++;
108-
src++;
91+
if constexpr (!std::is_signed_v<T>) {
92+
return x - bias<T> ();
93+
} else {
94+
return x;
10995
}
11096
}
11197

112-
// int->float conversions
113-
template <std::integral S>
114-
inline void convert_sample_array (float* dst, S const* src, std::size_t count)
98+
// Converts an integral sample to a narrower or wider integral type with the
99+
// same signedness.
100+
//
101+
// Caveat: Due to the use of shifts for performance instead of float multiplications, the relative error in _widening_
102+
// conversions can get as large as 0.038 to 0.039% of the true value (e.g. converting from 8-bit to 16/32-bit).
103+
template <integral_sample T, integral_sample U>
104+
constexpr auto widen_or_narrow_sample (T x) -> U
105+
requires is_same_signedness_v<T, U>
115106
{
116-
constexpr float a {1.0 / float (half_range<S> ())};
117-
constexpr float b {-zero<S>() * a};
118-
119-
S const* src_end = src + count;
107+
constexpr auto n_t {std::numeric_limits<T>::digits};
108+
constexpr auto n_u {std::numeric_limits<U>::digits};
120109

121-
while (src != src_end) {
122-
*dst = *src * a + b;
123-
dst++;
124-
src++;
110+
if constexpr (n_t > n_u) {
111+
return x >> (n_t - n_u);
112+
} else if constexpr (n_t < n_u) {
113+
return x << (n_u - n_t);
114+
} else {
115+
return x;
125116
}
126117
}
127118

128-
// float->int conversions
129-
template <std::integral D>
130-
inline void convert_sample_array (D* dst, float const* src, std::size_t count)
119+
// Converts an integral sample to 32-bit float.
120+
template <integral_sample T>
121+
constexpr auto to_float_sample (T x) -> float
131122
{
132-
constexpr auto a {float (half_range<D> ())};
133-
constexpr auto b {zero<D> ()};
123+
using U = std::make_unsigned_t<T>;
124+
using F = std::conditional_t<is_float_representable_v<T>, float, double>;
134125

135-
auto src_end {src + count};
126+
constexpr F factor = 1.0 / (0.5 * std::numeric_limits<U>::max ());
127+
constexpr F one = 1.0;
136128

137-
while (src != src_end) {
138-
*dst = *src * a + b;
139-
dst++;
140-
src++;
141-
}
129+
return to_unsigned_sample (x) * factor - one;
142130
}
143131

144-
// narrowing/widening int conversion (same signedness)
145-
template <std::integral D, std::integral S>
146-
requires (is_same_signedness_v<D, S> && sizeof (D) != sizeof (S))
147-
inline void convert_sample_array (D* dst, S const* src, std::size_t count)
132+
// Converts a 32-bit float sample to an integral type.
133+
template <integral_sample T>
134+
constexpr auto from_float_sample (float x) -> T
148135
{
149-
constexpr auto shift {shifter<D, S> ()};
136+
using U = std::make_unsigned_t<T>;
137+
using F = std::conditional_t<is_float_representable_v<T>, float, double>;
150138

151-
auto src_end {src + count};
139+
constexpr F factor = 0.5 * std::numeric_limits<U>::max ();
140+
constexpr F one = 1.0;
152141

153-
if constexpr (sizeof (S) > sizeof (D)) {
154-
// narrowing
155-
while (src != src_end) {
156-
*dst = *src >> shift;
157-
dst++;
158-
src++;
159-
}
142+
auto y {static_cast<U> ((x + one) * factor)};
143+
144+
if constexpr (std::is_signed_v<T>) {
145+
return to_signed_sample (y);
160146
} else {
161-
// widening
162-
while (src != src_end) {
163-
*dst = *src << shift;
164-
dst++;
165-
src++;
166-
}
147+
return y;
167148
}
168149
}
169150

170-
// narrowing/widening unsigned->signed int conversion
171-
template <std::signed_integral D, std::unsigned_integral S>
172-
requires (sizeof (D) != sizeof (S))
173-
inline void convert_sample_array (D* dst, S const* src, std::size_t count)
151+
// Overload for same format conversions.
152+
template <typename T>
153+
constexpr void convert_samples (std::span<T> dst, std::span<T const> src)
174154
{
175-
constexpr auto a {zero<D>()};
176-
constexpr auto shift {shifter<D, S> ()};
177-
178-
auto src_end {src + count};
155+
auto count {std::min (src.size (), dst.size ())};
156+
visual_mem_copy (dst.data (), src.data (), count * sizeof (T));
157+
}
179158

180-
if constexpr (sizeof (D) < sizeof (S)) {
181-
// narrowing
182-
while (src != src_end) {
183-
*dst = D(*src >> shift) - a;
184-
dst++;
185-
src++;
186-
}
187-
} else {
188-
// widening
189-
while (src != src_end) {
190-
*dst = D(*src << shift) - a;
191-
dst++;
192-
src++;
193-
}
194-
}
159+
// Overload for unsigned to unsigned integral conversions.
160+
template <unsigned_integral_sample D, integral_sample S>
161+
constexpr void convert_samples (std::span<D> dst, std::span<S const> src)
162+
{
163+
auto const count {std::min (src.size (), dst.size ())};
164+
std::transform (src.begin (), src.begin () + count, dst.begin (),
165+
[=] (auto x) {
166+
auto y {to_unsigned_sample (x)};
167+
return widen_or_narrow_sample<decltype (y), D> (y);
168+
});
195169
}
196170

197-
// narrowing/widening signed->unsigned int conversion
198-
template <std::unsigned_integral D, std::signed_integral S>
199-
requires (sizeof (D) != sizeof (S))
200-
inline void convert_sample_array (D* dst, S const* src, std::size_t count)
171+
// Overload for signed/unsigned to signed integral conversions.
172+
template <signed_integral_sample D, integral_sample S>
173+
constexpr void convert_samples (std::span<D> dst, std::span<S const> src)
201174
{
202-
constexpr auto a {zero<D>()};
203-
constexpr auto shift {shifter<D, S> ()};
175+
auto const count {std::min (src.size (), dst.size ())};
176+
std::transform (src.begin (), src.begin () + count, dst.begin (),
177+
[=] (auto x) {
178+
auto y {to_signed_sample (x)};
179+
return widen_or_narrow_sample<decltype (y), D> (y);
180+
});
181+
}
204182

205-
auto src_end {src + count};
183+
// Overload for integral to float conversions.
184+
template <integral_sample T>
185+
constexpr void convert_samples (std::span<float> dst, std::span<T const> src)
186+
{
187+
auto const count {std::min (src.size (), dst.size ())};
188+
std::transform (src.begin (), src.begin () + count, dst.begin (), to_float_sample<T>);
189+
}
206190

207-
if constexpr (sizeof (D) < sizeof (S)) {
208-
// narrowing
209-
while (src != src_end) {
210-
*dst = D(*src >> shift) + a;
211-
dst++;
212-
src++;
213-
}
214-
} else {
215-
// widening
216-
while (src != src_end) {
217-
*dst = D(*src << shift) + a;
218-
dst++;
219-
src++;
220-
}
221-
}
191+
// Overload for float to integral conversions.
192+
template <integral_sample T>
193+
constexpr void convert_samples (std::span<T> dst, std::span<float const> src)
194+
{
195+
auto const count {std::min (src.size (), dst.size ())};
196+
std::transform (src.begin (), src.begin () + count, dst.begin (), from_float_sample<T>);
222197
}
223198

199+
// Wrapper for convert_samples() for storing all of its variants in a single table.
224200
template <typename D, typename S>
225-
void convert (void* dst, void const* src, std::size_t size)
201+
void convert (void* dst, void const* src, std::size_t count)
226202
{
227-
convert_sample_array (static_cast<D*> (dst), static_cast<S const*> (src), size / sizeof (S));
203+
std::span const src_span {static_cast<S const*> (src), count};
204+
std::span const dst_span {static_cast<D*> (dst), count};
205+
convert_samples (dst_span, src_span);
228206
}
229207

230208
using ConvertFunc = void (*) (void*, void const*, std::size_t);

0 commit comments

Comments
 (0)