view AccessiblePeakMeter.cpp @ 0:c0ead20bda4d

first commit
author Fiore Martin <f.martin@qmul.ac.uk>
date Mon, 08 Jun 2015 11:49:43 +0100
parents
children 33aaa48d4d16
line wrap: on
line source
// 
// AccessiblePeakMeter.cpp 
//
// Author: Fiore Martin 
// Started from IPlugMultiTargets example in WDL-OL, by Oli Larkin - https://github.com/olilarkin/wdl-ol
//
// Licensed under the Cockos WDL License, see README.txt
//


#include "AccessiblePeakMeter.h"
#include "IPlug_include_in_plug_src.h"
#include "resource.h"


#include "IControl.h"
#include "IBitmapMonoText.h"
#include "AccessiblePeakMeter_controls.h"


inline double midi2Freq(int note) {
	return 440. * pow(2., (note - 69.) / 12.);
}

double toDBMeter(double val, double range)
{
	double db;
	if (val > 0)
		db = ::AmpToDB(val);
	else
		db = -999;
	return BOUNDED((db + 60) / range,0,1);
}

/* reference points for controls layout, by changing these numbers only
the widgets can be moved around and all the other bits (top/left/right
borders, labels etc.) will follow. X and Y refer to the top-left coord  */
enum ELayout {
	lDryX = 20,
	lDryY = 10,
	lWetX = 85,
	lWetY = 10,

	lFaderLen = 190,
	lPeakMeterX = 180,
	lPeaklMeterY = 30,

	lSonifTypeX = 20,
	lSonifTypeY = 200,

	lDecayRateX = 20,
	lDecayRateY = 90
};


enum EParams
{
	kDry = 0,
	kWet,
	kThreshold,
	kSonificationType,
	kMeterDecayRate,
	kNumParams
};


