From dbf391611d95f62b32346e16ddc727304426266e Mon Sep 17 00:00:00 2001 From: David Reid Date: Sat, 14 Feb 2026 20:26:20 +1000 Subject: [PATCH] Add resampling test. --- CMakeLists.txt | 66 +++++ tests/resampling/resampling.c | 491 ++++++++++++++++++++++++++++++++++ 2 files changed, 557 insertions(+) create mode 100644 tests/resampling/resampling.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d266df9..0d21a859 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -498,6 +498,51 @@ if(NOT MINIAUDIO_NO_LIBOPUS) endif() endif() +# libsamplerate +if(NOT TARGET samplerate) + # Try pkg-config first + find_package(PkgConfig QUIET) + if(PkgConfig_FOUND) + pkg_check_modules(samplerate IMPORTED_TARGET samplerate) + endif() + + if(TARGET PkgConfig::samplerate) + add_library(samplerate ALIAS PkgConfig::samplerate) + else() + message(STATUS "libsamplerate not found.") + endif() +endif() + +# speexdsp +if(NOT TARGET speexdsp) + # Try pkg-config first + find_package(PkgConfig QUIET) + if(PkgConfig_FOUND) + pkg_check_modules(speexdsp IMPORTED_TARGET speexdsp) + endif() + + if(TARGET PkgConfig::speexdsp) + add_library(speexdsp ALIAS PkgConfig::speexdsp) + else() + message(STATUS "speexdsp not found.") + endif() +endif() + +# soxr +if(NOT TARGET soxr) + # Try pkg-config first + find_package(PkgConfig QUIET) + if(PkgConfig_FOUND) + pkg_check_modules(soxr IMPORTED_TARGET soxr) + endif() + + if(TARGET PkgConfig::soxr) + add_library(soxr ALIAS PkgConfig::soxr) + else() + message(STATUS "soxr not found.") + endif() +endif() + # SteamAudio has an annoying SDK setup. In the lib folder there is a folder for each platform. We need to specify the # platform we're compiling for. @@ -837,6 +882,27 @@ if(MINIAUDIO_BUILD_TESTS) add_miniaudio_test(miniaudio_generation generation/generation.c) add_test(NAME miniaudio_generation COMMAND miniaudio_generation) + + add_miniaudio_test(miniaudio_resampling resampling/resampling.c) + #add_test(NAME miniaudio_resampling COMMAND miniaudio_resampling) + if(TARGET samplerate) + target_link_libraries(miniaudio_resampling PRIVATE samplerate) + target_compile_definitions(miniaudio_resampling PRIVATE MA_HAS_LIBSAMPLERATE) + else() + message(STATUS "libsamplerate not found. Sample rate conversion will be disabled in miniaudio_resampling.") + endif() + if(TARGET speexdsp) + target_link_libraries(miniaudio_resampling PRIVATE speexdsp) + target_compile_definitions(miniaudio_resampling PRIVATE MA_HAS_SPEEXDSP) + else() + message(STATUS "speexdsp not found. Speex resampler will be disabled in miniaudio_resampling.") + endif() + if(TARGET soxr) + target_link_libraries(miniaudio_resampling PRIVATE soxr) + target_compile_definitions(miniaudio_resampling PRIVATE MA_HAS_SOXR) + else() + message(STATUS "soxr not found. SoX resampler will be disabled in miniaudio_resampling.") + endif() endif() # Examples diff --git a/tests/resampling/resampling.c b/tests/resampling/resampling.c new file mode 100644 index 00000000..050876b6 --- /dev/null +++ b/tests/resampling/resampling.c @@ -0,0 +1,491 @@ +#define MA_NO_PIPEWIRE +#if 1 +#include "../../miniaudio.c" +#else +#define MINIAUDIO_IMPLEMENTATION +#include "../../miniaudio-11.h" +#endif + +#ifdef MA_HAS_LIBSAMPLERATE +#include +#endif + +#ifdef MA_HAS_SPEEXDSP +#include +#endif + +#ifdef MA_HAS_SOXR +#include +#endif + +#define RESAMPLING_LPF_ORDER 0 +#define RESAMPLING_FRAMES_PER_ITERATION 1024 * 1024 +#define RESAMPLING_ITERATION_COUNT 100 + +#define resampling_api_miniaudio 0 +#define resampling_api_libsamplerate 1 +#define resampling_api_speex 2 +#define resampling_api_soxr 4 + +typedef int resampling_api; + +const char* resampling_api_to_string(resampling_api api) +{ + if (api == resampling_api_miniaudio) { + return "miniaudio"; + } + if (api == resampling_api_libsamplerate) { + return "libsamplerate"; + } + if (api == resampling_api_speex) { + return "speex"; + } + if (api == resampling_api_soxr) { + return "soxr"; + } + + MA_ASSERT(!"Resampling API needs to be added to resampling_api_to_string()."); + return "unknown"; +} + +/* Modify this to control the resampling API. Comment out to force miniaudio. */ +/*#define RESAMPLING_LISTENING_TEST_API resampling_api_libsamplerate*/ + +#if defined(RESAMPLING_LISTENING_TEST_API) + #if RESAMPLING_LISTENING_TEST_API == resampling_api_libsamplerate && !defined(MA_HAS_LIBSAMPLERATE) + #error "libsamplerate is unavailable and cannot be used." + #endif +#endif + +#ifndef RESAMPLING_LISTENING_TEST_API +#define RESAMPLING_LISTENING_TEST_API resampling_api_miniaudio +#endif + +typedef struct +{ + ma_resampler resampler; + ma_waveform waveform; + ma_uint8 cache[4096]; + ma_uint32 cachedFrameCount; + ma_uint32 cachedFrameCap; + float sampleRateStep; + float sampleRateRatio; + float minSampleRateRatio; + float maxSampleRateRatio; + #ifdef MA_HAS_LIBSAMPLERATE + SRC_STATE* pLSRState; + #endif +} resampler_data_callback_data; + +static void resampler_data_callback(ma_device* pDevice, void* pFramesOut, const void* pFramesIn, ma_uint32 frameCount) +{ + resampler_data_callback_data* pResamplerData = (resampler_data_callback_data*)pDevice->pUserData; + ma_uint32 framesWritten; + ma_uint32 bpf; + + bpf = ma_get_bytes_per_frame(pResamplerData->resampler.format, pResamplerData->resampler.channels); + + framesWritten = 0; + while (framesWritten < frameCount) { + /* We resample from the cache. */ + while (pResamplerData->cachedFrameCount > 0 && framesWritten < frameCount) { + #if RESAMPLING_LISTENING_TEST_API == resampling_api_miniaudio + { + ma_uint64 frameCountIn; + ma_uint64 frameCountOut; + + frameCountIn = pResamplerData->cachedFrameCount; + frameCountOut = frameCount - framesWritten; + ma_resampler_process_pcm_frames(&pResamplerData->resampler, pResamplerData->cache, &frameCountIn, ma_offset_ptr(pFramesOut, framesWritten * bpf), &frameCountOut); + + MA_MOVE_MEMORY(pResamplerData->cache, ma_offset_ptr(pResamplerData->cache, frameCountIn * bpf), (pResamplerData->cachedFrameCount - frameCountIn) * bpf); + pResamplerData->cachedFrameCount -= frameCountIn; + framesWritten += frameCountOut; + } + #endif + + #if RESAMPLING_LISTENING_TEST_API == resampling_api_libsamplerate + { + SRC_DATA data; + + MA_ZERO_OBJECT(&data); + data.data_in = (const float*)pResamplerData->cache; + data.data_out = (float*)ma_offset_ptr(pFramesOut, framesWritten * bpf); + data.input_frames = pResamplerData->cachedFrameCount; + data.output_frames = frameCount - framesWritten; + data.src_ratio = 1 / pResamplerData->sampleRateRatio; + src_process(pResamplerData->pLSRState, &data); + + MA_MOVE_MEMORY(pResamplerData->cache, ma_offset_ptr(pResamplerData->cache, data.input_frames_used * bpf), (pResamplerData->cachedFrameCount - data.input_frames_used) * bpf); + pResamplerData->cachedFrameCount -= data.input_frames_used; + framesWritten += data.output_frames_gen; + } + #endif + } + + /* Reload the cache if necessary. */ + if (pResamplerData->cachedFrameCount == 0) { + ma_waveform_read_pcm_frames(&pResamplerData->waveform, pResamplerData->cache, pResamplerData->cachedFrameCap, NULL); + pResamplerData->cachedFrameCount = pResamplerData->cachedFrameCap; + } + } + + /* Sweep the sample rate. We'll just adjust the output rate. */ + pResamplerData->sampleRateRatio += pResamplerData->sampleRateStep; + /* */ if (pResamplerData->sampleRateRatio > pResamplerData->maxSampleRateRatio) { + pResamplerData->sampleRateRatio = pResamplerData->maxSampleRateRatio; + pResamplerData->sampleRateStep = -pResamplerData->sampleRateStep; + } else if (pResamplerData->sampleRateRatio < pResamplerData->minSampleRateRatio) { + pResamplerData->sampleRateRatio = pResamplerData->minSampleRateRatio; + pResamplerData->sampleRateStep = -pResamplerData->sampleRateStep; + } + + #if RESAMPLING_LISTENING_TEST_API == resampling_api_miniaudio + { + ma_resampler_set_rate_ratio(&pResamplerData->resampler, pResamplerData->sampleRateRatio); + } + #endif + #if RESAMPLING_LISTENING_TEST_API == resampling_api_libsamplerate + { + src_set_ratio(pResamplerData->pLSRState, 1 / pResamplerData->sampleRateRatio); + } + #endif + + (void)pFramesIn; +} + +void resampler_listening_test(void) +{ + ma_result result; + resampler_data_callback_data callbackData; + ma_resampler_config resamplerConfig; + ma_waveform_config waveformConfig; + ma_device_config deviceConfig; + ma_device device; + ma_format format; + ma_uint32 channels = 2; + + printf("Resampling Listening Test API: %s\n", resampling_api_to_string(RESAMPLING_LISTENING_TEST_API)); + + #if RESAMPLING_LISTENING_TEST_API == resampling_api_miniaudio + { + format = ma_format_f32; /* Switch this between f32 and s16 as required when testing miniaudio. */ + } + #endif + #if RESAMPLING_LISTENING_TEST_API == resampling_api_libsamplerate + { + format = ma_format_f32; /* Can only use f32 with libsamplerate. */ + + callbackData.pLSRState = src_new(SRC_SINC_FASTEST /*SRC_SINC_BEST_QUALITY*/, channels, NULL); + } + #endif + + resamplerConfig = ma_resampler_config_init(format, channels, 48000, 48000, ma_resample_algorithm_linear); + resamplerConfig.linear.lpfOrder = RESAMPLING_LPF_ORDER; + + result = ma_resampler_init(&resamplerConfig, NULL, &callbackData.resampler); + if (result != MA_SUCCESS) { + printf("Failed to initialize resampler."); + return; + } + + + waveformConfig = ma_waveform_config_init(resamplerConfig.format, resamplerConfig.channels, resamplerConfig.sampleRateIn, ma_waveform_type_sine, 0.2f, 440); + + result = ma_waveform_init(&waveformConfig, &callbackData.waveform); + if (result != MA_SUCCESS) { + printf("Failed to initialize waveform."); + return; + } + + + callbackData.cachedFrameCount = 0; + callbackData.cachedFrameCap = sizeof(callbackData.cache) / ma_get_bytes_per_frame(resamplerConfig.format, resamplerConfig.channels); + + + /* Default. */ + #if 1 + callbackData.sampleRateStep = 0.01f; + callbackData.minSampleRateRatio = 0.2f; + callbackData.maxSampleRateRatio = 5.0f; + #endif + + /* Low frequency upsample. */ + #if 0 + callbackData.sampleRateStep = 0.001f; + callbackData.minSampleRateRatio = 0.01f; + callbackData.maxSampleRateRatio = 0.09f; + #endif + + /* High frequency downsample */ + #if 0 + callbackData.sampleRateStep = 0.01f; + callbackData.minSampleRateRatio = 4.0f; + callbackData.maxSampleRateRatio = 6.0f; + #endif + + #if 1 + callbackData.sampleRateStep = 0.002f; + callbackData.minSampleRateRatio = 0.002f; + callbackData.maxSampleRateRatio = 0.2f; + #endif + + callbackData.sampleRateRatio = callbackData.minSampleRateRatio; + + + + deviceConfig = ma_device_config_init(ma_device_type_playback); + deviceConfig.playback.format = resamplerConfig.format; + deviceConfig.playback.channels = resamplerConfig.channels; + deviceConfig.sampleRate = resamplerConfig.sampleRateIn; + deviceConfig.periodSizeInFrames = /*2048; //*/256; + deviceConfig.dataCallback = resampler_data_callback; + deviceConfig.pUserData = &callbackData; + + + result = ma_device_init(NULL, &deviceConfig, &device); + if (result != MA_SUCCESS) { + printf("Failed to initialize device."); + return; + } + + ma_device_start(&device); + + ma_sleep(100000); + + ma_device_uninit(&device); + ma_waveform_uninit(&callbackData.waveform); + ma_resampler_uninit(&callbackData.resampler, NULL); +} + +static void profile_resampler_miniaudio(resampling_api api, ma_format format, ma_uint32 channels, ma_uint32 sampleRateIn, ma_uint32 sampleRateOut, ma_uint32 lpfOrder) +{ + ma_result result; + ma_timer timer; + double startTime; + void* pSamplesIn = NULL; + void* pSamplesOut = NULL; + ma_uint32 bpf = ma_get_bytes_per_frame(format, channels); + int i; + ma_uint32 framesOutCap = 1024; + + printf("%s: %s, %s, LPF %u, %u > %u: ", resampling_api_to_string(api), ((format == ma_format_s16) ? "s16" : "f32"), ((channels == 1) ? "mono" : "stereo"), lpfOrder, sampleRateIn, sampleRateOut); + + ma_timer_init(&timer); + + if (api == resampling_api_libsamplerate && format != ma_format_f32) { + printf("Format not supported.\n"); + return; + } + + pSamplesIn = ma_malloc(RESAMPLING_FRAMES_PER_ITERATION * bpf, NULL); + pSamplesOut = ma_malloc(framesOutCap * bpf, NULL); + + ma_debug_fill_pcm_frames_with_sine_wave((float*)pSamplesIn, RESAMPLING_FRAMES_PER_ITERATION, format, channels, sampleRateIn); /* The float* cast is to work around an API bug in miniaudio v0.11. It's harmless. */ + + if (api == resampling_api_miniaudio) { + ma_linear_resampler_config resamplerConfig; + ma_linear_resampler resampler; + + resamplerConfig = ma_linear_resampler_config_init(format, channels, sampleRateIn, sampleRateOut); + resamplerConfig.lpfOrder = lpfOrder; + + result = ma_linear_resampler_init(&resamplerConfig, NULL, &resampler); + if (result != MA_SUCCESS) { + printf("Failed to initialize resampler.\n"); + return; + } + + startTime = ma_timer_get_time_in_seconds(&timer); + { + for (i = 0; i < RESAMPLING_ITERATION_COUNT; i += 1) { + ma_uint64 totalFramesRead = 0; + + while (totalFramesRead < RESAMPLING_FRAMES_PER_ITERATION) { + ma_uint64 framesIn = RESAMPLING_FRAMES_PER_ITERATION - totalFramesRead; + ma_uint64 framesOut = framesOutCap; + + ma_linear_resampler_process_pcm_frames(&resampler, ma_offset_pcm_frames_ptr(pSamplesIn, totalFramesRead, format, channels), &framesIn, pSamplesOut, &framesOut); + totalFramesRead += framesIn; + } + } + } + printf("%f\n", ma_timer_get_time_in_seconds(&timer) - startTime); + } + + if (api == resampling_api_libsamplerate) { + #ifdef MA_HAS_LIBSAMPLERATE + { + #if 0 + SRC_SINC_BEST_QUALITY + SRC_SINC_MEDIUM_QUALITY + SRC_SINC_FASTEST + SRC_ZERO_ORDER_HOLD + SRC_LINEAR + #endif + + SRC_STATE* pState = src_new(SRC_LINEAR, channels, NULL); + + startTime = ma_timer_get_time_in_seconds(&timer); + { + for (i = 0; i < RESAMPLING_ITERATION_COUNT; i += 1) { + ma_uint64 totalFramesRead = 0; + + while (totalFramesRead < RESAMPLING_FRAMES_PER_ITERATION) { + SRC_DATA data; + + data.data_in = ma_offset_pcm_frames_ptr(pSamplesIn, totalFramesRead, format, channels); + data.data_out = pSamplesOut; + data.input_frames = RESAMPLING_FRAMES_PER_ITERATION - totalFramesRead; + data.output_frames = framesOutCap; + data.src_ratio = (double)sampleRateOut / sampleRateIn; /* I think libsamplerate's ratio is out/in, whereas miniaudio is in/out. Advice welcome if I am wrong about this. */ + src_process(pState, &data); + + totalFramesRead += data.output_frames_gen; + } + } + } + printf("%f\n", ma_timer_get_time_in_seconds(&timer) - startTime); + } + #else + { + printf("libsamplerate not available.\n"); + } + #endif + } + + if (api == resampling_api_speex) { + #ifdef MA_HAS_SPEEXDSP + { + int err; + SpeexResamplerState* pState = speex_resampler_init(channels, sampleRateIn, sampleRateOut, SPEEX_RESAMPLER_QUALITY_MIN, &err); + + if (err != RESAMPLER_ERR_SUCCESS) { + printf("Failed to initialize speex resampler.\n"); + return; + } + + startTime = ma_timer_get_time_in_seconds(&timer); + { + for (i = 0; i < RESAMPLING_ITERATION_COUNT; i += 1) { + ma_uint64 totalFramesRead = 0; + + while (totalFramesRead < RESAMPLING_FRAMES_PER_ITERATION) { + spx_uint32_t in_len = (spx_uint32_t)(RESAMPLING_FRAMES_PER_ITERATION - totalFramesRead); + spx_uint32_t out_len = (spx_uint32_t)framesOutCap; + + if (format == ma_format_f32) { + speex_resampler_process_interleaved_float(pState, ma_offset_pcm_frames_ptr(pSamplesIn, totalFramesRead, format, channels), &in_len, pSamplesOut, &out_len); + } else { + speex_resampler_process_interleaved_int(pState, ma_offset_pcm_frames_ptr(pSamplesIn, totalFramesRead, format, channels), &in_len, pSamplesOut, &out_len); + } + + totalFramesRead += in_len; + } + } + } + printf("%f\n", ma_timer_get_time_in_seconds(&timer) - startTime); + + speex_resampler_destroy(pState); + } + #else + { + printf("speex not available.\n"); + } + #endif + } + + if (api == resampling_api_soxr) { + #ifdef MA_HAS_SOXR + { + soxr_error_t error; + soxr_io_spec_t ioSpec; + soxr_quality_spec_t qualitySpec; + soxr_t pState; + + ioSpec = soxr_io_spec((format == ma_format_f32) ? SOXR_FLOAT32_I : SOXR_INT16_I, (format == ma_format_f32) ? SOXR_FLOAT32_I : SOXR_INT16_I); + qualitySpec = soxr_quality_spec(SOXR_QQ, 0); + pState = soxr_create((double)sampleRateIn, (double)sampleRateOut, channels, &error, &ioSpec, &qualitySpec, NULL); + + if (error != NULL) { + printf("Failed to initialize soxr resampler.\n"); + return; + } + + startTime = ma_timer_get_time_in_seconds(&timer); + { + for (i = 0; i < RESAMPLING_ITERATION_COUNT; i += 1) { + ma_uint64 totalFramesRead = 0; + + while (totalFramesRead < RESAMPLING_FRAMES_PER_ITERATION) { + size_t in_len = RESAMPLING_FRAMES_PER_ITERATION - totalFramesRead; + size_t out_len = 0; + + soxr_process(pState, ma_offset_pcm_frames_ptr(pSamplesIn, totalFramesRead, format, channels), in_len, &in_len, pSamplesOut, framesOutCap, &out_len); + + totalFramesRead += in_len; + } + } + } + printf("%f\n", ma_timer_get_time_in_seconds(&timer) - startTime); + + soxr_delete(pState); + } + #else + { + printf("soxr not available.\n"); + } + #endif + } +} + +void profile_resampler(void) +{ + unsigned int denormalsState; + + denormalsState = ma_disable_denormals(); + { + #if 1 + /* miniaudio */ + profile_resampler_miniaudio(resampling_api_miniaudio, ma_format_s16, 1, 44100, 48000, RESAMPLING_LPF_ORDER); + profile_resampler_miniaudio(resampling_api_miniaudio, ma_format_s16, 2, 44100, 48000, RESAMPLING_LPF_ORDER); + profile_resampler_miniaudio(resampling_api_miniaudio, ma_format_f32, 1, 44100, 48000, RESAMPLING_LPF_ORDER); + profile_resampler_miniaudio(resampling_api_miniaudio, ma_format_f32, 2, 44100, 48000, RESAMPLING_LPF_ORDER); + profile_resampler_miniaudio(resampling_api_miniaudio, ma_format_s16, 1, 48000, 44100, RESAMPLING_LPF_ORDER); + profile_resampler_miniaudio(resampling_api_miniaudio, ma_format_s16, 2, 48000, 44100, RESAMPLING_LPF_ORDER); + profile_resampler_miniaudio(resampling_api_miniaudio, ma_format_f32, 1, 48000, 44100, RESAMPLING_LPF_ORDER); + profile_resampler_miniaudio(resampling_api_miniaudio, ma_format_f32, 2, 48000, 44100, RESAMPLING_LPF_ORDER); + + /* libsamplerate */ + profile_resampler_miniaudio(resampling_api_libsamplerate, ma_format_f32, 1, 44100, 48000, RESAMPLING_LPF_ORDER); + profile_resampler_miniaudio(resampling_api_libsamplerate, ma_format_f32, 2, 44100, 48000, RESAMPLING_LPF_ORDER); + + /* speex */ + profile_resampler_miniaudio(resampling_api_speex, ma_format_s16, 1, 44100, 48000, RESAMPLING_LPF_ORDER); + profile_resampler_miniaudio(resampling_api_speex, ma_format_s16, 2, 44100, 48000, RESAMPLING_LPF_ORDER); + profile_resampler_miniaudio(resampling_api_speex, ma_format_f32, 1, 44100, 48000, RESAMPLING_LPF_ORDER); + profile_resampler_miniaudio(resampling_api_speex, ma_format_f32, 2, 44100, 48000, RESAMPLING_LPF_ORDER); + + /* soxr */ + profile_resampler_miniaudio(resampling_api_soxr, ma_format_s16, 1, 44100, 48000, RESAMPLING_LPF_ORDER); + profile_resampler_miniaudio(resampling_api_soxr, ma_format_s16, 2, 44100, 48000, RESAMPLING_LPF_ORDER); + profile_resampler_miniaudio(resampling_api_soxr, ma_format_f32, 1, 44100, 48000, RESAMPLING_LPF_ORDER); + profile_resampler_miniaudio(resampling_api_soxr, ma_format_f32, 2, 44100, 48000, RESAMPLING_LPF_ORDER); + #endif + } + ma_restore_denormals(denormalsState); + + + /* Finish off with a listening test. */ + resampler_listening_test(); +} + +int main(int argc, char** argv) +{ + profile_resampler(); + + (void)argc; + (void)argv; + + return 0; +} \ No newline at end of file