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.

906 lines
28KB

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