iPlug2 - C++ Audio Plug-in Framework
IVKeyboardControl.h
Go to the documentation of this file.
1 /*
2  ==============================================================================
3 
4  This file is part of the iPlug 2 library. Copyright (C) the iPlug 2 developers.
5 
6  See LICENSE.txt for more info.
7 
8  ==============================================================================
9 */
10 
11 #pragma once
12 
19 #include "IControl.h"
20 #include "IPlugMidi.h"
21 
22 BEGIN_IPLUG_NAMESPACE
23 BEGIN_IGRAPHICS_NAMESPACE
24 
25 /*
26 
27  IVKeyboardControl by Eugene Yakshin, 2018
28 
29  based on
30 
31  IKeyboardControl
32  (c) Theo Niessink 2009, 2010
33  <http://www.taletn.com/>
34 
35  This software is provided 'as-is', without any express or implied
36  warranty. In no event will the authors be held liable for any damages
37  arising from the use of this software.
38 
39  Permission is granted to anyone to use this software for any purpose,
40  including commercial applications, and to alter it and redistribute it
41  freely, subject to the following restrictions:
42 
43  1. The origin of this software must not be misrepresented; you must not
44  claim that you wrote the original software. If you use this software in a
45  product, an acknowledgment in the product documentation would be
46  appreciated but is not required.
47  2. Altered source versions must be plainly marked as such, and must not be
48  misrepresented as being the original software.
49  3. This notice may not be removed or altered from any source distribution.
50 
51 
52  This keyboard is runtime customizable. Any key range is supported.
53  Key proportions, colors and some other design elements can be changed at any time too.
54  See the interface for details.
55  */
56 
60 {
61 public:
62  static const IColor DEFAULT_BK_COLOR;
63  static const IColor DEFAULT_WK_COLOR;
64  static const IColor DEFAULT_PK_COLOR;
65  static const IColor DEFAULT_FR_COLOR;
66  static const IColor DEFAULT_HK_COLOR;
67 
68  IVKeyboardControl(const IRECT& bounds, int minNote = 48, int maxNote = 72, bool roundedKeys = false,
69  const IColor& WK_COLOR = DEFAULT_WK_COLOR,
70  const IColor& BK_COLOR = DEFAULT_BK_COLOR,
71  const IColor& PK_COLOR = DEFAULT_PK_COLOR,
72  const IColor& FR_COLOR = DEFAULT_FR_COLOR,
73  const IColor& HK_COLOR = DEFAULT_HK_COLOR)
74  : IControl(bounds, kNoParameter)
75  , mWK_COLOR(WK_COLOR)
76  , mBK_COLOR(BK_COLOR)
77  , mPK_COLOR(PK_COLOR)
78  , mFR_COLOR(FR_COLOR)
79  , mHK_COLOR(HK_COLOR)
80  , mRoundedKeys(roundedKeys)
81  {
82  mText.mFGColor = FR_COLOR;
83  mDblAsSingleClick = true;
84  bool keepWidth = !(bounds.W() <= 0.0);
85  if (bounds.W() <= 0.0)
86  {
87  mRECT.R = mRECT.L + mRECT.H();
88  mTargetRECT = mRECT;
89  }
90 
91  SetNoteRange(minNote, maxNote, keepWidth);
92  SetWantsMidi(true);
93  }
94 
95  void OnMouseDown(float x, float y, const IMouseMod& mod) override
96  {
97  int prevKey = mLastTouchedKey;
98  mLastTouchedKey = GetKeyAtPoint(x, y);
99 
100  SetKeyIsPressed(mLastTouchedKey, true);
101 
102  mMouseOverKey = mLastTouchedKey;
103 
104  if(mLastTouchedKey != prevKey)
105  {
106  mLastVelocity = GetVelocity(y);
107 
108  TriggerMidiMsgFromKeyPress(mLastTouchedKey, (int) (mLastVelocity * 127.f));
109  }
110 
111  SetDirty(true);
112  }
113 
114  void OnMouseUp(float x, float y, const IMouseMod& mod) override
115  {
116  if (mLastTouchedKey > -1)
117  {
118  SetKeyIsPressed(mLastTouchedKey, false);
119  TriggerMidiMsgFromKeyPress(mLastTouchedKey, 0);
120 
121  mLastTouchedKey = -1;
122  mMouseOverKey = -1;
123  mLastVelocity = 0.;
124 
125  SetDirty(false);
126  }
127  }
128 
129  void OnMouseOut() override
130  {
131  if (mLastTouchedKey > -1 || mShowNoteAndVel)
132  {
133  mLastTouchedKey = -1;
134  mMouseOverKey = -1;
135  mLastVelocity = 0.;
136  SetDirty(false);
137  }
138  }
139 
140  void OnMouseDrag(float x, float y, float dX, float dY, const IMouseMod& mod) override
141  {
142  int prevKey = mLastTouchedKey;
143  mLastTouchedKey = GetKeyAtPoint(x, y);
144 
145  SetKeyIsPressed(mLastTouchedKey, true);
146 
147  mMouseOverKey = mLastTouchedKey;
148 
149  if(mLastTouchedKey != prevKey)
150  {
151  mLastVelocity = GetVelocity(y);
152 
153  TriggerMidiMsgFromKeyPress(mLastTouchedKey, (int) (mLastVelocity * 127.f));
154 
155  TriggerMidiMsgFromKeyPress(prevKey, 0);
156  SetKeyIsPressed(prevKey, false);
157  }
158 
159  SetDirty(true);
160 
161  }
162 
163  void OnMouseOver(float x, float y, const IMouseMod& mod) override
164  {
165  if (mShowNoteAndVel)
166  {
167  mMouseOverKey = GetKeyAtPoint(x, y);
168  SetDirty(false);
169  }
170  }
171 
172  void OnTouchCancelled(float x, float y, const IMouseMod& mod) override
173  {
174  if (mLastTouchedKey > -1)
175  {
176  SetKeyIsPressed(mLastTouchedKey, false);
177  TriggerMidiMsgFromKeyPress(mLastTouchedKey, 0);
178 
179  mLastTouchedKey = -1;
180  mMouseOverKey = -1;
181  mLastVelocity = 0.;
182 
183  SetDirty(false);
184  }
185  }
186 
187  void OnResize() override
188  {
189  float r = mRECT.W() / mTargetRECT.W();
190  float dx = mRECT.L - mTargetRECT.L;
191  mWKWidth *= r;
192  for (int i = 0; i < NKeys(); ++i)
193  {
194  float* pKeyL = GetKeyXPos(i);
195  float d = *pKeyL - mRECT.L;
196  *pKeyL = mRECT.L + d * r + dx;
197  }
198 
199  mTargetRECT = mRECT;
200  SetDirty(false);
201  }
202 
203  void OnMidi(const IMidiMsg& msg) override
204  {
205  switch (msg.StatusMsg())
206  {
207  case IMidiMsg::kNoteOn:
208  SetNoteFromMidi(msg.NoteNumber(), (msg.Velocity() != 0));
209  break;
210  case IMidiMsg::kNoteOff:
211  SetNoteFromMidi(msg.NoteNumber(), false);
212  break;
213  case IMidiMsg::kControlChange:
214  if(msg.ControlChangeIdx() == IMidiMsg::kAllNotesOff)
215  ClearNotesFromMidi();
216  break;
217  default: break;
218  }
219 
220  SetDirty(false);
221  }
222 
223  void DrawKey(IGraphics& g, const IRECT& bounds, const IColor& color)
224  {
225  if(mRoundedKeys)
226  {
227  g.FillRoundRect(color, bounds, 0., 0., mRoundness, mRoundness/*, &blend*/);
228  }
229  else
230  g.FillRect(color, bounds/*, &blend*/);
231  }
232 
233  void Draw(IGraphics& g) override
234  {
235  IColor shadowColor = IColor(60, 0, 0, 0);
236 
237  float BKBottom = mRECT.T + mRECT.H() * mBKHeightRatio;
238  float BKWidth = GetBKWidth();
239 
240  // first draw white keys
241  for (int i = 0; i < NKeys(); ++i)
242  {
243  if (!IsBlackKey(i))
244  {
245  float kL = *GetKeyXPos(i);
246  IRECT keyBounds = IRECT(kL, mRECT.T, kL + mWKWidth, mRECT.B);
247 
248  DrawKey(g, keyBounds, i == mHighlight ? mHK_COLOR : mWK_COLOR);
249 
250  if (GetKeyIsPressed(i))
251  {
252  // draw played white key
253  DrawKey(g, keyBounds, mPK_COLOR);
254 
255  if (mDrawShadows)
256  {
257  IRECT shadowBounds = keyBounds;
258  shadowBounds.R = shadowBounds.L + 0.35f * shadowBounds.W();
259 
260  if(!mRoundedKeys)
261  g.FillRect(shadowColor, shadowBounds, &mBlend);
262  else {
263  g.FillRoundRect(shadowColor, shadowBounds, 0., 0., mRoundness, mRoundness, &mBlend); // this one looks strange with rounded corners
264  }
265  }
266  }
267  if (mDrawFrame && i != 0)
268  { // only draw the left border if it doesn't overlay mRECT left border
269  g.DrawLine(mFR_COLOR, kL, mRECT.T, kL, mRECT.B, &mBlend, mFrameThickness);
270  if (i == NKeys() - 2 && IsBlackKey(NKeys() - 1))
271  g.DrawLine(mFR_COLOR, kL + mWKWidth, mRECT.T, kL + mWKWidth, mRECT.B, &mBlend, mFrameThickness);
272  }
273  }
274  }
275 
276  // then blacks
277  for (int i = 0; i < NKeys(); ++i)
278  {
279  if (IsBlackKey(i))
280  {
281  float kL = *GetKeyXPos(i);
282  IRECT keyBounds = IRECT(kL, mRECT.T, kL + BKWidth, BKBottom);
283  // first draw underlying shadows
284  if (mDrawShadows && !GetKeyIsPressed(i) && i < NKeys() - 1)
285  {
286  IRECT shadowBounds = keyBounds;
287  float w = shadowBounds.W();
288  shadowBounds.L += 0.6f * w;
289  if (GetKeyIsPressed(i + 1))
290  {
291  // if white to the right is pressed, shadow is longer
292  w *= 1.3f;
293  shadowBounds.B = shadowBounds.T + 1.05f * shadowBounds.H();
294  }
295  shadowBounds.R = shadowBounds.L + w;
296  DrawKey(g, shadowBounds, shadowColor);
297  }
298  DrawKey(g, keyBounds, (i == mHighlight ? mHK_COLOR : mBK_COLOR.WithContrast(IsDisabled() ? GRAYED_ALPHA : 0.f)));
299 
300  if (GetKeyIsPressed(i))
301  {
302  // draw pressed black key
303  IColor cBP = mPK_COLOR;
304  cBP.A = (int) mBKAlpha;
305  g.FillRect(cBP, keyBounds, &mBlend);
306  }
307 
308  if(!mRoundedKeys)
309  {
310  // draw l, r and bottom if they don't overlay the mRECT borders
311  if (mBKHeightRatio != 1.0)
312  g.DrawLine(mFR_COLOR, kL, BKBottom, kL + BKWidth, BKBottom, &mBlend);
313  if (i > 0)
314  g.DrawLine(mFR_COLOR, kL, mRECT.T, kL, BKBottom, &mBlend);
315  if (i != NKeys() - 1)
316  g.DrawLine(mFR_COLOR, kL + BKWidth, mRECT.T, kL + BKWidth, BKBottom, &mBlend);
317  }
318  }
319  }
320 
321  if (mDrawFrame)
322  g.DrawRect(mFR_COLOR, mRECT, &mBlend, mFrameThickness);
323 
324  if (mShowNoteAndVel)
325  {
326  if (mMouseOverKey > -1)
327  {
328  IRECT r = IRECT(*GetKeyXPos(mMouseOverKey), mRECT.T, 0, 0);
329  r.B = r.T + 1.2f * mText.mSize;
330  r.R = r.L + 35.0f;
331  WDL_String t;
332  GetNoteNameStr(mMinNote + mMouseOverKey, false, t);
333  if (mLastTouchedKey > -1)
334  {
335  t.AppendFormatted(16, ", vel: %3.2f", mLastVelocity * 127.f);
336  r.R += 60.0;
337  }
338  float e = r.R - mRECT.R;
339  if (e > 0.0)
340  {
341  r.L -= e;
342  r.R -= e;
343  }
344  g.FillRect(mWK_COLOR, r, &mBlend);
345  g.DrawRect(mFR_COLOR, r, &mBlend);
346  g.DrawText(mText, t.Get(), r, &mBlend);
347  }
348  }
349 
350 #ifdef _DEBUG
351  //g.DrawRect(COLOR_GREEN, mTargetRECT);
352  //g.DrawRect(COLOR_BLUE, mRECT);
353  WDL_String ti;
354  ti.SetFormatted(32, "key: %d, vel: %3.2f", mLastTouchedKey, mLastVelocity * 127.f);
355  //ti.SetFormatted(16, "mBAlpha: %d", mBAlpha);
356  IText txt(20, COLOR_RED);
357  IRECT tr(mRECT.L + 20, mRECT.B - 20, mRECT.L + 160, mRECT.B);
358  g.DrawText(txt, ti.Get(), tr, &mBlend);
359 #endif
360  }
361 
362 #pragma mark -
363 
364  void SetNoteRange(int min, int max, bool keepWidth = true)
365  {
366  if (min < 0 || max < 0) return;
367  if (min < max)
368  {
369  mMinNote = min;
370  mMaxNote = max;
371  }
372  else
373  {
374  mMinNote = max;
375  mMaxNote = min;
376  }
377 
378  mPressedKeys.Resize(NKeys());
379  memset(mPressedKeys.Get(), 0, mPressedKeys.GetSize() * sizeof(bool));
380 
381  RecreateKeyBounds(keepWidth);
382  }
383 
384  void SetNoteFromMidi(int noteNum, bool played)
385  {
386  if (noteNum < mMinNote || noteNum > mMaxNote) return;
387  SetKeyIsPressed(noteNum - mMinNote, played);
388  }
389 
390  void SetKeyIsPressed(int key, bool pressed)
391  {
392  mPressedKeys.Get()[key] = pressed;
393  SetDirty(false);
394  }
395 
396  void SetKeyHighlight(int key)
397  {
398  mHighlight = key;
399  SetDirty(false);
400  }
401 
402  void ClearNotesFromMidi()
403  {
404  memset(mPressedKeys.Get(), 0, mPressedKeys.GetSize() * sizeof(bool));
405  SetDirty(false);
406  }
407 
408  void SetBlackToWhiteRatios(float widthRatio, float heightRatio = 0.6)
409  {
410  widthRatio = Clip(widthRatio, 0.1f, 1.f);
411  heightRatio = Clip(heightRatio, 0.1f, 1.f);
412 
413  float halfW = 0.5f * mWKWidth * mBKWidthRatio;
414  float r = widthRatio / mBKWidthRatio;
415  mBKWidthRatio = widthRatio;
416  mBKHeightRatio = heightRatio;
417 
418  for (int i = 0; i < NKeys(); ++i)
419  {
420  if (IsBlackKey(i))
421  {
422  float* pKeyL = GetKeyXPos(i);
423  float mid = *pKeyL + halfW;
424  *pKeyL = mid - halfW * r;
425  if (*pKeyL < mRECT.L)
426  *pKeyL = mRECT.L;
427  }
428  }
429 
430  SetDirty(false);
431  }
432 
433  void SetHeight(float h, bool keepAspectRatio = false)
434  {
435  if (h <= 0.0) return;
436  float r = h / mRECT.H();
437  mRECT.B = mRECT.T + mRECT.H() * r;
438 
439  mTargetRECT = mRECT;
440 
441  if (keepAspectRatio)
442  SetWidth(mRECT.W() * r);
443  SetDirty(false);
444  }
445 
446  void SetWidth(float w, bool keepAspectRatio = false)
447  {
448  if (w <= 0.0) return;
449  float r = w / mRECT.W();
450  mRECT.R = mRECT.L + mRECT.W() * r;
451  mWKWidth *= r;
452  for (int i = 0; i < NKeys(); ++i)
453  {
454  float* pKeyL = GetKeyXPos(i);
455  float d = *pKeyL - mRECT.L;
456  *pKeyL = mRECT.L + d * r;
457  }
458 
459  mTargetRECT = mRECT;
460 
461  if (keepAspectRatio)
462  SetHeight(mRECT.H() * r);
463 
464  SetDirty(false);
465  }
466 
467  void SetShowNotesAndVelocity(bool show)
468  {
469  mShowNoteAndVel = show;
470  }
471 
472  void SetColors(const IColor BKColor, const IColor& WKColor, const IColor& PKColor = DEFAULT_PK_COLOR, const IColor& FRColor = DEFAULT_FR_COLOR)
473  {
474  mBK_COLOR = BKColor;
475  mWK_COLOR = WKColor;
476  mPK_COLOR = PKColor;
477  mFR_COLOR = FRColor;
478 
479  mBKAlpha = (float) PKColor.A;
480 
481  if (mBKAlpha < 240.f)
482  {
483  const float lumWK = WKColor.GetLuminosity() * WKColor.A / 255.f;
484  const float adjustment = PKColor.A / 255.f;
485  const float lumPK = PKColor.GetLuminosity() * adjustment;
486  const float lumRes = (1.f - adjustment) * lumWK + lumPK;
487  const float lumDW = lumRes - lumWK;
488  const float lumBK = BKColor.GetLuminosity() * BKColor.A / 255.f;
489 
490  if ((lumDW < 0 && lumBK < lumWK) || (lumDW > 0 && lumBK > lumWK))
491  {
492  float dbWB = lumWK - lumBK; // not used in the conditions ^^ for readability
493  mBKAlpha += (255.f - mBKAlpha) * (1.f - dbWB * dbWB / 255.f / 255.f) + 0.5f;
494  }
495  else
496  mBKAlpha += lumDW + 0.5f;
497 
498  mBKAlpha = Clip(mBKAlpha, 15.f, 255.f);
499  }
500 
501  SetDirty(false);
502  }
503 
504  // returns pressed Midi note number
505  int GetMidiNoteNumberForKey(int key) const
506  {
507  if (key > -1) return mMinNote + key;
508  else return -1;
509  }
510 
511 // double GetVelocity() const { return mVelocity * 127.f; }
512 
513 private:
514  void RecreateKeyBounds(bool keepWidth)
515  {
516  if (keepWidth)
517  mWKWidth = 0.f;
518 
519  // create size-independent data.
520  mIsBlackKeyList.Resize(NKeys());
521  mKeyXPos.Resize(NKeys());
522 
523  float numWhites = 0.f;
524  for (int n = mMinNote, i = 0; n <= mMaxNote; ++n, i++)
525  {
526  if (n % 12 == 1 || n % 12 == 3 || n % 12 == 6 || n % 12 == 8 || n % 12 == 10)
527  {
528  mIsBlackKeyList.Get()[i] = true;
529  }
530  else
531  {
532  mIsBlackKeyList.Get()[i] = false;
533  numWhites += 1.f;
534  }
535  }
536 
537  // black key middle isn't aligned exactly between whites
538  float WKPadStart = 0.f; // 1st note may be black
539  float WKPadEnd = 0.f; // last note may be black
540 
541  auto GetShiftForPitchClass = [this](int pitch) {
542  // usually black key width + distance to the closest black key = white key width,
543  // and often b width is ~0.6 * w width
544  if (pitch == 0) return 0.f;
545  else if (pitch % 12 == 1) return 7.f / 12.f;
546  else if (pitch % 12 == 3) return 5.f / 12.f;
547  else if (pitch % 12 == 6) return 2.f / 3.f;
548  else if (pitch % 12 == 8) return 0.5f;
549  else if (pitch % 12 == 10) return 1.f / 3.f;
550  else return 0.f;
551  };
552 
553  WKPadStart = GetShiftForPitchClass(mMinNote);
554 
555  if (mMinNote != mMaxNote && IsBlackKey(mIsBlackKeyList.GetSize() - 1))
556  WKPadEnd = 1.f - GetShiftForPitchClass(mMaxNote);
557 
558  // build rects
559  if (mWKWidth == 0.f)
560  mWKWidth = 0.2f * mRECT.H(); // first call from the constructor
561 
562  if (keepWidth)
563  {
564  mWKWidth = mRECT.W();
565  if (numWhites)
566  mWKWidth /= (numWhites + mBKWidthRatio * (WKPadStart + WKPadEnd));
567  }
568 
569  float BKWidth = mWKWidth;
570 
571  if (numWhites)
572  BKWidth *= mBKWidthRatio;
573 
574  float prevWKLeft = mRECT.L;
575 
576  for (int k = 0; k < mIsBlackKeyList.GetSize(); ++k)
577  {
578  if (IsBlackKey(k))
579  {
580  float l = prevWKLeft;
581  if (k != 0)
582  {
583  l -= GetShiftForPitchClass(mMinNote + k) * BKWidth;
584  }
585  else prevWKLeft += WKPadStart * BKWidth;
586  mKeyXPos.Get()[k] = l;
587  }
588  else
589  {
590  mKeyXPos.Get()[k] = prevWKLeft;
591  prevWKLeft += mWKWidth;
592  }
593  }
594 
595  mTargetRECT = mRECT;
596  SetDirty(false);
597  }
598 
599  int GetKeyAtPoint(float x, float y)
600  {
601  IRECT clipRect = mRECT.GetPadded(-2);
602  clipRect.Constrain(x, y);
603 
604  float BKBottom = mRECT.T + mRECT.H() * mBKHeightRatio;
605  float BKWidth = GetBKWidth();
606 
607  // black keys are on top
608  int k = -1;
609  for (int i = 0; i < NKeys(); ++i)
610  {
611  if (IsBlackKey(i))
612  {
613  float kL = *GetKeyXPos(i);
614  IRECT keyBounds = IRECT(kL, mRECT.T, kL + BKWidth, BKBottom);
615  if (keyBounds.Contains(x, y))
616  {
617  k = i;
618  break;
619  }
620  }
621  }
622 
623  if (k == -1)
624  {
625  for (int i = 0; i < NKeys(); ++i)
626  {
627  if (!IsBlackKey(i))
628  {
629  float kL = *GetKeyXPos(i);
630  IRECT keyBounds = IRECT(kL, mRECT.T, kL + mWKWidth, mRECT.B);
631  if (keyBounds.Contains(x, y))
632  {
633  k = i;
634  break;
635  }
636  }
637  }
638  }
639 
640  return k;
641  }
642 
643  float GetVelocity(float yPos)
644  {
645  float velocity = 0.;
646 
647  if (mLastTouchedKey > -1)
648  {
649  float h = mRECT.H();
650 
651  if (IsBlackKey(mLastTouchedKey))
652  h *= mBKHeightRatio;
653 
654  float fracPos = (yPos - mRECT.T) / (0.95f * h); // 0.95 is to get max velocity around the bottom
655 
656  velocity = Clip(fracPos, 1.f / 127.f, 1.f);
657  }
658 
659  return velocity;
660  }
661 
662  void GetNoteNameStr(int midiNoteNum, bool addOctave, WDL_String& str)
663  {
664  int oct = midiNoteNum / 12;
665  midiNoteNum -= 12 * oct;
666  const char* notes[12] = { "C","C#","D","D#","E","F","F#","G","G#","A","A#","B" };
667  const char* n = notes[midiNoteNum];
668  str.Set(n);
669  if (addOctave)
670  str.AppendFormatted(2, "%d", --oct);
671  }
672 
673  bool IsBlackKey(int i) const { return *(mIsBlackKeyList.Get() + i); }
674 
675  float* GetKeyXPos(int i) { return mKeyXPos.Get() + i; }
676 
677  bool GetKeyIsPressed(int i) const { return *(mPressedKeys.Get() + i); }
678 
679  int NKeys() const { return mMaxNote - mMinNote + 1; }
680 
681  float GetBKWidth() const
682  {
683  float w = mWKWidth;
684  if (NKeys() > 1)
685  w *= mBKWidthRatio;
686  return w;
687  }
688 
689  void TriggerMidiMsgFromKeyPress(int key, int velocity)
690  {
691  IMidiMsg msg;
692 
693  const int nn = GetMidiNoteNumberForKey(key);
694 
695  if(velocity > 0)
696  msg.MakeNoteOnMsg(nn, velocity, 0);
697  else
698  msg.MakeNoteOffMsg(nn, 0);
699 
701  }
702 
703 protected:
704  IColor mWK_COLOR;
705  IColor mBK_COLOR;
706  IColor mPK_COLOR;
707  IColor mFR_COLOR;
708  IColor mHK_COLOR;
709 
710  bool mRoundedKeys = false;
711  float mRoundness = 5.f;
712  bool mDrawShadows = false;
713  bool mDrawFrame = true;
714  float mFrameThickness = 1.f;
715  bool mShowNoteAndVel = false;
716  float mWKWidth = 0.f;
717  float mBKWidthRatio = 0.6f;
718  float mBKHeightRatio = 0.6f;
719  float mBKAlpha = 100.f;
720  int mLastTouchedKey = -1;
721  float mLastVelocity = 0.f;
722  int mMouseOverKey = -1;
723  int mMinNote, mMaxNote;
724  WDL_TypedBuf<bool> mIsBlackKeyList;
725  WDL_TypedBuf<bool> mPressedKeys;
726  WDL_TypedBuf<float> mKeyXPos;
727  int mHighlight = -1;
728 };
729 
733 {
734  static constexpr int kSpringAnimationTime = 50;
735  static constexpr int kNumRungs = 10;
736 public:
737  static constexpr int kMessageTagSetPitchBendRange = 0;
738 
742  IWheelControl(const IRECT& bounds, IMidiMsg::EControlChangeMsg cc = IMidiMsg::EControlChangeMsg::kNoCC, int initBendRange = 12)
743  : ISliderControlBase(bounds, kNoParameter, EDirection::Vertical, DEFAULT_GEARING, 40.f)
744  , mPitchBendRange(initBendRange)
745  , mCC(cc)
746  {
747  mMenu.AddItem("1 semitone");
748  mMenu.AddItem("2 semitones");
749  mMenu.AddItem("Fifth");
750  mMenu.AddItem("Octave");
751 
752  SetValue(cc == IMidiMsg::EControlChangeMsg::kNoCC ? 0.5 : 0.);
753  SetWantsMidi(true);
754  SetActionFunction([cc](IControl* pControl){
755  IMidiMsg msg;
756  if(cc == IMidiMsg::EControlChangeMsg::kNoCC) // pitchbend
757  msg.MakePitchWheelMsg((pControl->GetValue() * 2.) - 1.);
758  else
759  msg.MakeControlChangeMsg(cc, pControl->GetValue());
760 
761  pControl->GetDelegate()->SendMidiMsgFromUI(msg);
762  });
763  }
764 
765  void Draw(IGraphics& g) override
766  {
767  IRECT handleBounds = mRECT.GetPadded(-10.f);
768  const float stepSize = handleBounds.H() / (float) kNumRungs;
769  g.FillRoundRect(DEFAULT_SHCOLOR, mRECT.GetPadded(-5.f));
770 
771  if(!g.CheckLayer(mLayer))
772  {
773  const IRECT layerRect = handleBounds.GetMidVPadded(handleBounds.H() + stepSize);
774 
775  if(layerRect.W() > 0 && layerRect.H() > 0)
776  {
777  g.StartLayer(this, layerRect);
778  g.DrawGrid(COLOR_BLACK.WithOpacity(0.5f), layerRect, 0.f, stepSize, nullptr, 2.f);
779  mLayer = g.EndLayer();
780  }
781  }
782 
783  // NanoVG only has 2 stop gradients
784  IRECT r = handleBounds.FracRectVertical(0.5, true);
785  g.PathRect(r);
786  g.PathFill(IPattern::CreateLinearGradient(r, EDirection::Vertical, {{COLOR_BLACK, 0.f},{COLOR_MID_GRAY, 1.f}}));
787  r = handleBounds.FracRectVertical(0.51f, false); // slight overlap
788  g.PathRect(r);
789  g.PathFill(IPattern::CreateLinearGradient(r, EDirection::Vertical, {{COLOR_MID_GRAY, 0.f},{COLOR_BLACK, 1.f}}));
790 
791  const float value = static_cast<float>(GetValue());
792  const float y = (handleBounds.H() - (stepSize)) * value;
793  const float triangleRamp = std::fabs(value-0.5f) * 2.f;
794 
795  g.DrawBitmap(mLayer->GetBitmap(), handleBounds, 0, (int) y);
796 
797  const IRECT cutoutBounds = handleBounds.GetFromBottom(stepSize).GetTranslated(0, -y);
798  g.PathRect(cutoutBounds);
799  g.PathFill(IPattern::CreateLinearGradient(cutoutBounds, EDirection::Vertical,
800  {
801  //TODO: this can be improved
802  {COLOR_BLACK.WithContrast(iplug::Lerp(0.f, 0.5f, triangleRamp)), 0.f},
803  {COLOR_BLACK.WithContrast(iplug::Lerp(0.5f, 0.f, triangleRamp)), 1.f}
804  }));
805 
806  g.DrawVerticalLine(COLOR_BLACK, cutoutBounds, 0.f);
807  g.DrawVerticalLine(COLOR_BLACK, cutoutBounds, 1.f);
808  g.DrawRect(COLOR_BLACK, handleBounds);
809  }
810 
811  void OnMidi(const IMidiMsg& msg) override
812  {
813  if(mCC == IMidiMsg::EControlChangeMsg::kNoCC)
814  {
815  if(msg.StatusMsg() == IMidiMsg::kPitchWheel)
816  {
817  SetValue((msg.PitchWheel() + 1.) * 0.5);
818  SetDirty(false);
819  }
820  }
821  else
822  {
823  if(msg.ControlChangeIdx() == mCC)
824  {
825  SetValue(msg.ControlChange(mCC));
826  SetDirty(false);
827  }
828  }
829  }
830 
831  void OnMouseWheel(float x, float y, const IMouseMod &mod, float d) override
832  {
833  /* NO-OP */
834  }
835 
836  void OnPopupMenuSelection(IPopupMenu* pSelectedMenu, int) override
837  {
838  if(pSelectedMenu)
839  {
840  switch (pSelectedMenu->GetChosenItemIdx())
841  {
842  case 0: mPitchBendRange = 1; break;
843  case 1: mPitchBendRange = 2; break;
844  case 2: mPitchBendRange = 7; break;
845  case 3:
846  default:
847  mPitchBendRange = 12; break;
848  }
849 
850  GetDelegate()->SendArbitraryMsgFromUI(kMessageTagSetPitchBendRange, GetTag(), sizeof(int), &mPitchBendRange);
851  }
852  }
853 
854  void OnMouseDown(float x, float y, const IMouseMod &mod) override
855  {
856  if(mod.R && mCC == IMidiMsg::EControlChangeMsg::kNoCC)
857  {
858  switch (mPitchBendRange)
859  {
860  case 1: mMenu.CheckItemAlone(0); break;
861  case 2: mMenu.CheckItemAlone(1); break;
862  case 7: mMenu.CheckItemAlone(2); break;
863  case 12: mMenu.CheckItemAlone(3); break;
864  default:
865  break;
866  }
867 
868  GetUI()->CreatePopupMenu(*this, mMenu, x, y);
869  }
870  else
872  }
873 
874  void OnMouseUp(float x, float y, const IMouseMod &mod) override
875  {
876  if(mCC == IMidiMsg::EControlChangeMsg::kNoCC) // pitchbend
877  {
878  double startValue = GetValue();
879  SetAnimation([startValue](IControl* pCaller) {
880  pCaller->SetValue(iplug::Lerp(startValue, 0.5, Clip(pCaller->GetAnimationProgress(), 0., 1.)));
881  if(pCaller->GetAnimationProgress() > 1.) {
882  pCaller->SetDirty(true);
883  pCaller->OnEndAnimation();
884  return;
885  }
886  }, kSpringAnimationTime);
887  }
888 
890  }
891 
892 private:
893  IPopupMenu mMenu;
894  int mPitchBendRange;
896  ILayerPtr mLayer;
897 };
898 
899 END_IGRAPHICS_NAMESPACE
900 END_IPLUG_NAMESPACE
void PathRect(const IRECT &bounds)
Add a rectangle to the current path.
Definition: IGraphics.cpp:2613
bool Contains(const IRECT &rhs) const
Returns true if this IRECT completely contains rhs.
void StartLayer(IControl *pOwner, const IRECT &r, bool cacheable=false)
Create an IGraphics layer.
Definition: IGraphics.cpp:1954
void Draw(IGraphics &g) override
Draw the control to the graphics context.
The lowest level base class of an IGraphics control.
Definition: IControl.h:42
Used to manage a rectangular area, independent of draw class/platform.
IGEditorDelegate * GetDelegate()
Gets a pointer to the class implementing the IEditorDelegate interface that handles parameter changes...
Definition: IControl.h:439
virtual void DrawGrid(const IColor &color, const IRECT &bounds, float gridSizeH, float gridSizeV, const IBlend *pBlend=0, float thickness=1.f)
Draw a grid to the graphics context.
Definition: IGraphics.cpp:2410
void OnTouchCancelled(float x, float y, const IMouseMod &mod) override
Implement this method to respond to a touch cancel event on this control.
virtual void DrawRect(const IColor &color, const IRECT &bounds, const IBlend *pBlend=0, float thickness=1.f)
Draw a rectangle to the graphics context.
Definition: IGraphics.cpp:2475
int GetTag() const
Get the control&#39;s tag.
Definition: IControl.h:411
Encapsulates a MIDI message and provides helper functions.
Definition: IPlugMidi.h:30
Used to manage mouse modifiers i.e.
void OnMouseDown(float x, float y, const IMouseMod &mod) override
Implement this method to respond to a mouse down event on this control.
void OnMouseUp(float x, float y, const IMouseMod &mod) override
Implement this method to respond to a mouse up event on this control.
void OnMouseDown(float x, float y, const IMouseMod &mod) override
Implement this method to respond to a mouse down event on this control.
Definition: IControl.cpp:886
void MakePitchWheelMsg(double value, int channel=0, int offset=0)
Create a pitch wheel/bend message.
Definition: IPlugMidi.h:170
void OnMidi(const IMidiMsg &msg) override
Implement to receive MIDI messages sent to the control if mWantsMidi == true, see IEditorDelegate:Sen...
EControlChangeMsg
Constants for MIDI CC messages.
Definition: IPlugMidi.h:49
T Lerp(T a, T b, T f)
Linear interpolate between values a and b.
IControl * SetActionFunction(IActionFunction actionFunc)
Set an Action Function for this control.
Definition: IControl.h:201
IColor WithContrast(float c) const
Returns a new contrasted IColor based on this one.
void OnMouseWheel(float x, float y, const IMouseMod &mod, float d) override
Implement this method to respond to a mouse wheel event on this control.
Used to manage color data, independent of draw class/platform.
IRECT GetTranslated(float x, float y) const
Get a translated copy of this rectangle.
int NoteNumber() const
Gets the MIDI note number.
Definition: IPlugMidi.h:255
IRECT FracRectVertical(float frac, bool fromTop=false) const
Returns a new IRECT with a height that is multiplied by frac.
void DrawText(const IText &text, const char *str, const IRECT &bounds, const IBlend *pBlend=0)
Draw some text to the graphics context in a specific rectangle.
Definition: IGraphics.cpp:631
virtual void SendArbitraryMsgFromUI(int msgTag, int ctrlTag=kNoTag, int dataSize=0, const void *pData=nullptr)
SendArbitraryMsgFromUI (Abbreviation: SAMFUI)
Vectorial "wheel" control for pitchbender/modwheel.
This file contains the base IControl implementation, along with some base classes for specific types ...
void OnPopupMenuSelection(IPopupMenu *pSelectedMenu, int) override
Implement this method to handle popup menu selection after IGraphics::CreatePopupMenu/IControl::Promp...
float R
Right side of the rectangle (X + W)
void OnMouseOver(float x, float y, const IMouseMod &mod) override
Implement this method to respond to a mouseover event on this control.
bool IsDisabled() const
Definition: IControl.h:356
void OnMouseOut() override
Implement this method to respond to a mouseout event on this control.
float H() const
double GetAnimationProgress() const
Get the progress in a control&#39;s animation, in the range 0-1.
Definition: IControl.cpp:429
void CreatePopupMenu(IControl &control, IPopupMenu &menu, const IRECT &bounds, int valIdx=0)
Shows a pop up/contextual menu in relation to a rectangular region of the graphics context...
Definition: IGraphics.cpp:1937
float W() const
IColor WithOpacity(float alpha) const
Returns a new IColor with a different opacity.
int Velocity() const
Get the velocity value of a NoteOn/NoteOff message.
Definition: IPlugMidi.h:270
void OnMidi(const IMidiMsg &msg) override
Implement to receive MIDI messages sent to the control if mWantsMidi == true, see IEditorDelegate:Sen...
void OnMouseDrag(float x, float y, float dX, float dY, const IMouseMod &mod) override
Implement this method to respond to a mouse drag event on this control.
A class for setting the contents of a pop up menu.
IText is used to manage font and text/text entry style for a piece of text on the UI...
bool CheckLayer(const ILayerPtr &layer)
Test to see if a layer needs drawing, for instance if the control&#39;s bounds were changed.
Definition: IGraphics.cpp:2009
virtual void SendMidiMsgFromUI(const IMidiMsg &msg)
SendMidiMsgFromUI (Abbreviation: SMMFUI) This method should be used when sending a MIDI message from ...
IWheelControl(const IRECT &bounds, IMidiMsg::EControlChangeMsg cc=IMidiMsg::EControlChangeMsg::kNoCC, int initBendRange=12)
Create a WheelControl.
void OnMouseDown(float x, float y, const IMouseMod &mod) override
Implement this method to respond to a mouse down event on this control.
IControl(const IRECT &bounds, int paramIdx=kNoParameter, IActionFunction actionFunc=nullptr)
Constructor.
Definition: IControl.cpp:81
virtual void PathFill(const IPattern &pattern, const IFillOptions &options=IFillOptions(), const IBlend *pBlend=0)=0
Fill the current current path.
double PitchWheel() const
Get the value from a Pitchwheel message.
Definition: IPlugMidi.h:321
void MakeNoteOffMsg(int noteNumber, int offset, int channel=0)
Make a Note Off message.
Definition: IPlugMidi.h:158
The lowest level base class of an IGraphics context.
Definition: IGraphics.h:86
void SetAnimation(IAnimationFunction func)
Set the animation function.
Definition: IControl.h:477
void OnMouseUp(float x, float y, const IMouseMod &mod) override
Implement this method to respond to a mouse up event on this control.
Definition: IControl.cpp:906
Vectorial keyboard control.
virtual void FillRoundRect(const IColor &color, const IRECT &bounds, float cornerRadius=5.f, const IBlend *pBlend=0)
Fill a rounded rectangle with a color.
Definition: IGraphics.cpp:2554
virtual void FillRect(const IColor &color, const IRECT &bounds, const IBlend *pBlend=0)
Fill a rectangular region of the graphics context with a color.
Definition: IGraphics.cpp:2547
BEGIN_IPLUG_NAMESPACE T Clip(T x, T lo, T hi)
Clips the value x between lo and hi.
void Constrain(float &x, float &y) const
Ensure the point (x,y) is inside this IRECT.
virtual void DrawBitmap(const IBitmap &bitmap, const IRECT &bounds, int srcX, int srcY, const IBlend *pBlend=0)=0
Draw a bitmap (raster) image to the graphics context.
A base class for slider/fader controls, to handle mouse action and Sender.
Definition: IControl.h:1221
EControlChangeMsg ControlChangeIdx() const
Gets the controller index of a CC message.
Definition: IPlugMidi.h:333
double GetValue(int valIdx=0) const
Get the control&#39;s value.
Definition: IControl.cpp:151
EStatusMsg StatusMsg() const
Gets the MIDI Status message.
Definition: IPlugMidi.h:243
IGraphics * GetUI()
Definition: IControl.h:452
float L
Left side of the rectangle (X)
ILayerPtr EndLayer()
End an IGraphics layer.
Definition: IGraphics.cpp:1977
MIDI and sysex structs/utilites.
void DrawVerticalLine(const IColor &color, const IRECT &bounds, float x, const IBlend *pBlend=0, float thickness=1.f)
Draw a vertical line, within a rectangular region of the graphics context.
Definition: IGraphics.cpp:740
static IPattern CreateLinearGradient(float x1, float y1, float x2, float y2, const std::initializer_list< IColorStop > &stops={})
Create a linear gradient IPattern.
IRECT GetPadded(float padding) const
Get a copy of this IRECT with each value padded by padding N.B.
void SetWantsMidi(bool enable=true)
Specify whether this control wants to know about MIDI messages sent to the UI.
Definition: IControl.h:414
void OnResize() override
Called when IControl is constructed or resized using SetRect().
void MakeControlChangeMsg(EControlChangeMsg idx, double value, int channel=0, int offset=0)
Create a CC message.
Definition: IPlugMidi.h:186
void MakeNoteOnMsg(int noteNumber, int velocity, int offset, int channel=0)
Make a Note On message.
Definition: IPlugMidi.h:145
virtual void SetValue(double value, int valIdx=0)
Set one of the control&#39;s values.
Definition: IControl.cpp:145
int GetLuminosity() const
std::unique_ptr< ILayer > ILayerPtr
ILayerPtr is a managed pointer for transferring the ownership of layers.
virtual void DrawLine(const IColor &color, float x1, float y1, float x2, float y2, const IBlend *pBlend=0, float thickness=1.f)
Draw a line to the graphics context.
Definition: IGraphics.cpp:2402
void OnMouseUp(float x, float y, const IMouseMod &mod) override
Implement this method to respond to a mouse up event on this control.
IRECT GetFromBottom(float amount) const
Get a subrect of this IRECT bounded in Y by &#39;amount&#39; and the bottom edge.
float T
Top of the rectangle (Y)
double ControlChange(EControlChangeMsg idx) const
Get the value of a CC message.
Definition: IPlugMidi.h:340
virtual void SetDirty(bool triggerAction=true, int valIdx=kNoValIdx)
Mark the control as dirty, i.e.
Definition: IControl.cpp:196
float B
Bottom of the rectangle (Y + H)
void Draw(IGraphics &g) override
Draw the control to the graphics context.
IRECT GetMidVPadded(float padding) const
Get a copy of this IRECT where its height = 2 * padding but the center point on the Y-axis has not ch...