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 }