f@0: #include "AccessibleSpectrumAnalyser.h" f@0: #include "IPlug_include_in_plug_src.h" f@0: #include "IControl.h" f@0: #include "resource.h" f@0: #include "IBitmapMonoText.h" f@0: f@0: #include f@0: f@0: #include "FreqView.h" f@0: #include "Controls.h" f@0: f@0: const int kNumPrograms = 1; f@0: f@0: #define FFT_FORWARD false f@0: f@0: enum EParams f@0: { f@0: kThreshold = 0, f@0: kSelStart, f@0: kSelSize, f@0: kDry, f@0: kWet, f@0: kNumParams f@0: }; f@0: f@0: enum ELayout f@0: { f@0: kWidth = GUI_WIDTH, f@0: kHeight = GUI_HEIGHT, f@0: f@0: lKnobSizeW = 52, f@0: lKnobSizeH = 48 + 19 + 19, // 48 for image, 19 for text f@0: f@0: lKnobsY = 315, f@0: f@0: lThresX = 40, f@0: f@0: lSelStartX = 250, f@0: lSelSizeX = 345, f@0: f@0: lDryX = 540, f@0: lWetX = 610, f@0: f@0: kKnobFrames = 60 f@0: }; f@0: f@0: AccessibleSpectrumAnalyser::AccessibleSpectrumAnalyser(IPlugInstanceInfo instanceInfo) : f@0: IPLUG_CTOR(kNumParams, kNumPrograms, instanceInfo), f@0: mDry(-6.0), f@0: mWet(-6.0), f@0: mFFTBufferIndx(0), f@0: mNormFactor(0), f@0: mBinSizeHz(44100/(2*kFFTbufSize)) f@0: { f@0: TRACE; f@0: f@0: /* init hanning window lookup table */ f@0: f@0: mHannTable.reserve(kFFTbufSize); f@0: f@0: for (int i = 0; i < kFFTbufSize; i++){ f@0: mHannTable.push_back( 0.5 * (1. - std::cos(k2pi * i / (kFFTbufSize - 1) )) ); f@0: } f@0: f@0: for (int i = 0; i < kFFTbufSize; i++) { f@0: mNormFactor += 0.5 * (1. - std::cos(k2pi * i / (double)(kFFTbufSize - 1))); f@0: } f@0: f@0: /* init FFT engine */ f@0: WDL_fft_init(); f@0: f@0: /* init parameters */ f@0: //arguments are: name, defaultVal, minVal, maxVal, step, label, group, shape f@0: f@0: GetParam(kThreshold)->InitDouble("Threshold", 0.0, -60.0, 6.2, 10, "dB"); f@0: f@0: GetParam(kSelStart)->InitDouble("Selection Start", 20, 20, 44100/2, 10, "Hz"); f@0: f@0: GetParam(kSelSize)->InitDouble("Selection Size", 50, kMinSelectionSize, 10000, 10, "Hz"); f@0: f@0: GetParam(kDry)->InitDouble("Dry", -6., -61.0, 0., 0.2, "dB"); f@0: GetParam(kDry)->SetDisplayText(-61.0, " -inf"); f@0: f@0: GetParam(kWet)->InitDouble("Wet", -6., -61.0, 0., 0.2, "dB"); f@0: GetParam(kWet)->SetDisplayText(-61.0, " -inf"); f@0: f@0: f@0: IGraphics* pGraphics = MakeGraphics(this, kWidth, kHeight); f@0: pGraphics->AttachPanelBackground(&kBgColor); f@0: f@0: mFreqView = new FreqView(this, IRECT(4, 40, GUI_WIDTH - 4, 300), kFFTbufSize/2); f@0: pGraphics->AttachControl(mFreqView); f@0: f@0: IBitmap knob = pGraphics->LoadIBitmap(KNOB_ID, KNOB_FN, kKnobFrames); f@0: /* text has info about the font-size, font-type etc. */ f@0: IText text = IText(14); f@0: f@0: /* attach dry and wet knobs to GUI */ f@0: pGraphics->AttachControl(new IKnobMultiControlText(this, f@0: IRECT(lThresX, lKnobsY, lThresX + lKnobSizeW, lKnobsY + lKnobSizeH ), f@0: kThreshold, f@0: &knob, f@0: &text, f@0: 27) ); f@0: f@0: pGraphics->AttachControl(new IKnobMultiControlText(this, f@0: IRECT(lSelStartX, lKnobsY, lSelStartX + lKnobSizeW, lKnobsY + lKnobSizeH ), f@0: kSelStart, f@0: &knob, f@0: &text, f@0: 27) ); f@0: f@0: pGraphics->AttachControl(new IKnobMultiControlText(this, f@0: IRECT(lSelSizeX, lKnobsY, lSelSizeX + lKnobSizeW, lKnobsY + lKnobSizeH ), f@0: kSelSize, f@0: &knob, f@0: &text, f@0: 27) ); f@0: f@0: pGraphics->AttachControl(new IKnobMultiControlText(this, f@0: IRECT(lDryX, lKnobsY, lDryX + lKnobSizeW, lKnobsY + lKnobSizeH ), f@0: kDry, f@0: &knob, f@0: &text, f@0: 27) ); f@0: f@0: pGraphics->AttachControl(new IKnobMultiControlText(this, f@0: IRECT(lWetX, lKnobsY, lWetX + lKnobSizeW, lKnobsY + lKnobSizeH), f@0: kWet, f@0: &knob, f@0: &text, f@0: 27) ); f@0: f@0: f@0: AttachGraphics(pGraphics); f@0: //MakePreset("preset 1", ... ); f@0: MakeDefaultPreset((char *) "-", kNumPrograms); f@0: f@0: } f@0: f@0: AccessibleSpectrumAnalyser::~AccessibleSpectrumAnalyser() {} f@0: f@0: /* f@0: from Ask Justin Frankel: f@0: first, call WDL_fft_init(). Then create a buffer of WDL_FFT_COMPLEX, populate its real components with some audio, f@0: the imaginary with 0s, then optionally apply a windowing function, then call WDL_fft(), f@0: then you can access the results using WDL_fft_permute() to get the correct indices. f@0: */ f@0: f@0: void AccessibleSpectrumAnalyser::ProcessDoubleReplacing(double** inputs, double** outputs, int nFrames) f@0: { f@0: // Mutex is already locked for us. f@0: f@0: double* in1 = inputs[0]; f@0: double* in2 = inputs[1]; f@0: double* out1 = outputs[0]; f@0: double* out2 = outputs[1]; f@0: f@0: for (int f = 0; f < nFrames; ++f, ++in1, ++in2, ++out1, ++out2){ f@0: f@0: mFFTbuffer[mFFTBufferIndx].re = (*in1 + *in2) / 2; f@0: /* apply Hann window */ f@0: mFFTbuffer[mFFTBufferIndx].re *= mHannTable[mFFTBufferIndx]; f@0: mFFTbuffer[mFFTBufferIndx].im = 0; f@0: f@0: /* when gathered enough samples perform FFT */ f@0: if (mFFTBufferIndx == kFFTbufSize - 1){ f@0: f@0: WDL_fft(mFFTbuffer, kFFTbufSize, FFT_FORWARD); f@0: f@0: float bins[kFFTbufSize / 2]; f@0: f@0: bool thresholdPassed = false; f@0: double maxClippingDiff = 0.0; f@0: int maxClippingBinIndx = 0; f@0: f@0: for (int i = 0; i < mFFTBufferIndx/2; i++){ f@0: f@0: int j = WDL_fft_permute(kFFTbufSize, i); // j is the index after permutation f@0: f@0: /* fill the bins to plot with the magnitudes of the FFT*/ f@0: bins[i] = magnitude(mFFTbuffer[j]); f@0: f@0: /* check bins within selection against threshold for sonification */ f@0: if( binWithinSelection(i) && bins[i] > mThreshold ){ f@0: f@0: thresholdPassed = true; f@0: f@0: /* find the clipping amount in dB */ f@0: double clippingDiff = fabs(::AmpToDB(bins[i]) - ::AmpToDB(mThreshold)); f@0: f@0: /* clipDiff will be rounded to the floor later (with a cast to int), f@0: but if it's very very close to the ceil, then let it be the ceil. */ f@0: const double ceilClippingDiff = ceil(clippingDiff); f@0: if ( ceilClippingDiff - clippingDiff < kClippingCeilingSnap ){ f@0: clippingDiff = ceilClippingDiff; f@0: } f@0: f@0: /* bound the difference to 12 semitones to prevent the sonification from going too high */ f@0: clippingDiff = BOUNDED(clippingDiff, 0.0, 12.0); f@0: f@0: if( clippingDiff > maxClippingDiff){ f@0: maxClippingDiff = clippingDiff; f@0: maxClippingBinIndx = i; f@0: } f@0: } f@0: f@0: } f@0: f@0: if(thresholdPassed){ f@0: /* sonify the difference between the amplitude and threshold of highest bin f@0: one db (rounded downward) is one tone, up to one octave (12 semitones) */ f@0: mSonification.ugen.setFrequency(midi2Freq(69 + int(maxClippingDiff))); f@0: f@0: /* pan the sonification according to the position of the bin in the spectrum */ f@0: mSonification.panning.calculatePanningCoeff( double(maxClippingBinIndx)/(mFFTBufferIndx/2) ); f@0: mSonification.envelope.keyOn(); f@0: } f@0: f@0: /* copy the bids in freqView for the graphics */ f@0: mFreqView->setBins(bins); f@0: mFFTBufferIndx = 0; f@0: f@0: }else{ f@0: f@0: /* increment index to gather more bins in the FFT buffer */ f@0: mFFTBufferIndx++; f@0: f@0: } f@0: f@0: f@0: /* now add the sonification to the audio */ f@0: f@0: /* when attack is done switch immediately to RELEASE (keyOff) * f@0: * so it goes like: attack->release->silence */ f@0: if (mSonification.envelope.getState() == stk::ADSR::DECAY) { f@0: mSonification.envelope.keyOff(); f@0: } f@0: f@0: /* add the sonification to the mix */ f@0: if (mSonification.envelope.getState() == stk::ADSR::ATTACK || f@0: mSonification.envelope.getState() == stk::ADSR::RELEASE) { f@0: f@0: const double env = mSonification.envelope.tick(); f@0: const double tick = mSonification.ugen.tick() * env; f@0: f@0: f@0: *out1 = mix(*in1, tick) * mSonification.panning.left; f@0: *out2 = mix(*in2, tick) * mSonification.panning.right; f@0: f@0: } else { // no sonification f@0: f@0: *out1 = mix(*in1, 0.0); f@0: *out2 = mix(*in2, 0.0); f@0: f@0: } f@0: } f@0: } f@0: f@0: void AccessibleSpectrumAnalyser::Reset() f@0: { f@0: TRACE; f@0: IMutexLock lock(this); f@0: f@0: mSonification.reset(GetSampleRate()); f@0: mBinSizeHz = GetSampleRate() / kFFTbufSize; f@0: f@0: mFreqView->setSampleRate(GetSampleRate()); f@0: mSelectionEnd = calculateSelectionEndFromView(); f@0: //double sr = GetSampleRate(); f@0: } f@0: f@0: void AccessibleSpectrumAnalyser::OnParamChange(int paramIdx) f@0: { f@0: IMutexLock lock(this); f@0: f@0: switch (paramIdx) f@0: { f@0: f@0: case kDry: f@0: if (GetParam(kDry)->Value() < -60.5 ){ f@0: mDry = 0.0; /* if the level goes below 60.5 dB, just bring it to silence */ f@0: } f@0: else { f@0: mDry = ::DBToAmp(GetParam(kDry)->Value()); f@0: } f@0: break; f@0: f@0: f@0: case kWet: f@0: f@0: if (GetParam(kWet)->Value() < -60.5 ){ f@0: mWet = 0.0; f@0: } f@0: else { f@0: mWet = ::DBToAmp(GetParam(kWet)->Value()); f@0: } f@0: break; f@0: f@0: f@0: case kSelStart: f@0: mFreqView->setSelectionStart(GetParam(kSelStart)->Value()); f@0: mSelectionEnd = calculateSelectionEndFromView(); f@0: break; f@0: f@0: case kSelSize: f@0: mFreqView->setSelectionSize(GetParam(kSelSize)->Value()); f@0: mSelectionEnd = calculateSelectionEndFromView(); f@0: break; f@0: f@0: case kThreshold: f@0: /* save threshold in amp for the sonification and in db for freq view */ f@0: mThreshold = GetParam(kThreshold)->DBToAmp(); f@0: mFreqView->setThreshold(GetParam(kThreshold)->Value()); f@0: break; f@0: f@0: default: f@0: break; f@0: } f@0: } f@0: f@0: f@0: f@0: void AccessibleSpectrumAnalyser::Sonification::reset(double srate) f@0: { f@0: ugen.reset(); f@0: f@0: envelope.setValue(0.0); f@0: envelope.setReleaseTime(0.2); f@0: envelope.setAttackTime(0.01); f@0: envelope.setSustainLevel(1); f@0: f@0: if (srate > 0.0){ f@0: ugen.setSampleRate(srate); f@0: envelope.setSampleRate(srate); f@0: } f@0: } f@0: f@0: f@0: const double AccessibleSpectrumAnalyser::k2pi = 2 * PI; f@0: const IColor AccessibleSpectrumAnalyser::kBgColor(255, 6, 120, 21); f@0: const double AccessibleSpectrumAnalyser::kClippingCeilingSnap = 0.05; f@0: const double AccessibleSpectrumAnalyser::kMinSelectionSize = 50; f@0: f@0: f@0: