using DTS.Common.Interface; using System; using System.Buffers; using System.Linq; using System.Threading.Tasks; namespace FftSharp { public static class Transform { /// /// Compute the discrete Fourier Transform (in-place) using the FFT algorithm. /// /// Data to transform in-place. Length must be a power of 2. public static void FFT(Complex[] buffer) { if (buffer is null) throw new ArgumentNullException(nameof(buffer)); FFT(buffer.AsSpan()); } /// /// Compute the discrete Fourier Transform (in-place) using the FFT algorithm. /// /// Data to transform in-place. Length must be a power of 2. public static void FFT(Span 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); } /// /// 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. /// private static void FFT_WithoutChecks(Span 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; } } } } /// /// Compute the inverse discrete Fourier Transform (in-place) using the FFT algorithm. /// /// Data to transform in-place. Length must be a power of 2. 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); } /// /// Reverse the sequence of bits in an integer (01101 -> 10110) /// 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); } /// /// Calculate sample frequency for each point in a FFT /// 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; } } /// /// Return the distance between each FFT point in frequency units (Hz) /// public static double FFTfreqPeriod(int sampleRate, int pointCount) { return .5 * sampleRate / pointCount; } /// /// Test if a number is an even power of 2 /// public static bool IsPowerOfTwo(int x) { return ((x & (x - 1)) == 0) && (x > 0); } /// /// Create an array of Complex data given the real component /// public static Complex[] MakeComplex(double[] real) { Complex[] com = new Complex[real.Length]; MakeComplex(com, real); return com; } /// /// Create an array of Complex data given the real component /// public static void MakeComplex(Span com, Span 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); } /// /// Compute the 1D discrete Fourier Transform using the Fast Fourier Transform (FFT) algorithm /// /// real input (must be an array with length that is a power of 2) /// transformed input 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; } /// /// Compute the 1D discrete Fourier Transform using the Fast Fourier Transform (FFT) algorithm /// /// real input (must be an array with length that is a power of 2) /// real component of transformed input 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; } /// /// Compute the 1D discrete Fourier Transform using the Fast Fourier Transform (FFT) algorithm /// /// Memory location of the results (must be an equal to input length / 2 + 1) /// real input (must be an array with length that is a power of 2) /// real component of transformed input public static void RFFT(Span destination, Span 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.Shared.Rent(input.Length); try { Span 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.Shared.Return(temp); } } /// /// Return a Complex array as an array of its absolute values /// /// /// 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; } /// /// Calculte power spectrum density (PSD) original (RMS) units /// /// real input public static double[] FFTmagnitude(double[] input) { double[] output = new double[input.Length / 2 + 1]; FFTmagnitude(output, input); return output; } /// /// Calculte power spectrum density (PSD) original (RMS) units /// /// Memory location of the results. /// real input public static void FFTmagnitude(Span destination, Span input) { if (!IsPowerOfTwo(input.Length)) throw new ArgumentException("Input length must be an even power of 2"); var temp = ArrayPool.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.Shared.Return(temp); } } /// /// Calculte power spectrum density (PSD) in dB units /// /// real input 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; } /// /// Calculte power spectrum density (PSD) in dB units /// /// Memory location of the results. /// real input public static void FFTpower(Span 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(this T[] array, int offset, int length) { T[] result = new T[length]; Array.Copy(array, offset, result, 0, length); return result; } } }