1 /**
2 High-level interfaces for providing FFT analysis, real FFT, and resynthesis from grains.
3
4 Copyright: Guillaume Piolat 2015.
5 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
6 */
7 module dplug.fft.fft;
8
9 import core.stdc.string;
10
11 import std.math;
12 import std.complex;
13
14 import dplug.dsp.window;
15 import dplug.core.math;
16 import dplug.core.vec;
17 import dplug.fft;
18
19
20 enum FFTDirection
21 {
22 FORWARD = 0,
23 REVERSE = 1
24 }
25
26 /// Perform in-place FFT.
27 /// Equivalent to `std.numeric.fft`, but this one is nothrow @nogc.
28 public void forwardFFT(T)(Complex!T[] buffer) nothrow @nogc
29 {
30 FFT_internal!(T, FFTDirection.FORWARD)(buffer);
31 }
32
33 /// Perform in-place inverse FFT.
34 /// Equivalent to `std.numeric.inverseFft`, but this one is nothrow @nogc.
35 public void inverseFFT(T)(Complex!T[] buffer) nothrow @nogc
36 {
37 FFT_internal!(T, FFTDirection.REVERSE)(buffer);
38 }
39
40 // PERF: use pfft instead would be much faster
41 private void FFT_internal(T, FFTDirection direction)(Complex!T[] buffer) pure nothrow @nogc
42 {
43 static int intFloorLog2(int i) pure nothrow @nogc @safe
44 {
45 assert(i >= 1);
46 int result = 0;
47 while (i > 1)
48 {
49 i = i / 2;
50 result = result + 1;
51 }
52 return result;
53 }
54
55 int size = cast(int)(buffer.length);
56 assert(isPowerOfTwo(size));
57 int m = intFloorLog2(size);
58
59 Complex!T* pbuffer = buffer.ptr;
60
61 // do the bit reversal
62 int i2 = cast(int)size / 2;
63 int j = 0;
64 for (int i = 0; i < size - 1; ++i)
65 {
66 if (i < j)
67 {
68 auto tmp = pbuffer[i];
69 pbuffer[i] = pbuffer[j];
70 pbuffer[j] = tmp;
71 }
72
73 int k = i2;
74 while(k <= j)
75 {
76 j = j - k;
77 k = k / 2;
78 }
79 j += k;
80 }
81
82 // compute the FFT
83 Complex!T c = Complex!T(-1, 0);
84 int l2 = 1;
85 for (int l = 0; l < m; ++l)
86 {
87 int l1 = l2;
88 l2 = l2 * 2;
89 Complex!T u = Complex!T(1, 0);
90 for (int j2 = 0; j2 < l1; ++j2)
91 {
92 int i = j2;
93 while (i < size)
94 {
95 int i1 = i + l1;
96 Complex!T t1 = u * pbuffer[i1];
97 pbuffer[i1] = pbuffer[i] - t1;
98 pbuffer[i] += t1;
99 i += l2;
100 }
101 u = u * c;
102 }
103
104 T newImag = sqrt((1 - c.re) / 2);
105 static if (direction == FFTDirection.FORWARD)
106 newImag = -newImag;
107 T newReal = sqrt((1 + c.re) / 2);
108 c = Complex!T(newReal, 1.0f * newImag);
109 }
110
111 // scaling when doing the reverse transformation, to avoid being multiplied by size
112 static if (direction == FFTDirection.REVERSE)
113 {
114 T divider = 1 / cast(T)size;
115 for (int i = 0; i < size; ++i)
116 {
117 pbuffer[i] = pbuffer[i] * divider;
118 }
119 }
120 }
121
122
123 // should operate the same as Phobos FFT
124 unittest
125 {
126 import std.complex;
127 import std.numeric: fft;
128
129 bool approxEqualArrBuiltin(Complex!double[] a, Complex!double[] b) pure
130 {
131 foreach(i; 0..a.length)
132 {
133 if (!isClose(a[i].re, b[i].re))
134 return false;
135 if (!isClose(a[i].im, b[i].im))
136 return false;
137 }
138 return true;
139 }
140
141 bool approxEqualArr(Complex!double[] a, Complex!double[] b) pure
142 {
143 foreach(i; 0..a.length)
144 {
145 if (!isClose(a[i].re, b[i].re))
146 return false;
147 if (!isClose(a[i].im, b[i].im))
148 return false;
149 }
150 return true;
151 }
152
153 //BuiltinComplex!double[] A = [1+0i, 13-4i, 5-5i, 0+2i];
154 Complex!double[] Abis = [Complex!double(1, 0), Complex!double(13, -4), Complex!double(5,-5), Complex!double(0,2)];
155 Complex!double[] fftARef = fft(Abis);
156
157 auto B = Abis.dup;
158 forwardFFT!double(B);
159 assert(approxEqualArr(B, fftARef));
160 inverseFFT!double(B);
161 assert(approxEqualArrBuiltin(B, Abis));
162 }
163
164 /// From a signal, output chunks of determined size, with optional overlap.
165 /// Introduces approximately windowSize/2 samples delay.
166 struct Segmenter(T)
167 {
168 nothrow:
169 @nogc:
170
171 int segmentSize() pure const
172 {
173 return _segmentSize;
174 }
175
176 int analysisPeriod() pure const
177 {
178 return _analysisPeriod;
179 }
180
181 /// To call at initialization and whenever samplerate changes.
182 /// segmentSize = size of sound segments, expressed in samples.
183 /// analysisPeriod = period of analysis results, allow to be more precise frequentially, expressed in samples.
184 void initialize(int segmentSize, int analysisPeriod)
185 {
186 assert(analysisPeriod <= segmentSize); // no support for zero overlap
187
188 // 1-sized FFT support
189 if (analysisPeriod == 0)
190 analysisPeriod = 1;
191
192 _segmentSize = segmentSize;
193 _analysisPeriod = analysisPeriod;
194
195 // clear input delay
196 _buffer.reallocBuffer(_segmentSize);
197 _buffer[] = 0;
198 _index = 0;
199 }
200
201 ~this()
202 {
203 _buffer.reallocBuffer(0);
204 }
205
206 @disable this(this);
207
208 // Push one sample, eventually call the delegate to process a segment.
209 bool feed(T x, scope void delegate(T[] segment) nothrow @nogc processSegment = null)
210 {
211 _buffer[_index] = x;
212 _index = _index + 1;
213 if (_index >= _segmentSize)
214 {
215 // process segment (optional)
216 if (processSegment !is null)
217 processSegment(_buffer[0.._segmentSize]);
218
219 // rotate buffer
220 {
221 int samplesToDrop = _analysisPeriod;
222 assert(0 < samplesToDrop && samplesToDrop <= _segmentSize);
223 int remainingSamples = _segmentSize - samplesToDrop;
224
225 // FUTURE: use ring buffer instead of copy?
226 memmove(_buffer.ptr, _buffer.ptr + samplesToDrop, T.sizeof * remainingSamples);
227 _index = remainingSamples;
228
229 }
230 return true;
231 }
232 else
233 return false;
234 }
235
236 /// Returns: Internal buffer.
237 T[] buffer()
238 {
239 return _buffer;
240 }
241
242 private:
243 T[] _buffer;
244 int _segmentSize; // in samples
245 int _analysisPeriod; // in samples
246 int _index;
247 }
248
249
250 /// From short term windowed data, output the summed signal.
251 /// Segments can be irregular and have different size.
252 struct ShortTermReconstruction(T)
253 {
254 nothrow:
255 @nogc:
256 /// maxSimultSegments is the maximum number of simulatneously summed samples.
257 /// maxSegmentLength in samples
258 void initialize(int maxSimultSegments, int maxSegmentLength)
259 {
260 _maxSegmentLength = maxSegmentLength;
261 _maxSimultSegments = maxSimultSegments;
262
263 _desc.reallocBuffer(maxSimultSegments);
264 _mergedBuffer.reallocBuffer(_maxSegmentLength * _maxSimultSegments);
265
266 for (int i = 0; i < _maxSimultSegments; ++i)
267 {
268 _desc[i].playOffset = 0; // initially inactive
269 _desc[i].length = 0;
270 _desc[i].buffer = &_mergedBuffer[maxSegmentLength*i];
271 }
272 }
273
274 ~this()
275 {
276 _mergedBuffer.reallocBuffer(0);
277 _desc.reallocBuffer(0);
278 }
279
280 @disable this(this);
281
282 // Copy segment to a free slot, and start its summing.
283 // The first sample of this segment will be played at next() call if delay is 0.
284 void startSegment(T[] newSegment, int delay = 0)
285 {
286 assert(newSegment.length <= _maxSegmentLength);
287 int i = allocSegmentSlot();
288 int len = cast(int)(newSegment.length);
289 _desc[i].playOffset = -delay;
290 _desc[i].length = len;
291 _desc[i].buffer[0..len] = newSegment[]; // copy segment
292 }
293
294 // Same, but with the input being split into two slices A ~ B. This is a common case
295 // when summing zero-phase windows in STFT analysis.
296 void startSegmentSplitted(T[] segmentA, T[] segmentB, int delay = 0)
297 {
298 int i = allocSegmentSlot();
299 int lenA = cast(int)(segmentA.length);
300 int lenB = cast(int)(segmentB.length);
301 assert(lenA + lenB <= _maxSegmentLength);
302
303 _desc[i].playOffset = -delay;
304 _desc[i].length = lenA + lenB;
305 _desc[i].buffer[0..lenA] = segmentA[]; // copy segment part A
306 _desc[i].buffer[lenA..lenA+lenB] = segmentB[]; // copy segment part B
307 }
308
309 T nextSample()
310 {
311 float sum = 0;
312 foreach(ref desc; _desc)
313 {
314 if (desc.playOffset < desc.length)
315 {
316 if (desc.playOffset >= 0)
317 sum += desc.buffer[desc.playOffset];
318 desc.playOffset += 1;
319 }
320 }
321 return sum;
322 }
323
324 void nextBuffer(T* outAudio, int frames)
325 {
326 outAudio[0..frames] = 0;
327
328 // Add each pending segment
329 foreach(ref desc; _desc)
330 {
331 const int offset = desc.playOffset;
332 const int len = desc.length;
333 if (offset < len)
334 {
335 // Compute relative time event for the segment
336 int startOfSegment = -offset;
337 int endOfSegment = startOfSegment + len;
338
339 // Compute the area in 0..frames we can playback the segment
340 int startOfSumming = startOfSegment;
341 if (startOfSumming < 0)
342 startOfSumming = 0;
343 if (startOfSumming >= frames)
344 startOfSumming = frames;
345 int endOfSumming = endOfSegment;
346 if (endOfSumming >= frames)
347 endOfSumming = frames;
348
349 int count = endOfSumming - startOfSumming;
350 assert(count >= 0);
351
352 const(T)* segmentData = desc.buffer + offset;
353
354 // PERF: this can be optimized further
355 for (int i = startOfSumming; i < endOfSumming; ++i)
356 {
357 outAudio[i] += segmentData[i];
358 }
359 desc.playOffset = offset + frames;
360 }
361 // else disabled segment
362 }
363 }
364
365 private:
366
367 struct SegmentDesc
368 {
369 int playOffset; // offset in this segment
370 int length; // length in this segment
371 T* buffer; // 0..length => data for this segment
372
373 bool active() pure const nothrow @nogc
374 {
375 return playOffset < length;
376 }
377 }
378 int _maxSimultSegments;
379 int _maxSegmentLength;
380 SegmentDesc[] _desc;
381 T[] _mergedBuffer; // Allocation for every segment buffer.
382
383 int allocSegmentSlot()
384 {
385 for (int i = 0; i < _maxSimultSegments; ++i)
386 if (!_desc[i].active())
387 return i;
388 assert(false); // maxSimultSegments too small, or usage error
389 }
390 }
391
392 /// From a signal, output short term FFT data.
393 /// Variable overlap.
394 /// Introduces approximately windowSize/2 samples delay.
395 /// Uses a real FFT to gain some speed.
396 struct FFTAnalyzer(T)
397 {
398 public:
399
400 /// To call at initialization and whenever samplerate changes.
401 /// windowSize = size of window, expressed in samples
402 /// fftSize = size of FFT. Must be power-of-two and >= windowSize. Missing samples are zero-padded in time domain.
403 /// analysisPeriod = period of analysis results, allow to be more precise frequentially, expressed in samples.
404 /// Basic overlap is achieved with windowSize = 2 * analysisPeriod
405 /// if zeroPhaseWindowing = true, "zero phase" windowing is used
406 /// (center of window is at first sample, zero-padding happen at center)
407 void initialize(int windowSize,
408 int fftSize,
409 int analysisPeriod,
410 WindowDesc windowDesc,
411 bool zeroPhaseWindowing) nothrow @nogc
412 {
413 assert(isPowerOfTwo(fftSize));
414 assert(fftSize >= windowSize);
415
416 _zeroPhaseWindowing = zeroPhaseWindowing;
417
418 _fftSize = fftSize;
419
420 _window.initialize(windowDesc, windowSize);
421 _windowSize = windowSize;
422
423 // account for window shape
424 _scaleFactor = fftSize / _window.sumOfWindowSamples();
425
426 // account for overlap
427 _scaleFactor *= cast(float)(analysisPeriod) / windowSize;
428
429 _segmenter.initialize(windowSize, analysisPeriod);
430
431 _timeData.reallocBuffer(fftSize);
432 _rfft.initialize(fftSize);
433 }
434
435 ~this()
436 {
437 _timeData.reallocBuffer(0);
438 }
439
440 /// Gets the RFFT object which allows to perform efficient inverse FFT with the same pre-computed tables.
441 ref RFFT!T realFFT()
442 {
443 return _rfft;
444 }
445
446 bool feed(T x, Complex!T[] fftData) nothrow @nogc
447 {
448 void processSegment(T[] segment) nothrow @nogc
449 {
450 int windowSize = _windowSize;
451 assert(segment.length == _windowSize);
452
453 T scaleFactor = _scaleFactor;
454
455 if (_zeroPhaseWindowing)
456 {
457 // "Zero Phase" windowing
458 // Through clever reordering, phase of ouput coefficients will relate to the
459 // center of the window
460 //_
461 // \_ _/
462 // \ /
463 // \ /
464 // \_____________/____
465 int center = (_windowSize - 1) / 2; // position of center bin
466 int nLeft = _windowSize - center;
467 for (int i = 0; i < nLeft; ++i)
468 _timeData[i] = (segment[center + i] * _window[center + i] * scaleFactor);
469
470 int nPadding = _fftSize - _windowSize;
471 for (int i = 0; i < nPadding; ++i)
472 _timeData[nLeft + i] = 0;
473
474 for (int i = 0; i < center; ++i)
475 _timeData[nLeft + nPadding + i] = (segment[i] * _window[i] * scaleFactor);
476 }
477 else
478 {
479 // "Normal" windowing
480 // Phase of output coefficient will relate to the start of the buffer
481 // _
482 // _/ \_
483 // / \
484 // / \
485 //_/ \____________
486
487 // fill FFT buffer and multiply by window
488 for (int i = 0; i < _windowSize; ++i)
489 _timeData[i] = (segment[i] * _window[i] * scaleFactor);
490
491 // zero-padding
492 for (int i = _windowSize; i < _fftSize; ++i)
493 _timeData[i] = 0;
494 }
495
496 // If you fail here, you are giving a larger slice than strictly necessary to FFTAnalyzer.
497 // This can cause hard to find memory corruption if you read the slice one bin too far.
498 // Give a slice with length of exactly _fftSize/2+1.
499 assert(fftData.length == _fftSize/2+1, "FFTAnalyzer is given too large a slice");
500 _rfft.forwardTransform(_timeData[], fftData[0.._fftSize/2+1]);
501 }
502
503 return _segmenter.feed(x, &processSegment);
504 }
505
506 private:
507 Segmenter!T _segmenter;
508 bool _zeroPhaseWindowing;
509 int _fftSize; // in samples
510
511 Window!T _window;
512 int _windowSize; // in samples
513
514 T _scaleFactor; // account to the shape of the windowing function
515
516 RFFT!T _rfft;
517 T[] _timeData;
518 }
519
520 unittest
521 {
522 FFTAnalyzer!float a;
523 a.initialize(1024, 2048, 512, WindowDesc(WindowType.hann, WindowAlignment.left), true);
524
525 FFTAnalyzer!double b;
526 b.initialize(1024, 2048, 512, WindowDesc(WindowType.hann, WindowAlignment.right), false);
527 }
528
529
530 /// Converts a normalized frequency to a FFT bin.
531 /// Params:
532 /// normalizedFrequency = Frequency in cycles per sample.
533 /// fftSize = Size of FFT.
534 /// Returns: Corresponding fractional bin.
535 float convertNormalizedFrequencyToFFTBin(float normalizedFrequency, int fftSize) nothrow @nogc
536 {
537 return (normalizedFrequency * fftSize);
538 }
539
540 /// Converts a frequency to a FFT bin.
541 /// Returns: Corresponding fractional bin.
542 float convertFrequencyToFFTBin(float frequencyHz, float samplingRate, int fftSize) nothrow @nogc
543 {
544 return (frequencyHz * fftSize) / samplingRate;
545 }
546
547 /// Converts a frequency to a FFT bin.
548 /// Returns: Corresponding fractional bin.
549 float convertFrequencyToFFTBinInv(float frequencyHz, float invSamplingRate, int fftSize) nothrow @nogc
550 {
551 return (frequencyHz * fftSize) * invSamplingRate;
552 }
553
554 /// Converts a FFT bin to a frequency.
555 /// Returns: Corresponding center frequency.
556 float convertFFTBinToFrequency(float fftBin, int fftSize, float samplingRate) nothrow @nogc
557 {
558 return (samplingRate * fftBin) / fftSize;
559 }
560
561 /// Converts a FFT bin to a frequency.
562 /// Returns: Corresponding center frequency.
563 float convertFFTBinToFrequencyInv(float fftBin, float invFFTSize, float samplingRate) nothrow @nogc
564 {
565 return (samplingRate * fftBin) * invFFTSize;
566 }
567
568 /// Converts a FFT bin to a normalized frequency.
569 /// Params:
570 /// fftBin = Bin index in the FFT.
571 /// fftSize = Size of FFT.
572 /// Returns: Corresponding normalized frequency
573 float convertFFTBinToNormalizedFrequency(float fftBin, int fftSize) nothrow @nogc
574 {
575 return fftBin / fftSize;
576 }
577
578
579 /// Converts a FFT bin to a normalized frequency.
580 /// Params:
581 /// fftBin = Bin index of the FFT.
582 /// invFFTSize = Inverse size of FFT.
583 /// Returns: Corresponding normalized frequency.
584 float convertFFTBinToNormalizedFrequencyInv(float fftBin, float invFFTSize) nothrow @nogc
585 {
586 return fftBin * invFFTSize;
587 }
588
589 /// Perform a FFT from a real signal, saves up CPU.
590 struct RFFT(T)
591 {
592 public:
593 nothrow:
594 @nogc:
595
596 void initialize(int length)
597 {
598 _length = length;
599 _internal.initialize(length);
600
601 int newAlignment = cast(int)_internal.alignment(length);
602
603 // if the alignement changes, we can't reuse that buffer
604 if (_alignment != -1 && _alignment != newAlignment)
605 {
606 _buffer.reallocBuffer(0, _alignment);
607 }
608
609 _buffer.reallocBuffer(length, newAlignment);
610 _alignment = newAlignment;
611 }
612
613 ~this()
614 {
615 if (_buffer != null)
616 _buffer.reallocBuffer(0, _alignment);
617 }
618
619 @disable this(this);
620
621 void forwardTransform(const(T)[] timeData, Complex!T[] outputBins)
622 {
623 _buffer[] = timeData[];
624
625 // Perform real FFT
626 _internal.rfft(_buffer);
627
628 //_buffer[] =0;
629 // At this point, f contains:
630 // f destination array (frequency bins)
631 // f[0...length(x)/2] = real values,
632 // f[length(x)/2+1...length(x)-1] = imaginary values of coefficents 1...length(x)/2-1.
633 // So we have to reshuffle them to have nice complex bins.
634 int mid = _length/2;
635 outputBins[0] = Complex!T(_buffer[0], 0);
636 for(int i = 1; i < mid; ++i)
637 outputBins[i] = Complex!T(_buffer[i], _buffer[mid+i]);
638 outputBins[mid] = Complex!T(_buffer[mid], 0); // for length 1, this still works
639 }
640
641 /**
642 * Compute the inverse FFT of the array. Perform post-scaling.
643 *
644 * Params:
645 * inputBins = Source arrays (N/2 + 1 frequency bins).
646 * timeData = Destination array (N time samples).
647 *
648 * Note:
649 * This transform has the benefit you don't have to conjugate the "mirrored" part of the FFT.
650 * Excess data in imaginary part of DC and Nyquist bins are ignored.
651 */
652 void reverseTransform(Complex!T[] inputBins, T[] timeData)
653 {
654 // On inverse transform, scale down result
655 T invMultiplier = cast(T)1 / _length;
656
657 // Shuffle input frequency bins, and scale down.
658 int mid = _length/2;
659 for(int i = 0; i <= mid; ++i)
660 _buffer[i] = inputBins[i].re * invMultiplier;
661 for(int i = mid+1; i < _length; ++i)
662 _buffer[i] = inputBins[i-mid].im * invMultiplier;
663
664 // At this point, the format in f is:
665 // f [0...length(x)/2] = real values
666 // f [length(x)/2+1...length(x)-1] = negative imaginary values of coefficents 1...length(x)/2-1.
667 // Which is suitable for the RealFFT algorithm.
668 _internal.irfft(_buffer);
669
670 // Perf: use scaling from pfft
671 timeData[] = _buffer[];
672 }
673
674 private:
675 // Required alignment for RFFT buffers.
676 int _alignment = -1;
677
678 // pfft object
679 Rfft!T _internal;
680
681 // length of FFT
682 int _length;
683
684 // temporary buffer since pfft is in-place
685 T[] _buffer;
686 }
687
688
689 unittest
690 {
691 for (int i = 0; i < 16; ++i)
692 {
693 RFFT!float rfft;
694 rfft.initialize(128);
695 rfft.initialize(2048);
696 }
697 }
698
699 /// From an impulse, computes a minimum-phase impulse
700 /// Courtesy of kasaudio, based on Aleksey Vaneev's algorithm
701 /// See: http://www.kvraudio.com/forum/viewtopic.php?t=197881
702 /// MAYDO: does it preserve amplitude?
703 ///
704 /// Params:
705 /// tempoStorate Should be at least `tempBufferSizeForMinPhase` items.
706 void minimumPhaseImpulse(T)(T[] inoutImpulse, Complex!T[] tempStorage) nothrow @nogc // alloc free version
707 {
708 assert(tempStorage.length >= tempBufferSizeForMinPhase(inoutImpulse));
709
710 int N = cast(int)(inoutImpulse.length);
711 int fftSize = cast(int)( nextPow2HigherOrEqual(inoutImpulse.length * 4));
712 assert(fftSize >= N);
713 int halfFFTSize = fftSize / 2;
714
715 if (tempStorage.length < fftSize)
716 assert(false); // crash
717
718 auto kernel = tempStorage;
719
720 // Put the real impulse in a larger buffer
721 for (int i = 0; i < N; ++i)
722 kernel[i] = Complex!T(inoutImpulse[i], 0);
723 for (int i = N; i < fftSize; ++i)
724 kernel[i] = Complex!T(0, 0);
725
726 forwardFFT!T(kernel[]);
727
728 // Take the log-modulus of spectrum
729 for (int i = 0; i < fftSize; ++i)
730 kernel[i] = Complex!T( log(std.complex.abs(kernel[i])), 0);
731
732 // Back to real cepstrum
733 inverseFFT!T(kernel[]);
734
735 // Apply a cepstrum window, not sure how this works
736 kernel[0] = Complex!T(kernel[0].re, 0);
737 for (int i = 1; i < halfFFTSize; ++i)
738 kernel[i] = Complex!T(kernel[i].re * 2, 0);
739 kernel[halfFFTSize] = Complex!T(kernel[halfFFTSize].re, 0);
740 for (int i = halfFFTSize + 1; i < fftSize; ++i)
741 kernel[i] = Complex!T(0, 0);
742
743 forwardFFT!T(kernel[]);
744
745 for (int i = 0; i < fftSize; ++i)
746 kernel[i] = complexExp!T(kernel[i]);
747
748 inverseFFT!T(kernel[]);
749
750 for (int i = 0; i < N; ++i)
751 inoutImpulse[i] = kernel[i].re;
752 }
753 unittest
754 {
755 double[256] impulse;
756 foreach(size_t i, ref double d; impulse)
757 d = i;
758 Complex!double[] tempStorage = new Complex!double[tempBufferSizeForMinPhase(impulse[])];
759 minimumPhaseImpulse!double(impulse[], tempStorage);
760 }
761
762 /// Returns: Length of temporary buffer needed for `minimumPhaseImpulse`.
763 int tempBufferSizeForMinPhase(T)(T[] inputImpulse) nothrow @nogc
764 {
765 return cast(int)( nextPow2HigherOrEqual(inputImpulse.length * 4)); // PERF: too much?
766 }
767
768 private Complex!T complexExp(T)(Complex!T z) nothrow @nogc
769 {
770 T mag = exp(z.re);
771 return Complex!T( (mag * cos(z.im)) , (mag * sin(z.im)) );
772 }