Mercurial > hg > apm
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;