PDA

View Full Version : My first IPlug: Combo Model V


Tale
01-13-2010, 10:41 AM
Hi all,

I have just released a sneak preview of my first real IPlug project. It is a transistor organ simulation called Combo Model V. Although it has only a very basic GUI (for now anyway), it does feature stepped sliders (drawbars) and a keyboard:

http://www.taletn.com/temp/20100113-combov.jpg (http://www.martinic.com/combov/)

My project is not open source (sorry!), but if anyone is interested I am willing to share the IPlug related stuff; just ask.

If you want to give the VSTi a try you can download the DLL from my company's website:
http://www.martinic.com/combov/

cc_
01-16-2010, 01:23 AM
My project is not open source (sorry!), but if anyone is interested I am willing to share the IPlug related stuff; just ask.


What did you add? I'm always interested in IPlug related stuff!

Tale
01-18-2010, 02:13 AM
Here's my organ drawbar control. It's a reversed vertical fader with 9 discrete steps (0..8). You can drag its handle/tip to move the drawbar, or you can click on the numbers (usually) displayed on the drawbar itself.

// Handles the GUI control for the drawbars. The value for len is the height
// of the drawbar, *not* including the tip/handle.

class IDrawbarControl: public IControl
{
public:
IDrawbarControl(IPlugBase* pPlug, int x, int y, int len, int paramIdx, IBitmap* pBitmap): IControl(pPlug, &IRECT(x, y, x + pBitmap->W, y + pBitmap->H), paramIdx),
mLen(len), mBitmap(*pBitmap) {}
~IDrawbarControl() {}

void OnMouseDown(int x, int y, IMouseMod* pMod)
{
if (pMod->R)
{
PromptUserInput();
return;
}

y -= mRECT.T;
if (y < int(mValue * (double)mLen))
{
mValue = mValue - floor((double)y / (double)mLen / 0.125) * 0.125;
SetDirty();
}
}

void OnMouseDrag(int x, int y, int dX, int dY, IMouseMod* pMod)
{
mValue += (double)dY / (double)mLen;
SetDirty();
}

bool Draw(IGraphics* pGraphics)
{
return pGraphics->DrawBitmap(&mBitmap, &mRECT, 0, int((1. - floor(mValue / 0.125 + 0.5) * 0.125) * (double)mLen), &mBlend);
}

protected:
int mLen;
IBitmap mBitmap;
};

You should feed this control a single bitmap which contains the drawbar including the handle. You'll need to specify the length (height) of the drawbar (without the handle) when you attach it to the plug's GUI:

MyPlug::MyPlug(IPlugInstanceInfo instanceInfo): IPLUG_CTOR(kNumParams, 0, instanceInfo)
{
...

int width = 500, height = 426;
IGraphics* pGraphics = MakeGraphics(this, width, height);

// Add drawbar control
IBitmap bitmap = pGraphics->LoadIBitmap(DRAWBAR_ID, DRAWBAR_FN);
int x = 48, y = 86;
int len = 62;
pGraphics->AttachControl(new IDrawbarControl(this, x, y, len, kMyDrawbar, &bitmap));

AttachGraphics(pGraphics);

...
}


I will also post my keyboard control, but not right now as I really need to get some work done.

cc_
01-19-2010, 01:12 AM
Cool, I don't think I'll be using the drawbar any time soon, but a keyboard would be useful...

yhertogh
01-19-2010, 01:49 AM
cool plug-in.

I hit the random key and i got instant B-52's :-D

Yves

l0calh05t
01-19-2010, 03:28 AM
Awesome! Thank you very much for that control, I just started writing my first synth (only fx before... and mostly dynamics processing) which is... a drawbar organ!

Tale
01-19-2010, 10:45 AM
Thanks! :)

--

Here's my keyboard control:

#define MIN_MIDI_NOTE 48 // C3
#define MAX_MIDI_NOTE 96 // C7

#define NUM_MIDI_NOTES (MAX_MIDI_NOTE - MIN_MIDI_NOTE + 1)

