view AccessibleSpectrumAnalyser.cpp @ 1:2ca5d7440b5c tip

added README
author Fiore Martin <f.martin@qmul.ac.uk>
date Fri, 26 Feb 2016 16:11:20 +0000
parents 3004dd663202
children
line wrap: on
line source
#include "AccessibleSpectrumAnalyser.h"
#include "IPlug_include_in_plug_src.h"
#include "IControl.h"
#include "resource.h"
#include "IBitmapMonoText.h"

#include <cmath>

#include "FreqView.h"
#include "Controls.h"

const int kNumPrograms = 1;

#define FFT_FORWARD false

enum EParams
{
  kThreshold = 0,
  kSelStart,
  kSelSize,
  kDry,
  kWet,
  kNumParams
};

enum ELayout
{
  kWidth = GUI_WIDTH,
  kHeight = GUI_HEIGHT,
  
  lKnobSizeW = 52,
  lKnobSizeH = 48 + 19 + 19, // 48 for image, 19 for text
  
  lKnobsY = 315,
  
  lThresX = 40,
  
  lSelStartX = 250,
  lSelSizeX = 345,
  
  lDryX = 540,
  lWetX = 610,
  
  kKnobFrames = 60
};

AccessibleSpectrumAnalyser::AccessibleSpectrumAnalyser(IPlugInstanceInfo instanceInfo) :
  IPLUG_CTOR(kNumParams, kNumPrograms, instanceInfo),
  mDry(-6.0),
  mWet(-6.0),
  mFFTBufferIndx(0),
  mNormFactor(0),
  mBinSizeHz(44100/(2*kFFTbufSize))
{
  TRACE;
  
  /* init hanning window lookup table */
  
  mHannTable.reserve(kFFTbufSize);
  
  for (int i = 0; i < kFFTbufSize; i++){
    mHannTable.push_back( 0.5 * (1. - std::cos(k2pi * i / (kFFTbufSize - 1) )) );
  }
  
  for (int i = 0; i < kFFTbufSize; i++) {
    mNormFactor += 0.5 * (1. - std::cos(k2pi * i / (double)(kFFTbufSize - 1)));
  }
  
  /* init FFT engine */
  WDL_fft_init();
  
  /* init parameters */
  //arguments are: name, defaultVal, minVal, maxVal, step, label, group, shape
  
  GetParam(kThreshold)->InitDouble("Threshold", 0.0, -60.0, 6.2, 10, "dB");
  
  GetParam(kSelStart)->InitDouble("Selection Start", 20, 20, 44100/2, 10, "Hz");
  
  GetParam(kSelSize)->InitDouble("Selection Size", 50, kMinSelectionSize, 10000, 10, "Hz");
  
  GetParam(kDry)->InitDouble("Dry", -6., -61.0, 0., 0.2, "dB");
  GetParam(kDry)->SetDisplayText(-61.0, " -inf");
  
  GetParam(kWet)->InitDouble("Wet", -6., -61.0, 0., 0.2, "dB");
  GetParam(kWet)->SetDisplayText(-61.0, " -inf");
  
  
  IGraphics* pGraphics = MakeGraphics(this, kWidth, kHeight);
  pGraphics->AttachPanelBackground(&kBgColor);
  
  mFreqView = new FreqView(this, IRECT(4, 40, GUI_WIDTH - 4, 300), kFFTbufSize/2);
  pGraphics->AttachControl(mFreqView);
  
  IBitmap knob = pGraphics->LoadIBitmap(KNOB_ID, KNOB_FN, kKnobFrames);
  /* text has info about the font-size, font-type etc. */
  IText text = IText(14);
  
  /* attach dry and wet knobs to GUI */
  pGraphics->AttachControl(new IKnobMultiControlText(this,
                                                     IRECT(lThresX, lKnobsY, lThresX + lKnobSizeW, lKnobsY + lKnobSizeH ),
                                                     kThreshold,
                                                     &knob,
                                                     &text,
                                                     27) );
  
  pGraphics->AttachControl(new IKnobMultiControlText(this,
                                                     IRECT(lSelStartX, lKnobsY, lSelStartX + lKnobSizeW, lKnobsY + lKnobSizeH ),
                                                     kSelStart,
                                                     &knob,
                                                     &text,
                                                     27) );
  
  pGraphics->AttachControl(new IKnobMultiControlText(this,
                                                     IRECT(lSelSizeX, lKnobsY, lSelSizeX + lKnobSizeW, lKnobsY + lKnobSizeH ),
                                                     kSelSize,
                                                     &knob,
                                                     &text,
                                                     27) );
  
  pGraphics->AttachControl(new IKnobMultiControlText(this,
                                                     IRECT(lDryX, lKnobsY, lDryX + lKnobSizeW, lKnobsY + lKnobSizeH ),
                                                     kDry,
                                                     &knob,
                                                     &text,
                                                     27) );
  
  pGraphics->AttachControl(new IKnobMultiControlText(this,
                                                     IRECT(lWetX, lKnobsY, lWetX + lKnobSizeW, lKnobsY + lKnobSizeH),
                                                     kWet,
                                                     &knob,
                                                     &text,
                                                     27) );
  
  
  AttachGraphics(pGraphics);
  //MakePreset("preset 1", ... );
  MakeDefaultPreset((char *) "-", kNumPrograms);
  
}