AccessiblePeakMeter::AccessiblePeakMeter(IPlugInstanceInfo instanceInfo)
  : IPLUG_CTOR(kNumParams, NUM_PRESETS, instanceInfo),
  mDry(DRYWET_DEFAULT),
  mWet(DRYWET_DEFAULT),
  mMeterDecayRate(1.0),
  mSampleRate(44100.),
  mThreshold(1.0)
{
  TRACE;
  
  for (int i = 0; i < MAX_CHANNELS; i++) {
	  mPrevPeak[i] = 0.0;
  }

  //arguments are: name, defaultVal, minVal, maxVal, step, label
  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");
  GetParam(kThreshold)->InitDouble("Threshold", 0.0, -60.0, 6.2, 0.2, "dB"); 
  GetParam(kSonificationType)->InitEnum("Meter Type", METERTYPE_DEFAULT, 2);
  GetParam(kSonificationType)->SetDisplayText(SONIFICATION_TYPE_CONTINUOUS, "Continuous");
  GetParam(kSonificationType)->SetDisplayText(SONIFICATION_TYPE_CLIPPING, "Clipping");
  GetParam(kMeterDecayRate)->InitDouble("Decay", 1.0, 0.05, 1.0, 0.05, "sec."); 
  
  IGraphics* pGraphics = MakeGraphics(this, GUI_WIDTH, GUI_HEIGHT);
  pGraphics->AttachBackground(BG_ID, BG_FN);

  /* load bitmaps for fader, knob and switch button */
  IBitmap knob = pGraphics->LoadIBitmap(KNOB_ID, KNOB_FN, NUM_KNOB_FRAMES);
  IBitmap faderBmap = pGraphics->LoadIBitmap(FADER_ID, FADER_FN);
  IBitmap aSwitch = pGraphics->LoadIBitmap(SWITCH_ID, SWITCH_FN,2);

  /* text has info about the font-size, font-type etc. */
  IText text = IText(14);

  /* attach sonification type switch to the GUI */
  pGraphics->AttachControl(new ISwitchPopUpControl(this, lSonifTypeX ,lSonifTypeY, kSonificationType, &aSwitch));
  pGraphics->AttachControl(new ITextControl(this, IRECT(lSonifTypeX+10, lSonifTypeY - 20, lSonifTypeX + 110, lSonifTypeY ), &text, "Sonification Type"));
  
  /* attach dry and wet knobs to GUI */
  pGraphics->AttachControl(new IKnobMultiControlText(this, IRECT(lDryX, lDryY, lDryX + 52, lDryY + 48 + 19 + 19 ), kDry, &knob, &text, 27)); // 48 for image, 19 for text 
  pGraphics->AttachControl(new IKnobMultiControlText(this, IRECT(lWetX, lWetY, lWetX + 52, lWetY + 48 + 19 + 19), kWet, &knob, &text, 27));

  /* attach decay rate knob to the GUI */
  pGraphics->AttachControl(new IKnobMultiControlText(this, IRECT(lDecayRateX, lDecayRateY, lDecayRateX + 48 , lDecayRateY + 48 + 19 + 19 ), kMeterDecayRate, &knob, &text, 33));

  /* attach fader display, which shows the fader value, to GUI */
  ITextControl *faderText = new ITextControl(this, IRECT(lPeakMeterX+60, lPeaklMeterY + lFaderLen, lPeakMeterX + faderBmap.W + 95, lPeaklMeterY + lFaderLen + 20), &text);
  pGraphics->AttachControl(faderText);

  /* attach the fader to GUI */
  pGraphics->AttachControl(new IFaderVertText(this, lPeakMeterX, lPeaklMeterY, lFaderLen, kThreshold, &faderBmap, faderText));

  pGraphics->AttachControl(new ITextControl(this, IRECT(lPeakMeterX, lPeaklMeterY - 20, lPeakMeterX + 100, lPeaklMeterY), &text, "Peak Level Meter"));
  pGraphics->AttachControl(new ITextControl(this, IRECT(lPeakMeterX-20, lPeaklMeterY + lFaderLen, lPeakMeterX + 75, lPeaklMeterY + lFaderLen + 20), &text, "Threshold: "));

  /* attach peak meters to GUI. 
   Half the bitmap height is added to the peak meters on both top and bottom to prevent the
   triangular fader from going past the peak meters span 
   */
  const int halfFaderBmapLen = faderBmap.W / 2;
  mMeterIdx[0] = pGraphics->AttachControl(new IPeakMeterVert(this, 
	  IRECT(lPeakMeterX + 25, lPeaklMeterY + halfFaderBmapLen, lPeakMeterX + 45, lPeaklMeterY + 170 + halfFaderBmapLen), GetParam(kThreshold)->GetDefaultNormalized()));
  mMeterIdx[1] = pGraphics->AttachControl(new IPeakMeterVert(this,
	  IRECT(lPeakMeterX + 50, lPeaklMeterY + halfFaderBmapLen, lPeakMeterX + 70, lPeaklMeterY + lFaderLen - halfFaderBmapLen), GetParam(kThreshold)->GetDefaultNormalized()));

  AttachGraphics(pGraphics);

  /* add presets */
  MakePreset("Detect Clipping", DRYWET_DEFAULT, DRYWET_DEFAULT, THRESHOLD_DEFAULT, SONIFICATION_TYPE_CLIPPING, METERDECAY_DEFAULT);
  MakePreset("Sonify Audio", DRYWET_DEFAULT, DRYWET_DEFAULT, THRESHOLD_DEFAULT, SONIFICATION_TYPE_CONTINUOUS, METERDECAY_DEFAULT);
}

AccessiblePeakMeter::~AccessiblePeakMeter() {}


void AccessiblePeakMeter::ProcessDoubleReplacing(double** inputs, double** outputs, int nFrames)
{
	if(mSonification.type == SONIFICATION_TYPE_CONTINUOUS) {
		 addContinuousSonification(inputs, outputs, nFrames);
	} else {
		addClippingSonification(inputs, outputs, nFrames);
	}
}


void AccessiblePeakMeter::Reset()
{
  TRACE;
  IMutexLock lock(this);

  mSampleRate = GetSampleRate();
  
  mSonification.reset(mSampleRate);

  for (int i = 0; i < MAX_CHANNELS; i++) {
      mPrevPeak[i] = 0.0;	  
  }
  
}

