Files
DP44/DataPRO/FftSharp/Transform.cs

528 lines
21 KiB
C#
Raw Normal View History

2026-04-17 14:55:32 -04:00
using DTS.Common.Interface;
using System;
using System.Buffers;
using System.Linq;
using System.Threading.Tasks;
namespace FftSharp
{
public static class Transform
{
/// <summary>
/// Compute the discrete Fourier Transform (in-place) using the FFT algorithm.
/// </summary>
/// <param name="buffer">Data to transform in-place. Length must be a power of 2.</param>
public static void FFT(Complex[] buffer)
{
if (buffer is null)
throw new ArgumentNullException(nameof(buffer));
FFT(buffer.AsSpan());
}
/// <summary>
/// Compute the discrete Fourier Transform (in-place) using the FFT algorithm.
/// </summary>
/// <param name="buffer">Data to transform in-place. Length must be a power of 2.</param>
public static void FFT(Span<Complex> buffer)
{
if (buffer.Length == 0)
throw new ArgumentException("Buffer must not be empty");
if (!IsPowerOfTwo(buffer.Length))
throw new ArgumentException("Buffer length must be a power of 2");
FFT_WithoutChecks(buffer);
}
/// <summary>
/// High performance FFT function.
/// Complex input will be transformed in place.
/// Input array length must be a power of two. This length is NOT validated.
/// Running on an array with an invalid length may throw an invalid index exception.
/// </summary>
private static void FFT_WithoutChecks(Span<Complex> buffer)
{
for (int i = 1; i < buffer.Length; i++)
{
int j = BitReverse(i, buffer.Length);
if (j > i)
(buffer[j], buffer[i]) = (buffer[i], buffer[j]);
}
for (int i = 1; i <= buffer.Length / 2; i *= 2)
{
double mult1 = -Math.PI / i;
for (int j = 0; j < buffer.Length; j += (i * 2))
{
for (int k = 0; k < i; k++)
{
int evenI = j + k;
int oddI = j + k + i;
Complex temp = new Complex(Math.Cos(mult1 * k), Math.Sin(mult1 * k));
temp *= buffer[oddI];
buffer[oddI] = buffer[evenI] - temp;
buffer[evenI] += temp;
}
}
}
}
/// <summary>
/// Compute the inverse discrete Fourier Transform (in-place) using the FFT algorithm.
/// </summary>
/// <param name="buffer">Data to transform in-place. Length must be a power of 2.</param>
public static void IFFT(Complex[] buffer)
{
if (buffer is null)
throw new ArgumentNullException(nameof(buffer));
if (buffer.Length == 0)
throw new ArgumentException("Buffer must not be empty");
if (!IsPowerOfTwo(buffer.Length))
throw new ArgumentException("Buffer length must be a power of 2");
IFFT_WithoutChecks(buffer);
}
private static void IFFT_WithoutChecks(Complex[] buffer)
{
// invert the imaginary component
for (int i = 0; i < buffer.Length; i++)
buffer[i] = new Complex(buffer[i].Real, -buffer[i].Imaginary);
// perform a forward Fourier transform
FFT(buffer);
// invert the imaginary component again and scale the output
for (int i = 0; i < buffer.Length; i++)
buffer[i] = new Complex(
real: buffer[i].Real / buffer.Length,
imaginary: -buffer[i].Imaginary / buffer.Length);
}
/// <summary>
/// Reverse the sequence of bits in an integer (01101 -> 10110)
/// </summary>
private static int BitReverse(int value, int maxValue)
{
int maxBitCount = (int)Math.Log(maxValue, 2);
int output = value;
int bitCount = maxBitCount - 1;
value >>= 1;
while (value > 0)
{
output = (output << 1) | (value & 1);
bitCount -= 1;
value >>= 1;
}
return (output << bitCount) & ((1 << maxBitCount) - 1);
}
/// <summary>
/// Calculate sample frequency for each point in a FFT
/// </summary>
public static double[] FFTfreq(double sampleRate, int pointCount, bool oneSided = true)
{
double[] freqs = new double[pointCount];
if (oneSided)
{
double fftPeriodHz = sampleRate / pointCount / 2;
// freqs start at 0 and approach maxFreq
for (int i = 0; i < pointCount; i++)
freqs[i] = i * fftPeriodHz;
return freqs;
}
else
{
double fftPeriodHz = sampleRate / pointCount;
// first half: freqs start a 0 and approach maxFreq
int halfIndex = pointCount / 2;
for (int i = 0; i < halfIndex; i++)
freqs[i] = i * fftPeriodHz;
// second half: then start at -maxFreq and approach 0
for (int i = halfIndex; i < pointCount; i++)
freqs[i] = -(pointCount - i) * fftPeriodHz;
return freqs;
}
}
/// <summary>
/// Return the distance between each FFT point in frequency units (Hz)
/// </summary>
public static double FFTfreqPeriod(int sampleRate, int pointCount)
{
return .5 * sampleRate / pointCount;
}
/// <summary>
/// Test if a number is an even power of 2
/// </summary>
public static bool IsPowerOfTwo(int x)
{
return ((x & (x - 1)) == 0) && (x > 0);
}
/// <summary>
/// Create an array of Complex data given the real component
/// </summary>
public static Complex[] MakeComplex(double[] real)
{
Complex[] com = new Complex[real.Length];
MakeComplex(com, real);
return com;
}
/// <summary>
/// Create an array of Complex data given the real component
/// </summary>
public static void MakeComplex(Span<Complex> com, Span<double> real)
{
if (com.Length != real.Length)
throw new ArgumentOutOfRangeException("Input length must match");
for (int i = 0; i < real.Length; i++)
com[i] = new Complex(real[i], 0);
}
/// <summary>
/// Compute the 1D discrete Fourier Transform using the Fast Fourier Transform (FFT) algorithm
/// </summary>
/// <param name="input">real input (must be an array with length that is a power of 2)</param>
/// <returns>transformed input</returns>
public static Complex[] FFT(double[] input)
{
if (input is null)
throw new ArgumentNullException(nameof(input));
if (input.Length == 0)
throw new ArgumentException("Input must not be empty");
if (!IsPowerOfTwo(input.Length))
throw new ArgumentException("Input length must be an even power of 2");
Complex[] buffer = MakeComplex(input);
FFT(buffer);
return buffer;
}
/// <summary>
/// Compute the 1D discrete Fourier Transform using the Fast Fourier Transform (FFT) algorithm
/// </summary>
/// <param name="input">real input (must be an array with length that is a power of 2)</param>
/// <returns>real component of transformed input</returns>
public static Complex[] RFFT(double[] input)
{
if (input is null)
throw new ArgumentNullException(nameof(input));
if (input.Length == 0)
throw new ArgumentException("Input must not be empty");
if (!IsPowerOfTwo(input.Length))
throw new ArgumentException("Input length must be an even power of 2");
Complex[] realBuffer = new Complex[input.Length / 2 + 1];
RFFT(realBuffer, input);
return realBuffer;
}
/// <summary>
/// Compute the 1D discrete Fourier Transform using the Fast Fourier Transform (FFT) algorithm
/// </summary>
/// <param name="destination">Memory location of the results (must be an equal to input length / 2 + 1)</param>
/// <param name="input">real input (must be an array with length that is a power of 2)</param>
/// <returns>real component of transformed input</returns>
public static void RFFT(Span<Complex> destination, Span<double> input)
{
if (!IsPowerOfTwo(input.Length))
throw new ArgumentException("Input length must be an even power of 2");
if (destination.Length != input.Length / 2 + 1)
throw new ArgumentException("Destination length must be an equal to input length / 2 + 1");
Complex[] temp = ArrayPool<Complex>.Shared.Rent(input.Length);
try
{
Span<Complex> buffer = temp;
MakeComplex(buffer, input);
FFT(buffer);
buffer.Slice(0, destination.Length).CopyTo(destination);
}
catch (Exception ex)
{
throw new Exception("Could not calculate RFFT", ex);
}
finally
{
ArrayPool<Complex>.Shared.Return(temp);
}
}
/// <summary>
/// Return a Complex array as an array of its absolute values
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static double[] Absolute(Complex[] input)
{
double[] output = new double[input.Length];
for (int i = 0; i < output.Length; i++)
output[i] = input[i].Magnitude;
return output;
}
/// <summary>
/// Calculte power spectrum density (PSD) original (RMS) units
/// </summary>
/// <param name="input">real input</param>
public static double[] FFTmagnitude(double[] input)
{
double[] output = new double[input.Length / 2 + 1];
FFTmagnitude(output, input);
return output;
}
/// <summary>
/// Calculte power spectrum density (PSD) original (RMS) units
/// </summary>
/// <param name="destination">Memory location of the results.</param>
/// <param name="input">real input</param>
public static void FFTmagnitude(Span<double> destination, Span<double> input)
{
if (!IsPowerOfTwo(input.Length))
throw new ArgumentException("Input length must be an even power of 2");
var temp = ArrayPool<Complex>.Shared.Rent(destination.Length);
try
{
var buffer = temp.AsSpan(0, destination.Length);
// first calculate the FFT
RFFT(buffer, input);
// first point (DC component) is not doubled
destination[0] = buffer[0].Magnitude / input.Length;
// subsequent points are doubled to account for combined positive and negative frequencies
for (int i = 1; i < buffer.Length; i++)
destination[i] = 2 * buffer[i].Magnitude / input.Length;
}
catch (Exception ex)
{
throw new Exception("Could not calculate FFT magnitude", ex);
}
finally
{
ArrayPool<Complex>.Shared.Return(temp);
}
}
/// <summary>
/// Calculte power spectrum density (PSD) in dB units
/// </summary>
/// <param name="input">real input</param>
public static double[] FFTpower(double[] input)
{
if (!IsPowerOfTwo(input.Length))
throw new ArgumentException("Input length must be an even power of 2");
double[] output = FFTmagnitude(input);
Parallel.For(0, output.Length, i => output[i] = 2 * 10 * Math.Log10(output[i]));
return output;
}
/// <summary>
/// Calculte power spectrum density (PSD) in dB units
/// </summary>
/// <param name="destination">Memory location of the results.</param>
/// <param name="input">real input</param>
public static void FFTpower(Span<double> destination, double[] input)
{
if (!IsPowerOfTwo(input.Length))
throw new ArgumentException("Input length must be an even power of 2");
FFTmagnitude(destination, input);
for (int i = 0; i < destination.Length; i++)
destination[i] = 2 * 10 * Math.Log10(destination[i]);
}
public static double[] PSD_Welch(double[] input, long sampleRate, WindowType windowType, int windowWidth, int overlapPct, WindowAveragingType averagingType, SetReadCalcProgressValueDelegate SetProgress = null)
{
if (!IsPowerOfTwo(windowWidth))
throw new ArgumentException("Window width must be an even power of 2");
object counter_lock = new object();
var completed = 0;
var progress = 0D;
// "The signal is split up into overlapping segments: the original data segment is split up into L data segments of length M, overlapping by D points"
var overlapWidth = (int)(overlapPct / 100D * windowWidth);
var step = windowWidth - overlapWidth;
double[][] segments = new double[input.Length / step][];
if (null != SetProgress) SetProgress(string.Format(DTS.Common.Strings.Strings.GeneratingPSD, DTS.Common.Strings.Strings.GeneratingPSD_CreatingSegments), 0D);
Parallel.For(0, input.Length / step, index =>
{
var i = index * step;
var width = i + windowWidth < input.Length ? windowWidth : input.Length - i;
var newSegment = new double[windowWidth];
Array.Copy(input, i, newSegment, 0, width);
segments[i / step] = newSegment;
lock (counter_lock)
{
completed++;
var pct = (double)completed / (input.Length / step) * 100;
if (Math.Floor(pct) > progress)
{
progress = pct;
if (null != SetProgress) SetProgress(String.Empty, progress);//only update on whole % changes
}
}
});
if (null != SetProgress) SetProgress(string.Empty, 100D);
// "After the data is split up into overlapping segments, the individual L data segments have a window applied to them"
if (null != SetProgress) SetProgress(string.Format(DTS.Common.Strings.Strings.GeneratingPSD, DTS.Common.Strings.Strings.GeneratingPSD_ApplyingWindows), 0D);
var window = Window.GetWindow(windowType);
window.Create(segments[0].Length, false);
completed = 0;
progress = 0D;
Parallel.For(0, segments.Length, i =>
{
window.ApplyInPlace(segments[i], false);
lock (counter_lock)
{
completed++;
var pct = (double)completed / segments.Length * 100;
if (Math.Floor(pct) > progress)
{
progress = pct;
if (null != SetProgress) SetProgress(String.Empty, progress);//only update on whole % changes
}
}
});
if (null != SetProgress) SetProgress(string.Empty, 100D);
// "After doing the above, the periodogram is calculated by computing the discrete Fourier transform...
if (null != SetProgress) SetProgress(string.Format(DTS.Common.Strings.Strings.GeneratingPSD, DTS.Common.Strings.Strings.GeneratingPSD_CalculatingFFTs), 0D);
completed = 0;
progress = 0D;
Complex[][] windowFFTs = new Complex[segments.Length][];
Parallel.For(0, segments.Length, i =>
{
windowFFTs[i] = RFFT(segments[i]);
lock (counter_lock)
{
completed++;
var pct = (double)completed / segments.Length * 100;
if (Math.Floor(pct) > progress)
{
progress = pct;
if (null != SetProgress) SetProgress(String.Empty, progress);//only update on whole % changes
}
}
});
if (null != SetProgress) SetProgress(string.Empty, 100D);
//...and then computing the squared magnitude of the result.
// The individual periodograms are then averaged, which reduces the variance of the individual power measurements. The end result is an array of power measurements vs. frequency 'bin'."
if (null != SetProgress) SetProgress(string.Format(DTS.Common.Strings.Strings.GeneratingPSD, DTS.Common.Strings.Strings.GeneratingPSD_CalculatingResults), 0D);
var scale = sampleRate * windowWidth;
var denominator = sampleRate * segments.Length * windowWidth;
var results = new double[windowFFTs[0].Length];
progress = 0D;
Parallel.For(0, results.Length, i =>
{
switch (averagingType)
{
case WindowAveragingType.PeakHoldMax:
var max = windowFFTs.Max(fft => fft[i].MagnitudeSquared * 2);
results[i] = Math.Abs(max / scale);
break;
case WindowAveragingType.PeakHoldMin:
var min = windowFFTs.Where(fft => fft[i].MagnitudeSquared != 0).Min(fft => fft[i].MagnitudeSquared * 2);
results[i] = Math.Abs(min / scale);
break;
case WindowAveragingType.Averaging:
default:
var numerator = windowFFTs.Sum(fft => fft[i].MagnitudeSquared * 2);
results[i] = Math.Abs(numerator / denominator);
break;
}
lock (counter_lock)
{
completed++;
var pct = (double)completed / segments.Length * 100;
if (Math.Floor(pct) > progress)
{
progress = pct;
if (null != SetProgress) SetProgress(String.Empty, progress);//only update on whole % changes
}
}
});
if (null != SetProgress) SetProgress(string.Empty, 100D);
return results;
}
public static double MelToFreq(double mel)
{
return 700 * (Math.Pow(10, mel / 2595d) - 1);
}
public static double MelFromFreq(double frequencyHz)
{
return 2595 * Math.Log10(1 + frequencyHz / 700);
}
public static double[] MelScale(double[] fft, int sampleRate, int melBinCount)
{
double freqMax = sampleRate / 2;
double maxMel = MelFromFreq(freqMax);
double[] fftMel = new double[melBinCount];
double melPerBin = maxMel / (melBinCount + 1);
for (int binIndex = 0; binIndex < melBinCount; binIndex++)
{
double melLow = melPerBin * binIndex;
double melHigh = melPerBin * (binIndex + 2);
double freqLow = MelToFreq(melLow);
double freqHigh = MelToFreq(melHigh);
int indexLow = (int)(fft.Length * freqLow / freqMax);
int indexHigh = (int)(fft.Length * freqHigh / freqMax);
int indexSpan = indexHigh - indexLow;
double binScaleSum = 0;
for (int i = 0; i < indexSpan; i++)
{
double binFrac = (double)i / indexSpan;
double indexScale = (binFrac < .5) ? binFrac * 2 : 1 - binFrac;
binScaleSum += indexScale;
fftMel[binIndex] += fft[indexLow + i] * indexScale;
}
fftMel[binIndex] /= binScaleSum;
}
return fftMel;
}
public static T[] SubArray<T>(this T[] array, int offset, int length)
{
T[] result = new T[length];
Array.Copy(array, offset, result, 0, length);
return result;
}
}
}