f@0
|
1 #include "AccessibleSpectrumAnalyser.h"
|
f@0
|
2 #include "IPlug_include_in_plug_src.h"
|
f@0
|
3 #include "IControl.h"
|
f@0
|
4 #include "resource.h"
|
f@0
|
5 #include "IBitmapMonoText.h"
|
f@0
|
6
|
f@0
|
7 #include <cmath>
|
f@0
|
8
|
f@0
|
9 #include "FreqView.h"
|
f@0
|
10 #include "Controls.h"
|
f@0
|
11
|
f@0
|
12 const int kNumPrograms = 1;
|
f@0
|
13
|
f@0
|
14 #define FFT_FORWARD false
|
f@0
|
15
|
f@0
|
16 enum EParams
|
f@0
|
17 {
|
f@0
|
18 kThreshold = 0,
|
f@0
|
19 kSelStart,
|
f@0
|
20 kSelSize,
|
f@0
|
21 kDry,
|
f@0
|
22 kWet,
|
f@0
|
23 kNumParams
|
f@0
|
24 };
|
f@0
|
25
|
f@0
|
26 enum ELayout
|
f@0
|
27 {
|
f@0
|
28 kWidth = GUI_WIDTH,
|
f@0
|
29 kHeight = GUI_HEIGHT,
|
f@0
|
30
|
f@0
|
31 lKnobSizeW = 52,
|
f@0
|
32 lKnobSizeH = 48 + 19 + 19, // 48 for image, 19 for text
|
f@0
|
33
|
f@0
|
34 lKnobsY = 315,
|
f@0
|
35
|
f@0
|
36 lThresX = 40,
|
f@0
|
37
|
f@0
|
38 lSelStartX = 250,
|
f@0
|
39 lSelSizeX = 345,
|
f@0
|
40
|
f@0
|
41 lDryX = 540,
|
f@0
|
42 lWetX = 610,
|
f@0
|
43
|
f@0
|
44 kKnobFrames = 60
|
f@0
|
45 };
|
f@0
|
46
|
f@0
|
47 AccessibleSpectrumAnalyser::AccessibleSpectrumAnalyser(IPlugInstanceInfo instanceInfo) :
|
f@0
|
48 IPLUG_CTOR(kNumParams, kNumPrograms, instanceInfo),
|
f@0
|
49 mDry(-6.0),
|
f@0
|
50 mWet(-6.0),
|
f@0
|
51 mFFTBufferIndx(0),
|
f@0
|
52 mNormFactor(0),
|
f@0
|
53 mBinSizeHz(44100/(2*kFFTbufSize))
|
f@0
|
54 {
|
f@0
|
55 TRACE;
|
f@0
|
56
|
f@0
|
57 /* init hanning window lookup table */
|
f@0
|
58
|
f@0
|
59 mHannTable.reserve(kFFTbufSize);
|
f@0
|
60
|
f@0
|
61 for (int i = 0; i < kFFTbufSize; i++){
|
f@0
|
62 mHannTable.push_back( 0.5 * (1. - std::cos(k2pi * i / (kFFTbufSize - 1) )) );
|
f@0
|
63 }
|
f@0
|
64
|
f@0
|
65 for (int i = 0; i < kFFTbufSize; i++) {
|
f@0
|
66 mNormFactor += 0.5 * (1. - std::cos(k2pi * i / (double)(kFFTbufSize - 1)));
|
f@0
|
67 }
|
f@0
|
68
|
f@0
|
69 /* init FFT engine */
|
f@0
|
70 WDL_fft_init();
|
f@0
|
71
|
f@0
|
72 /* init parameters */
|
f@0
|
73 //arguments are: name, defaultVal, minVal, maxVal, step, label, group, shape
|
f@0
|
74
|
f@0
|
75 GetParam(kThreshold)->InitDouble("Threshold", 0.0, -60.0, 6.2, 10, "dB");
|
f@0
|
76
|
f@0
|
77 GetParam(kSelStart)->InitDouble("Selection Start", 20, 20, 44100/2, 10, "Hz");
|
f@0
|
78
|
f@0
|
79 GetParam(kSelSize)->InitDouble("Selection Size", 50, kMinSelectionSize, 10000, 10, "Hz");
|
f@0
|
80
|
f@0
|
81 GetParam(kDry)->InitDouble("Dry", -6., -61.0, 0., 0.2, "dB");
|
f@0
|
82 GetParam(kDry)->SetDisplayText(-61.0, " -inf");
|
f@0
|
83
|
f@0
|
84 GetParam(kWet)->InitDouble("Wet", -6., -61.0, 0., 0.2, "dB");
|
f@0
|
85 GetParam(kWet)->SetDisplayText(-61.0, " -inf");
|
f@0
|
86
|
f@0
|
87
|
f@0
|
88 IGraphics* pGraphics = MakeGraphics(this, kWidth, kHeight);
|
f@0
|
89 pGraphics->AttachPanelBackground(&kBgColor);
|
f@0
|
90
|
f@0
|
91 mFreqView = new FreqView(this, IRECT(4, 40, GUI_WIDTH - 4, 300), kFFTbufSize/2);
|
f@0
|
92 pGraphics->AttachControl(mFreqView);
|
f@0
|
93
|
f@0
|
94 IBitmap knob = pGraphics->LoadIBitmap(KNOB_ID, KNOB_FN, kKnobFrames);
|
f@0
|
95 /* text has info about the font-size, font-type etc. */
|
f@0
|
96 IText text = IText(14);
|
f@0
|
97
|
f@0
|
98 /* attach dry and wet knobs to GUI */
|
f@0
|
99 pGraphics->AttachControl(new IKnobMultiControlText(this,
|
f@0
|
100 IRECT(lThresX, lKnobsY, lThresX + lKnobSizeW, lKnobsY + lKnobSizeH ),
|
f@0
|
101 kThreshold,
|
f@0
|
102 &knob,
|
f@0
|
103 &text,
|
f@0
|
104 27) );
|
f@0
|
105
|
f@0
|
106 pGraphics->AttachControl(new IKnobMultiControlText(this,
|
f@0
|
107 IRECT(lSelStartX, lKnobsY, lSelStartX + lKnobSizeW, lKnobsY + lKnobSizeH ),
|
f@0
|
108 kSelStart,
|
f@0
|
109 &knob,
|
f@0
|
110 &text,
|
f@0
|
111 27) );
|
f@0
|
112
|
f@0
|
113 pGraphics->AttachControl(new IKnobMultiControlText(this,
|
f@0
|
114 IRECT(lSelSizeX, lKnobsY, lSelSizeX + lKnobSizeW, lKnobsY + lKnobSizeH ),
|
f@0
|
115 kSelSize,
|
f@0
|
116 &knob,
|
f@0
|
117 &text,
|
f@0
|
118 27) );
|
f@0
|
119
|
f@0
|
120 pGraphics->AttachControl(new IKnobMultiControlText(this,
|
f@0
|
121 IRECT(lDryX, lKnobsY, lDryX + lKnobSizeW, lKnobsY + lKnobSizeH ),
|
f@0
|
122 kDry,
|
f@0
|
123 &knob,
|
f@0
|
124 &text,
|
f@0
|
125 27) );
|
f@0
|
126
|
f@0
|
127 pGraphics->AttachControl(new IKnobMultiControlText(this,
|
f@0
|
128 IRECT(lWetX, lKnobsY, lWetX + lKnobSizeW, lKnobsY + lKnobSizeH),
|
f@0
|
129 kWet,
|
f@0
|
130 &knob,
|
f@0
|
131 &text,
|
f@0
|
132 27) );
|
f@0
|
133
|
f@0
|
134
|
f@0
|
135 AttachGraphics(pGraphics);
|
f@0
|
136 //MakePreset("preset 1", ... );
|
f@0
|
137 MakeDefaultPreset((char *) "-", kNumPrograms);
|
f@0
|
138
|
f@0
|
139 }
|
f@0
|
140
|
f@0
|
141 AccessibleSpectrumAnalyser::~AccessibleSpectrumAnalyser() {}
|
f@0
|
142
|
f@0
|
143 /*
|
f@0
|
144 from Ask Justin Frankel:
|
f@0
|
145 first, call WDL_fft_init(). Then create a buffer of WDL_FFT_COMPLEX, populate its real components with some audio,
|
f@0
|
146 the imaginary with 0s, then optionally apply a windowing function, then call WDL_fft(),
|
f@0
|
147 then you can access the results using WDL_fft_permute() to get the correct indices.
|
f@0
|
148 */
|
f@0
|
149
|
f@0
|
150 void AccessibleSpectrumAnalyser::ProcessDoubleReplacing(double** inputs, double** outputs, int nFrames)
|
f@0
|
151 {
|
f@0
|
152 // Mutex is already locked for us.
|
f@0
|
153
|
f@0
|
154 double* in1 = inputs[0];
|
f@0
|
155 double* in2 = inputs[1];
|
f@0
|
156 double* out1 = outputs[0];
|
f@0
|
157 double* out2 = outputs[1];
|
f@0
|
158
|
f@0
|
159 for (int f = 0; f < nFrames; ++f, ++in1, ++in2, ++out1, ++out2){
|
f@0
|
160
|
f@0
|
161 mFFTbuffer[mFFTBufferIndx].re = (*in1 + *in2) / 2;
|
f@0
|
162 /* apply Hann window */
|
f@0
|
163 mFFTbuffer[mFFTBufferIndx].re *= mHannTable[mFFTBufferIndx];
|
f@0
|
164 mFFTbuffer[mFFTBufferIndx].im = 0;
|
f@0
|
165
|
f@0
|
166 /* when gathered enough samples perform FFT */
|
f@0
|
167 if (mFFTBufferIndx == kFFTbufSize - 1){
|
f@0
|
168
|
f@0
|
169 WDL_fft(mFFTbuffer, kFFTbufSize, FFT_FORWARD);
|
f@0
|
170
|
f@0
|
171 float bins[kFFTbufSize / 2];
|
f@0
|
172
|
f@0
|
173 bool thresholdPassed = false;
|
f@0
|
174 double maxClippingDiff = 0.0;
|
f@0
|
175 int maxClippingBinIndx = 0;
|
f@0
|
176
|
f@0
|
177 for (int i = 0; i < mFFTBufferIndx/2; i++){
|
f@0
|
178
|
f@0
|
179 int j = WDL_fft_permute(kFFTbufSize, i); // j is the index after permutation
|
f@0
|
180
|
f@0
|
181 /* fill the bins to plot with the magnitudes of the FFT*/
|
f@0
|
182 bins[i] = magnitude(mFFTbuffer[j]);
|
f@0
|
183
|
f@0
|
184 /* check bins within selection against threshold for sonification */
|
f@0
|
185 if( binWithinSelection(i) && bins[i] > mThreshold ){
|
f@0
|
186
|
f@0
|
187 thresholdPassed = true;
|
f@0
|
188
|
f@0
|
189 /* find the clipping amount in dB */
|
f@0
|
190 double clippingDiff = fabs(::AmpToDB(bins[i]) - ::AmpToDB(mThreshold));
|
f@0
|
191
|
f@0
|
192 /* clipDiff will be rounded to the floor later (with a cast to int),
|
f@0
|
193 but if it's very very close to the ceil, then let it be the ceil. */
|
f@0
|
194 const double ceilClippingDiff = ceil(clippingDiff);
|
f@0
|
195 if ( ceilClippingDiff - clippingDiff < kClippingCeilingSnap ){
|
f@0
|
196 clippingDiff = ceilClippingDiff;
|
f@0
|
197 }
|
f@0
|
198
|
f@0
|
199 /* bound the difference to 12 semitones to prevent the sonification from going too high */
|
f@0
|
200 clippingDiff = BOUNDED(clippingDiff, 0.0, 12.0);
|
f@0
|
201
|
f@0
|
202 if( clippingDiff > maxClippingDiff){
|
f@0
|
203 maxClippingDiff = clippingDiff;
|
f@0
|
204 maxClippingBinIndx = i;
|
f@0
|
205 }
|
f@0
|
206 }
|
f@0
|
207
|
f@0
|
208 }
|
f@0
|
209
|
f@0
|
210 if(thresholdPassed){
|
f@0
|
211 /* sonify the difference between the amplitude and threshold of highest bin
|
f@0
|
212 one db (rounded downward) is one tone, up to one octave (12 semitones) */
|
f@0
|
213 mSonification.ugen.setFrequency(midi2Freq(69 + int(maxClippingDiff)));
|
f@0
|
214
|
f@0
|
215 /* pan the sonification according to the position of the bin in the spectrum */
|
f@0
|
216 mSonification.panning.calculatePanningCoeff( double(maxClippingBinIndx)/(mFFTBufferIndx/2) );
|
f@0
|
217 mSonification.envelope.keyOn();
|
f@0
|
218 }
|
f@0
|
219
|
f@0
|
220 /* copy the bids in freqView for the graphics */
|
f@0
|
221 mFreqView->setBins(bins);
|
f@0
|
222 mFFTBufferIndx = 0;
|
f@0
|
223
|
f@0
|
224 }else{
|
f@0
|
225
|
f@0
|
226 /* increment index to gather more bins in the FFT buffer */
|
f@0
|
227 mFFTBufferIndx++;
|
f@0
|
228
|
f@0
|
229 }
|
f@0
|
230
|
f@0
|
231
|
f@0
|
232 /* now add the sonification to the audio */
|
f@0
|
233
|
f@0
|
234 /* when attack is done switch immediately to RELEASE (keyOff) *
|
f@0
|
235 * so it goes like: attack->release->silence */
|
f@0
|
236 if (mSonification.envelope.getState() == stk::ADSR::DECAY) {
|
f@0
|
237 mSonification.envelope.keyOff();
|
f@0
|
238 }
|
f@0
|
239
|
f@0
|
240 /* add the sonification to the mix */
|
f@0
|
241 if (mSonification.envelope.getState() == stk::ADSR::ATTACK ||
|
f@0
|
242 mSonification.envelope.getState() == stk::ADSR::RELEASE) {
|
f@0
|
243
|
f@0
|
244 const double env = mSonification.envelope.tick();
|
f@0
|
245 const double tick = mSonification.ugen.tick() * env;
|
f@0
|
246
|
f@0
|
247
|
f@0
|
248 *out1 = mix(*in1, tick) * mSonification.panning.left;
|
f@0
|
249 *out2 = mix(*in2, tick) * mSonification.panning.right;
|
f@0
|
250
|
f@0
|
251 } else { // no sonification
|
f@0
|
252
|
f@0
|
253 *out1 = mix(*in1, 0.0);
|
f@0
|
254 *out2 = mix(*in2, 0.0);
|
f@0
|
255
|
f@0
|
256 }
|
f@0
|
257 }
|
f@0
|
258 }
|
f@0
|
259
|
f@0
|
260 void AccessibleSpectrumAnalyser::Reset()
|
f@0
|
261 {
|
f@0
|
262 TRACE;
|
f@0
|
263 IMutexLock lock(this);
|
f@0
|
264
|
f@0
|
265 mSonification.reset(GetSampleRate());
|
f@0
|
266 mBinSizeHz = GetSampleRate() / kFFTbufSize;
|
f@0
|
267
|
f@0
|
268 mFreqView->setSampleRate(GetSampleRate());
|
f@0
|
269 mSelectionEnd = calculateSelectionEndFromView();
|
f@0
|
270 //double sr = GetSampleRate();
|
f@0
|
271 }
|
f@0
|
272
|
f@0
|
273 void AccessibleSpectrumAnalyser::OnParamChange(int paramIdx)
|
f@0
|
274 {
|
f@0
|
275 IMutexLock lock(this);
|
f@0
|
276
|
f@0
|
277 switch (paramIdx)
|
f@0
|
278 {
|
f@0
|
279
|
f@0
|
280 case kDry:
|
f@0
|
281 if (GetParam(kDry)->Value() < -60.5 ){
|
f@0
|
282 mDry = 0.0; /* if the level goes below 60.5 dB, just bring it to silence */
|
f@0
|
283 }
|
f@0
|
284 else {
|
f@0
|
285 mDry = ::DBToAmp(GetParam(kDry)->Value());
|
f@0
|
286 }
|
f@0
|
287 break;
|
f@0
|
288
|
f@0
|
289
|
f@0
|
290 case kWet:
|
f@0
|
291
|
f@0
|
292 if (GetParam(kWet)->Value() < -60.5 ){
|
f@0
|
293 mWet = 0.0;
|
f@0
|
294 }
|
f@0
|
295 else {
|
f@0
|
296 mWet = ::DBToAmp(GetParam(kWet)->Value());
|
f@0
|
297 }
|
f@0
|
298 break;
|
f@0
|
299
|
f@0
|
300
|
f@0
|
301 case kSelStart:
|
f@0
|
302 mFreqView->setSelectionStart(GetParam(kSelStart)->Value());
|
f@0
|
303 mSelectionEnd = calculateSelectionEndFromView();
|
f@0
|
304 break;
|
f@0
|
305
|
f@0
|
306 case kSelSize:
|
f@0
|
307 mFreqView->setSelectionSize(GetParam(kSelSize)->Value());
|
f@0
|
308 mSelectionEnd = calculateSelectionEndFromView();
|
f@0
|
309 break;
|
f@0
|
310
|
f@0
|
311 case kThreshold:
|
f@0
|
312 /* save threshold in amp for the sonification and in db for freq view */
|
f@0
|
313 mThreshold = GetParam(kThreshold)->DBToAmp();
|
f@0
|
314 mFreqView->setThreshold(GetParam(kThreshold)->Value());
|
f@0
|
315 break;
|
f@0
|
316
|
f@0
|
317 default:
|
f@0
|
318 break;
|
f@0
|
319 }
|
f@0
|
320 }
|
f@0
|
321
|
f@0
|
322
|
f@0
|
323
|
f@0
|
324 void AccessibleSpectrumAnalyser::Sonification::reset(double srate)
|
f@0
|
325 {
|
f@0
|
326 ugen.reset();
|
f@0
|
327
|
f@0
|
328 envelope.setValue(0.0);
|
f@0
|
329 envelope.setReleaseTime(0.2);
|
f@0
|
330 envelope.setAttackTime(0.01);
|
f@0
|
331 envelope.setSustainLevel(1);
|
f@0
|
332
|
f@0
|
333 if (srate > 0.0){
|
f@0
|
334 ugen.setSampleRate(srate);
|
f@0
|
335 envelope.setSampleRate(srate);
|
f@0
|
336 }
|
f@0
|
337 }
|
f@0
|
338
|
f@0
|
339
|
f@0
|
340 const double AccessibleSpectrumAnalyser::k2pi = 2 * PI;
|
f@0
|
341 const IColor AccessibleSpectrumAnalyser::kBgColor(255, 6, 120, 21);
|
f@0
|
342 const double AccessibleSpectrumAnalyser::kClippingCeilingSnap = 0.05;
|
f@0
|
343 const double AccessibleSpectrumAnalyser::kMinSelectionSize = 50;
|
f@0
|
344
|
f@0
|
345
|
f@0
|
346
|