class IKeyboardControl: public IControl
{
public:
IKeyboardControl(IPlugBase* pPlug, IRECT* pR, IBitmap* pWhiteKey, IBitmap* pBlackKey): IControl(pPlug, pR, -1),
mWhiteKey(*pWhiteKey), mBlackKey(*pBlackKey), mOctaveWidth(mWhiteKey.W * 7), mBlackKeyOffset(mBlackKey.W / 2), mNumOctaves(mRECT.W() / mOctaveWidth), mKey(-1) {}
~IKeyboardControl() {}

void OnMouseDown(int x, int y, IMouseMod* pMod)
{
if (!pMod->L) return;

// Skip if this key is already being played using the mouse.
int key = GetMouseNote(x, y);
if (key == mKey) return;

// Send a Note Off for the previous key (if any).
if (mKey != -1) SendNoteOff();
// Send a Note On for the new key.
mKey = key;
if (mKey != -1) SendNoteOn();

// Update the keyboard in the GUI.
SetDirty();
}

void OnMouseUp(int x, int y, IMouseMod* pMod)
{
// Skip if no key is playing.
if (mKey == -1) return;

// Send a Note Off.
SendNoteOff();
mKey = -1;

// Update the keyboard in the GUI.
SetDirty();
}

void OnMouseDrag(int x, int y, int dX, int dY, IMouseMod* pMod) { OnMouseDown(x, y, pMod); }

// Draws only the keys that are currently playing.
bool Draw(IGraphics* pGraphics)
{
// Skip if no keys are playing.
// if (((MyPlug*)mPlug)->HowManyKeys() == 0) return true;

// White keys
IRECT r(mRECT.L, mRECT.T, mRECT.L + mWhiteKey.W, mRECT.T + mWhiteKey.H);
int note = 0;
while (note < NUM_MIDI_NOTES)
{
// Draw the key.
if (((MyPlug*)mPlug)->GetKeyStatus(note)) pGraphics->DrawBitmap(&mWhiteKey, &r, 0, &mBlend);

// Next, please!
int n = note % 12;
if (n == 4 || n == 11) // E or B
++note;
else
note += 2;
r.L += mWhiteKey.W;
r.R += mWhiteKey.W;
}

// Black keys
r.L = mRECT.L + mWhiteKey.W - mBlackKeyOffset;
if (mBlackKey.W % 2) --r.L;
r.R = r.L + mBlackKey.W;
r.B = mRECT.T + mBlackKey.H;
note = 1;
while (note < NUM_MIDI_NOTES)
{
// Draw the key.
if (((MyPlug*)mPlug)->GetKeyStatus(note)) pGraphics->DrawBitmap(&mBlackKey, &r, 0, &mBlend);

// Next, please!
int n = note % 12;
if (n == 3 || n == 10) // Eb or Bb
{
note += 3;
r.L += 2 * mWhiteKey.W;
r.R += 2 * mWhiteKey.W;
}
else
{
note += 2;
r.L += mWhiteKey.W;
r.R += mWhiteKey.W;
}
}

return true;
}

protected:
// Returns the note (key) number at the (x, y) coordinates.
int GetMouseNote(int x, int y)
{
// Skip if the coordinates are outside the keyboard's rectangle.
if (x < mTargetRECT.L || x >= mTargetRECT.R || y < mTargetRECT.T || y >= mTargetRECT.B) return -1;
x -= mTargetRECT.L;
y -= mTargetRECT.T;

// Calculate the octave.
int octave = x / mOctaveWidth;
x -= octave * mOctaveWidth;

// Is it a black key?
int note = -1;
int h = mBlackKey.H;
if (y < h && octave < mNumOctaves)
{
int n = (x - mBlackKeyOffset) / mWhiteKey.W;
if (n >= 0 && n < 2) // C#..D#
{
int offset = (n + 1) * mWhiteKey.W - mBlackKeyOffset;
if (x >= offset && x < offset + mBlackKey.W) note = n * 2 + 1;
}
else if (n >= 3 && n < 6) // F#..A#
{
int offset = (n + 1) * mWhiteKey.W - mBlackKeyOffset;
if (x >= offset && x < offset + mBlackKey.W) note = n * 2;
}
}

// If it's not a black key...
if (note == -1)
{
// ... then it must be a white key.
int n = x / mWhiteKey.W;
if (n < 3) // C..E
note = n * 2;
else // F..B
note = n * 2 - 1;
h = mWhiteKey.H;
}

// Calculate the velocity depeding on the vertical coordinate
// relative to the key height.
mVelocity = 1 + int((double)y / (double)h * 126. + 0.5);

return note + octave * 12;
}

// Sends a Note On/Off MIDI message to the plug-in.
void SendNoteOn() { mPlug->ProcessMidiMsg(&IMidiMsg(0, IMidiMsg::kNoteOn << 4, MIN_MIDI_NOTE + mKey, mVelocity)); }
void SendNoteOff() { mPlug->ProcessMidiMsg(&IMidiMsg(0, IMidiMsg::kNoteOff << 4, MIN_MIDI_NOTE + mKey, 64 )); }

IBitmap mWhiteKey, mBlackKey;
int mOctaveWidth, mBlackKeyOffset, mNumOctaves;
int mKey, mVelocity;
};