void AccessiblePeakMeter::OnParamChange(int paramIdx)
{
  IMutexLock lock(this);

  switch (paramIdx)
  {
	case kDry:
		/* if the level goes below 60.5 dB, just bring it to silence */
		if (GetParam(kDry)->Value() < -60.5 ){
			mDry = 0.0;
		}
		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 kThreshold:
		mThreshold = GetParam(kThreshold)->DBToAmp();
      break;

	case kMeterDecayRate :
		mMeterDecayRate = 1.0 / GetParam(kMeterDecayRate)->Value();
	  break;

	case kSonificationType:
		mSonification.type = GetParam(kSonificationType)->Int();
		
		mSonification.reset();

		for (int i = 0; i < MAX_CHANNELS; i++) {
			mPrevPeak[i] = 0.0;
			mSonification.ugen[i].setFrequency(mSonification.type == SONIFICATION_TYPE_CLIPPING ? 440.0 : 0.0);
		}
		break;

    default:
      break;
  }
}

void AccessiblePeakMeter::addClippingSonification(double** inputs, double** outputs, int nFrames) {
	// Mutex is already locked for us.

	for (unsigned int channel = 0; channel < NInChannels(); channel++) {
		double* in = inputs[channel];
		double* out = outputs[channel];
		double peak = 0.0;

		/* find the max absolute value in the block of samples */
		for (int offset = 0; offset < nFrames; ++offset, ++in, ++out) {
			const double ampl = fabs(*in); // amplitude
			peak = IPMAX(peak, ampl); // find max peak for this block 
			
			if (ampl > mThreshold) {
				/* find the clipping amount in dB */
				double clippingDiff = fabs(::AmpToDB(ampl) - ::AmpToDB(mThreshold));

				/* clipDiff will be rounded downward later, but if it's very very 
				   close to the ceil, then let it be the ceil.
				 */
				const double ceilClippingDiff = ceil(clippingDiff);
				if (ceilClippingDiff - clippingDiff < CLIPPING_CEILING_SNAP){
					clippingDiff = ceilClippingDiff;
				}

				if (clippingDiff > mSonification.clipping.maxDiff[channel]){
					/* bound the difference to 12 semitones to prevent the sonification from going too high */
					mSonification.clipping.maxDiff[channel] = BOUNDED(clippingDiff, 0.0, 12.0);
				}
				
				/* sonify the difference between the amplitude and threshold *
				* one db (rounded downward) is one tone, up to one octave (12 semitones)        */
				mSonification.ugen[channel].setFrequency(midi2Freq(69 + (int)(mSonification.clipping.maxDiff[channel])));
				mSonification.clipping.envelope[channel].keyOn();
			}

			/* when attack is done switch immediately to RELEASE (keyOff) *
			 * so it goes like: attack->release->silence                  */
			if (mSonification.clipping.envelope[channel].getState() == stk::ADSR::DECAY) {
				mSonification.clipping.envelope[channel].keyOff();
			}

			/* add the sonification to the mix */
			if (mSonification.clipping.envelope[channel].getState() == stk::ADSR::ATTACK || 
					mSonification.clipping.envelope[channel].getState() == stk::ADSR::RELEASE) {
				const double env = mSonification.clipping.envelope[channel].tick();
				const double tick = mSonification.ugen[channel].tick() * env;
				*out = mix(*in, tick);
			} else { // no sonification
				mSonification.clipping.maxDiff[channel] = 0.0; // reset max clipping diff 
				*out = mix(*in, 0.0); // still honours the user's knobs settings
			}
		}

		/* now draw the peak meter with the maximum of this block of samples */

		const double deltaT = nFrames / mSampleRate;
		const double decayAmount = deltaT * mMeterDecayRate;

		peak = ::toDBMeter(peak, DB_RANGE);

		/* max between new peak and old peak decay wins */
		peak = IPMAX(peak, mPrevPeak[channel] - decayAmount);
		
		/* save the peak for next block of samples */
		mPrevPeak[channel] = peak;
		
		/* update the GUI */
		if (GetGUI()) {
			GetGUI()->SetControlFromPlug(mMeterIdx[channel], peak);
		}
	}
	
}

