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