1 /** 2 * Various window types. 3 * 4 * Copyright: Copyright Auburn Sounds 2015 and later. 5 * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 * Authors: Guillaume Piolat 7 */ 8 module dplug.dsp.window; 9 10 import std.math; 11 12 import dplug.core.vec; 13 14 // Useful reference: 15 // https://en.wikipedia.org/wiki/Window_function 16 // These window generation functions are explicit with the end-points, 17 // else it would be very easy to get confused and ignorant of these issues. 18 19 enum WindowType 20 { 21 /// Constant window. 22 rect, 23 24 /// Triangular window. You probably want something better. 25 bartlett, 26 27 /// Good default choice if you lack ideas. 28 hann, 29 30 hamming, 31 32 nuttall, 33 34 blackmannNutall, 35 36 blackmannHarris, 37 38 flatTopSR785, // Flat top window 39 40 /// Kaiser-Bessel window, this one need a parameter alpha (typical values: 1.0 to 4.0) 41 /// Note: Kaiser windows are talked about in terms of parameters alpha or beta. 42 kaiserBessel, 43 44 // Older names 45 46 /// Please use the new name instead. 47 RECT = rect, 48 49 /// Please use the new name instead. 50 BARTLETT = bartlett, 51 52 /// Please use the new name instead. 53 HANN = hann, 54 55 /// Please use the new name instead. 56 HAMMING = hamming, 57 58 /// Please use the new name instead. 59 NUTTALL = nuttall, 60 61 /// Please use the new name instead. 62 BLACKMANN_NUTTALL = blackmannNutall, 63 64 /// Please use the new name instead. 65 BLACKMANN_HARRIS = blackmannHarris, 66 67 /// Please use the new name instead. 68 FLATTOP_SR785 = flatTopSR785, 69 70 /// Please use the new name instead. 71 KAISER_BESSEL = kaiserBessel, 72 } 73 74 75 /// How "aligned" the window is in its support. 76 /// Very important, see Issue #236. 77 /// When choosing a window, you probably want to take a hard look about `WindowSymmetry` 78 /// because it has implications for latency and correctness. 79 enum WindowAlignment 80 { 81 /// The window is asymmetric and if it goes to zero (eg: HANN), it will have one zero 82 /// at the FIRST coefficient output[N-1]. 83 /// Its center is exactly at output[(N/2)-1] which is an integer delay for even window 84 /// lengths. 85 /// This loose one sample of latency, however it has the easiest to compute latency 86 /// in most usage. 87 right, 88 89 /// The window is asymmetric and if it goes to zero (eg: HANN), it will have one zero 90 /// at the LAST coefficient output[N-1]. 91 /// Its center is exactly at output[(N/2)-1] which is an integer delay for even window 92 /// lengths. 93 /// This has the best latency characteristics, however this might make the latency 94 /// computation itself trickier. 95 left, 96 97 /// The window is symmetric and if it goes to zero (eg: HANN), it will have two zero 98 /// coefficients. 99 /// Its center is exactly between samples at position output[(N - 1)/2] which is NOT 100 /// an integer delay for even window lengths. 101 /// Such a window might also break derivativeness. 102 /// However FOR HISTORICAL REASONS THIS WAS THE DEFAULT SETTINGS. 103 /// IT IS ADVISED NOT TO USE IT, FOR THE CONSEQUENCES ARE SUBTLE AND OFTEN HARMFUL. 104 symmetric, 105 } 106 107 108 struct WindowDesc 109 { 110 public: 111 nothrow: 112 @nogc: 113 114 /// Construct a window description, with a foolproof constructor. 115 this(WindowType type, WindowAlignment alignment, float param = float.nan) 116 { 117 this.type = type; 118 this.alignment = alignment; 119 this.param = param; 120 } 121 122 /// Construct a window description, for support with previous 123 deprecated("Because of subtle issues creeping with window end points, please provide an explicit WindowAlignment. Use the other WindowDesc constructor, see window.d for more information.") 124 this(WindowType type, float param = float.nan) 125 { 126 this.type = type; 127 this.alignment = WindowAlignment.symmetric; // because it was the default before 128 this.param = param; 129 } 130 131 private: 132 WindowType type; 133 134 // TODO: this is a bad default! You'll probably want WindowAlignment.right instead. 135 // Make sure it isn't used implicitely anymore. 136 // Then deprecate WindowAlignment.symmetric. 137 WindowAlignment alignment = WindowAlignment.symmetric; 138 139 float param; 140 } 141 142 /// Generates a window described by `windowDesc`, with periodicity of 143 /// `outputWindow.length`. 144 void generateWindow(T)(WindowDesc desc, T[] outputWindow) pure nothrow @nogc 145 { 146 int N = cast(int)(outputWindow.length); 147 for (int i = 0; i < N; ++i) 148 { 149 outputWindow[i] = cast(T)(evalWindow(desc, i, N)); 150 } 151 } 152 153 /// Multiplies the given slice in-place by a window described by `windowDesc`, 154 /// whose periodicity is `inoutImpulse.length`. 155 void multiplyByWindow(T)(T[] inoutImpulse, WindowDesc windowDesc) pure nothrow @nogc 156 { 157 int N = cast(int)(inoutImpulse.length); 158 for (int i = 0; i < N; ++i) 159 { 160 inoutImpulse[i] *= evalWindow(windowDesc, i, N); 161 } 162 } 163 164 deprecated void generateNormalizedWindow(T)(WindowDesc desc, T[] output) pure nothrow @nogc 165 { 166 int N = cast(int)(output.length); 167 T sum = 0; 168 for (int i = 0; i < N; ++i) 169 { 170 output[i] = cast(T)(evalWindow(desc, i, N)); 171 sum += output[i]; 172 } 173 T invSum = 1 / sum; 174 for (int i = 0; i < N; ++i) 175 output[i] *= invSum; 176 } 177 178 // Note: N is the number of output samples, not necessarily the periodicity 179 double evalWindow(WindowDesc desc, int n, int N) pure nothrow @nogc 180 { 181 final switch(desc.alignment) with (WindowAlignment) 182 { 183 case right: 184 return evalWindowInternal(desc, n, N); 185 186 case left: 187 // left are just a rotation of the right-aligned window 188 if (n == 0) 189 return evalWindowInternal(desc, N - 1, N); 190 else 191 return evalWindowInternal(desc, n + 1, N); 192 193 case symmetric: 194 // Symmetric is just a shorter window 195 return evalWindowInternal(desc, n, N - 1); 196 } 197 } 198 199 200 struct Window(T) if (is(T == float) || is(T == double)) 201 { 202 void initialize(WindowDesc desc, int lengthInSamples) nothrow @nogc 203 { 204 _lengthInSamples = lengthInSamples; 205 _window.reallocBuffer(lengthInSamples); 206 generateWindow!T(desc, _window); 207 } 208 209 ~this() nothrow @nogc 210 { 211 _window.reallocBuffer(0); 212 } 213 214 double sumOfWindowSamples() pure const nothrow @nogc 215 { 216 double result = 0; 217 foreach(windowValue; _window) 218 result += windowValue; 219 return result; 220 } 221 222 @disable this(this); 223 224 T[] _window = null; 225 int _lengthInSamples; 226 alias _window this; 227 } 228 229 230 private: 231 232 // Generates WindowAlignment.right window types. 233 // N is the periodicity. 234 // Does NOT look at desc.alignment, which is taken care of in `evalWindow`. 235 double evalWindowInternal(WindowDesc desc, int n, int N) pure nothrow @nogc 236 { 237 static double computeKaiserFunction(double alpha, int n, int N) pure nothrow @nogc 238 { 239 static double I0(double x) pure nothrow @nogc 240 { 241 double sum = 1; 242 double mx = x * 0.5; 243 double denom = 1; 244 double numer = 1; 245 for (int m = 1; m <= 32; m++) 246 { 247 numer *= mx; 248 denom *= m; 249 double term = numer / denom; 250 sum += term * term; 251 } 252 return sum; 253 } 254 255 double piAlpha = PI * alpha; 256 double C = 2.0 * n / cast(double)N - 1.0; 257 double result = I0(piAlpha * sqrt(1.0 - C * C)) / I0(piAlpha); 258 return result; 259 } 260 261 final switch(desc.type) 262 { 263 case WindowType.RECT: 264 return 1.0; 265 266 case WindowType.BARTLETT: 267 { 268 double nm1 = cast(double)N / 2; 269 return 1 - abs(n - nm1) / nm1; 270 } 271 272 case WindowType.HANN: 273 { 274 double phi = (2 * PI * n ) / N; 275 return 0.5 - 0.5 * cos(phi); 276 } 277 278 case WindowType.HAMMING: 279 { 280 double phi = (2 * PI * n ) / N; 281 return 0.54 - 0.46 * cos(phi); 282 } 283 284 case WindowType.NUTTALL: 285 { 286 double phi = (2 * PI * n ) / N; 287 return 0.355768 - 0.487396 * cos(phi) + 0.144232 * cos(2 * phi) - 0.012604 * cos(3 * phi); 288 } 289 290 case WindowType.BLACKMANN_HARRIS: 291 { 292 double phi = (2 * PI * n ) / N; 293 return 0.35875 - 0.48829 * cos(phi) + 0.14128 * cos(2 * phi) - 0.01168 * cos(3 * phi); 294 } 295 296 case WindowType.BLACKMANN_NUTTALL: 297 { 298 double phi = (2 * PI * n ) / N; 299 return 0.3635819 - 0.4891775 * cos(phi) + 0.1365995 * cos(2 * phi) - 0.0106411 * cos(3 * phi); 300 } 301 302 case WindowType.FLATTOP_SR785: 303 { 304 double phi = (2 * PI * n ) / cast(double)N; 305 return 1 - 1.93 * cos(phi) 306 + 1.29 * cos(2 * phi) 307 - 0.388 * cos(3 * phi) 308 + 0.028 * cos(4 * phi); 309 } 310 311 case WindowType.KAISER_BESSEL: 312 { 313 return computeKaiserFunction(desc.param, n, N); 314 } 315 } 316 } 317 318 319 // Tests the general shape of windows. 320 unittest 321 { 322 void checkIsSymmetric(T)(T[] inp) 323 { 324 for (int i = 0; i < inp.length/2; ++i) 325 { 326 // Window generation should be precise 327 assert(approxEqual(inp[i], inp[$-1-i], 1e-10)); 328 } 329 } 330 331 void checkMaxInCenter(T)(T[] inp) 332 { 333 bool odd = (inp.length & 1) != 0; 334 if (odd) 335 { 336 for (int i = 0; i < inp.length/2; ++i) 337 { 338 assert(inp[i] <= inp[inp.length/2]); 339 assert(inp[$-1-i] <= inp[inp.length/2]); 340 } 341 } 342 else 343 { 344 for (int i = 0; i < inp.length/2-1; ++i) 345 { 346 // take either of the centers 347 assert(inp[i] <= inp[inp.length/2-1]); 348 assert(inp[$-1-i] <= inp[inp.length/2-1]); 349 } 350 } 351 } 352 353 void testAllWindows(T)(int size) 354 { 355 Window!T window; 356 357 foreach(type; WindowType.min..WindowType.max+1) 358 { 359 foreach(alignment; WindowAlignment.min..WindowAlignment.max+1) 360 { 361 // Note: only Kaiser-Bessel have a parameter for now, so take 2 362 WindowDesc desc = WindowDesc(cast(WindowType)type, 363 cast(WindowAlignment)alignment, 364 2.0); 365 366 window.initialize(desc, size); 367 368 //import std.stdio; 369 //writeln(desc); 370 371 final switch(alignment) with (WindowAlignment) 372 { 373 case WindowAlignment.symmetric: 374 checkIsSymmetric(window[0..$]); 375 checkMaxInCenter(window[0..$]); 376 break; 377 378 case WindowAlignment.right: 379 checkIsSymmetric(window[1..$]); 380 checkMaxInCenter(window[1..$]); 381 break; 382 383 case WindowAlignment.left: 384 checkIsSymmetric(window[0..$-1]); 385 checkMaxInCenter(window[0..$-1]); 386 break; 387 } 388 } 389 } 390 } 391 392 testAllWindows!float(64); 393 testAllWindows!double(32); 394 395 // It should be possible to generate odd-sized window, 396 // though in practice with WindowAlignement you don't need them. 397 testAllWindows!float(5); 398 }