namespace TravelEar.Core;
///
/// In-place radix-2 complex FFT over interleaved (re, im) pairs, the shape the phase vocoder
/// wants (). Unnormalised in both directions, like the classic
/// smbFft: a forward then inverse pass scales by the frame length. Allocation-free; the twiddles
/// are computed per stage.
///
public static class Fft
{
/// True for a power of two at least 2.
public static bool IsValidLength(int n) => n >= 2 && (n & (n - 1)) == 0;
// [impl->REQ-MIXER-RESYNTH]
///
/// Transforms (2 * n floats, re/im interleaved) in place; sign -1 is
/// the forward transform, +1 the inverse.
///
public static void Transform(Span buffer, int n, int sign)
{
if (!IsValidLength(n)) throw new ArgumentException("The length must be a power of two.", nameof(n));
if (buffer.Length < 2 * n) throw new ArgumentException("The buffer holds fewer than n complex values.", nameof(buffer));
// Bit-reversal permutation.
var bits = 0;
for (var t = n; t > 1; t >>= 1) bits++;
for (var i = 0; i < n; i++)
{
var r = ReverseBits(i, bits);
if (r > i)
{
(buffer[2 * i], buffer[2 * r]) = (buffer[2 * r], buffer[2 * i]);
(buffer[2 * i + 1], buffer[2 * r + 1]) = (buffer[2 * r + 1], buffer[2 * i + 1]);
}
}
// Danielson-Lanczos stages.
for (var le = 2; le <= n; le <<= 1)
{
var half = le >> 1;
var angle = sign * MathF.PI / half;
var wpr = MathF.Cos(angle);
var wpi = MathF.Sin(angle);
var wr = 1f;
var wi = 0f;
for (var j = 0; j < half; j++)
{
for (var i = j; i < n; i += le)
{
var k = i + half;
var tr = wr * buffer[2 * k] - wi * buffer[2 * k + 1];
var ti = wr * buffer[2 * k + 1] + wi * buffer[2 * k];
buffer[2 * k] = buffer[2 * i] - tr;
buffer[2 * k + 1] = buffer[2 * i + 1] - ti;
buffer[2 * i] += tr;
buffer[2 * i + 1] += ti;
}
var next = wr * wpr - wi * wpi;
wi = wr * wpi + wi * wpr;
wr = next;
}
}
}
private static int ReverseBits(int value, int bits)
{
var r = 0;
for (var b = 0; b < bits; b++)
{
r = (r << 1) | (value & 1);
value >>= 1;
}
return r;
}
/// The magnitude of bin of a transformed buffer.
public static float Magnitude(ReadOnlySpan buffer, int k)
=> MathF.Sqrt(buffer[2 * k] * buffer[2 * k] + buffer[2 * k + 1] * buffer[2 * k + 1]);
///
/// The bin with the largest magnitude in [1, n/2) of a real signal's forward transform,
/// as a frequency in Hz — the test-side "what pitch is this" probe.
///
public static float DominantFrequency(ReadOnlySpan signal, int n, int sampleRate)
{
var work = new float[2 * n];
for (var i = 0; i < n && i < signal.Length; i++)
{
var window = 0.5f - 0.5f * MathF.Cos(2f * MathF.PI * i / n);
work[2 * i] = signal[i] * window;
}
Transform(work, n, -1);
var best = 1;
var bestMag = 0f;
for (var k = 1; k < n / 2; k++)
{
var m = Magnitude(work, k);
if (m > bestMag) { bestMag = m; best = k; }
}
return best * (float)sampleRate / n;
}
}