1 /**
2 * Copyright: Copyright Auburn Sounds 2015 and later.
3 * License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
4 * Authors:   Guillaume Piolat
5 */
6 module dplug.dsp.fft;
7 
8 import core.stdc.string;
9 
10 import std.complex;
11 import std.math;
12 
13 import dplug.dsp.window;
14 import dplug.core.math;
15 import dplug.core.alignedbuffer;
16 
17 enum FFTDirection
18 {
19     FORWARD = 0,
20     REVERSE = 1
21 }
22 
23 /// Perform in-place FFT.
24 /// Equivalent to `std.numeric.fft`, but this one is nothrow @nogc.
25 void forwardFFT(T)(Complex!T[] buffer) nothrow @nogc
26 {
27     FFT_internal!(T, FFTDirection.FORWARD)(buffer);
28 }
29 
30 /// Perform in-place inverse FFT.
31 /// Equivalent to `std.numeric.inverseFft`, but this one is nothrow @nogc.
32 void inverseFFT(T)(Complex!T[] buffer) nothrow @nogc
33 {
34     FFT_internal!(T, FFTDirection.REVERSE)(buffer);
35 }
36 
37 private void FFT_internal(T, FFTDirection direction)(Complex!T[] buffer) nothrow @nogc
38 {
39     int size = cast(int)(buffer.length);
40     assert(isPowerOfTwo(size));
41     int m = iFloorLog2(size);
42 
43     // do the bit reversal
44     int i2 = cast(int)size / 2;
45     int j = 0;
46     for (int i = 0; i < size - 1; ++i)
47     {
48         if (i < j)
49         {
50             auto tmp = buffer[i];
51             buffer[i] = buffer[j];
52             buffer[j] = tmp;
53         }
54 
55         int k = i2;
56         while(k <= j)
57         {
58             j = j - k;
59             k = k / 2;
60         }
61         j += k;
62     }
63 
64     // compute the FFT
65     Complex!T c = Complex!T(-1);
66     int l2 = 1;
67     for (int l = 0; l < m; ++l)
68     {
69         int l1 = l2;
70         l2 = l2 * 2;
71         Complex!T u = 1;
72         for (int j2 = 0; j2 < l1; ++j2)
73         {
74             int i = j2;
75             while (i < size)
76             {
77                 int i1 = i + l1;
78                 Complex!T t1 = u * buffer[i1];
79                 buffer[i1] = buffer[i] - t1;
80                 buffer[i] += t1;
81                 i += l2;
82             }
83             u = u * c;
84         }
85 
86         T newImag = sqrt((1 - c.re) / 2);
87         static if (direction == FFTDirection.FORWARD)
88             newImag = -newImag;
89         T newReal = sqrt((1 + c.re) / 2);
90         c = Complex!T(newReal, newImag);
91     }
92 
93     // scaling when doing the reverse transformation, to avoid being multiplied by size
94     static if (direction == FFTDirection.REVERSE)
95     {
96         T divider = 1 / cast(T)size;
97         for (int i = 0; i < size; ++i)
98         {
99             buffer[i].re = buffer[i].re * divider;
100             buffer[i].im = buffer[i].im * divider;
101         }
102     }
103 }
104 
105 
106 // should operate the same as Phobos FFT
107 unittest
108 {
109     import std.numeric: approxEqual, fft;
110 
111     bool approxEqualArr(Complex!double[] a, Complex!double[] b) pure
112     {
113         foreach(i; 0..a.length)
114         {
115             if (!approxEqual(a[i].re, b[i].re))
116                 return false;
117             if (!approxEqual(a[i].im, b[i].im))
118                 return false;
119         }
120         return true;
121     }
122 
123     Complex!double[] A = [Complex!double(1, 0), Complex!double(13, -4), Complex!double(5, -5), Complex!double(0, 2)];
124     auto fftARef = fft(A);
125 
126     auto B = A.dup;
127     forwardFFT(B);
128     assert(approxEqualArr(B, fftARef));
129     inverseFFT(B);
130     assert(approxEqualArr(B, A));
131 }
132 
133 /// From a signal, output chunks of determined size, with optional overlap.
134 /// Introduces approximately windowSize/2 samples delay.
135 struct Segmenter(T)
136 {
137 nothrow:
138 @nogc:
139 
140     int segmentSize() pure const 
141     {
142         return _segmentSize;
143     }
144 
145     int analysisPeriod() pure const
146     {
147         return _analysisPeriod;
148     }
149 
150     /// To call at initialization and whenever samplerate changes.
151     /// segmentSize = size of sound segments, expressed in samples.
152     /// analysisPeriod = period of analysis results, allow to be more precise frequentially, expressed in samples.
153     void initialize(int segmentSize, int analysisPeriod)
154     {
155         assert(analysisPeriod <= segmentSize); // no support for zero overlap
156 
157         // 1-sized FFT support
158         if (analysisPeriod == 0)
159             analysisPeriod = 1;
160 
161         _segmentSize = segmentSize;
162         _analysisPeriod = analysisPeriod;
163 
164         // clear input delay
165         _buffer.reallocBuffer(_segmentSize);
166         _buffer[] = 0;
167         _index = 0;
168     }
169 
170     ~this()
171     {
172         _buffer.reallocBuffer(0);
173     }
174 
175     @disable this(this);
176 
177     // Push one sample, eventually call the delegate to process a segment.
178     bool feed(T x, scope void delegate(T[] segment) nothrow @nogc processSegment = null)
179     {
180         _buffer[_index] = x;
181         _index = _index + 1;
182         if (_index >= _segmentSize)
183         {
184             // process segment (optional)
185             if (processSegment !is null)
186                 processSegment(_buffer[0.._segmentSize]);
187 
188             // rotate buffer
189             {
190                 int samplesToDrop = _analysisPeriod;
191                 assert(0 < samplesToDrop && samplesToDrop <= _segmentSize);
192                 int remainingSamples = _segmentSize - samplesToDrop;
193 
194                 // FUTURE: use ring buffer instead of copy?
195                 memmove(_buffer.ptr, _buffer.ptr + samplesToDrop, T.sizeof * remainingSamples);
196                 _index = remainingSamples;
197 
198             }
199             return true;
200         }
201         else
202             return false;
203     }
204 
205     /// Returns: Internal buffer.
206     T[] buffer()
207     {
208         return _buffer;
209     }
210 
211 private:
212     T[] _buffer;
213     int _segmentSize;     // in samples
214     int _analysisPeriod; // in samples
215     int _index;
216 }
217 
218 
219 /// From short term windowed data, output the summed signal.
220 /// Segments can be irregular and have different size.
221 struct ShortTermReconstruction
222 {
223 nothrow:
224 @nogc:
225     /// maxSimultSegments is the maximum number of simulatneously summed samples.
226     /// maxSegmentLength in samples
227     void initialize(int maxSimultSegments, int maxSegmentLength)
228     {
229         _maxSegmentLength = maxSegmentLength;
230         _maxSimultSegments = maxSimultSegments;
231         _desc.reallocBuffer(maxSimultSegments);
232         for (int i = 0; i < _maxSimultSegments; ++i)
233         {
234             _desc[i].playOffset = 0;
235             _desc[i].length = 0;
236             _desc[i].buffer = null;
237             _desc[i].buffer.reallocBuffer(maxSegmentLength);
238             //reallocBuffer(_desc[i].buffer, maxSegmentLength);
239         } //)
240     }
241 
242     ~this()
243     {
244         if (_desc !is null)
245             for (int i = 0; i < _maxSimultSegments; ++i)
246                 _desc[i].buffer.reallocBuffer(0);
247         _desc.reallocBuffer(0);
248     }
249 
250     @disable this(this);
251 
252     // Copy segment to a free slot, and start its summing.
253     // The first sample of this segment will be played at next() call if delay is 0.
254     void startSegment(float[] newSegment, int delay = 0)
255     {
256         assert(newSegment.length <= _maxSegmentLength);
257 
258         for (int i = 0; i < _maxSimultSegments; ++i)
259         {
260             if (!_desc[i].active())
261             {
262                 int len = cast(int)(newSegment.length);
263                 _desc[i].playOffset = -delay;
264                 _desc[i].length = len;
265                 _desc[i].buffer[0..len] = newSegment[]; // copy segment
266                 return;
267             }
268         }
269 
270         assert(false); // maxSimultSegments too small, or usage error
271     }
272 
273     // Get next sample, update segment statuses.
274     deprecated("Use nextSample instead") alias next = nextSample;
275     float nextSample()
276     {
277         float sum = 0;
278         foreach(ref desc; _desc)
279         {
280             if (desc.playOffset < desc.length)
281             {
282                 if (desc.playOffset > 0)
283                     sum += desc.buffer[desc.playOffset];
284                 desc.playOffset += 1;
285             }
286         }
287         return sum;
288     }
289 
290 private:
291 
292     struct SegmentDesc
293     {
294         int playOffset; // offset in this segment
295         int length; // length in this segment
296         float[] buffer; // 0..length => data for this segment
297 
298         bool active() pure const nothrow @nogc
299         {
300             return playOffset < length;
301         }
302     }
303     int _maxSimultSegments;
304     int _maxSegmentLength;
305     SegmentDesc[] _desc;
306 }
307 
308 /// From a signal, output short term FFT data.
309 /// Variable overlap.
310 /// Introduces approximately windowSize/2 samples delay.
311 struct FFTAnalyzer(T)
312 {
313 public:
314 
315     /// To call at initialization and whenever samplerate changes.
316     /// windowSize = size of window, expressed in samples
317     /// fftSize = size of FFT. Must be power-of-two and >= windowSize. Missing samples are zero-padded in time domain.
318     /// analysisPeriod = period of analysis results, allow to be more precise frequentially, expressed in samples.
319     /// Basic overlap is achieved with windowSize = 2 * analysisPeriod
320     /// if zeroPhaseWindowing = true, "zero phase" windowing is used
321     /// (center of window is at first sample, zero-padding happen at center)
322     void initialize(int windowSize, int fftSize, int analysisPeriod, WindowDesc windowDesc, bool zeroPhaseWindowing) nothrow @nogc
323     {
324         assert(isPowerOfTwo(fftSize));
325         assert(fftSize >= windowSize);
326 
327         _zeroPhaseWindowing = zeroPhaseWindowing;
328 
329         _fftSize = fftSize;
330 
331         _window.initialize(windowDesc, windowSize);
332         _windowSize = windowSize;
333 
334         // account for window shape
335         _scaleFactor = fftSize / _window.sumOfWindowSamples();
336 
337         // account for overlap
338         _scaleFactor *= cast(float)(analysisPeriod) / windowSize;
339 
340         _segmenter.initialize(windowSize, analysisPeriod);
341     }
342 
343     bool feed(float x, Complex!T[] fftData) nothrow @nogc
344     {
345         void processSegment(T[] segment) nothrow @nogc
346         {
347             int windowSize = _windowSize;
348             assert(segment.length == _windowSize);
349 
350             T scaleFactor = _scaleFactor;
351 
352             if (_zeroPhaseWindowing)
353             {
354                 // "Zero Phase" windowing
355                 // Through clever reordering, phase of ouput coefficients will relate to the
356                 // center of the window
357                 //_
358                 // \_                   _/
359                 //   \                 /
360                 //    \               /
361                 //     \_____________/____
362                 int center = (_windowSize - 1) / 2; // position of center bin
363                 int nLeft = _windowSize - center;
364                 for (int i = 0; i < nLeft; ++i)
365                     fftData[i] = segment[center + i] * _window[center + i] * scaleFactor;
366 
367                 int nPadding = _fftSize - _windowSize;
368                 for (int i = 0; i < nPadding; ++i)
369                     fftData[nLeft + i] = 0.0f;
370 
371                 for (int i = 0; i < center; ++i)
372                     fftData[nLeft + nPadding + i] = segment[i] * _window[i] * scaleFactor;
373             }
374             else
375             {
376                 // "Normal" windowing
377                 // Phase of output coefficient will relate to the start of the buffer
378                 //      _
379                 //    _/ \_
380                 //   /     \
381                 //  /       \
382                 //_/         \____________
383 
384                 // fill FFT buffer and multiply by window
385                 for (int i = 0; i < _windowSize; ++i)
386                     fftData[i] = segment[i] * _window[i] * scaleFactor;
387 
388                 // zero-padding
389                 for (int i = _windowSize; i < _fftSize; ++i)
390                     fftData[i] = 0.0f;
391             }
392 
393             // perform forward FFT on this slice
394             forwardFFT!T(fftData[0.._fftSize]);
395         }
396 
397         return _segmenter.feed(x, &processSegment);
398     }
399 
400 private:
401     Segmenter!T _segmenter;
402     bool _zeroPhaseWindowing;
403     int _fftSize;        // in samples
404 
405     Window!T _window;
406     int _windowSize;     // in samples
407 
408     T _scaleFactor; // account to the shape of the windowing function
409 }
410 
411 unittest
412 {
413     FFTAnalyzer!float a;
414     a.initialize(1024, 2048, 512, WindowDesc(WindowType.HANN), true);
415 
416     FFTAnalyzer!double b;
417     b.initialize(1024, 2048, 512, WindowDesc(WindowType.HANN), false);
418 }
419 
420 
421 /// Converts a normalized frequency to a FFT bin.
422 /// Params:
423 ///     normalizedFrequency frequency in cycles per sample
424 ///     fftSize size of FFT
425 /// Returns: Corresponding fractional bin.
426 float convertNormalizedFrequencyToFFTBin(float normalizedFrequency, int fftSize) nothrow @nogc
427 {
428     return (normalizedFrequency * fftSize);
429 }
430 
431 /// Converts a frequency to a FFT bin.
432 /// Returns: Corresponding fractional bin.
433 float convertFrequencyToFFTBin(float frequencyHz, float samplingRate, int fftSize) nothrow @nogc
434 {
435     return (frequencyHz * fftSize) / samplingRate;
436 }
437 
438 /// Converts a frequency to a FFT bin.
439 /// Returns: Corresponding fractional bin.
440 float convertFrequencyToFFTBinInv(float frequencyHz, float invSamplingRate, int fftSize) nothrow @nogc
441 {
442     return (frequencyHz * fftSize) * invSamplingRate;
443 }
444 
445 /// Converts a FFT bin to a frequency.
446 /// Returns: Corresponding center frequency.
447 float convertFFTBinToFrequency(float fftBin, int fftSize, float samplingRate) nothrow @nogc
448 {
449     return (samplingRate * fftBin) / fftSize;
450 }
451 
452 /// Converts a FFT bin to a frequency.
453 /// Returns: Corresponding center frequency.
454 float convertFFTBinToFrequencyInv(float fftBin, float invFFTSize, float samplingRate) nothrow @nogc
455 {
456     return (samplingRate * fftBin) * invFFTSize;
457 }
458 
459 /// Converts a FFT bin to a normalized frequency.
460 /// Params:
461 ///     fftBin bin index of the FFT
462 ///     fftSize size of FFT
463 /// Returns: Corresponding normalized frequency
464 float convertFFTBinToNormalizedFrequency(float fftBin, int fftSize) nothrow @nogc
465 {
466     return fftBin / fftSize;
467 }
468 
469 
470 /// Converts a FFT bin to a normalized frequency.
471 /// Params:
472 ///     fftBin Bin index of the FFT.
473 ///     invFFTSize Inverse size of FFT.
474 /// Returns: Corresponding normalized frequency.
475 float convertFFTBinToNormalizedFrequencyInv(float fftBin, float invFFTSize) nothrow @nogc
476 {
477     return fftBin * invFFTSize;
478 }