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