diff --git a/mini_al.h b/mini_al.h index ed024495..08792bb4 100644 --- a/mini_al.h +++ b/mini_al.h @@ -677,8 +677,8 @@ typedef enum typedef enum { mal_dither_mode_none = 0, - //mal_dither_mode_rectangle, - //mal_dither_mode_triangle + mal_dither_mode_rectangle, + mal_dither_mode_triangle } mal_dither_mode; typedef enum @@ -888,6 +888,7 @@ typedef struct mal_uint32 sampleRateOut; mal_channel channelMapOut[MAL_MAX_CHANNELS]; mal_channel_mix_mode channelMixMode; + mal_dither_mode ditherMode; mal_src_algorithm srcAlgorithm; mal_bool32 allowDynamicSampleRate; mal_dsp_read_proc onRead; @@ -1924,6 +1925,39 @@ mal_bool32 mal_channel_map_contains_channel_position(mal_uint32 channels, const ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Format Conversion +// ================= +// The format converter serves two purposes: +// 1) Conversion between data formats (u8 to f32, etc.) +// 2) Interleaving and deinterleaving +// +// When initializing a converter, you specify the input and output formats (u8, s16, etc.) and read callbacks. There are two read callbacks - one for +// interleaved input data (onRead) and another for deinterleaved input data (onReadDeinterleaved). You implement whichever is most convenient for you. You +// can implement both, but it's not recommended as it just introduces unnecessary complexity. +// +// To read data as interleaved samples, use mal_format_converter_read(). Otherwise use mal_format_converter_read_deinterleaved(). +// +// Dithering +// --------- +// The format converter also supports dithering. Dithering can be set using ditherMode variable in the config, like so. +// +// pConfig->ditherMode = mal_dither_mode_rectangle; +// +// The different dithering modes include the following, in order of efficiency: +// - None: mal_dither_mode_none +// - Rectangle: mal_dither_mode_rectangle +// - Triangle: mal_dither_mode_triangle +// +// Note that even if the dither mode is set to something other than mal_dither_mode_none, it will be ignored for conversions where dithering is not needed. +// Dithering is available for the following conversions: +// - s16 -> u8 +// - s24 -> u8 +// - s32 -> u8 +// - f32 -> u8 +// - s24 -> s16 +// - s32 -> s16 +// - f32 -> s16 +// +// Note that it is not an error to pass something other than mal_dither_mode_none for conversions where dither is not used. It will just be ignored. // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -3063,6 +3097,90 @@ static MAL_INLINE float mal_mix_f32(float x, float y, float a) return x*(1-a) + y*a; } +static MAL_INLINE float mal_scale_to_range_f32(float x, float lo, float hi) +{ + return lo + x*(hi-lo); +} + + + +// Random Number Generation +// +// mini_al uses the LCG random number generation algorithm. This is good enough for audio. +// +// Note that mini_al's LCG implementation uses global state which is _not_ thread-local. When this is called across +// multiple threads, results will be unpredictable. However, it won't crash and results will still be random enough +// for mini_al's purposes. +#define MAL_LCG_M 4294967296 +#define MAL_LCG_A 1103515245 +#define MAL_LCG_C 12345 +static mal_int32 g_malLCG; + +void mal_seed(mal_int32 seed) +{ + g_malLCG = seed; +} + +mal_int32 mal_rand_s32() +{ + mal_int32 lcg = g_malLCG; + mal_int32 r = (MAL_LCG_A * lcg + MAL_LCG_C) % MAL_LCG_M; + g_malLCG = r; + return r; +} + +double mal_rand_f64() +{ + return (mal_rand_s32() + 0x80000000) / (double)0x7FFFFFFF; +} + +float mal_rand_f32() +{ + return (float)mal_rand_f64(); +} + +static MAL_INLINE float mal_rand_range_f32(float lo, float hi) +{ + return mal_scale_to_range_f32(mal_rand_f32(), lo, hi); +} + +static MAL_INLINE mal_int32 mal_rand_range_s32(mal_int32 lo, mal_int32 hi) +{ + double x = mal_rand_f64(); + return lo + (mal_int32)(x*(hi-lo)); +} + + +static MAL_INLINE float mal_dither_f32(mal_dither_mode ditherMode, float ditherMin, float ditherMax) +{ + if (ditherMode == mal_dither_mode_rectangle) { + float a = mal_rand_range_f32(ditherMin, ditherMax); + return a; + } + if (ditherMode == mal_dither_mode_triangle) { + float a = mal_rand_range_f32(ditherMin, 0); + float b = mal_rand_range_f32(0, ditherMax); + return a + b; + } + + return 0; +} + +static MAL_INLINE mal_int32 mal_dither_s32(mal_dither_mode ditherMode, mal_int32 ditherMin, mal_int32 ditherMax) +{ + if (ditherMode == mal_dither_mode_rectangle) { + mal_int32 a = mal_rand_range_s32(ditherMin, ditherMax); + return a; + } + if (ditherMode == mal_dither_mode_triangle) { + mal_int32 a = mal_rand_range_s32(ditherMin, 0); + mal_int32 b = mal_rand_range_s32(0, ditherMax); + return a + b; + } + + return 0; +} + // Splits a buffer into parts of equal length and of the given alignment. The returned size of the split buffers will be a // multiple of the alignment. The alignment must be a power of 2. @@ -16977,17 +17095,34 @@ void mal_pcm_deinterleave_u8(void** dst, const void* src, mal_uint64 frameCount, // s16 void mal_pcm_s16_to_u8__reference(void* dst, const void* src, mal_uint64 count, mal_dither_mode ditherMode) { - (void)ditherMode; - mal_uint8* dst_u8 = (mal_uint8*)dst; const mal_int16* src_s16 = (const mal_int16*)src; - mal_uint64 i; - for (i = 0; i < count; i += 1) { - mal_int16 x = src_s16[i]; - x = x >> 8; - x = x + 128; - dst_u8[i] = (mal_uint8)x; + if (ditherMode == mal_dither_mode_none) { + mal_uint64 i; + for (i = 0; i < count; i += 1) { + mal_int16 x = src_s16[i]; + x = x >> 8; + x = x + 128; + dst_u8[i] = (mal_uint8)x; + } + } else { + mal_uint64 i; + for (i = 0; i < count; i += 1) { + mal_int16 x = src_s16[i]; + + // Dither. Don't overflow. + mal_int32 dither = mal_dither_s32(ditherMode, -0x80, 0x7F); + if ((x + dither) <= 0x7FFF) { + x = (mal_int16)(x + dither); + } else { + x = 0x7FFF; + } + + x = x >> 8; + x = x + 128; + dst_u8[i] = (mal_uint8)x; + } } } @@ -17216,15 +17351,32 @@ void mal_pcm_deinterleave_s16(void** dst, const void* src, mal_uint64 frameCount // s24 void mal_pcm_s24_to_u8__reference(void* dst, const void* src, mal_uint64 count, mal_dither_mode ditherMode) { - (void)ditherMode; - mal_uint8* dst_u8 = (mal_uint8*)dst; const mal_uint8* src_s24 = (const mal_uint8*)src; - mal_uint64 i; - for (i = 0; i < count; i += 1) { - mal_int8 x = (mal_int8)src_s24[i*3 + 2] + 128; - dst_u8[i] = (mal_uint8)x; + if (ditherMode == mal_dither_mode_none) { + mal_uint64 i; + for (i = 0; i < count; i += 1) { + mal_int8 x = (mal_int8)src_s24[i*3 + 2] + 128; + dst_u8[i] = (mal_uint8)x; + } + } else { + mal_uint64 i; + for (i = 0; i < count; i += 1) { + mal_int32 x = (mal_int32)(((mal_uint32)(src_s24[i*3+0]) << 8) | ((mal_uint32)(src_s24[i*3+1]) << 16) | ((mal_uint32)(src_s24[i*3+2])) << 24); + + // Dither. Don't overflow. + mal_int32 dither = mal_dither_s32(ditherMode, -0x800000, 0x7FFFFF); + if ((mal_int64)x + dither <= 0x7FFFFFFF) { + x = x + dither; + } else { + x = 0x7FFFFFFF; + } + + x = x >> 24; + x = x + 128; + dst_u8[i] = (mal_uint8)x; + } } } @@ -17256,16 +17408,32 @@ void mal_pcm_s24_to_u8(void* dst, const void* src, mal_uint64 count, mal_dither_ void mal_pcm_s24_to_s16__reference(void* dst, const void* src, mal_uint64 count, mal_dither_mode ditherMode) { - (void)ditherMode; - mal_int16* dst_s16 = (mal_int16*)dst; const mal_uint8* src_s24 = (const mal_uint8*)src; - mal_uint64 i; - for (i = 0; i < count; i += 1) { - mal_uint16 dst_lo = ((mal_uint16)src_s24[i*3 + 1]); - mal_uint16 dst_hi = ((mal_uint16)src_s24[i*3 + 2]) << 8; - dst_s16[i] = (mal_int16)dst_lo | dst_hi; + if (ditherMode == mal_dither_mode_none) { + mal_uint64 i; + for (i = 0; i < count; i += 1) { + mal_uint16 dst_lo = ((mal_uint16)src_s24[i*3 + 1]); + mal_uint16 dst_hi = ((mal_uint16)src_s24[i*3 + 2]) << 8; + dst_s16[i] = (mal_int16)dst_lo | dst_hi; + } + } else { + mal_uint64 i; + for (i = 0; i < count; i += 1) { + mal_int32 x = (mal_int32)(((mal_uint32)(src_s24[i*3+0]) << 8) | ((mal_uint32)(src_s24[i*3+1]) << 16) | ((mal_uint32)(src_s24[i*3+2])) << 24); + + // Dither. Don't overflow. + mal_int32 dither = mal_dither_s32(ditherMode, -0x8000, 0x7FFF); + if ((mal_int64)x + dither <= 0x7FFFFFFF) { + x = x + dither; + } else { + x = 0x7FFFFFFF; + } + + x = x >> 16; + dst_s16[i] = (mal_int16)x; + } } } @@ -17459,17 +17627,34 @@ void mal_pcm_deinterleave_s24(void** dst, const void* src, mal_uint64 frameCount // s32 void mal_pcm_s32_to_u8__reference(void* dst, const void* src, mal_uint64 count, mal_dither_mode ditherMode) { - (void)ditherMode; - mal_uint8* dst_u8 = (mal_uint8*)dst; const mal_int32* src_s32 = (const mal_int32*)src; - mal_uint64 i; - for (i = 0; i < count; i += 1) { - mal_int32 x = src_s32[i]; - x = x >> 24; - x = x + 128; - dst_u8[i] = (mal_uint8)x; + if (ditherMode == mal_dither_mode_none) { + mal_uint64 i; + for (i = 0; i < count; i += 1) { + mal_int32 x = src_s32[i]; + x = x >> 24; + x = x + 128; + dst_u8[i] = (mal_uint8)x; + } + } else { + mal_uint64 i; + for (i = 0; i < count; i += 1) { + mal_int32 x = src_s32[i]; + + // Dither. Don't overflow. + mal_int32 dither = mal_dither_s32(ditherMode, -0x800000, 0x7FFFFF); + if ((mal_int64)x + dither <= 0x7FFFFFFF) { + x = x + dither; + } else { + x = 0x7FFFFFFF; + } + + x = x >> 24; + x = x + 128; + dst_u8[i] = (mal_uint8)x; + } } } @@ -17501,16 +17686,32 @@ void mal_pcm_s32_to_u8(void* dst, const void* src, mal_uint64 count, mal_dither_ void mal_pcm_s32_to_s16__reference(void* dst, const void* src, mal_uint64 count, mal_dither_mode ditherMode) { - (void)ditherMode; - mal_int16* dst_s16 = (mal_int16*)dst; const mal_int32* src_s32 = (const mal_int32*)src; - mal_uint64 i; - for (i = 0; i < count; i += 1) { - mal_int32 x = src_s32[i]; - x = x >> 16; - dst_s16[i] = (mal_int16)x; + if (ditherMode == mal_dither_mode_none) { + mal_uint64 i; + for (i = 0; i < count; i += 1) { + mal_int32 x = src_s32[i]; + x = x >> 16; + dst_s16[i] = (mal_int16)x; + } + } else { + mal_uint64 i; + for (i = 0; i < count; i += 1) { + mal_int32 x = src_s32[i]; + + // Dither. Don't overflow. + mal_int32 dither = mal_dither_s32(ditherMode, -0x8000, 0x7FFF); + if ((mal_int64)x + dither <= 0x7FFFFFFF) { + x = x + dither; + } else { + x = 0x7FFFFFFF; + } + + x = x >> 16; + dst_s16[i] = (mal_int16)x; + } } } @@ -17542,7 +17743,7 @@ void mal_pcm_s32_to_s16(void* dst, const void* src, mal_uint64 count, mal_dither void mal_pcm_s32_to_s24__reference(void* dst, const void* src, mal_uint64 count, mal_dither_mode ditherMode) { - (void)ditherMode; + (void)ditherMode; // No dithering for s32 -> s24. mal_uint8* dst_s24 = (mal_uint8*)dst; const mal_int32* src_s32 = (const mal_int32*)src; @@ -17592,7 +17793,7 @@ void mal_pcm_s32_to_s32(void* dst, const void* src, mal_uint64 count, mal_dither void mal_pcm_s32_to_f32__reference(void* dst, const void* src, mal_uint64 count, mal_dither_mode ditherMode) { - (void)ditherMode; + (void)ditherMode; // No dithering for s32 -> f32. float* dst_f32 = (float*)dst; const mal_int32* src_s32 = (const mal_int32*)src; @@ -17700,14 +17901,20 @@ void mal_pcm_deinterleave_s32(void** dst, const void* src, mal_uint64 frameCount // f32 void mal_pcm_f32_to_u8__reference(void* dst, const void* src, mal_uint64 count, mal_dither_mode ditherMode) { - (void)ditherMode; - mal_uint8* dst_u8 = (mal_uint8*)dst; const float* src_f32 = (const float*)src; + float ditherMin = 0; + float ditherMax = 0; + if (ditherMode != mal_dither_mode_none) { + ditherMin = 1.0f / -128; + ditherMax = 1.0f / 127; + } + mal_uint64 i; for (i = 0; i < count; i += 1) { float x = src_f32[i]; + x = x + mal_dither_f32(ditherMode, ditherMin, ditherMax); x = ((x < -1) ? -1 : ((x > 1) ? 1 : x)); // clip x = x + 1; // -1..1 to 0..2 x = x * 127.5f; // 0..2 to 0..255 @@ -17744,14 +17951,20 @@ void mal_pcm_f32_to_u8(void* dst, const void* src, mal_uint64 count, mal_dither_ void mal_pcm_f32_to_s16__reference(void* dst, const void* src, mal_uint64 count, mal_dither_mode ditherMode) { - (void)ditherMode; - mal_int16* dst_s16 = (mal_int16*)dst; const float* src_f32 = (const float*)src; + float ditherMin = 0; + float ditherMax = 0; + if (ditherMode != mal_dither_mode_none) { + ditherMin = 1.0f / -32768; + ditherMax = 1.0f / 32767; + } + mal_uint64 i; for (i = 0; i < count; i += 1) { float x = src_f32[i]; + x = x + mal_dither_f32(ditherMode, ditherMin, ditherMax); x = ((x < -1) ? -1 : ((x > 1) ? 1 : x)); // clip #if 0 @@ -17796,7 +18009,7 @@ void mal_pcm_f32_to_s16(void* dst, const void* src, mal_uint64 count, mal_dither void mal_pcm_f32_to_s24__reference(void* dst, const void* src, mal_uint64 count, mal_dither_mode ditherMode) { - (void)ditherMode; + (void)ditherMode; // No dithering for f32 -> s24. mal_uint8* dst_s24 = (mal_uint8*)dst; const float* src_f32 = (const float*)src; @@ -17851,7 +18064,7 @@ void mal_pcm_f32_to_s24(void* dst, const void* src, mal_uint64 count, mal_dither void mal_pcm_f32_to_s32__reference(void* dst, const void* src, mal_uint64 count, mal_dither_mode ditherMode) { - (void)ditherMode; + (void)ditherMode; // No dithering for f32 -> s32. mal_int32* dst_s32 = (mal_int32*)dst; const float* src_f32 = (const float*)src; @@ -19586,6 +19799,7 @@ mal_result mal_dsp_init(const mal_dsp_config* pConfig, mal_dsp* pDSP) preFormatConverterConfig.formatIn = pConfig->formatIn; preFormatConverterConfig.formatOut = mal_format_f32; preFormatConverterConfig.channels = pConfig->channelsIn; + preFormatConverterConfig.ditherMode = pConfig->ditherMode; preFormatConverterConfig.streamFormatIn = mal_stream_format_pcm; preFormatConverterConfig.streamFormatOut = mal_stream_format_pcm; preFormatConverterConfig.onRead = mal_dsp__pre_format_converter_on_read; @@ -19604,6 +19818,7 @@ mal_result mal_dsp_init(const mal_dsp_config* pConfig, mal_dsp* pDSP) postFormatConverterConfig.formatIn = pConfig->formatIn; postFormatConverterConfig.formatOut = pConfig->formatOut; postFormatConverterConfig.channels = pConfig->channelsOut; + postFormatConverterConfig.ditherMode = pConfig->ditherMode; postFormatConverterConfig.streamFormatIn = mal_stream_format_pcm; postFormatConverterConfig.streamFormatOut = mal_stream_format_pcm; postFormatConverterConfig.pUserData = pDSP; diff --git a/tests/mal_dithering.c b/tests/mal_dithering.c new file mode 100644 index 00000000..d57f9c19 --- /dev/null +++ b/tests/mal_dithering.c @@ -0,0 +1,138 @@ +#define MAL_USE_REFERENCE_CONVERSION_APIS +#define MAL_IMPLEMENTATION +#include "../mini_al.h" + +// Two converters are needed here. One for converting f32 samples from the sine wave generator to the input format, +// and another for converting the input format to the output format for device output. +mal_sine_wave sineWave; +mal_format_converter converterIn; +mal_format_converter converterOut; + +mal_uint32 on_convert_samples_in(mal_format_converter* pConverter, mal_uint32 frameCount, void* pFrames, void* pUserData) +{ + (void)pUserData; + mal_assert(pConverter->config.formatIn == mal_format_f32); + + mal_sine_wave* pSineWave = (mal_sine_wave*)pConverter->config.pUserData; + mal_assert(pSineWave); + + return (mal_uint32)mal_sine_wave_read(pSineWave, frameCount, pFrames); +} + +mal_uint32 on_convert_samples_out(mal_format_converter* pConverter, mal_uint32 frameCount, void* pFrames, void* pUserData) +{ + (void)pUserData; + + mal_format_converter* pConverterIn = (mal_format_converter*)pConverter->config.pUserData; + mal_assert(pConverterIn != NULL); + + return (mal_uint32)mal_format_converter_read(pConverterIn, frameCount, pFrames, NULL); +} + +mal_uint32 on_send_to_device__original(mal_device* pDevice, mal_uint32 frameCount, void* pFrames) +{ + (void)pDevice; + mal_assert(pDevice->format == mal_format_f32); + mal_assert(pDevice->channels == 1); + + return (mal_uint32)mal_sine_wave_read(&sineWave, frameCount, pFrames); +} + +mal_uint32 on_send_to_device__dithered(mal_device* pDevice, mal_uint32 frameCount, void* pFrames) +{ + mal_assert(pDevice->channels == 1); + + mal_format_converter* pConverter = (mal_format_converter*)pDevice->pUserData; + mal_assert(pConverter != NULL); + mal_assert(pDevice->format == pConverter->config.formatOut); + + return (mal_uint32)mal_format_converter_read(pConverter, frameCount, pFrames, NULL); +} + +int do_dithering_test() +{ + mal_sine_wave_init(0.5, 400, 48000, &sineWave); + + + mal_device_config config = mal_device_config_init_playback(mal_format_f32, 1, 0, on_send_to_device__original); + mal_device device; + mal_result result; + + // We first play the sound the way it's meant to be played. + result = mal_device_init(NULL, mal_device_type_playback, NULL, &config, NULL, &device); + if (result != MAL_SUCCESS) { + return -1; + } + + result = mal_device_start(&device); + if (result != MAL_SUCCESS) { + return -2; + } + + printf("Press Enter to play enable dithering.\n"); + getchar(); + mal_device_uninit(&device); + + + // Now we play the sound after it's run through a dithered format converter. + mal_sine_wave_init(0.5, 400, 48000, &sineWave); + + mal_format srcFormat = mal_format_s24; + mal_format dstFormat = mal_format_s16; + mal_dither_mode ditherMode = mal_dither_mode_triangle; + + mal_format_converter_config converterInConfig; + mal_zero_object(&converterInConfig); + converterInConfig.formatIn = mal_format_f32; // <-- From the sine wave generator. + converterInConfig.formatOut = srcFormat; + converterInConfig.channels = config.channels; + converterInConfig.ditherMode = mal_dither_mode_none; + converterInConfig.onRead = on_convert_samples_in; + converterInConfig.pUserData = &sineWave; + result = mal_format_converter_init(&converterInConfig, &converterIn); + if (result != MAL_SUCCESS) { + return -3; + } + + mal_format_converter_config converterOutConfig; + mal_zero_object(&converterInConfig); + converterOutConfig.formatIn = srcFormat; + converterOutConfig.formatOut = dstFormat; + converterOutConfig.channels = config.channels; + converterOutConfig.ditherMode = ditherMode; + converterOutConfig.onRead = on_convert_samples_out; + converterOutConfig.pUserData = &converterIn; + result = mal_format_converter_init(&converterOutConfig, &converterOut); + if (result != MAL_SUCCESS) { + return -3; + } + + config = mal_device_config_init_playback(converterOutConfig.formatOut, 1, 0, on_send_to_device__dithered); + + result = mal_device_init(NULL, mal_device_type_playback, NULL, &config, &converterOut, &device); + if (result != MAL_SUCCESS) { + return -1; + } + + result = mal_device_start(&device); + if (result != MAL_SUCCESS) { + return -2; + } + + printf("Press Enter to stop.\n"); + getchar(); + + return 0; +} + +int main(int argc, char** argv) +{ + (void)argc; + (void)argv; + + + do_dithering_test(); + + + return 0; +} \ No newline at end of file diff --git a/tests/mal_test_0.vcxproj b/tests/mal_test_0.vcxproj index da80aff1..26433077 100644 --- a/tests/mal_test_0.vcxproj +++ b/tests/mal_test_0.vcxproj @@ -260,6 +260,7 @@ + true true @@ -269,12 +270,12 @@ true - false - false - false - false - false - false + true + true + true + true + true + true true diff --git a/tests/mal_test_0.vcxproj.filters b/tests/mal_test_0.vcxproj.filters index 1e1facfd..33c2259c 100644 --- a/tests/mal_test_0.vcxproj.filters +++ b/tests/mal_test_0.vcxproj.filters @@ -24,6 +24,9 @@ Source Files + + Source Files +