The control assumes the keyboard (with all keys released) is already drawn in the GUI's background. So when attaching the conrol to the plug-in it needs only two bitmaps: one with a single depressed white key, and one with a black key. It also needs to know the rectangular area that holds the entire keyboard:

IBitmap white = pGraphics->LoadIBitmap(WHITE_KEY_ID, WHITE_KEY_FN, 1);
IBitmap black = pGraphics->LoadIBitmap(BLACK_KEY_ID, BLACK_KEY_FN, 1);
mGuiKeyboard = new IKeyboardControl(this, &IRECT(33, 301, 467, 393), &white, &black);
pGraphics->AttachControl(mGuiKeyboard);

As you can see I store a pointer to the keyboard control in a member variable (mGuiKeyboard). That way if I want to update the keyboard in the GUI from within my plug-in (e.g. because it's received a MIDI Note On/Off message), I can simply call mGuiKeyboard->SetDirty();.

The plug-in should keep track of which keys are playing, and provide a method that returns this status:

bool MyPlug::GetKeyStatus(int key)
{
// To-do: Lookup the key's status, and return true if it's playing, false if it's not.
}

I think that about wraps it up... Any questions? :D

Shan
02-04-2010, 01:29 AM
It seems to run in PT quite well via the FXpansion VST to RTAS wrapper. It doesn't have a mono version(which I think can be added to the code easy enough) so I set it up to do mono in the VST wrapper. Here is a screen shot of both the mono and stereo version inserted:

http://stash.reaper.fm/oldsb/786131/ComboV.png

http://stash.reaper.fm/oldsb/786140/wrap.png

Shane

Tale
02-04-2010, 02:24 AM
It seems to run in PT quite well via the FXpansion VST to RTAS wrapper.
Cool, thanks for testing.

It doesn't have a mono version(which I think can be added to the code easy enough) so I set it up to do mono in the VST wrapper.
At first I had a 1-channel (mono) only version, but then FL Studio would only output audio on the left channel, so I changed it to fake stereo (the left and right channels are always the same). I guess I still have to figure out how I can add a mono option again.

--

Meanwhile I've working on a new GUI. Here's a screendump of what I have so far:

http://www.taletn.com/temp/20100204-combov.jpg

This means I had the change the keyboard control code a bit, because the flat/sharp keys are not exactly centered in between the regular keys anymore. When I'm done I could post the new code (if anyone's still interested).

olilarkin
02-04-2010, 04:08 AM
wow, thanks for sharing the keyboard code tale! i have some gui code i will share at some point too.

If anyone has a polyphony management class, that would be really handy for me at the moment :-)

oli

RRokkenAudio
02-04-2010, 04:46 AM
Is it possible for someone to get all the contributions and add it to iplug? And make a killer iplug example?

~Rob.

Shan
02-06-2010, 08:51 AM
Meanwhile I've working on a new GUI. Here's a screendump of what I have so far:

Nice! Looks like a Vox Continental to me. :D

Shane

Tale
02-08-2010, 03:42 AM
Nice! Looks like a Vox Continental to me. :D
:D

