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 }