The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

907 lines
28KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2017 - ROLI Ltd.
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 5 End-User License
  8. Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
  9. 27th April 2017).
  10. End User License Agreement: www.juce.com/juce-5-licence
  11. Privacy Policy: www.juce.com/juce-5-privacy-policy
  12. Or: You may also use this code under the terms of the GPL v3 (see
  13. www.gnu.org/licenses).
  14. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  15. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  16. DISCLAIMED.
  17. ==============================================================================
  18. */
  19. namespace juce
  20. {
  21. static const uint8 whiteNotes[] = { 0, 2, 4, 5, 7, 9, 11 };
  22. static const uint8 blackNotes[] = { 1, 3, 6, 8, 10 };
  23. struct MidiKeyboardComponent::UpDownButton : public Button
  24. {
  25. UpDownButton (MidiKeyboardComponent& c, int d)
  26. : Button ({}), owner (c), delta (d)
  27. {
  28. }
  29. void clicked() override
  30. {
  31. auto note = owner.getLowestVisibleKey();
  32. if (delta < 0)
  33. note = (note - 1) / 12;
  34. else
  35. note = note / 12 + 1;
  36. owner.setLowestVisibleKey (note * 12);
  37. }
  38. using Button::clicked;
  39. void paintButton (Graphics& g, bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override
  40. {
  41. owner.drawUpDownButton (g, getWidth(), getHeight(),
  42. shouldDrawButtonAsHighlighted, shouldDrawButtonAsDown,
  43. delta > 0);
  44. }
  45. private:
  46. MidiKeyboardComponent& owner;
  47. const int delta;
  48. JUCE_DECLARE_NON_COPYABLE (UpDownButton)
  49. };
  50. //==============================================================================
  51. MidiKeyboardComponent::MidiKeyboardComponent (MidiKeyboardState& s, Orientation o)
  52. : state (s), orientation (o)
  53. {
  54. scrollDown.reset (new UpDownButton (*this, -1));
  55. scrollUp .reset (new UpDownButton (*this, 1));
  56. addChildComponent (scrollDown.get());
  57. addChildComponent (scrollUp.get());
  58. // initialise with a default set of qwerty key-mappings..
  59. int note = 0;
  60. for (char c : "awsedftgyhujkolp;")
  61. setKeyPressForNote (KeyPress (c, 0, 0), note++);
  62. mouseOverNotes.insertMultiple (0, -1, 32);
  63. mouseDownNotes.insertMultiple (0, -1, 32);
  64. colourChanged();
  65. setWantsKeyboardFocus (true);
  66. state.addListener (this);
  67. startTimerHz (20);
  68. }
  69. MidiKeyboardComponent::~MidiKeyboardComponent()
  70. {
  71. state.removeListener (this);
  72. }
  73. //==============================================================================
  74. void MidiKeyboardComponent::setKeyWidth (float widthInPixels)
  75. {
  76. jassert (widthInPixels > 0);
  77. if (keyWidth != widthInPixels) // Prevent infinite recursion if the width is being computed in a 'resized()' call-back
  78. {
  79. keyWidth = widthInPixels;
  80. resized();
  81. }
  82. }
  83. void MidiKeyboardComponent::setScrollButtonWidth (int widthInPixels)
  84. {
  85. jassert (widthInPixels > 0);
  86. if (scrollButtonWidth != widthInPixels)
  87. {
  88. scrollButtonWidth = widthInPixels;
  89. resized();
  90. }
  91. }
  92. void MidiKeyboardComponent::setOrientation (Orientation newOrientation)
  93. {
  94. if (orientation != newOrientation)
  95. {
  96. orientation = newOrientation;
  97. resized();
  98. }
  99. }
  100. void MidiKeyboardComponent::setAvailableRange (int lowestNote, int highestNote)
  101. {
  102. jassert (lowestNote >= 0 && lowestNote <= 127);
  103. jassert (highestNote >= 0 && highestNote <= 127);
  104. jassert (lowestNote <= highestNote);
  105. if (rangeStart != lowestNote || rangeEnd != highestNote)
  106. {
  107. rangeStart = jlimit (0, 127, lowestNote);
  108. rangeEnd = jlimit (0, 127, highestNote);
  109. firstKey = jlimit ((float) rangeStart, (float) rangeEnd, firstKey);
  110. resized();
  111. }
  112. }
  113. void MidiKeyboardComponent::setLowestVisibleKey (int noteNumber)
  114. {
  115. setLowestVisibleKeyFloat ((float) noteNumber);
  116. }
  117. void MidiKeyboardComponent::setLowestVisibleKeyFloat (float noteNumber)
  118. {
  119. noteNumber = jlimit ((float) rangeStart, (float) rangeEnd, noteNumber);
  120. if (noteNumber != firstKey)
  121. {
  122. bool hasMoved = (((int) firstKey) != (int) noteNumber);
  123. firstKey = noteNumber;
  124. if (hasMoved)
  125. sendChangeMessage();
  126. resized();
  127. }
  128. }
  129. void MidiKeyboardComponent::setScrollButtonsVisible (bool newCanScroll)
  130. {
  131. if (canScroll != newCanScroll)
  132. {
  133. canScroll = newCanScroll;
  134. resized();
  135. }
  136. }
  137. void MidiKeyboardComponent::colourChanged()
  138. {
  139. setOpaque (findColour (whiteNoteColourId).isOpaque());
  140. repaint();
  141. }
  142. //==============================================================================
  143. void MidiKeyboardComponent::setMidiChannel (int midiChannelNumber)
  144. {
  145. jassert (midiChannelNumber > 0 && midiChannelNumber <= 16);
  146. if (midiChannel != midiChannelNumber)
  147. {
  148. resetAnyKeysInUse();
  149. midiChannel = jlimit (1, 16, midiChannelNumber);
  150. }
  151. }
  152. void MidiKeyboardComponent::setMidiChannelsToDisplay (int midiChannelMask)
  153. {
  154. midiInChannelMask = midiChannelMask;
  155. shouldCheckState = true;
  156. }
  157. void MidiKeyboardComponent::setVelocity (float v, bool useMousePosition)
  158. {
  159. velocity = jlimit (0.0f, 1.0f, v);
  160. useMousePositionForVelocity = useMousePosition;
  161. }
  162. //==============================================================================
  163. Range<float> MidiKeyboardComponent::getKeyPosition (int midiNoteNumber, float targetKeyWidth) const
  164. {
  165. jassert (midiNoteNumber >= 0 && midiNoteNumber < 128);
  166. static const float notePos[] = { 0.0f, 1 - blackNoteWidthRatio * 0.6f,
  167. 1.0f, 2 - blackNoteWidthRatio * 0.4f,
  168. 2.0f,
  169. 3.0f, 4 - blackNoteWidthRatio * 0.7f,
  170. 4.0f, 5 - blackNoteWidthRatio * 0.5f,
  171. 5.0f, 6 - blackNoteWidthRatio * 0.3f,
  172. 6.0f };
  173. auto octave = midiNoteNumber / 12;
  174. auto note = midiNoteNumber % 12;
  175. auto start = octave * 7.0f * targetKeyWidth + notePos[note] * targetKeyWidth;
  176. auto width = MidiMessage::isMidiNoteBlack (note) ? blackNoteWidthRatio * targetKeyWidth : targetKeyWidth;
  177. return { start, start + width };
  178. }
  179. Range<float> MidiKeyboardComponent::getKeyPos (int midiNoteNumber) const
  180. {
  181. return getKeyPosition (midiNoteNumber, keyWidth)
  182. - xOffset
  183. - getKeyPosition (rangeStart, keyWidth).getStart();
  184. }
  185. Rectangle<float> MidiKeyboardComponent::getRectangleForKey (int note) const
  186. {
  187. jassert (note >= rangeStart && note <= rangeEnd);
  188. auto pos = getKeyPos (note);
  189. auto x = pos.getStart();
  190. auto w = pos.getLength();
  191. if (MidiMessage::isMidiNoteBlack (note))
  192. {
  193. auto blackNoteLength = getBlackNoteLength();
  194. switch (orientation)
  195. {
  196. case horizontalKeyboard: return { x, 0, w, blackNoteLength };
  197. case verticalKeyboardFacingLeft: return { getWidth() - blackNoteLength, x, blackNoteLength, w };
  198. case verticalKeyboardFacingRight: return { 0, getHeight() - x - w, blackNoteLength, w };
  199. default: jassertfalse; break;
  200. }
  201. }
  202. else
  203. {
  204. switch (orientation)
  205. {
  206. case horizontalKeyboard: return { x, 0, w, (float) getHeight() };
  207. case verticalKeyboardFacingLeft: return { 0, x, (float) getWidth(), w };
  208. case verticalKeyboardFacingRight: return { 0, getHeight() - x - w, (float) getWidth(), w };
  209. default: jassertfalse; break;
  210. }
  211. }
  212. return {};
  213. }
  214. float MidiKeyboardComponent::getKeyStartPosition (int midiNoteNumber) const
  215. {
  216. return getKeyPos (midiNoteNumber).getStart();
  217. }
  218. float MidiKeyboardComponent::getTotalKeyboardWidth() const noexcept
  219. {
  220. return getKeyPos (rangeEnd).getEnd();
  221. }
  222. int MidiKeyboardComponent::getNoteAtPosition (Point<float> p)
  223. {
  224. float v;
  225. return xyToNote (p, v);
  226. }
  227. int MidiKeyboardComponent::xyToNote (Point<float> pos, float& mousePositionVelocity)
  228. {
  229. if (! reallyContains (pos.toInt(), false))
  230. return -1;
  231. auto p = pos;
  232. if (orientation != horizontalKeyboard)
  233. {
  234. p = { p.y, p.x };
  235. if (orientation == verticalKeyboardFacingLeft)
  236. p = { p.x, getWidth() - p.y };
  237. else
  238. p = { getHeight() - p.x, p.y };
  239. }
  240. return remappedXYToNote (p + Point<float> (xOffset, 0), mousePositionVelocity);
  241. }
  242. int MidiKeyboardComponent::remappedXYToNote (Point<float> pos, float& mousePositionVelocity) const
  243. {
  244. auto blackNoteLength = getBlackNoteLength();
  245. if (pos.getY() < blackNoteLength)
  246. {
  247. for (int octaveStart = 12 * (rangeStart / 12); octaveStart <= rangeEnd; octaveStart += 12)
  248. {
  249. for (int i = 0; i < 5; ++i)
  250. {
  251. auto note = octaveStart + blackNotes[i];
  252. if (note >= rangeStart && note <= rangeEnd)
  253. {
  254. if (getKeyPos (note).contains (pos.x - xOffset))
  255. {
  256. mousePositionVelocity = jmax (0.0f, pos.y / blackNoteLength);
  257. return note;
  258. }
  259. }
  260. }
  261. }
  262. }
  263. for (int octaveStart = 12 * (rangeStart / 12); octaveStart <= rangeEnd; octaveStart += 12)
  264. {
  265. for (int i = 0; i < 7; ++i)
  266. {
  267. auto note = octaveStart + whiteNotes[i];
  268. if (note >= rangeStart && note <= rangeEnd)
  269. {
  270. if (getKeyPos (note).contains (pos.x - xOffset))
  271. {
  272. auto whiteNoteLength = (orientation == horizontalKeyboard) ? getHeight() : getWidth();
  273. mousePositionVelocity = jmax (0.0f, pos.y / (float) whiteNoteLength);
  274. return note;
  275. }
  276. }
  277. }
  278. }
  279. mousePositionVelocity = 0;
  280. return -1;
  281. }
  282. //==============================================================================
  283. void MidiKeyboardComponent::repaintNote (int noteNum)
  284. {
  285. if (noteNum >= rangeStart && noteNum <= rangeEnd)
  286. repaint (getRectangleForKey (noteNum).getSmallestIntegerContainer());
  287. }
  288. void MidiKeyboardComponent::paint (Graphics& g)
  289. {
  290. g.fillAll (findColour (whiteNoteColourId));
  291. auto lineColour = findColour (keySeparatorLineColourId);
  292. auto textColour = findColour (textLabelColourId);
  293. for (int octave = 0; octave < 128; octave += 12)
  294. {
  295. for (int white = 0; white < 7; ++white)
  296. {
  297. auto noteNum = octave + whiteNotes[white];
  298. if (noteNum >= rangeStart && noteNum <= rangeEnd)
  299. drawWhiteNote (noteNum, g, getRectangleForKey (noteNum),
  300. state.isNoteOnForChannels (midiInChannelMask, noteNum),
  301. mouseOverNotes.contains (noteNum), lineColour, textColour);
  302. }
  303. }
  304. float x1 = 0.0f, y1 = 0.0f, x2 = 0.0f, y2 = 0.0f;
  305. auto width = getWidth();
  306. auto height = getHeight();
  307. if (orientation == verticalKeyboardFacingLeft)
  308. {
  309. x1 = width - 1.0f;
  310. x2 = width - 5.0f;
  311. }
  312. else if (orientation == verticalKeyboardFacingRight)
  313. x2 = 5.0f;
  314. else
  315. y2 = 5.0f;
  316. auto x = getKeyPos (rangeEnd).getEnd();
  317. auto shadowCol = findColour (shadowColourId);
  318. if (! shadowCol.isTransparent())
  319. {
  320. g.setGradientFill (ColourGradient (shadowCol, x1, y1, shadowCol.withAlpha (0.0f), x2, y2, false));
  321. switch (orientation)
  322. {
  323. case horizontalKeyboard: g.fillRect (0.0f, 0.0f, x, 5.0f); break;
  324. case verticalKeyboardFacingLeft: g.fillRect (width - 5.0f, 0.0f, 5.0f, x); break;
  325. case verticalKeyboardFacingRight: g.fillRect (0.0f, 0.0f, 5.0f, x); break;
  326. default: break;
  327. }
  328. }
  329. if (! lineColour.isTransparent())
  330. {
  331. g.setColour (lineColour);
  332. switch (orientation)
  333. {
  334. case horizontalKeyboard: g.fillRect (0.0f, height - 1.0f, x, 1.0f); break;
  335. case verticalKeyboardFacingLeft: g.fillRect (0.0f, 0.0f, 1.0f, x); break;
  336. case verticalKeyboardFacingRight: g.fillRect (width - 1.0f, 0.0f, 1.0f, x); break;
  337. default: break;
  338. }
  339. }
  340. auto blackNoteColour = findColour (blackNoteColourId);
  341. for (int octave = 0; octave < 128; octave += 12)
  342. {
  343. for (int black = 0; black < 5; ++black)
  344. {
  345. auto noteNum = octave + blackNotes[black];
  346. if (noteNum >= rangeStart && noteNum <= rangeEnd)
  347. drawBlackNote (noteNum, g, getRectangleForKey (noteNum),
  348. state.isNoteOnForChannels (midiInChannelMask, noteNum),
  349. mouseOverNotes.contains (noteNum), blackNoteColour);
  350. }
  351. }
  352. }
  353. void MidiKeyboardComponent::drawWhiteNote (int midiNoteNumber, Graphics& g, Rectangle<float> area,
  354. bool isDown, bool isOver, Colour lineColour, Colour textColour)
  355. {
  356. auto c = Colours::transparentWhite;
  357. if (isDown) c = findColour (keyDownOverlayColourId);
  358. if (isOver) c = c.overlaidWith (findColour (mouseOverKeyOverlayColourId));
  359. g.setColour (c);
  360. g.fillRect (area);
  361. auto text = getWhiteNoteText (midiNoteNumber);
  362. if (text.isNotEmpty())
  363. {
  364. auto fontHeight = jmin (12.0f, keyWidth * 0.9f);
  365. g.setColour (textColour);
  366. g.setFont (Font (fontHeight).withHorizontalScale (0.8f));
  367. switch (orientation)
  368. {
  369. case horizontalKeyboard: g.drawText (text, area.withTrimmedLeft (1.0f).withTrimmedBottom (2.0f), Justification::centredBottom, false); break;
  370. case verticalKeyboardFacingLeft: g.drawText (text, area.reduced (2.0f), Justification::centredLeft, false); break;
  371. case verticalKeyboardFacingRight: g.drawText (text, area.reduced (2.0f), Justification::centredRight, false); break;
  372. default: break;
  373. }
  374. }
  375. if (! lineColour.isTransparent())
  376. {
  377. g.setColour (lineColour);
  378. switch (orientation)
  379. {
  380. case horizontalKeyboard: g.fillRect (area.withWidth (1.0f)); break;
  381. case verticalKeyboardFacingLeft: g.fillRect (area.withHeight (1.0f)); break;
  382. case verticalKeyboardFacingRight: g.fillRect (area.removeFromBottom (1.0f)); break;
  383. default: break;
  384. }
  385. if (midiNoteNumber == rangeEnd)
  386. {
  387. switch (orientation)
  388. {
  389. case horizontalKeyboard: g.fillRect (area.expanded (1.0f, 0).removeFromRight (1.0f)); break;
  390. case verticalKeyboardFacingLeft: g.fillRect (area.expanded (0, 1.0f).removeFromBottom (1.0f)); break;
  391. case verticalKeyboardFacingRight: g.fillRect (area.expanded (0, 1.0f).removeFromTop (1.0f)); break;
  392. default: break;
  393. }
  394. }
  395. }
  396. }
  397. void MidiKeyboardComponent::drawBlackNote (int /*midiNoteNumber*/, Graphics& g, Rectangle<float> area,
  398. bool isDown, bool isOver, Colour noteFillColour)
  399. {
  400. auto c = noteFillColour;
  401. if (isDown) c = c.overlaidWith (findColour (keyDownOverlayColourId));
  402. if (isOver) c = c.overlaidWith (findColour (mouseOverKeyOverlayColourId));
  403. g.setColour (c);
  404. g.fillRect (area);
  405. if (isDown)
  406. {
  407. g.setColour (noteFillColour);
  408. g.drawRect (area);
  409. }
  410. else
  411. {
  412. g.setColour (c.brighter());
  413. auto sideIndent = 1.0f / 8.0f;
  414. auto topIndent = 7.0f / 8.0f;
  415. auto w = area.getWidth();
  416. auto h = area.getHeight();
  417. switch (orientation)
  418. {
  419. case horizontalKeyboard: g.fillRect (area.reduced (w * sideIndent, 0).removeFromTop (h * topIndent)); break;
  420. case verticalKeyboardFacingLeft: g.fillRect (area.reduced (0, h * sideIndent).removeFromRight (w * topIndent)); break;
  421. case verticalKeyboardFacingRight: g.fillRect (area.reduced (0, h * sideIndent).removeFromLeft (w * topIndent)); break;
  422. default: break;
  423. }
  424. }
  425. }
  426. void MidiKeyboardComponent::setOctaveForMiddleC (int octaveNum)
  427. {
  428. octaveNumForMiddleC = octaveNum;
  429. repaint();
  430. }
  431. String MidiKeyboardComponent::getWhiteNoteText (int midiNoteNumber)
  432. {
  433. if (midiNoteNumber % 12 == 0)
  434. return MidiMessage::getMidiNoteName (midiNoteNumber, true, true, octaveNumForMiddleC);
  435. return {};
  436. }
  437. void MidiKeyboardComponent::drawUpDownButton (Graphics& g, int w, int h,
  438. bool mouseOver,
  439. bool buttonDown,
  440. bool movesOctavesUp)
  441. {
  442. g.fillAll (findColour (upDownButtonBackgroundColourId));
  443. float angle = 0;
  444. switch (orientation)
  445. {
  446. case horizontalKeyboard: angle = movesOctavesUp ? 0.0f : 0.5f; break;
  447. case verticalKeyboardFacingLeft: angle = movesOctavesUp ? 0.25f : 0.75f; break;
  448. case verticalKeyboardFacingRight: angle = movesOctavesUp ? 0.75f : 0.25f; break;
  449. default: jassertfalse; break;
  450. }
  451. Path path;
  452. path.addTriangle (0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.5f);
  453. path.applyTransform (AffineTransform::rotation (MathConstants<float>::twoPi * angle, 0.5f, 0.5f));
  454. g.setColour (findColour (upDownButtonArrowColourId)
  455. .withAlpha (buttonDown ? 1.0f : (mouseOver ? 0.6f : 0.4f)));
  456. g.fillPath (path, path.getTransformToScaleToFit (1.0f, 1.0f, w - 2.0f, h - 2.0f, true));
  457. }
  458. void MidiKeyboardComponent::setBlackNoteLengthProportion (float ratio) noexcept
  459. {
  460. jassert (ratio >= 0.0f && ratio <= 1.0f);
  461. if (blackNoteLengthRatio != ratio)
  462. {
  463. blackNoteLengthRatio = ratio;
  464. resized();
  465. }
  466. }
  467. float MidiKeyboardComponent::getBlackNoteLength() const noexcept
  468. {
  469. auto whiteNoteLength = orientation == horizontalKeyboard ? getHeight() : getWidth();
  470. return whiteNoteLength * blackNoteLengthRatio;
  471. }
  472. void MidiKeyboardComponent::setBlackNoteWidthProportion (float ratio) noexcept
  473. {
  474. jassert (ratio >= 0.0f && ratio <= 1.0f);
  475. if (blackNoteWidthRatio != ratio)
  476. {
  477. blackNoteWidthRatio = ratio;
  478. resized();
  479. }
  480. }
  481. void MidiKeyboardComponent::resized()
  482. {
  483. auto w = getWidth();
  484. auto h = getHeight();
  485. if (w > 0 && h > 0)
  486. {
  487. if (orientation != horizontalKeyboard)
  488. std::swap (w, h);
  489. auto kx2 = getKeyPos (rangeEnd).getEnd();
  490. if ((int) firstKey != rangeStart)
  491. {
  492. auto kx1 = getKeyPos (rangeStart).getStart();
  493. if (kx2 - kx1 <= w)
  494. {
  495. firstKey = (float) rangeStart;
  496. sendChangeMessage();
  497. repaint();
  498. }
  499. }
  500. scrollDown->setVisible (canScroll && firstKey > (float) rangeStart);
  501. xOffset = 0;
  502. if (canScroll)
  503. {
  504. auto scrollButtonW = jmin (scrollButtonWidth, w / 2);
  505. auto r = getLocalBounds();
  506. if (orientation == horizontalKeyboard)
  507. {
  508. scrollDown->setBounds (r.removeFromLeft (scrollButtonW));
  509. scrollUp ->setBounds (r.removeFromRight (scrollButtonW));
  510. }
  511. else if (orientation == verticalKeyboardFacingLeft)
  512. {
  513. scrollDown->setBounds (r.removeFromTop (scrollButtonW));
  514. scrollUp ->setBounds (r.removeFromBottom (scrollButtonW));
  515. }
  516. else
  517. {
  518. scrollDown->setBounds (r.removeFromBottom (scrollButtonW));
  519. scrollUp ->setBounds (r.removeFromTop (scrollButtonW));
  520. }
  521. auto endOfLastKey = getKeyPos (rangeEnd).getEnd();
  522. float mousePositionVelocity;
  523. auto spaceAvailable = w;
  524. auto lastStartKey = remappedXYToNote ({ endOfLastKey - spaceAvailable, 0 }, mousePositionVelocity) + 1;
  525. if (lastStartKey >= 0 && ((int) firstKey) > lastStartKey)
  526. {
  527. firstKey = (float) jlimit (rangeStart, rangeEnd, lastStartKey);
  528. sendChangeMessage();
  529. }
  530. xOffset = getKeyPos ((int) firstKey).getStart();
  531. }
  532. else
  533. {
  534. firstKey = (float) rangeStart;
  535. }
  536. scrollUp->setVisible (canScroll && getKeyPos (rangeEnd).getStart() > w);
  537. repaint();
  538. }
  539. }
  540. //==============================================================================
  541. void MidiKeyboardComponent::handleNoteOn (MidiKeyboardState*, int /*midiChannel*/, int /*midiNoteNumber*/, float /*velocity*/)
  542. {
  543. shouldCheckState = true; // (probably being called from the audio thread, so avoid blocking in here)
  544. }
  545. void MidiKeyboardComponent::handleNoteOff (MidiKeyboardState*, int /*midiChannel*/, int /*midiNoteNumber*/, float /*velocity*/)
  546. {
  547. shouldCheckState = true; // (probably being called from the audio thread, so avoid blocking in here)
  548. }
  549. //==============================================================================
  550. void MidiKeyboardComponent::resetAnyKeysInUse()
  551. {
  552. if (! keysPressed.isZero())
  553. {
  554. for (int i = 128; --i >= 0;)
  555. if (keysPressed[i])
  556. state.noteOff (midiChannel, i, 0.0f);
  557. keysPressed.clear();
  558. }
  559. for (int i = mouseDownNotes.size(); --i >= 0;)
  560. {
  561. auto noteDown = mouseDownNotes.getUnchecked(i);
  562. if (noteDown >= 0)
  563. {
  564. state.noteOff (midiChannel, noteDown, 0.0f);
  565. mouseDownNotes.set (i, -1);
  566. }
  567. mouseOverNotes.set (i, -1);
  568. }
  569. }
  570. void MidiKeyboardComponent::updateNoteUnderMouse (const MouseEvent& e, bool isDown)
  571. {
  572. updateNoteUnderMouse (e.getEventRelativeTo (this).position, isDown, e.source.getIndex());
  573. }
  574. void MidiKeyboardComponent::updateNoteUnderMouse (Point<float> pos, bool isDown, int fingerNum)
  575. {
  576. float mousePositionVelocity = 0.0f;
  577. auto newNote = xyToNote (pos, mousePositionVelocity);
  578. auto oldNote = mouseOverNotes.getUnchecked (fingerNum);
  579. auto oldNoteDown = mouseDownNotes.getUnchecked (fingerNum);
  580. auto eventVelocity = useMousePositionForVelocity ? mousePositionVelocity * velocity : velocity;
  581. if (oldNote != newNote)
  582. {
  583. repaintNote (oldNote);
  584. repaintNote (newNote);
  585. mouseOverNotes.set (fingerNum, newNote);
  586. }
  587. if (isDown)
  588. {
  589. if (newNote != oldNoteDown)
  590. {
  591. if (oldNoteDown >= 0)
  592. {
  593. mouseDownNotes.set (fingerNum, -1);
  594. if (! mouseDownNotes.contains (oldNoteDown))
  595. state.noteOff (midiChannel, oldNoteDown, eventVelocity);
  596. }
  597. if (newNote >= 0 && ! mouseDownNotes.contains (newNote))
  598. {
  599. state.noteOn (midiChannel, newNote, eventVelocity);
  600. mouseDownNotes.set (fingerNum, newNote);
  601. }
  602. }
  603. }
  604. else if (oldNoteDown >= 0)
  605. {
  606. mouseDownNotes.set (fingerNum, -1);
  607. if (! mouseDownNotes.contains (oldNoteDown))
  608. state.noteOff (midiChannel, oldNoteDown, eventVelocity);
  609. }
  610. }
  611. void MidiKeyboardComponent::mouseMove (const MouseEvent& e)
  612. {
  613. updateNoteUnderMouse (e, false);
  614. }
  615. void MidiKeyboardComponent::mouseDrag (const MouseEvent& e)
  616. {
  617. float mousePositionVelocity;
  618. auto newNote = xyToNote (e.position, mousePositionVelocity);
  619. if (newNote >= 0 && mouseDraggedToKey (newNote, e))
  620. updateNoteUnderMouse (e, true);
  621. }
  622. bool MidiKeyboardComponent::mouseDownOnKey (int, const MouseEvent&) { return true; }
  623. bool MidiKeyboardComponent::mouseDraggedToKey (int, const MouseEvent&) { return true; }
  624. void MidiKeyboardComponent::mouseUpOnKey (int, const MouseEvent&) {}
  625. void MidiKeyboardComponent::mouseDown (const MouseEvent& e)
  626. {
  627. float mousePositionVelocity;
  628. auto newNote = xyToNote (e.position, mousePositionVelocity);
  629. if (newNote >= 0 && mouseDownOnKey (newNote, e))
  630. updateNoteUnderMouse (e, true);
  631. }
  632. void MidiKeyboardComponent::mouseUp (const MouseEvent& e)
  633. {
  634. updateNoteUnderMouse (e, false);
  635. float mousePositionVelocity;
  636. auto note = xyToNote (e.position, mousePositionVelocity);
  637. if (note >= 0)
  638. mouseUpOnKey (note, e);
  639. }
  640. void MidiKeyboardComponent::mouseEnter (const MouseEvent& e)
  641. {
  642. updateNoteUnderMouse (e, false);
  643. }
  644. void MidiKeyboardComponent::mouseExit (const MouseEvent& e)
  645. {
  646. updateNoteUnderMouse (e, false);
  647. }
  648. void MidiKeyboardComponent::mouseWheelMove (const MouseEvent&, const MouseWheelDetails& wheel)
  649. {
  650. auto amount = (orientation == horizontalKeyboard && wheel.deltaX != 0)
  651. ? wheel.deltaX : (orientation == verticalKeyboardFacingLeft ? wheel.deltaY
  652. : -wheel.deltaY);
  653. setLowestVisibleKeyFloat (firstKey - amount * keyWidth);
  654. }
  655. void MidiKeyboardComponent::timerCallback()
  656. {
  657. if (shouldCheckState)
  658. {
  659. shouldCheckState = false;
  660. for (int i = rangeStart; i <= rangeEnd; ++i)
  661. {
  662. bool isOn = state.isNoteOnForChannels (midiInChannelMask, i);
  663. if (keysCurrentlyDrawnDown[i] != isOn)
  664. {
  665. keysCurrentlyDrawnDown.setBit (i, isOn);
  666. repaintNote (i);
  667. }
  668. }
  669. }
  670. }
  671. //==============================================================================
  672. void MidiKeyboardComponent::clearKeyMappings()
  673. {
  674. resetAnyKeysInUse();
  675. keyPressNotes.clear();
  676. keyPresses.clear();
  677. }
  678. void MidiKeyboardComponent::setKeyPressForNote (const KeyPress& key, int midiNoteOffsetFromC)
  679. {
  680. removeKeyPressForNote (midiNoteOffsetFromC);
  681. keyPressNotes.add (midiNoteOffsetFromC);
  682. keyPresses.add (key);
  683. }
  684. void MidiKeyboardComponent::removeKeyPressForNote (int midiNoteOffsetFromC)
  685. {
  686. for (int i = keyPressNotes.size(); --i >= 0;)
  687. {
  688. if (keyPressNotes.getUnchecked (i) == midiNoteOffsetFromC)
  689. {
  690. keyPressNotes.remove (i);
  691. keyPresses.remove (i);
  692. }
  693. }
  694. }
  695. void MidiKeyboardComponent::setKeyPressBaseOctave (int newOctaveNumber)
  696. {
  697. jassert (newOctaveNumber >= 0 && newOctaveNumber <= 10);
  698. keyMappingOctave = newOctaveNumber;
  699. }
  700. bool MidiKeyboardComponent::keyStateChanged (bool /*isKeyDown*/)
  701. {
  702. bool keyPressUsed = false;
  703. for (int i = keyPresses.size(); --i >= 0;)
  704. {
  705. auto note = 12 * keyMappingOctave + keyPressNotes.getUnchecked (i);
  706. if (keyPresses.getReference(i).isCurrentlyDown())
  707. {
  708. if (! keysPressed[note])
  709. {
  710. keysPressed.setBit (note);
  711. state.noteOn (midiChannel, note, velocity);
  712. keyPressUsed = true;
  713. }
  714. }
  715. else
  716. {
  717. if (keysPressed[note])
  718. {
  719. keysPressed.clearBit (note);
  720. state.noteOff (midiChannel, note, 0.0f);
  721. keyPressUsed = true;
  722. }
  723. }
  724. }
  725. return keyPressUsed;
  726. }
  727. bool MidiKeyboardComponent::keyPressed (const KeyPress& key)
  728. {
  729. return keyPresses.contains (key);
  730. }
  731. void MidiKeyboardComponent::focusLost (FocusChangeType)
  732. {
  733. resetAnyKeysInUse();
  734. }
  735. } // namespace juce