Is it possible for someone to get all the contributions and add it to iplug?
I don't know about that, but I have documented and packaged my contributions (IDrawbarControl, IKeyboardControl, midiqueue) as C header files, so they can more easily be included (see attacked .zip file).

RRokkenAudio
02-08-2010, 03:50 AM
woo! thanks!

cc_
02-09-2010, 02:43 PM
I mailed schwa about getting my IPlug changes into the wdl distribution, he's going to look at it when he gets time.

For anyone that wants them now I've attached a zip file. See the file cc_iplug.txt in there for more details, there is a whole bunch of stuff.

Edit: The latest code is here: http://forum.cockos.com/showthread.php?t=52820

Tale
02-10-2010, 03:08 AM
Cool!

Tale
04-10-2010, 11:02 AM
I've just released a new version of Combo Model V:
http://www.martinic.com/combov/

It now uses my new keyboard control, otherwise it isn't very interesting from an IPlug point of view. (Although it does use a seperate thread to recalculate wave tables inspired by this (http://forum.cockos.com/showthread.php?t=52582) forum thread, but I haven't made a reusable component for this.)

Tale
04-21-2010, 03:24 PM
I don't know about that, but I have documented and packaged my contributions (IDrawbarControl, IKeyboardControl, midiqueue) as C header files, so they can more easily be included (see attacked .zip file).
I've updated my contributions:

IDrawbarControl:
+ Made OnMouse*() and Draw() methods virtual
+ Added specialized OnMouseWheel() method

IKeyboardControl:
+ Treat double-clicks a single-clicks
+ Made OnMouse*() and Draw() methods virtual
+ Disabled mouse wheel

The updated tale_iplug_contribs.zip can be downloaded here:
http://www.taletn.com/wdl/

cc_
04-22-2010, 01:14 AM
Cool!

I see you used the d parameter in your OnMouseWheel function... have you seen any problems with the control not moving if the wheel is turned slowly? I found that d was set to 0 if you turned the wheel slowly, because each event was only a fraction ( 1/3 I think ).

I was thinking of fixing this in IPlug either by accumulating the fractions and only calling OnMouseWheel when it got to more than 1, or maybe changing d to be a double.

Tale
04-22-2010, 11:26 AM
I see you used the d parameter in your OnMouseWheel function... have you seen any problems with the control not moving if the wheel is turned slowly? I found that d was set to 0 if you turned the wheel slowly, because each event was only a fraction ( 1/3 I think ).
No problems here in REAPER, but in other hosts I don't know... I will try.

cc_
04-22-2010, 11:54 AM
I don't think it depends on the sequencer, probably depends on the mouse. There's some info here:

http://blogs.msdn.com/oldnewthing/archive/2003/08/07/54615.aspx

But I have noticed it more on my Mac than on my PC.

Tale
04-22-2010, 12:34 PM
Ok. Nevertheless I've just tried using the mouse wheel on my Combo Model V in VSTHost, and... nothing. B4-II does react to the mouse wheel in VSTHost. I guess I will have to try more hosts, and more mice.

cc_
04-23-2010, 12:56 AM
Ah, interesting, maybe the host does have an effect. Maybe some host batch up more events before they send them? I see the problem in Reaper on OSX, but not on Reaper on Windows...

Tale
04-23-2010, 04:56 AM
I can only test on Windows, so I don't know about OSX.

Meanwhile I have tried the mouse wheel in a number of different hosts (FL Studio 9.0.3, Cantabile 2.0.0.2055, VSTHost 1.47), but none of them seem to ever call OnMouseWheel(). In all of these hosts B4-II does see the mouse wheel.

Tale
04-28-2010, 04:29 AM
I've tried more hosts (SONAR 8, Ableton Live 8), and no mouse wheel support in any of them. Still, mouse support in REAPER works excellent.

cc_
04-28-2010, 08:14 AM
I just tried Live7 on windows. Something strange happened, at first the wheel did not work, but if I right clicked a control to start then hit enter to finish editing then the wheel started working (in any control I hovered over).

B4 also behaved a bit funnily, again at first the wheel didn't work, but in its case it was enough just to move one of the controls to get the wheel working.

Tale
04-28-2010, 10:45 AM
Yeah, I also had the weird behaviour with B4, even in REAPER.

