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