AccessibleSpectrumAnalyser::~AccessibleSpectrumAnalyser() {}

/*
 from Ask Justin Frankel:
 first, call WDL_fft_init(). Then create a buffer of WDL_FFT_COMPLEX, populate its real components with some audio,
 the imaginary with 0s, then optionally apply a windowing function, then call WDL_fft(),
 then you can access the results using WDL_fft_permute() to get the correct indices.
 */

void AccessibleSpectrumAnalyser::ProcessDoubleReplacing(double** inputs, double** outputs, int nFrames)
{
  // Mutex is already locked for us.
  
  double* in1 = inputs[0];
  double* in2 = inputs[1];
  double* out1 = outputs[0];
  double* out2 = outputs[1];
  
  for (int f = 0; f < nFrames; ++f, ++in1, ++in2, ++out1, ++out2){
    
    mFFTbuffer[mFFTBufferIndx].re = (*in1 + *in2) / 2;
    /* apply Hann window */
    mFFTbuffer[mFFTBufferIndx].re *= mHannTable[mFFTBufferIndx];
    mFFTbuffer[mFFTBufferIndx].im = 0;
    
    /* when gathered enough samples perform FFT */
    if (mFFTBufferIndx == kFFTbufSize - 1){
      
      WDL_fft(mFFTbuffer, kFFTbufSize, FFT_FORWARD);
      
      float bins[kFFTbufSize / 2];
      
      bool thresholdPassed = false;
      double maxClippingDiff = 0.0;
      int maxClippingBinIndx = 0;
      
      for (int i = 0; i < mFFTBufferIndx/2; i++){
        
        int j = WDL_fft_permute(kFFTbufSize, i); // j is the index after permutation
        
        /* fill the bins to plot with the magnitudes of the FFT*/
        bins[i] = magnitude(mFFTbuffer[j]);
        
        /* check bins within selection against threshold for sonification */
        if( binWithinSelection(i) && bins[i] > mThreshold ){
          
          thresholdPassed = true;
          
          /* find the clipping amount in dB */
          double clippingDiff = fabs(::AmpToDB(bins[i]) - ::AmpToDB(mThreshold));
          
          /* clipDiff will be rounded to the floor later (with a cast to int),
             but if it's very very close to the ceil, then let it be the ceil. */
          const double ceilClippingDiff = ceil(clippingDiff);
          if ( ceilClippingDiff - clippingDiff < kClippingCeilingSnap ){
            clippingDiff = ceilClippingDiff;
          }
          
          /* bound the difference to 12 semitones to prevent the sonification from going too high */
          clippingDiff = BOUNDED(clippingDiff, 0.0, 12.0);
          
          if( clippingDiff > maxClippingDiff){
            maxClippingDiff = clippingDiff;
            maxClippingBinIndx = i;
          }
        }
        
      }
      
      if(thresholdPassed){
        /* sonify the difference between the amplitude and threshold of highest bin
          one db (rounded downward) is one tone, up to one octave (12 semitones)   */
        mSonification.ugen.setFrequency(midi2Freq(69 + int(maxClippingDiff)));
        
        /* pan the sonification according to the position of the bin in the spectrum */
        mSonification.panning.calculatePanningCoeff( double(maxClippingBinIndx)/(mFFTBufferIndx/2) );
        mSonification.envelope.keyOn();
      }
      
      /* copy the bids in freqView for the graphics */
      mFreqView->setBins(bins);
      mFFTBufferIndx = 0;
    
    }else{
      
      /* increment index to gather more bins in the FFT buffer */
      mFFTBufferIndx++;
    
    }
    
    
    /* now add the sonification to the audio */
    
    /* when attack is done switch immediately to RELEASE (keyOff) *
     * so it goes like: attack->release->silence                  */
    if (mSonification.envelope.getState() == stk::ADSR::DECAY) {
      mSonification.envelope.keyOff();
    }
    
    /* add the sonification to the mix */
    if (mSonification.envelope.getState() == stk::ADSR::ATTACK ||
        mSonification.envelope.getState() == stk::ADSR::RELEASE) {
      
      const double env = mSonification.envelope.tick();
      const double tick = mSonification.ugen.tick() * env;
      
      
      *out1 = mix(*in1, tick) * mSonification.panning.left;
      *out2 = mix(*in2, tick) * mSonification.panning.right;
      
    } else { // no sonification
      
      *out1 = mix(*in1, 0.0);
      *out2 = mix(*in2, 0.0);
      
    }
  }
}

