Browse Source

VCO: Use linear interpolation instead of Catmull-Rom. Generate minBLEP impulse with doubles instead of floats.

v2
Andrew Belt 1 month ago
parent
commit
6e908fd93b
1 changed files with 136 additions and 30 deletions
  1. +136
    -30
      src/VCO.cpp

+ 136
- 30
src/VCO.cpp View File

@@ -13,18 +13,6 @@ inline T sin_pi_9(T x) {
return x * (T(1) - x2) * (T(3.141521108990) + x2 * (T(-2.024773594411) + x2 * (T(0.517493161073) + x2 * T(-0.063691343423))));
}

/** Catmull-Rom cubic interpolation
t is the fractional position between y1 and y2.
*/
template <typename T>
T catmullRomInterpolate(T y0, T y1, T y2, T y3, T t) {
T t2 = t * t;
T t3 = t2 * t;
return y1 + T(0.5) * t * (y2 - y0)
+ t2 * (y0 - T(2.5)*y1 + T(2)*y2 - T(0.5)*y3)
+ t3 * (T(-0.5)*y0 + T(1.5)*y1 - T(1.5)*y2 + T(0.5)*y3);
}

/** One-pole low-pass filter, exponential moving average.
-6 dB/octave slope.
Useful for leaky integrators and smoothing control signals.
@@ -90,37 +78,157 @@ struct OnePoleHighpass : OnePoleLowpass {
};


/** Simple radix-2 Cooley-Tukey FFT. n must be a power of 2. */
template <typename T>
void fft(std::complex<T>* x, int n, bool inverse = false) {
// Bit reversal permutation
for (int i = 1, j = 0; i < n; i++) {
int bit = n >> 1;
for (; j & bit; bit >>= 1)
j ^= bit;
j ^= bit;
if (i < j)
std::swap(x[i], x[j]);
}

// Cooley-Tukey butterflies
for (int len = 2; len <= n; len <<= 1) {
T ang = (inverse ? 1 : -1) * 2 * M_PI / len;
std::complex<T> wn(std::cos(ang), std::sin(ang));
for (int i = 0; i < n; i += len) {
std::complex<T> w(1);
for (int j = 0; j < len / 2; j++) {
std::complex<T> u = x[i + j];
std::complex<T> v = x[i + j + len / 2] * w;
x[i + j] = u + v;
x[i + j + len / 2] = u - v;
w *= wn;
}
}
}

if (inverse) {
for (int i = 0; i < n; i++)
x[i] /= T(n);
}
}


template <typename T>
inline T blackmanHarris(T p) {
return
+ T(0.35875)
- T(0.48829) * simd::cos(T(2 * M_PI) * p)
+ T(0.14128) * simd::cos(T(4 * M_PI) * p)
- T(0.01168) * simd::cos(T(6 * M_PI) * p);
}


/** Computes the minimum-phase bandlimited step (minBLEP), an impulse step response that rises from 0 to 1.
Z: number of zero-crossings on each side of the original symmetric sync signal
O: oversample factor
output: must be length `(2 * Z) * O`.

Algorithm from "Hard Sync Without Aliasing" by Eli Brandt (2001).
https://www.cs.cmu.edu/~eli/papers/icmc01-hardsync.pdf
*/
inline void minBlepImpulse(int z, int o, float* output) {
// Symmetric sinc impulse with `z` zero-crossings on each side
int n = 2 * z * o;
std::complex<double>* x = new std::complex<double>[n];
for (int i = 0; i < n; i++) {
double p = (double) i / o - z;
x[i] = (p == 0.0) ? 1.0 : std::sin(M_PI * p) / (M_PI * p);
}

// Apply window
for (int i = 0; i < n; i++) {
x[i] *= blackmanHarris(i / (double) (n - 1));
}

// Reconstruct impulse response with minimum phase
fft(x, n);

// Take log of magnitude and set phase to zero.
// Limit frequency bin to e^-10
for (int i = 0; i < n; i++) {
x[i] = std::log(std::abs(x[i]));
}


// Transform to cepstral domain.
fft(x, n, true);

// Apply minimum-phase window in cepstral domain
// Double positive quefrencies, zero negative quefrencies
for (int i = 1; i < n / 2; i++) {
x[i] *= 2.0;
}
for (int i = n / 2; i < n; i++) {
x[i] = 0.0;
}

// Transform back to frequency domain
fft(x, n);

// Take complex exponential of each bin
for (int i = 0; i < n; i++) {
x[i] = std::exp(x[i]);
}

// Transform to time domain
fft(x, n, true);

// Integrate
double total = 0.0;
for (int i = 0; i < n; i++) {
total += x[i].real();
x[i] = total;
}

// Renormalize so last sample is 1
for (int i = 0; i < n; i++) {
output[i] = x[i].real() / total;
}

FILE* f = fopen("plugins/Fundamental/minblep.txt", "w");
DEFER({fclose(f);});
for (int i = 0; i < n; i++) {
fprintf(f, "%.12f\n", output[i]);
}

delete[] x;
}


