diff --git a/research/ma_lpf.h b/research/ma_lpf.h new file mode 100644 index 00000000..dd93f60c --- /dev/null +++ b/research/ma_lpf.h @@ -0,0 +1,237 @@ +#ifndef ma_lpf_h +#define ma_lpf_h + +/* +TODO: + - Document passthrough behaviour of the biquad filter and how it doesn't update previous inputs and outputs. + - Document how changing biquad constants requires reinitialization of the filter (due to issue above). +*/ + +typedef struct +{ + ma_format format; + ma_uint32 channels; + float a0; + float a1; + float a2; + float b0; + float b1; + float b2; +} ma_biquad_config; + +ma_biquad_config ma_biquad_config_init(ma_format format, ma_uint32 channels, float a0, float a1, float a2, float b0, float b1, float b2); + +typedef struct +{ + ma_biquad_config config; + ma_bool32 isPassthrough; + ma_uint32 prevFrameCount; + float x1[MA_MAX_CHANNELS]; /* x[n-1] */ + float x2[MA_MAX_CHANNELS]; /* x[n-2] */ + float y1[MA_MAX_CHANNELS]; /* y[n-1] */ + float y2[MA_MAX_CHANNELS]; /* y[n-2] */ +} ma_biquad; + +ma_result ma_biquad_init(const ma_biquad_config* pConfig, ma_biquad* pBQ); +ma_result ma_biquad_process(ma_biquad* pBQ, void* pFramesOut, const void* pFramesIn, ma_uint64 frameCount); + + +typedef struct +{ + ma_format format; + ma_uint32 channels; + ma_uint32 sampleRate; + ma_uint32 cutoffFrequency; +} ma_lpf_config; + +ma_lpf_config ma_lpf_config_init(ma_format format, ma_uint32 channels, ma_uint32 sampleRate, ma_uint32 cutoffFrequency); + +typedef struct +{ + ma_biquad bq; /* The low-pass filter is implemented as a biquad filter. */ + ma_lpf_config config; +} ma_lpf; + +ma_result ma_lpf_init(const ma_lpf_config* pConfig, ma_lpf* pLPF); +ma_result ma_lpf_process(ma_lpf* pLPF, void* pFramesOut, const void* pFramesIn, ma_uint64 frameCount); + +#endif /* ma_lpf_h */ + + + + +#if defined(MINIAUDIO_IMPLEMENTATION) + +ma_biquad_config ma_biquad_config_init(ma_format format, ma_uint32 channels, float a0, float a1, float a2, float b0, float b1, float b2) +{ + ma_biquad_config config; + + MA_ZERO_OBJECT(&config); + config.format = format; + config.channels = channels; + config.a0 = a0; + config.a1 = a1; + config.a2 = a2; + config.b0 = b0; + config.b1 = b1; + config.b2 = b2; + + return config; +} + +ma_result ma_biquad_init(const ma_biquad_config* pConfig, ma_biquad* pBQ) +{ + if (pBQ == NULL) { + return MA_INVALID_ARGS; + } + + MA_ZERO_OBJECT(pBQ); + + if (pConfig == NULL) { + return MA_INVALID_ARGS; + } + + if (pConfig->a0 == 0) { + return MA_INVALID_ARGS; /* Division by zero. */ + } + + /* Currently only supporting f32, but support for other formats will be added later. */ + if (pConfig->format != ma_format_f32) { + return MA_INVALID_ARGS; + } + + pBQ->config = *pConfig; + + if (pConfig->a0 == 1 && pConfig->a1 == 0 && pConfig->a2 == 0 && + pConfig->b0 == 1 && pConfig->b1 == 0 && pConfig->b2 == 0) { + pBQ->isPassthrough = MA_TRUE; + } + + /* Normalize. */ + pBQ->config.a1 /= pBQ->config.a0; + pBQ->config.a2 /= pBQ->config.a0; + pBQ->config.b0 /= pBQ->config.a0; + pBQ->config.b1 /= pBQ->config.a0; + pBQ->config.b2 /= pBQ->config.a0; + + return MA_SUCCESS; +} + +ma_result ma_biquad_process(ma_biquad* pBQ, void* pFramesOut, const void* pFramesIn, ma_uint64 frameCount) +{ + ma_uint32 n; + ma_uint32 c; + float a1 = pBQ->config.a1; + float a2 = pBQ->config.a2; + float b0 = pBQ->config.b0; + float b1 = pBQ->config.b1; + float b2 = pBQ->config.b2; + + if (pBQ == NULL || pFramesOut == NULL || pFramesIn == NULL) { + return MA_INVALID_ARGS; + } + + /* Fast path for passthrough. */ + if (pBQ->isPassthrough) { + ma_copy_memory_64(pFramesOut, pFramesIn, frameCount * ma_get_bytes_per_frame(pBQ->config.format, pBQ->config.channels)); + return MA_SUCCESS; + } + + /* Currently only supporting f32. */ + if (pBQ->config.format == ma_format_f32) { + float* pY = ( float*)pFramesOut; + const float* pX = (const float*)pFramesIn; + + for (n = 0; n < frameCount; n += 1) { + for (c = 0; c < pBQ->config.channels; c += 1) { + float x2 = pBQ->x2[c]; + float x1 = pBQ->x1[c]; + float x0 = pX[n*pBQ->config.channels + c]; + float y2 = pBQ->y2[c]; + float y1 = pBQ->y1[c]; + float y0 = b0*x0 + b1*x1 + b2*x2 - a1*y1 - a2*y2; + + pY[n*pBQ->config.channels + c] = y0; + pBQ->x2[c] = x1; + pBQ->x1[c] = x0; + pBQ->y2[c] = y1; + pBQ->y1[c] = y0; + } + } + } else { + return MA_INVALID_ARGS; /* Format not supported. Should never hit this because it's checked in ma_biquad_init(). */ + } + + return MA_SUCCESS; +} + + +ma_lpf_config ma_lpf_config_init(ma_format format, ma_uint32 channels, ma_uint32 sampleRate, ma_uint32 cutoffFrequency) +{ + ma_lpf_config config; + + MA_ZERO_OBJECT(&config); + config.format = format; + config.channels = channels; + config.sampleRate = sampleRate; + config.cutoffFrequency = cutoffFrequency; + + return config; +} + +ma_result ma_lpf_init(const ma_lpf_config* pConfig, ma_lpf* pLPF) +{ + ma_result result; + ma_biquad_config bqConfig; + double q; + double w; + double s; + double c; + double a; + + if (pLPF == NULL) { + return MA_INVALID_ARGS; + } + + MA_ZERO_OBJECT(pLPF); + + if (pConfig == NULL) { + return MA_INVALID_ARGS; + } + + pLPF->config = *pConfig; + + q = 1 / sqrt(2); + w = 2 * MA_PI_D * pConfig->cutoffFrequency / pConfig->sampleRate; + s = sin(w); + c = cos(w); + a = s / (2*q); + + bqConfig.a0 = (float)( 1 + a); + bqConfig.a1 = (float)(-2 * c); + bqConfig.a2 = (float)( 1 - a); + bqConfig.b0 = (float)((1 - c) / 2); + bqConfig.b1 = (float)( 1 - c); + bqConfig.b2 = (float)((1 - c) / 2); + + bqConfig.format = pConfig->format; + bqConfig.channels = pConfig->channels; + + result = ma_biquad_init(&bqConfig, &pLPF->bq); + if (result != MA_SUCCESS) { + return result; + } + + return MA_SUCCESS; +} + +ma_result ma_lpf_process(ma_lpf* pLPF, void* pFramesOut, const void* pFramesIn, ma_uint64 frameCount) +{ + if (pLPF == NULL) { + return MA_INVALID_ARGS; + } + + return ma_biquad_process(&pLPF->bq, pFramesOut, pFramesIn, frameCount); +} + +#endif \ No newline at end of file diff --git a/research/ma_resampler.h b/research/ma_resampler.h index 8d941db8..d2b7c9fc 100644 --- a/research/ma_resampler.h +++ b/research/ma_resampler.h @@ -3,49 +3,43 @@ #ifndef ma_resampler_h #define ma_resampler_h +#include "ma_lpf.h" + typedef enum { - ma_resample_algorithm_linear = 0, /* Fastest, lowest quality. */ - ma_resample_algorithm_linear_lpf, /* Linear with a biquad low pass filter. */ + ma_resample_algorithm_linear_lpf = 0, /* Linear with a biquad low pass filter. Default. */ + ma_resample_algorithm_linear, /* Fastest, lowest quality. */ } ma_resample_algorithm; typedef struct { - ma_resample_algorithm algorithm; + ma_format format; /* Must be either ma_format_f32 or ma_format_s16. */ + ma_uint32 channels; ma_uint32 sampleRateIn; ma_uint32 sampleRateOut; - ma_uint32 channels; - ma_format format; /* Must be either ma_format_f32 or ma_format_s16. */ + ma_resample_algorithm algorithm; struct { int _unused; } linear; struct { - int _unused; + ma_uint32 cutoffFrequency; } linearLPF; } ma_resampler_config; +ma_resampler_config ma_resampler_config_init(ma_format format, ma_uint32 channels, ma_uint32 sampleRateIn, ma_uint32 sampleRateOut, ma_resample_algorithm algorithm); + typedef struct { ma_resampler_config config; + float timeX; /* Input time. */ + float timeY; /* Output time. */ union { struct { - float timeX; /* Input time. */ - float timeY; /* Output time. */ - struct - { - float yprev1; /* y-1 */ - float yprev2; /* y-2 */ - float a0; - float a1; - float a2; - float b0; - float b1; - float b2; - } lpf; + ma_lpf lpf; } linear; } state; } ma_resampler; @@ -75,7 +69,7 @@ It is an error for [pFramesOut] to be non-NULL and [pFrameCountOut] to be NULL. It is an error for both [pFrameCountOut] and [pFrameCountIn] to be NULL. */ -ma_result ma_resampler_process(ma_resampler* pResampler, ma_uint64* pFrameCountOut, void* pFramesOut, ma_uint64* pFrameCountIn, void* pFramesIn); +ma_result ma_resampler_process(ma_resampler* pResampler, ma_uint64* pFrameCountOut, void* pFramesOut, ma_uint64* pFrameCountIn, const void* pFramesIn); /* @@ -99,37 +93,52 @@ Implementation #define MA_RESAMPLER_MAX_RATIO 48.0 #endif +ma_resampler_config ma_resampler_config_init(ma_format format, ma_uint32 channels, ma_uint32 sampleRateIn, ma_uint32 sampleRateOut, ma_resample_algorithm algorithm) +{ + ma_resampler_config config; + + MA_ZERO_OBJECT(&config); + config.format = format; + config.channels = channels; + config.sampleRateIn = sampleRateIn; + config.sampleRateOut = sampleRateOut; + config.algorithm = algorithm; + + return config; +} + ma_result ma_resampler_init(const ma_resampler_config* pConfig, ma_resampler* pResampler) { + ma_result result; + if (pConfig == NULL || pResampler == NULL) { return MA_INVALID_ARGS; } MA_ZERO_OBJECT(pResampler); pResampler->config = *pConfig; + pResampler->timeX = 0.0f; + pResampler->timeY = 0.0f; switch (pConfig->algorithm) { case ma_resample_algorithm_linear: { - pResampler->state.linear.timeX = 0.0f; - pResampler->state.linear.timeY = 0.0f; } break; case ma_resample_algorithm_linear_lpf: { - pResampler->state.linear.timeX = 0.0f; - pResampler->state.linear.timeY = 0.0f; - pResampler->state.linear.lpf.yprev1 = 0.0f; - pResampler->state.linear.lpf.yprev2 = 0.0f; + ma_lpf_config lpfConfig; + + lpfConfig = ma_lpf_config_init(pConfig->format, pConfig->channels, pConfig->sampleRateOut, pConfig->linearLPF.cutoffFrequency); + if (lpfConfig.cutoffFrequency == 0) { + lpfConfig.cutoffFrequency = ma_min(pConfig->sampleRateIn, pConfig->sampleRateOut) / 2; + } - /* TODO: Biquad LPF filter coefficients. */ - pResampler->state.linear.lpf.a0 = 0.0f; - pResampler->state.linear.lpf.a1 = 0.0f; - pResampler->state.linear.lpf.a2 = 0.0f; - pResampler->state.linear.lpf.b0 = 0.0f; - pResampler->state.linear.lpf.b1 = 0.0f; - pResampler->state.linear.lpf.b2 = 0.0f; + result = ma_lpf_init(&lpfConfig, &pResampler->state.linear.lpf); + if (result != MA_SUCCESS) { + return result; + } } break; default: return MA_INVALID_ARGS; @@ -138,7 +147,7 @@ ma_result ma_resampler_init(const ma_resampler_config* pConfig, ma_resampler* pR return MA_SUCCESS; } -static ma_result ma_resampler_process__seek__linear(ma_resampler* pResampler, ma_uint64* pFrameCountOut, ma_uint64* pFrameCountIn, void* pFramesIn) +static ma_result ma_resampler_process__seek__linear(ma_resampler* pResampler, ma_uint64* pFrameCountOut, ma_uint64* pFrameCountIn, const void* pFramesIn) { MA_ASSERT(pResampler != NULL); @@ -163,13 +172,13 @@ static ma_result ma_resampler_process__seek__linear(ma_resampler* pResampler, ma return MA_SUCCESS; } -static ma_result ma_resampler_process__seek__linear_lpf(ma_resampler* pResampler, ma_uint64* pFrameCountOut, ma_uint64* pFrameCountIn, void* pFramesIn) +static ma_result ma_resampler_process__seek__linear_lpf(ma_resampler* pResampler, ma_uint64* pFrameCountOut, ma_uint64* pFrameCountIn, const void* pFramesIn) { /* TODO: Proper linear LPF implementation. */ return ma_resampler_process__seek__linear(pResampler, pFrameCountOut, pFrameCountIn, pFramesIn); } -static ma_result ma_resampler_process__seek(ma_resampler* pResampler, ma_uint64* pFrameCountOut, ma_uint64* pFrameCountIn, void* pFramesIn) +static ma_result ma_resampler_process__seek(ma_resampler* pResampler, ma_uint64* pFrameCountOut, ma_uint64* pFrameCountIn, const void* pFramesIn) { MA_ASSERT(pResampler != NULL); @@ -190,7 +199,7 @@ static ma_result ma_resampler_process__seek(ma_resampler* pResampler, ma_uint64* } -static ma_result ma_resampler_process__read__linear(ma_resampler* pResampler, ma_uint64* pFrameCountOut, void* pFramesOut, ma_uint64* pFrameCountIn, void* pFramesIn) +static ma_result ma_resampler_process__read__linear(ma_resampler* pResampler, ma_uint64* pFrameCountOut, void* pFramesOut, ma_uint64* pFrameCountIn, const void* pFramesIn) { MA_ASSERT(pResampler != NULL); MA_ASSERT(pFramesOut != NULL); @@ -205,13 +214,13 @@ static ma_result ma_resampler_process__read__linear(ma_resampler* pResampler, ma return MA_SUCCESS; } -static ma_result ma_resampler_process__read__linear_lpf(ma_resampler* pResampler, ma_uint64* pFrameCountOut, void* pFramesOut, ma_uint64* pFrameCountIn, void* pFramesIn) +static ma_result ma_resampler_process__read__linear_lpf(ma_resampler* pResampler, ma_uint64* pFrameCountOut, void* pFramesOut, ma_uint64* pFrameCountIn, const void* pFramesIn) { /* TODO: Proper linear LPF implementation. */ return ma_resampler_process__read__linear(pResampler, pFrameCountOut, pFramesOut, pFrameCountIn, pFramesIn); } -static ma_result ma_resampler_process__read(ma_resampler* pResampler, ma_uint64* pFrameCountOut, void* pFramesOut, ma_uint64* pFrameCountIn, void* pFramesIn) +static ma_result ma_resampler_process__read(ma_resampler* pResampler, ma_uint64* pFrameCountOut, void* pFramesOut, ma_uint64* pFrameCountIn, const void* pFramesIn) { MA_ASSERT(pResampler != NULL); MA_ASSERT(pFramesOut != NULL); @@ -237,13 +246,13 @@ static ma_result ma_resampler_process__read(ma_resampler* pResampler, ma_uint64* } } -ma_result ma_resampler_process(ma_resampler* pResampler, ma_uint64* pFrameCountOut, void* pFramesOut, ma_uint64* pFrameCountIn, void* pFramesIn) +ma_result ma_resampler_process(ma_resampler* pResampler, ma_uint64* pFrameCountOut, void* pFramesOut, ma_uint64* pFrameCountIn, const void* pFramesIn) { if (pResampler == NULL) { return MA_INVALID_ARGS; } - if (pFrameCountOut != NULL && pFrameCountIn == NULL) { + if (pFrameCountOut == NULL && pFrameCountIn == NULL) { return MA_INVALID_ARGS; } diff --git a/research/tests/ma_lpf_test_0.c b/research/tests/ma_lpf_test_0.c new file mode 100644 index 00000000..a1136ead --- /dev/null +++ b/research/tests/ma_lpf_test_0.c @@ -0,0 +1,109 @@ +#define DR_FLAC_IMPLEMENTATION +#include "../../extras/dr_flac.h" /* Enables FLAC decoding. */ +#define DR_MP3_IMPLEMENTATION +#include "../../extras/dr_mp3.h" /* Enables MP3 decoding. */ +#define DR_WAV_IMPLEMENTATION +#include "../../extras/dr_wav.h" /* Enables WAV decoding. */ + + +#define MA_DEBUG_OUTPUT +#define MINIAUDIO_IMPLEMENTATION +#include "../../miniaudio.h" +#include "../ma_lpf.h" + +ma_lpf g_lpf; + +void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) +{ + ma_uint32 framesProcessed = 0; + ma_decoder* pDecoder = (ma_decoder*)pDevice->pUserData; + if (pDecoder == NULL) { + return; + } + + /* We need to read into a temporary buffer and then run it through the low pass filter. */ + while (framesProcessed < frameCount) { + float tempBuffer[4096]; + ma_uint32 framesToProcessThisIteration; + + framesToProcessThisIteration = frameCount - framesProcessed; + if (framesToProcessThisIteration > ma_countof(tempBuffer)/pDecoder->internalChannels) { + framesToProcessThisIteration = ma_countof(tempBuffer)/pDecoder->internalChannels; + } + + #if 0 + ma_decoder_read_pcm_frames(pDecoder, ma_offset_ptr(pOutput, framesProcessed * ma_get_bytes_per_frame(pDevice->playback.format, pDevice->playback.channels)), framesToProcessThisIteration); + #else + ma_decoder_read_pcm_frames(pDecoder, tempBuffer, framesToProcessThisIteration); + + /* Out the results from the low pass filter straight into our output buffer. */ + ma_lpf_process(&g_lpf, ma_offset_ptr(pOutput, framesProcessed * ma_get_bytes_per_frame(pDevice->playback.format, pDevice->playback.channels)), tempBuffer, framesToProcessThisIteration); + #endif + + framesProcessed += framesToProcessThisIteration; + } + + (void)pInput; +} + +int main(int argc, char** argv) +{ + ma_result result; + ma_decoder_config decoderConfig; + ma_decoder decoder; + ma_lpf_config lpfConfig; + ma_device_config deviceConfig; + ma_device device; + + if (argc < 2) { + printf("No input file.\n"); + return -1; + } + + decoderConfig = ma_decoder_config_init(ma_format_f32, 0, 0); + + result = ma_decoder_init_file(argv[1], &decoderConfig, &decoder); + if (result != MA_SUCCESS) { + return -2; + } + + + lpfConfig.format = decoderConfig.format; + lpfConfig.channels = decoder.internalChannels; + lpfConfig.sampleRate = decoder.internalSampleRate; + lpfConfig.cutoffFrequency = lpfConfig.sampleRate / 4; + + result = ma_lpf_init(&lpfConfig, &g_lpf); + if (result != MA_SUCCESS) { + return -100; + } + + + deviceConfig = ma_device_config_init(ma_device_type_playback); + deviceConfig.playback.format = decoder.outputFormat; + deviceConfig.playback.channels = decoder.outputChannels; + deviceConfig.sampleRate = decoder.outputSampleRate; + deviceConfig.dataCallback = data_callback; + deviceConfig.pUserData = &decoder; + + if (ma_device_init(NULL, &deviceConfig, &device) != MA_SUCCESS) { + printf("Failed to open playback device.\n"); + ma_decoder_uninit(&decoder); + return -3; + } + + if (ma_device_start(&device) != MA_SUCCESS) { + printf("Failed to start playback device.\n"); + ma_device_uninit(&device); + ma_decoder_uninit(&decoder); + return -4; + } + + printf("Press Enter to quit..."); + getchar(); + + ma_device_uninit(&device); + ma_decoder_uninit(&decoder); + + return 0; +} diff --git a/tests/ma_test_0.vcxproj b/tests/ma_test_0.vcxproj index 357f3fc6..de27b068 100644 --- a/tests/ma_test_0.vcxproj +++ b/tests/ma_test_0.vcxproj @@ -327,13 +327,14 @@ true - false - false - false - false - false - false + true + true + true + true + true + true + true true @@ -409,6 +410,7 @@ + diff --git a/tests/ma_test_0.vcxproj.filters b/tests/ma_test_0.vcxproj.filters index 6b18d42e..442e827b 100644 --- a/tests/ma_test_0.vcxproj.filters +++ b/tests/ma_test_0.vcxproj.filters @@ -69,6 +69,9 @@ Source Files + + Source Files + @@ -77,5 +80,8 @@ Source Files + + Source Files + \ No newline at end of file