// mixer interpolation LUT generator

#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <math.h>
#include "../ft2_header.h"
#include "../ft2_video.h" // showErrorMsgBox()
#include "ft2_mix_interpolation.h"

typedef struct
{
	double kaiserBeta, sincCutoff;
} sincKernel_t;

static sincKernel_t sincKernelConfig[SINC_KERNELS] =
{
	/* These parameters have been borrowed from the OpenMPT project
	** (with cutoff edit for kernel #1).
	**
	** The beta for kernel #1 is based on a stopband attenuation close to
	** the 16-bit audio noise floor (~96.33dB).
	**
	** It's quite difficult to tweak these numbers without causing either
	** ringing or aliasing in some cases. If anyone has good experience on
	** designing low-tap windowed-sinc kernels for a use case like this, and
	** knows of a good way to figure out what numbers to use here, PLEASE
	** contact me (contact details can be found at the bottom of 16-bits.org).
	**
	** The beta for kernel #1 is based on a stopband attenuation close to
	** the 16-bit audio noise floor (~96.33dB).
	*/

	// beta,  cutoff
	{ 9.6377, 1.000 }, // kernel #1
	{ 8.5000, 0.500 }, // kernel #2
	{ 7.0000, 0.425 }  // kernel #3
};

// globalized
float *fCubicSplineLUT, *fSinc[SINC_KERNELS], *fSinc8[SINC_KERNELS], *fSinc16[SINC_KERNELS];
uint64_t sincRatio1, sincRatio2;
// ----------

static void calcPolyphaseSincLUT(float *fOut, int32_t numTaps, double kaiserBeta, double sincCutoff);

bool setupMixerInterpolationTables(void)
{
	// cubic spline (4-point)

	fCubicSplineLUT = (float *)malloc(CUBIC_SPLINE_TAPS * INTRP_PHASES * sizeof (float));
	if (fCubicSplineLUT == NULL)
	{
		showErrorMsgBox("Not enough memory!");
		return false;
	}

	float *fPtr = fCubicSplineLUT;
	for (int32_t i = 0; i < INTRP_PHASES; i++)
	{
		const double x1 = i * (1.0 / INTRP_PHASES);
		const double x2 = x1 * x1; // x^2
		const double x3 = x2 * x1; // x^3

		const double t1 = (x1 * -0.5) + (x2 *  1.0) + (x3 * -0.5);
		const double t2 =               (x2 * -2.5) + (x3 *  1.5) + 1.0;
		const double t3 = (x1 *  0.5) + (x2 *  2.0) + (x3 * -1.5);
		const double t4 =               (x2 * -0.5) + (x3 *  0.5);

		*fPtr++ = (float)t1; // tap #1 at sample offset -1
		*fPtr++ = (float)t2; // tap #2 at sample offset  0 (center)
		*fPtr++ = (float)t3; // tap #3 at sample offset  1
		*fPtr++ = (float)t4; // tap #4 at sample offset  2
	}

	// windowed-sinc (8-point/16-point)

	sincKernel_t *k = sincKernelConfig;
	for (int32_t i = 0; i < SINC_KERNELS; i++, k++)
	{
		 fSinc8[i] = (float *)malloc(INTRP_PHASES * SINC8_TAPS  * sizeof (float));
		fSinc16[i] = (float *)malloc(INTRP_PHASES * SINC16_TAPS * sizeof (float));

		if (fSinc8[i] == NULL || fSinc16[i] == NULL)
		{
			showErrorMsgBox("Not enough memory!");
			return false;
		}

		calcPolyphaseSincLUT( fSinc8[i], SINC8_TAPS,  k->kaiserBeta, k->sincCutoff);
		calcPolyphaseSincLUT(fSinc16[i], SINC16_TAPS, k->kaiserBeta, k->sincCutoff);
	}

	// resampling ratios for sinc kernel selection
	sincRatio1 = (uint64_t)(1.1875 * MIXER_FRAC_SCALE); // fSinc[0] if <=
	sincRatio2 = (uint64_t)(1.5000 * MIXER_FRAC_SCALE); // fSinc[1] if <=, else fSinc[2] if >

	return true;
}

void freeMixerInterpolationTables(void)
{
	if (fCubicSplineLUT != NULL)
	{
		free(fCubicSplineLUT);
		fCubicSplineLUT = NULL;
	}

	for (int32_t i = 0; i < SINC_KERNELS; i++)
	{
		if (fSinc8[i] != NULL)
		{
			free(fSinc8[i]);
			fSinc8[i] = NULL;
		}

		if (fSinc16[i] != NULL)
		{
			free(fSinc16[i]);
			fSinc16[i] = NULL;
		}
	}
}

// zeroth-order modified Bessel function of the first kind (series approximation)
static inline double besselI0(double z)
{
	double s = 1.0, ds = 1.0, d = 2.0;
	const double zz = z * z;

	do
	{
		ds *= zz / (d * d);
		s += ds;
		d += 2.0;
	}
	while (ds > s*(1E-12));

	return s;
}

static inline double sinc(double x, double cutoff)
{
	if (x == 0.0)
	{
		return cutoff;
	}
	else
	{
		x *= PI;
		return sin(cutoff * x) / x;
	}
}

// note: numTaps must be 2^n!
static void calcPolyphaseSincLUT(float *fOut, int32_t numTaps, double kaiserBeta, double sincCutoff)
{
	const double besselI0BetaMul = 1.0 / besselI0(kaiserBeta);
	const int32_t pointBits = (int32_t)log2(numTaps);
	const int32_t pointMask = numTaps - 1;
	const int32_t centerPoint = (numTaps / 2) - 1;
	const double xMul = 1.0 / (numTaps / 2);

	for (int32_t i = 0; i < numTaps * INTRP_PHASES; i++)
	{
		const double x = ((i & pointMask) - centerPoint) - ((i >> pointBits) * (1.0 / INTRP_PHASES));

		// Kaiser-Bessel window
		const double n = x * xMul;
		const double window = besselI0(kaiserBeta * sqrt(1.0 - n * n)) * besselI0BetaMul;

		fOut[i] = (float)(sinc(x, sincCutoff) * window);
	}
}