/** Holds a precomputed minBLEP impulse response, reordered to efficiently insert into an audio buffer.
*/
template <int Z, int O>
struct MinBlep {
/** Reordered impulse response for cubic interpolation, minus 1.0.
Z dimension has +1 padding at start (for z-1 wrap) and +4 at end (for SIMD + z+1 wrap).
Access via getImpulse() which handles O wrapping and Z offset.
/** Reordered impulse response for linear interpolation, minus 1.0.
Z dimension has +4 padding at end for SIMD and o+1 wrap.
Access via getImpulse() which handles O wrapping.
*/
float impulseReordered[O][1 + 2 * Z + 4] = {};
float impulseReordered[O][2 * Z + 4] = {};

MinBlep() {
float impulse[2 * Z][O];
dsp::minBlepImpulse(Z, O, &impulse[0][0]);
minBlepImpulse(Z, O, &impulse[0][0]);

// Fill impulseReordered with transposed and pre-subtracted data
// Storage index = z + 1 (to allow z = -1 access at index 0)
for (int o = 0; o < O; o++) {
// z = -1: before impulse starts
impulseReordered[o][0] = -1.f;
// z = 0 to 2*Z-1: actual impulse data
for (int z = 0; z < 2 * Z; z++) {
impulseReordered[o][1 + z] = impulse[z][o] - 1.f;
impulseReordered[o][z] = impulse[z][o] - 1.f;
}
// z = 2*Z to 2*Z+3: after impulse ends (for SIMD padding)
for (int z = 2 * Z; z < 2 * Z + 4; z++) {
impulseReordered[o][1 + z] = 0.f;
impulseReordered[o][z] = 0.f;
}
}
}

/** Get pointer to impulse data, handling o wrapping and z offset. */
/** Get pointer to impulse data, handling o wrapping. */
const float* getImpulse(int o, int z) const {
z += o / O;
o = o % O;
@@ -129,8 +237,8 @@ struct MinBlep {
o += O;
}
// assert(0 <= o && o < O);
// assert(-1 <= z && z < 2 * Z + 4);
return &impulseReordered[o][z + 1];
// assert(0 <= z && z < 2 * Z + 4);
return &impulseReordered[o][z];
}

/** Places a discontinuity at 0 < p <= 1 relative to the current frame.
@@ -149,14 +257,12 @@ struct MinBlep {
int o = (int) subsample;
float t = subsample - o;

// For each zero crossing, interpolate impulse response from oversample points
using simd::float_4;
// For each zero crossing, linearly interpolate impulse response from oversample points
for (int z = 0; z < 2 * Z; z += 4) {
float_4 y0 = float_4::load(getImpulse(o - 1, z));
using simd::float_4;
float_4 y1 = float_4::load(getImpulse(o, z));
float_4 y2 = float_4::load(getImpulse(o + 1, z));
float_4 y3 = float_4::load(getImpulse(o + 2, z));
float_4 y = catmullRomInterpolate(y0, y1, y2, y3, float_4(t));
float_4 y = y1 + t * (y2 - y1);
y *= magnitude;

// Write all 4 samples to buffer


Loading…
Cancel
Save