void AccessibleSpectrumAnalyser::Reset()
{
  TRACE;
  IMutexLock lock(this);
  
  mSonification.reset(GetSampleRate());
  mBinSizeHz = GetSampleRate() / kFFTbufSize;
  
  mFreqView->setSampleRate(GetSampleRate());
  mSelectionEnd = calculateSelectionEndFromView();
  //double sr = GetSampleRate();
}

void AccessibleSpectrumAnalyser::OnParamChange(int paramIdx)
{
  IMutexLock lock(this);
  
  switch (paramIdx)
  {
      
    case kDry:
      if (GetParam(kDry)->Value() < -60.5 ){
        mDry = 0.0; /* if the level goes below 60.5 dB, just bring it to silence */
      }
      else {
        mDry = ::DBToAmp(GetParam(kDry)->Value());
      }
      break;
      
      
    case kWet:
      
      if (GetParam(kWet)->Value() < -60.5 ){
        mWet = 0.0;
      }
      else {
        mWet = ::DBToAmp(GetParam(kWet)->Value());
      }
      break;
      
      
    case kSelStart:
      mFreqView->setSelectionStart(GetParam(kSelStart)->Value());
      mSelectionEnd = calculateSelectionEndFromView();
      break;
      
    case kSelSize:
      mFreqView->setSelectionSize(GetParam(kSelSize)->Value());
      mSelectionEnd = calculateSelectionEndFromView();
      break;
      
    case kThreshold:
      /* save threshold in amp for the sonification and in db for freq view */
      mThreshold = GetParam(kThreshold)->DBToAmp();
      mFreqView->setThreshold(GetParam(kThreshold)->Value());
      break;
      
    default:
      break;
  }
}



void AccessibleSpectrumAnalyser::Sonification::reset(double srate)
{
  ugen.reset();

  envelope.setValue(0.0);
  envelope.setReleaseTime(0.2);
  envelope.setAttackTime(0.01);
  envelope.setSustainLevel(1);
  
  if (srate > 0.0){
    ugen.setSampleRate(srate);
    envelope.setSampleRate(srate);
  }
}


const double AccessibleSpectrumAnalyser::k2pi = 2 * PI;
const IColor AccessibleSpectrumAnalyser::kBgColor(255, 6, 120, 21);
const double AccessibleSpectrumAnalyser::kClippingCeilingSnap = 0.05;
const double AccessibleSpectrumAnalyser::kMinSelectionSize = 50;