I will try my IPlug in Live 8 again tomorrow at work, to see if your right-click technique also works for me.

Yes, you're right. After having right-clicked a control VSTHost "suddenly" starts seeing the mouse wheel for all controls. However, after the plug has lost focus the mouse wheel is not seen again until you right-click some control again.

Tale
04-28-2010, 01:28 PM
Now knowing that the mouse wheel does work after a right-click, I thought I'd [grab a beer and] have another look at IGraphicsWin::WndProc().

Apparently mouse wheel messages only make it to plug-in's window after the window has explicitly received focus. Simply clicking in the plug-in's window doesn't sent a WM_SETFOCUS message to the window, so any mouse wheel messages are lost. After a right-click a WM_SETFOCUS message is sent to the plug-in's window, so that's why mouse wheel messages do work from then on (or at least until the window loses focus again).

Here's a solution that I think more or less works (but which will require more testing, and probably more thinking):

case WM_MOUSEACTIVATE: {
SetFocus(hWnd);
return MA_ACTIVATE;
}

You add this case to the switch(msg) statement in IGraphicsWin::WndProc().

This sets the focus to the plug-in's window when it receives its first mouse click. So you do need to click inside the plug-in's window at least once first, but after that the mouse wheel works fine.

cc_
04-28-2010, 02:15 PM
Cool! I was trying to figure it out, but getting nowhere... will try your fix tomorrow.

cc_
04-29-2010, 08:30 AM
I tried the focus thing in Cubase 4.5 , Sonar5, FL8, Live7... seems to work as you said it would.

It's not quite the same as other plugins though, they seem to get focus for the mousewheel as soon as you open the editor or click on the title bar (no need to click inside the plugin window).

Tale
04-29-2010, 02:15 PM
Setting focus when you open the editor is easy (and probably a good idea indeed). You just add SetFocus() to the WM_CREATE block:

if (msg == WM_CREATE) {
LPCREATESTRUCT lpcs = (LPCREATESTRUCT) lParam;
SetWindowLongPtr(hWnd, GWLP_USERDATA, (LPARAM) (lpcs->lpCreateParams));
int mSec = int(1000.0 / sFPS);
SetTimer(hWnd, IPLUG_TIMER_ID, mSec, NULL);
SetFocus(hWnd); // <--- Add this line
return 0;
}

However, setting focus when you click on the title bar is trickier. I think the problem is that the window frame including the title bar is provided by the host, i.e. it's not actually part of the plug-in window. So when you click on the titlebar this will cause window messages to be sent to the host providing the frame, but not to the plug-in window itself.

Tale
05-05-2010, 07:02 AM
I haven't come up with a better solution for the mouse wheel, so today I've released Combo Model V 0.0.6 beta with the SetFocus() on creation and mouse activation. Now we'll wait and see if I get any complaints. :p

cc_
05-06-2010, 05:01 AM
Let me know how it goes.

I was thinking maybe handling WM_ACTIVATE might be the way to get the clicking on the title working, but didn't get round to trying it yet.

Tale
05-06-2010, 05:46 AM
I've already tried that, but in both REAPER and FL Studio WM_ACTIVATE never reaches IGraphicsWin::WndProc(). It might work in other hosts though.

MaxBro
05-10-2010, 11:38 AM
In Carbon window on OSX setting focus is simple, you can use: SetUserFocusWindow(pWindow);
But in Cocoa, I couldn't find and understand, how to do it... CC_ may be you know?

Tale
05-10-2010, 01:37 PM
Cool. But do we know whether Carbon and/or Cocoa require focus for mouse wheel support?

MaxBro
05-10-2010, 01:40 PM
MouseWheel works great without focus on Carbon and Cocoa. You need focus only for keyboard input (arrow keys for example).

Tale
05-14-2010, 07:03 AM
Thanks for checking/sharing. Carbon and Cocoa don't need the focus thing then.

Meanwhile I've released yet another beta version of my plug-in, still with the SetFocus() on WM_CREATE and WM_MOUSEACTIVATE. By now my patched code has been (implicitly) tested by a lot of people, and I've had no complains. This means that there probably are no side effects, so I'm keeping the patched code.