void AccessiblePeakMeter::addContinuousSonification(double** inputs, double** outputs, int nFrames) {
	// Mutex is already locked for us.
	
	const int nChannels = NInChannels();

	const double deltaT = nFrames / mSampleRate;
	const double decayAmount = deltaT * mMeterDecayRate;

	for (int channel = 0; channel < nChannels; channel++){
		double peak = 0.0;
		double *in = inputs[channel];

		/* find the max absolute value in the block of samples */
		for (int offset = 0; offset < nFrames; ++offset, ++in) {
			peak = IPMAX(peak, fabs(*in));
		}

		/* pick the max between new audio and peak meter decaying */
		peak = ::toDBMeter(peak, DB_RANGE);
		peak = IPMAX(peak, mPrevPeak[channel] - decayAmount); 

		/* set the sonification frequency according to the last peak value */
		const double sonifFreq = SONIFICATION_RANGE * peak;
		mSonification.ugen[channel].setFrequency(sonifFreq);

		/* If level goes below audible level just hush the sonification.    *
		 * this avoids DC offset when sonification frequency gets too low.  * 
		 * Uses an envelope to bring the sonification volume down smoothly 
		 */
		if (sonifFreq < MIN_SONIFICATION_FREQ){
			/* turn the sonification off, if it's not off already */
			if (mSonification.continous.isOn[channel]){
				mSonification.continous.isOn[channel] = false;
				mSonification.continous.envelope[channel].setTarget(0.0);
			}
		}
		else if (!mSonification.continous.isOn[channel]){
			/* if the sonification frequency goes past MIN_SONIFICATION_FREQ *
			   turn it on again unless it's already on                       */
			mSonification.continous.envelope[channel].setValue(1.0);
			mSonification.continous.isOn[channel] = true;
		}

		in = inputs[channel];
		double *out = outputs[channel];
		/* add peak meter line continuous sonification to output */
		for (int offset = 0; offset < nFrames; ++offset, ++in, ++out) {
			double tick = mSonification.ugen[channel].tick();
			tick *= mSonification.continous.envelope[channel].tick(); // apply envelope 
			/* write the output buffer: mix original audio + sonification */
			*out = mix(*in, tick);
		}

		/* save the peaks for next block of samples */
		mPrevPeak[channel] = peak;

		/* update the GUI */
		if (GetGUI()) {
			GetGUI()->SetControlFromPlug(mMeterIdx[channel], peak);
		}
	}
}

//Called by the standalone wrapper if someone clicks about
bool AccessiblePeakMeter::HostRequestingAboutBox()
{
  IMutexLock lock(this);
  if(GetGUI())
  {
	// do nothing 
  }
  return true;
}


// -------------- static variables init -------------

const double AccessiblePeakMeter::DRYWET_DEFAULT = -6.0;
const int AccessiblePeakMeter::METERTYPE_DEFAULT = 1;
const double AccessiblePeakMeter::METERDECAY_DEFAULT = 60.0;
const double AccessiblePeakMeter::THRESHOLD_DEFAULT = 0.0;

const double AccessiblePeakMeter::DB_RANGE = 66.0;
const double AccessiblePeakMeter::SONIFICATION_RANGE = 2000;
const int AccessiblePeakMeter::SONIFICATION_TYPE_CLIPPING = 1;
const int AccessiblePeakMeter::SONIFICATION_TYPE_CONTINUOUS = 0;
const double AccessiblePeakMeter::BEEP_TIME = 0.2;
const double AccessiblePeakMeter::MIN_SONIFICATION_FREQ = 20.0;
const double AccessiblePeakMeter::CLIPPING_CEILING_SNAP = 0.05;

const int AccessiblePeakMeter::NUM_KNOB_FRAMES = 60;
const int AccessiblePeakMeter::NUM_PRESETS = 2;