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.

508 lines
18KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2022 - 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 7 End-User License
  8. Agreement and JUCE Privacy Policy.
  9. End User License Agreement: www.juce.com/juce-7-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. struct MPEKeyboardComponent::MPENoteComponent : public Component
  21. {
  22. MPENoteComponent (MPEKeyboardComponent& o, uint16 sID, uint8 initial, float noteOnVel, float press)
  23. : owner (o),
  24. radiusScale (owner.getKeyWidth() / 1.5f),
  25. noteOnVelocity (noteOnVel),
  26. pressure (press),
  27. sourceID (sID),
  28. initialNote (initial)
  29. {
  30. }
  31. float getStrikeRadius() const { return 5.0f + getNoteOnVelocity() * radiusScale * 2.0f; }
  32. float getPressureRadius() const { return 5.0f + getPressure() * radiusScale * 2.0f; }
  33. float getNoteOnVelocity() const { return noteOnVelocity; }
  34. float getPressure() const { return pressure; }
  35. Point<float> getCentrePos() const { return getBounds().toFloat().getCentre(); }
  36. void paint (Graphics& g) override
  37. {
  38. auto strikeSize = getStrikeRadius() * 2.0f;
  39. auto pressSize = getPressureRadius() * 2.0f;
  40. auto bounds = getLocalBounds().toFloat();
  41. g.setColour (owner.findColour (noteCircleFillColourId));
  42. g.fillEllipse (bounds.withSizeKeepingCentre (strikeSize, strikeSize));
  43. g.setColour (owner.findColour (noteCircleOutlineColourId));
  44. g.drawEllipse (bounds.withSizeKeepingCentre (pressSize, pressSize), 1.0f);
  45. }
  46. //==========================================================================
  47. MPEKeyboardComponent& owner;
  48. float radiusScale = 0.0f, noteOnVelocity = 0.0f, pressure = 0.5f;
  49. uint16 sourceID = 0;
  50. uint8 initialNote = 0;
  51. bool isLatched = true;
  52. };
  53. //==============================================================================
  54. MPEKeyboardComponent::MPEKeyboardComponent (MPEInstrument& instr, Orientation orientationToUse)
  55. : KeyboardComponentBase (orientationToUse),
  56. instrument (instr)
  57. {
  58. updateZoneLayout();
  59. colourChanged();
  60. setKeyWidth (25.0f);
  61. instrument.addListener (this);
  62. }
  63. MPEKeyboardComponent::~MPEKeyboardComponent()
  64. {
  65. instrument.removeListener (this);
  66. }
  67. //==============================================================================
  68. void MPEKeyboardComponent::drawKeyboardBackground (Graphics& g, Rectangle<float> area)
  69. {
  70. g.setColour (findColour (whiteNoteColourId));
  71. g.fillRect (area);
  72. }
  73. void MPEKeyboardComponent::drawWhiteKey (int midiNoteNumber, Graphics& g, Rectangle<float> area)
  74. {
  75. if (midiNoteNumber % 12 == 0)
  76. {
  77. auto fontHeight = jmin (12.0f, getKeyWidth() * 0.9f);
  78. auto text = MidiMessage::getMidiNoteName (midiNoteNumber, true, true, getOctaveForMiddleC());
  79. g.setColour (findColour (textLabelColourId));
  80. g.setFont (Font (fontHeight).withHorizontalScale (0.8f));
  81. switch (getOrientation())
  82. {
  83. case horizontalKeyboard:
  84. g.drawText (text, area.withTrimmedLeft (1.0f).withTrimmedBottom (2.0f),
  85. Justification::centredBottom, false);
  86. break;
  87. case verticalKeyboardFacingLeft:
  88. g.drawText (text, area.reduced (2.0f), Justification::centredLeft, false);
  89. break;
  90. case verticalKeyboardFacingRight:
  91. g.drawText (text, area.reduced (2.0f), Justification::centredRight, false);
  92. break;
  93. default:
  94. break;
  95. }
  96. }
  97. }
  98. void MPEKeyboardComponent::drawBlackKey (int /*midiNoteNumber*/, Graphics& g, Rectangle<float> area)
  99. {
  100. g.setColour (findColour (whiteNoteColourId));
  101. g.fillRect (area);
  102. g.setColour (findColour (blackNoteColourId));
  103. if (isHorizontal())
  104. {
  105. g.fillRoundedRectangle (area.toFloat().reduced ((area.getWidth() / 2.0f) - (getBlackNoteWidth() / 12.0f),
  106. area.getHeight() / 4.0f), 1.0f);
  107. }
  108. else
  109. {
  110. g.fillRoundedRectangle (area.toFloat().reduced (area.getWidth() / 4.0f,
  111. (area.getHeight() / 2.0f) - (getBlackNoteWidth() / 12.0f)), 1.0f);
  112. }
  113. }
  114. void MPEKeyboardComponent::colourChanged()
  115. {
  116. setOpaque (findColour (whiteNoteColourId).isOpaque());
  117. repaint();
  118. }
  119. //==========================================================================
  120. MPEValue MPEKeyboardComponent::mousePositionToPitchbend (int initialNote, Point<float> mousePos)
  121. {
  122. auto constrainedMousePos = [&]
  123. {
  124. auto horizontal = isHorizontal();
  125. auto posToCheck = jlimit (0.0f,
  126. horizontal ? (float) getWidth() - 1.0f : (float) getHeight(),
  127. horizontal ? mousePos.x : mousePos.y);
  128. auto bottomKeyRange = getRectangleForKey (jmax (getRangeStart(), initialNote - perNotePitchbendRange));
  129. auto topKeyRange = getRectangleForKey (jmin (getRangeEnd(), initialNote + perNotePitchbendRange));
  130. auto lowerLimit = horizontal ? bottomKeyRange.getCentreX()
  131. : getOrientation() == Orientation::verticalKeyboardFacingRight ? topKeyRange.getCentreY()
  132. : bottomKeyRange.getCentreY();
  133. auto upperLimit = horizontal ? topKeyRange.getCentreX()
  134. : getOrientation() == Orientation::verticalKeyboardFacingRight ? bottomKeyRange.getCentreY()
  135. : topKeyRange.getCentreY();
  136. posToCheck = jlimit (lowerLimit, upperLimit, posToCheck);
  137. return horizontal ? Point<float> (posToCheck, 0.0f)
  138. : Point<float> (0.0f, posToCheck);
  139. }();
  140. auto note = getNoteAndVelocityAtPosition (constrainedMousePos, true).note;
  141. if (note == -1)
  142. {
  143. jassertfalse;
  144. return {};
  145. }
  146. auto fractionalSemitoneBend = [&]
  147. {
  148. auto noteRect = getRectangleForKey (note);
  149. switch (getOrientation())
  150. {
  151. case horizontalKeyboard: return (constrainedMousePos.x - noteRect.getCentreX()) / noteRect.getWidth();
  152. case verticalKeyboardFacingRight: return (noteRect.getCentreY() - constrainedMousePos.y) / noteRect.getHeight();
  153. case verticalKeyboardFacingLeft: return (constrainedMousePos.y - noteRect.getCentreY()) / noteRect.getHeight();
  154. }
  155. jassertfalse;
  156. return 0.0f;
  157. }();
  158. auto totalNumSemitones = ((float) note + fractionalSemitoneBend) - (float) initialNote;
  159. return MPEValue::fromUnsignedFloat (jmap (totalNumSemitones, (float) -perNotePitchbendRange, (float) perNotePitchbendRange, 0.0f, 1.0f));
  160. }
  161. MPEValue MPEKeyboardComponent::mousePositionToTimbre (Point<float> mousePos)
  162. {
  163. auto delta = [mousePos, this]
  164. {
  165. switch (getOrientation())
  166. {
  167. case horizontalKeyboard: return mousePos.y;
  168. case verticalKeyboardFacingLeft: return (float) getWidth() - mousePos.x;
  169. case verticalKeyboardFacingRight: return mousePos.x;
  170. }
  171. jassertfalse;
  172. return 0.0f;
  173. }();
  174. return MPEValue::fromUnsignedFloat (jlimit (0.0f, 1.0f, 1.0f - (delta / getWhiteNoteLength())));
  175. }
  176. void MPEKeyboardComponent::mouseDown (const MouseEvent& e)
  177. {
  178. auto newNote = getNoteAndVelocityAtPosition (e.position).note;
  179. if (newNote >= 0)
  180. {
  181. auto channel = channelAssigner->findMidiChannelForNewNote (newNote);
  182. instrument.noteOn (channel, newNote, MPEValue::fromUnsignedFloat (velocity));
  183. sourceIDMap[e.source.getIndex()] = instrument.getNote (instrument.getNumPlayingNotes() - 1).noteID;
  184. instrument.pitchbend (channel, MPEValue::centreValue());
  185. instrument.timbre (channel, mousePositionToTimbre (e.position));
  186. instrument.pressure (channel, MPEValue::fromUnsignedFloat (e.isPressureValid()
  187. && useMouseSourcePressureForStrike ? e.pressure
  188. : pressure));
  189. }
  190. }
  191. void MPEKeyboardComponent::mouseDrag (const MouseEvent& e)
  192. {
  193. auto noteID = sourceIDMap[e.source.getIndex()];
  194. auto note = instrument.getNoteWithID (noteID);
  195. if (! note.isValid())
  196. return;
  197. auto noteComponent = std::find_if (noteComponents.begin(),
  198. noteComponents.end(),
  199. [noteID] (auto& comp) { return comp->sourceID == noteID; });
  200. if (noteComponent == noteComponents.end())
  201. return;
  202. if ((*noteComponent)->isLatched && std::abs (isHorizontal() ? e.getDistanceFromDragStartX()
  203. : e.getDistanceFromDragStartY()) > roundToInt (getKeyWidth() / 4.0f))
  204. {
  205. (*noteComponent)->isLatched = false;
  206. }
  207. auto channel = channelAssigner->findMidiChannelForExistingNote (note.initialNote);
  208. if (! (*noteComponent)->isLatched)
  209. instrument.pitchbend (channel, mousePositionToPitchbend (note.initialNote, e.position));
  210. instrument.timbre (channel, mousePositionToTimbre (e.position));
  211. instrument.pressure (channel, MPEValue::fromUnsignedFloat (e.isPressureValid()
  212. && useMouseSourcePressureForStrike ? e.pressure
  213. : pressure));
  214. }
  215. void MPEKeyboardComponent::mouseUp (const MouseEvent& e)
  216. {
  217. auto note = instrument.getNoteWithID (sourceIDMap[e.source.getIndex()]);
  218. if (! note.isValid())
  219. return;
  220. instrument.noteOff (channelAssigner->findMidiChannelForExistingNote (note.initialNote),
  221. note.initialNote, MPEValue::fromUnsignedFloat (lift));
  222. channelAssigner->noteOff (note.initialNote);
  223. sourceIDMap.erase (e.source.getIndex());
  224. }
  225. void MPEKeyboardComponent::focusLost (FocusChangeType)
  226. {
  227. for (auto& comp : noteComponents)
  228. {
  229. auto note = instrument.getNoteWithID (comp->sourceID);
  230. if (note.isValid())
  231. instrument.noteOff (channelAssigner->findMidiChannelForExistingNote (note.initialNote),
  232. note.initialNote, MPEValue::fromUnsignedFloat (lift));
  233. }
  234. }
  235. //==============================================================================
  236. void MPEKeyboardComponent::updateZoneLayout()
  237. {
  238. {
  239. const ScopedLock noteLock (activeNotesLock);
  240. activeNotes.clear();
  241. }
  242. noteComponents.clear();
  243. if (instrument.isLegacyModeEnabled())
  244. {
  245. channelAssigner = std::make_unique<MPEChannelAssigner> (instrument.getLegacyModeChannelRange());
  246. perNotePitchbendRange = instrument.getLegacyModePitchbendRange();
  247. }
  248. else
  249. {
  250. auto layout = instrument.getZoneLayout();
  251. if (layout.isActive())
  252. {
  253. auto zone = layout.getLowerZone().isActive() ? layout.getLowerZone()
  254. : layout.getUpperZone();
  255. channelAssigner = std::make_unique<MPEChannelAssigner> (zone);
  256. perNotePitchbendRange = zone.perNotePitchbendRange;
  257. }
  258. else
  259. {
  260. channelAssigner.reset();
  261. }
  262. }
  263. }
  264. void MPEKeyboardComponent::addNewNote (MPENote note)
  265. {
  266. noteComponents.push_back (std::make_unique<MPENoteComponent> (*this, note.noteID, note.initialNote,
  267. note.noteOnVelocity.asUnsignedFloat(),
  268. note.pressure.asUnsignedFloat()));
  269. auto& comp = noteComponents.back();
  270. addAndMakeVisible (*comp);
  271. comp->toBack();
  272. }
  273. void MPEKeyboardComponent::handleNoteOns (std::set<MPENote>& notesToUpdate)
  274. {
  275. for (auto& note : notesToUpdate)
  276. {
  277. if (! std::any_of (noteComponents.begin(),
  278. noteComponents.end(),
  279. [note] (auto& comp) { return comp->sourceID == note.noteID; }))
  280. {
  281. addNewNote (note);
  282. }
  283. }
  284. }
  285. void MPEKeyboardComponent::handleNoteOffs (std::set<MPENote>& notesToUpdate)
  286. {
  287. auto removePredicate = [&notesToUpdate] (std::unique_ptr<MPENoteComponent>& comp)
  288. {
  289. return std::none_of (notesToUpdate.begin(),
  290. notesToUpdate.end(),
  291. [&comp] (auto& note) { return comp->sourceID == note.noteID; });
  292. };
  293. noteComponents.erase (std::remove_if (std::begin (noteComponents),
  294. std::end (noteComponents),
  295. removePredicate),
  296. std::end (noteComponents));
  297. if (noteComponents.empty())
  298. stopTimer();
  299. }
  300. void MPEKeyboardComponent::updateNoteComponentBounds (const MPENote& note, MPENoteComponent& noteComponent)
  301. {
  302. auto xPos = [&]
  303. {
  304. const auto currentNote = note.initialNote + (float) note.totalPitchbendInSemitones;
  305. const auto noteBend = currentNote - std::floor (currentNote);
  306. const auto noteBounds = getRectangleForKey ((int) currentNote);
  307. const auto nextNoteBounds = getRectangleForKey ((int) currentNote + 1);
  308. const auto horizontal = isHorizontal();
  309. const auto distance = noteBend * (horizontal ? nextNoteBounds.getCentreX() - noteBounds.getCentreX()
  310. : nextNoteBounds.getCentreY() - noteBounds.getCentreY());
  311. return (horizontal ? noteBounds.getCentreX() : noteBounds.getCentreY()) + distance;
  312. }();
  313. auto yPos = [&]
  314. {
  315. const auto currentOrientation = getOrientation();
  316. const auto timbrePosition = (currentOrientation == horizontalKeyboard
  317. || currentOrientation == verticalKeyboardFacingRight ? 1.0f - note.timbre.asUnsignedFloat()
  318. : note.timbre.asUnsignedFloat());
  319. return timbrePosition * getWhiteNoteLength();
  320. }();
  321. const auto centrePos = (isHorizontal() ? Point<float> (xPos, yPos)
  322. : Point<float> (yPos, xPos));
  323. const auto radius = jmax (noteComponent.getStrikeRadius(), noteComponent.getPressureRadius());
  324. noteComponent.setBounds (Rectangle<float> (radius * 2.0f, radius * 2.0f)
  325. .withCentre (centrePos)
  326. .getSmallestIntegerContainer());
  327. }
  328. static bool operator< (const MPENote& n1, const MPENote& n2) noexcept { return n1.noteID < n2.noteID; }
  329. void MPEKeyboardComponent::updateNoteComponents()
  330. {
  331. std::set<MPENote> notesToUpdate;
  332. {
  333. ScopedLock noteLock (activeNotesLock);
  334. for (const auto& note : activeNotes)
  335. if (note.second)
  336. notesToUpdate.insert (note.first);
  337. };
  338. handleNoteOns (notesToUpdate);
  339. handleNoteOffs (notesToUpdate);
  340. for (auto& comp : noteComponents)
  341. {
  342. auto noteForComponent = std::find_if (notesToUpdate.begin(),
  343. notesToUpdate.end(),
  344. [&comp] (auto& note) { return note.noteID == comp->sourceID; });
  345. if (noteForComponent != notesToUpdate.end())
  346. {
  347. comp->pressure = noteForComponent->pressure.asUnsignedFloat();
  348. updateNoteComponentBounds (*noteForComponent, *comp);
  349. comp->repaint();
  350. }
  351. }
  352. }
  353. void MPEKeyboardComponent::timerCallback()
  354. {
  355. updateNoteComponents();
  356. }
  357. //==============================================================================
  358. void MPEKeyboardComponent::noteAdded (MPENote newNote)
  359. {
  360. {
  361. const ScopedLock noteLock (activeNotesLock);
  362. activeNotes.push_back ({ newNote, true });
  363. }
  364. startTimerHz (30);
  365. }
  366. void MPEKeyboardComponent::updateNoteData (MPENote& changedNote)
  367. {
  368. const ScopedLock noteLock (activeNotesLock);
  369. for (auto& note : activeNotes)
  370. {
  371. if (note.first.noteID == changedNote.noteID)
  372. {
  373. note.first = changedNote;
  374. note.second = true;
  375. return;
  376. }
  377. }
  378. }
  379. void MPEKeyboardComponent::notePressureChanged (MPENote changedNote)
  380. {
  381. updateNoteData (changedNote);
  382. }
  383. void MPEKeyboardComponent::notePitchbendChanged (MPENote changedNote)
  384. {
  385. updateNoteData (changedNote);
  386. }
  387. void MPEKeyboardComponent::noteTimbreChanged (MPENote changedNote)
  388. {
  389. updateNoteData (changedNote);
  390. }
  391. void MPEKeyboardComponent::noteReleased (MPENote finishedNote)
  392. {
  393. const ScopedLock noteLock (activeNotesLock);
  394. activeNotes.erase (std::remove_if (std::begin (activeNotes),
  395. std::end (activeNotes),
  396. [finishedNote] (auto& note) { return note.first.noteID == finishedNote.noteID; }),
  397. std::end (activeNotes));
  398. }
  399. void MPEKeyboardComponent::zoneLayoutChanged()
  400. {
  401. MessageManager::callAsync ([this] { updateZoneLayout(); });
  402. }
  403. } // namespace juce