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