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.fir;
7 
8 import std.range,
9        std.math,
10        std.complex;
11 
12 import dplug.core,
13        dplug.dsp.fft,
14        dplug.dsp.delayline,
15        dplug.dsp.window;
16 
17 // FUTURE:
18 // - bandstop/bandstop IR
19 // - naive convolution
20 
21 // sinc impulse functions
22 
23 void generateLowpassImpulse(T)(T[] output, double cutoff, double samplerate) nothrow @nogc
24 {
25     checkFilterParams(output.length, cutoff, samplerate);
26     double cutoffNormalized = cutoff / samplerate;
27     double wc = cutoffNormalized * 2.0 * PI;
28 
29     int len = cast(int)(output.length);
30     for (int i = 0; i < len; ++i)
31     {
32         int x = i - (len / 2);
33         if (x == 0)
34             output[i] = wc;
35         else
36             output[i] = sin(wc * x) / cast(double)x;
37     }
38     normalizeImpulse(output);
39 }
40 
41 void generateHighpassImpulse(T)(T[] output, double cutoff, double samplerate) nothrow @nogc
42 {
43     checkFilterParams(output.length, cutoff, samplerate);
44     double cutoffNormalized = cutoff / samplerate;
45     double wc = cutoffNormalized * 2.0 * PI;
46 
47     int len = cast(int)(output.length);
48     for (int i = 0; i < len; ++i)
49     {
50         int x = i - (len / 2);
51         if (x == 0)
52             output[i] = 1 - wc;
53         else
54             output[i] = -sin(wc * x) / cast(double)x;
55     }
56     normalizeImpulse(output);
57 }
58 
59 void generateHilbertTransformer(T)(T[] outImpulse, WindowDesc windowDesc, double samplerate) nothrow @nogc
60 {
61     int size = cast(int)(outImpulse.length);
62     assert(isOdd(size));
63     int center = size / 2;
64 
65     for (int i = 0; i < center; ++i)
66     {
67         int xi = i - center;
68         double x = cast(double)xi;
69         if (isEven(xi))
70         {
71             outImpulse[i] = 0;
72             outImpulse[size - 1 - i] = 0;
73         }
74         else
75         {
76             double y = x * cast(double)PI / 2;
77             double sine = sin(y);
78             T value = cast(T)(-sine*sine / y);
79             value *= evalWindow(windowDesc, i, size);
80             outImpulse[i] = value;
81             outImpulse[size - 1 - i] = -value;
82         }
83     }
84     outImpulse[center] = 0;
85 }
86 
87 
88 /// Normalize impulse response.
89 /// Scale to make sum = 1.
90 void normalizeImpulse(T)(T[] inoutImpulse) nothrow @nogc
91 {
92     int size = cast(int)(inoutImpulse.length);
93     double sum = 0;
94     for (int i = 0; i < size; ++i)
95         sum += inoutImpulse[i];
96 
97     double invSum = 1 / sum;
98     for (int i = 0; i < size; ++i)
99         inoutImpulse[i] = cast(T)(inoutImpulse[i] * invSum);
100 }
101 
102 /// Returns: Length of temporary buffer needed for `minimumPhaseImpulse`.
103 int tempBufferSizeForMinPhase(T)(T[] inputImpulse) nothrow @nogc
104 {
105     return cast(int)( nextPowerOf2(inputImpulse.length) );
106 }
107 
108 /// From an impulse, computes a minimum-phase impulse
109 /// Courtesy of kasaudio, based on Aleksey Vaneev's algorithm
110 /// See: http://www.kvraudio.com/forum/viewtopic.php?t=197881
111 /// MAYDO: does it preserve amplitude?
112 void minimumPhaseImpulse(T)(T[] inoutImpulse,  Complex!T[] tempStorage) nothrow @nogc // alloc free version
113 {
114     assert(tempStorage.length >= tempBufferSizeForMinPhase(inoutImpulse));
115 
116     int N = cast(int)(inoutImpulse.length);
117     int fftSize = cast(int)( nextPowerOf2(inoutImpulse.length) );
118     assert(fftSize >= N);
119     int halfFFTSize = fftSize / 2;
120 
121     if (tempStorage.length < fftSize)
122         assert(false); // crash
123 
124     auto kernel = tempStorage;
125 
126     for (int i = 0; i < N; ++i)
127         kernel[i] = inoutImpulse[i];
128 
129     for (int i = N; i < fftSize; ++i)
130         kernel[i] = 0;
131 
132     forwardFFT!T(kernel[]);
133 
134     for (int i = 0; i < fftSize; ++i)
135         kernel[i] = log(abs(kernel[i]));
136 
137     inverseFFT!T(kernel[]);
138 
139     for (int i = 1; i < halfFFTSize; ++i)
140         kernel[i] *= 2;
141 
142     for (int i = halfFFTSize + 1; i < halfFFTSize; ++i)
143         kernel[i] = 0;
144 
145     forwardFFT!T(kernel[]);
146 
147     for (int i = 0; i < fftSize; ++i)
148         kernel[i] = complexExp(kernel[i]);
149 
150     inverseFFT!T(kernel[]);
151 
152     for (int i = 0; i < N; ++i)
153         inoutImpulse[i] = kernel[i].re;
154 }
155 
156 private Complex!T complexExp(T)(Complex!T z) nothrow @nogc
157 {
158     T mag = exp(z.re);
159     return Complex!T(mag * cos(z.re), mag * sin(z.im));
160 }
161 
162 
163 private static void checkFilterParams(size_t length, double cutoff, double sampleRate) nothrow @nogc
164 {
165     assert((length & 1) == 0, "FIR impulse length must be even");
166     assert(cutoff * 2 < sampleRate, "2x the cutoff exceed sampling rate, Nyquist disapproving");
167 }
168 
169 
170 unittest
171 {
172     double[256] lp_impulse;
173     double[256] hp_impulse;
174 
175     generateLowpassImpulse(lp_impulse[], 40.0, 44100.0);
176     generateHighpassImpulse(hp_impulse[], 40.0, 44100.0);
177 
178     Complex!double[] tempStorage = new Complex!double[tempBufferSizeForMinPhase(lp_impulse[])];
179     minimumPhaseImpulse(lp_impulse[], tempStorage);
180 
181     generateHilbertTransformer(lp_impulse[0..$-1], WindowDesc(WindowType.BLACKMANN_HARRIS), 44100.0);
182 }
183 
184 
185 // Composed of a delay-line, and an inpulse.
186 struct FIR(T)
187 {
188     /// Initializes the FIR filter. It's up to you to fill the impulse with something worthwhile.
189     void initialize(int sizeOfImpulse) nothrow @nogc
190     {
191         assert(sizeOfImpulse > 0);
192         _delayline.initialize(sizeOfImpulse);
193         _impulse.reallocBuffer(sizeOfImpulse);
194         _impulse[] = T.nan;
195     }
196 
197     ~this() nothrow @nogc
198     {
199         _impulse.reallocBuffer(0);
200         _tempBuffer.reallocBuffer(0);
201         _windowBuffer.reallocBuffer(0);
202     }
203 
204     @disable this(this);
205 
206     /// Returns: Filtered input sample, naive convolution.
207     T nextSample(T input) nothrow @nogc
208     {
209         _delayline.feedSample(input);
210         T sum = 0;
211         int N = cast(int)impulse.length;
212         for (int i = 0; i < N; ++i)
213             sum += _impulse.ptr[i] * _delayline.sampleFull(i);
214         return sum;
215     }
216 
217     /// Returns: Impulse response. If you write it, you can call makeMinimumPhase() next.
218     inout(T)[] impulse() inout nothrow @nogc
219     {
220         return _impulse;
221     }
222 
223     void makeMinimumPhase() nothrow @nogc
224     {
225         int sizeOfTemp = tempBufferSizeForMinPhase(_impulse);
226         _tempBuffer.reallocBuffer(sizeOfTemp);
227         minimumPhaseImpulse!T(_impulse, _tempBuffer);
228     }
229 
230     void applyWindow(WindowDesc windowDesc) nothrow @nogc
231     {
232         _windowBuffer.reallocBuffer(_impulse.length);
233         generateWindow(windowDesc, _windowBuffer);
234         foreach(i; 0.._impulse.length)
235         {
236             _impulse[i] *= _windowBuffer[i];
237         }
238     }
239 
240 private:
241     T[] _impulse;
242 
243     Delayline!T _delayline;
244     Complex!T[] _tempBuffer;
245     T[] _windowBuffer;
246 }
247 
248 unittest
249 {
250     FIR!double fir;
251     fir.initialize(32);
252     generateLowpassImpulse(fir.impulse(), 40.0, 44100.0);
253     fir.makeMinimumPhase();
254     fir.applyWindow(WindowDesc(WindowType.HANN));
255 }