Commit 994c86fc authored by David Reid's avatar David Reid

Add initial support for dithering.

parent d50c5d22
......@@ -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;
......
#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
......@@ -260,6 +260,7 @@
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="mal_dithering.c" />
<ClCompile Include="mal_profiling.c">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
......@@ -269,12 +270,12 @@
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="mal_test_0.c">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">false</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">false</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">false</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">false</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">false</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">false</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="mal_test_0.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
......
......@@ -24,6 +24,9 @@
<ClCompile Include="mal_profiling.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="mal_dithering.c">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\mini_al.h">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment