PDA

View Full Version : ISubject.h


cc_
11-06-2009, 01:11 AM
I needed something general so I could get informed when a control changed. So I implemented a simple observer/subject thing and made IControl inherit from that, so observers can just register with the control and get called when the control is marked dirty.

I'll post up an example of using it when I get a chance, but the basic idea is to make a class that contains the control you want to observe and make that class inherit from IObserverInterface:


#include "IControl.h"
class WControl : public IObserverInterface
{
...
protected:
IControl * mpControl;
...


Then in WControl make the control and call RegisterObserver on it:


mpControl = new IwhateverControl...
mpControl->RegisterObserver(this);


Finally your class should implement an update function - this is what gets called when the control is dirty:


void WControl::Update() {
// check the state of the control and do stuff here
}


Here is ISubject.h . It is a little different from the one I posted on the other thread, because I found some problems with that. This one has been tested pretty well.


#ifndef _ISUBJECT_
#define _ISUBJECT_

#include "../ptrlist.h"

/*
* This is based on the Observer design pattern, but because I need
* to make sure all the NotifyObservers calls happen on the correct
* thread there is SubjectDirty() method which the subject should call
* whenever it changes (on any thread). The when you're in the
* correct thread call NotifyObservers() for all the subjects and
* the call will only be done for those that are dirty (also unsets dirty).
*
* Example: if you want to be able to watch an IControl, then
* IControl should be a public ISubject and should call SubjectDirty()
* in its SetDirty() call* (which can be called from either the GUI
* thread [if the control changed from the GUI] or another thread
* in the case of parameter automation.
* Then when you are in the GUI thread for sure you should go through
* all the controls and call NotifyObservers() on each. A convinient
* place to do this is IGraphics::IsDirty().
* An observer should be a IObserverInterface and call
* Register to register itself with the IControl.
* The update has no parameters, so the observer needs to
* keep track of who it's watching (if this is a problem the update
* method could be changed to deliver a pointer to the subject).
*
* If there are no observers the overhead should just be one extra compare
* (mpObs is NULL). Observers are stored in a WDL_PtrList.
*
* * note: we can't use the IControl's dirty flag as the dirty flag
* for the subject, because a call might cause a subject we have
* already checked to go dirty and then that subject could be drawn
* (clearing the IControl dirty flag) and the update would never be called.
*/

// The Abstract Observer Interface
class IObserverInterface
{
public:
virtual void Update() = 0;
virtual ~IObserverInterface() {}
};

// The Subject
class ISubject
{
public:
ISubject() : mpObs(NULL), mDirty(false) {}
virtual ~ISubject() { delete mpObs; }

void RegisterObserver(IObserverInterface* ob) {
// wdl pointer list has 32 byte granularity as we don't expect many observers
if ( ! mpObs) mpObs = new WDL_PtrList<IObserverInterface>(32);
if (-1 == mpObs->Find(ob))
mpObs->Add(ob);
}
void RemoveObserver(IObserverInterface* ob) {
// note: it is safe to call remove even if you didn't register
// find will return -1 and delete will do nothing
if (mpObs) {
mpObs->Delete(mpObs->Find(ob));
}
}

void inline NotifyObservers() {
if (mpObs && mDirty) {
mDirty = false;
int n = mpObs->GetSize();
for (int i = 0; i < n; ++i) {
IObserverInterface* ob = mpObs->Get(i);
ob->Update();
}
}
}
void inline SubjectDirty() {
mDirty=true;
}
private:
WDL_PtrList<IObserverInterface> *mpObs;
bool mDirty;
};



I found I couldn't really call remove because the IControls get destroyed before my objects when the sequencer exits.

And the changes to IControl.h :


#include "ISubject.h"

class IControl : public ISubject


In IControl.cpp in SetDirty after mDirty = true;


SubjectDirty();


In IGraphics.cpp in IsDirty after IControl* pControl = *ppControl;


pControl->NotifyObservers();

cc_
11-18-2009, 01:45 PM
OK, as promised, here's an example of using ISubject.h

It's based on IPlugExample (just drop it in and compile??).

Actually it's two examples, one for each I've been asked about... there's a text read out of the pan position and there's a button you can click to nudge the pan control to the right.

Unfortunately I was too lazy to add any new bitmaps to the example so the button just uses the knob bitmap (to the right of the pan knob), but click it and you should see the pan knob move to the right.

Here's the whole of the modified IPlugExample.cpp, just look for the NudgeButton and PanReadOut classes to see how it works.


#include "IPlugExample.h"
#include "../IPlug_include_in_plug_src.h"
#include "../IControl.h"
#include "../ISubject.h"
#include "resource.h"
#include <math.h>

const int kNumPrograms = 1;

enum EParams {
kGainL = 0,
kGainR,
kPan,
kChannelSw,
kNumParams
};

enum EChannelSwitch {
kDefault = 0,
kReversed,
kAllLeft,
kAllRight,
kOff,
kNumChannelSwitchEnums
};

enum ELayout
{
kW = 400,
kH = 200,

kSwitch_N = 5, // # of sub-bitmaps.
kMeter_N = 51, // # of sub-bitmaps.

kFader_Len = 150,
kGainL_X = 80,
kGainL_Y = 20,
kGainR_X = 350,
kGainR_Y = 20,

kSwitch_X = 20,
kSwitch_Y = 40,

kPan_X = 225,
kPan_Y = 145,

kMeterL_X = 135,
kMeterL_Y = 20,
kMeterR_X = 250,
kMeterR_Y = 20
};

// normal contact controls don't work as expected if you click them fast
// setting mDblAsSingleClick fixes this
class IContactControlNoDblClick : public IContactControl {
public:
IContactControlNoDblClick(IPlugBase* pPlug, int x, int y, int paramIdx, IBitmap* pBitmap)
: IContactControl(pPlug, x, y, paramIdx, pBitmap) {
mDblAsSingleClick=true;
}
~IContactControlNoDblClick() {}
};

// A contact button that changes the control pNudgee by nudgeAmount each time
// it is clicked
class NudgeButton : public IObserverInterface {
public:
NudgeButton(IPlugBase* pPlug, IGraphics *pGraphics, int x, int y, IBitmap* pBitmap,
IControl *pNudgee,double nudgeAmount) :
mpNudgee(pNudgee), mNudgeAmount(nudgeAmount), mLastValue(0.0)
{
mpButton = new IContactControlNoDblClick(pPlug, x, y, -1, pBitmap);
pGraphics->AttachControl(mpButton);
mpButton->RegisterObserver(this);
}
void Update() {
double buttonValue = mpButton->GetValue();
if (buttonValue == 1.0 && mLastValue == 0.0) {
mpNudgee->SetValueFromUserInput( mpNudgee->GetValue() + mNudgeAmount );
}
mLastValue = buttonValue;
}
protected:
double mNudgeAmount;
double mLastValue;
IControl *mpNudgee;
IContactControl *mpButton;
};

// A stupid text readout for pan controls
class PanReadOut : public IObserverInterface
{
public:
PanReadOut(IPlugBase* pPlug, IGraphics* pGraphics, IRECT* pR, IText* pText, IControl *pSubject) :
mpSubject(pSubject) {
mpTextControl = new ITextControl(pPlug, pR , pText , "");
pGraphics->AttachControl(mpTextControl);
mpSubject->RegisterObserver(this);
}

void Update() {
double value = mpSubject->GetValue();
char buf[100];
char *p;
if ( value == 0.5 ) { p = "dead centre"; }
else if ( value == 1.0 ) { p = "loony right"; }
else if ( value == 0.0 ) { p = "loony left"; }
else {
sprintf(buf,"%2.0f%%",(value-0.5)*200);
p = buf;
}
mpTextControl->SetTextFromPlug(p);
}
protected:
IControl *mpSubject;
ITextControl *mpTextControl;
};

PlugExample::PlugExample(IPlugInstanceInfo instanceInfo)
: IPLUG_CTOR(kNumParams, 6, instanceInfo), prevL(0.0), prevR(0.0)
{
TRACE;

// Define parameter ranges, display units, labels.

GetParam(kGainL)->InitDouble("Gain L", 0.0, -44.0, 12.0, 0.1, "dB");
GetParam(kGainL)->NegateDisplay();
GetParam(kGainR)->InitDouble("Gain R", 0.0, -44.0, 12.0, 0.1, "dB");
GetParam(kPan)->InitInt("Pan", 0, -100, 100, "%");

// Params can be enums.

GetParam(kChannelSw)->InitEnum("Channel", kDefault, kNumChannelSwitchEnums);
GetParam(kChannelSw)->SetDisplayText(kDefault, "default");
GetParam(kChannelSw)->SetDisplayText(kReversed, "reversed");
GetParam(kChannelSw)->SetDisplayText(kAllLeft, "all L");
GetParam(kChannelSw)->SetDisplayText(kAllRight, "all R");
GetParam(kChannelSw)->SetDisplayText(kOff, "mute");

MakePreset("preset 1", -5.0, 5.0, 17, kReversed);
MakePreset("preset 2", -15.0, 25.0, 37, kAllRight);
MakeDefaultPreset("-", 4);

// Instantiate a graphics engine.

IGraphics* pGraphics = MakeGraphics(this, kW, kH); // MakeGraphics(this, kW, kH);
pGraphics->AttachBackground(BG_ID, BG_FN);

// Attach controls to the graphics engine. Controls are automatically associated
// with a parameter if you construct the control with a parameter index.

// Attach a couple of meters, not associated with any parameter,
// which we keep indexes for, so we can push updates from the plugin class.

IBitmap bitmap = pGraphics->LoadIBitmap(METER_ID, METER_FN, kMeter_N);
mMeterIdx_L = pGraphics->AttachControl(new IBitmapControl(this, kMeterL_X, kMeterL_Y, &bitmap));
mMeterIdx_R = pGraphics->AttachControl(new IBitmapControl(this, kMeterR_X, kMeterR_Y, &bitmap));

// Attach a couple of faders, associated with the parameters GainL and GainR.

bitmap = pGraphics->LoadIBitmap(FADER_ID, FADER_FN);
pGraphics->AttachControl(new IFaderControl(this, kGainL_X, kGainL_Y, kFader_Len, kGainL, &bitmap, kVertical));
pGraphics->AttachControl(new IFaderControl(this, kGainR_X, kGainR_Y, kFader_Len, kGainR, &bitmap, kVertical));

// Attach a 5-position switch associated with the ChannelSw parameter.

bitmap = pGraphics->LoadIBitmap(TOGGLE_ID, TOGGLE_FN, kSwitch_N);
pGraphics->AttachControl(new ISwitchControl(this, kSwitch_X, kSwitch_Y, kChannelSw, &bitmap));

// Attach a rotating knob associated with the Pan parameter.

bitmap = pGraphics->LoadIBitmap(KNOB_ID, KNOB_FN);
IControl *pPanControl = new IKnobRotaterControl(this, kPan_X, kPan_Y, kPan, &bitmap);
pGraphics->AttachControl( pPanControl );
IRECT r1(kPan_X - 100 , 160, kPan_X, 180 );
IText txt(&COLOR_WHITE);
PanReadOut *pPanReadOut = new PanReadOut(this, pGraphics, &r1, &txt, pPanControl);

// this uses the knob bitmap because I couldn't be bothered to include another image
// click the knob to nudge the pan control to the right
new NudgeButton(this, pGraphics, kPan_X + 100, kPan_Y, &bitmap,pPanControl,0.1);


// See IControl.h for other control types,
// IKnobMultiControl, ITextControl, IBitmapOverlayControl, IFileSelectorControl, IGraphControl, etc.

// Attach the graphics engine to the plugin.

AttachGraphics(pGraphics);

// No cleanup necessary, the graphics engine manages all of its resources and cleans up when closed.
}

void PlugExample::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];

double gain1 = GetParam(kGainL)->DBToAmp();
double gain2 = GetParam(kGainR)->DBToAmp();
double pan = 0.01 * GetParam(kPan)->Value();
EChannelSwitch chanSwitch = (EChannelSwitch) int(GetParam(kChannelSw)->Value());
double peakL = 0.0, peakR = 0.0;

for (int s = 0; s < nFrames; ++s, ++in1, ++in2, ++out1, ++out2) {

*out1 = *in1 * gain1 * (1.0 - pan);
*out2 = *in2 * gain2 * (1.0 + pan);

// In an actual plugin you'd switch outside of the sample loop,
// it's very inefficient to switch on every sample like this.

switch (chanSwitch) {

case kReversed:
SWAP(*out1, *out2);
break;

case kAllLeft:
*out1 += *out2;
*out2 = 0.0;
break;

case kAllRight:
*out2 += *out1;
*out1 = 0.0;
break;

case kOff:
*out1 = *out2 = 0.0;
break;

default:
break;
}

peakL = MAX(peakL, fabs(*out1));
peakR = MAX(peakR, fabs(*out2));
}

const double METER_ATTACK = 0.6, METER_DECAY = 0.1;
double xL = (peakL < prevL ? METER_DECAY : METER_ATTACK);
double xR = (peakR < prevR ? METER_DECAY : METER_ATTACK);

peakL = peakL * xL + prevL * (1.0 - xL);
peakR = peakR * xR + prevR * (1.0 - xR);

prevL = peakL;
prevR = peakR;

if (GetGUI()) {
GetGUI()->SetControlFromPlug(mMeterIdx_L, peakL);
GetGUI()->SetControlFromPlug(mMeterIdx_R, peakR);
}
}