JUCE-7.0.12-0-g4f43011b96 JUCE-7.0.12-0-g4f43011b96
JUCE — C++ application framework with suport for VST, VST3, LV2 audio plug-ins

« « « Anklang Documentation
Loading...
Searching...
No Matches
juce_MPEKeyboardComponent.cpp
Go to the documentation of this file.
1 /*
2 ==============================================================================
3
4 This file is part of the JUCE library.
5 Copyright (c) 2022 - 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 7 End-User License
11 Agreement and JUCE Privacy Policy.
12
13 End User License Agreement: www.juce.com/juce-7-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
26namespace juce
27{
28
30{
32 : owner (o),
33 radiusScale (owner.getKeyWidth() / 1.5f),
34 noteOnVelocity (noteOnVel),
35 pressure (press),
36 sourceID (sID),
37 initialNote (initial)
38 {
39 }
40
41 float getStrikeRadius() const { return 5.0f + getNoteOnVelocity() * radiusScale * 2.0f; }
42 float getPressureRadius() const { return 5.0f + getPressure() * radiusScale * 2.0f; }
43
44 float getNoteOnVelocity() const { return noteOnVelocity; }
45 float getPressure() const { return pressure; }
46
47 Point<float> getCentrePos() const { return getBounds().toFloat().getCentre(); }
48
49 void paint (Graphics& g) override
50 {
51 auto strikeSize = getStrikeRadius() * 2.0f;
52 auto pressSize = getPressureRadius() * 2.0f;
53 auto bounds = getLocalBounds().toFloat();
54
55 g.setColour (owner.findColour (noteCircleFillColourId));
56 g.fillEllipse (bounds.withSizeKeepingCentre (strikeSize, strikeSize));
57
58 g.setColour (owner.findColour (noteCircleOutlineColourId));
59 g.drawEllipse (bounds.withSizeKeepingCentre (pressSize, pressSize), 1.0f);
60 }
61
62 //==============================================================================
64
65 float radiusScale = 0.0f, noteOnVelocity = 0.0f, pressure = 0.5f;
66 uint16 sourceID = 0;
67 uint8 initialNote = 0;
68 bool isLatched = true;
69};
70
71//==============================================================================
74 instrument (instr)
75{
76 updateZoneLayout();
78 setKeyWidth (25.0f);
79
80 instrument.addListener (this);
81}
82
87
88//==============================================================================
89void MPEKeyboardComponent::drawKeyboardBackground (Graphics& g, Rectangle<float> area)
90{
91 g.setColour (findColour (whiteNoteColourId));
92 g.fillRect (area);
93}
94
95void MPEKeyboardComponent::drawWhiteKey (int midiNoteNumber, Graphics& g, Rectangle<float> area)
96{
97 if (midiNoteNumber % 12 == 0)
98 {
99 auto fontHeight = jmin (12.0f, getKeyWidth() * 0.9f);
101
102 g.setColour (findColour (textLabelColourId));
103 g.setFont (Font (fontHeight).withHorizontalScale (0.8f));
104
105 switch (getOrientation())
106 {
107 case horizontalKeyboard:
108 g.drawText (text, area.withTrimmedLeft (1.0f).withTrimmedBottom (2.0f),
110 break;
111 case verticalKeyboardFacingLeft:
112 g.drawText (text, area.reduced (2.0f), Justification::centredLeft, false);
113 break;
114 case verticalKeyboardFacingRight:
115 g.drawText (text, area.reduced (2.0f), Justification::centredRight, false);
116 break;
117 default:
118 break;
119 }
120 }
121}
122
123void MPEKeyboardComponent::drawBlackKey (int /*midiNoteNumber*/, Graphics& g, Rectangle<float> area)
124{
125 g.setColour (findColour (whiteNoteColourId));
126 g.fillRect (area);
127
128 g.setColour (findColour (blackNoteColourId));
129
130 if (isHorizontal())
131 {
132 g.fillRoundedRectangle (area.toFloat().reduced ((area.getWidth() / 2.0f) - (getBlackNoteWidth() / 12.0f),
133 area.getHeight() / 4.0f), 1.0f);
134 }
135 else
136 {
137 g.fillRoundedRectangle (area.toFloat().reduced (area.getWidth() / 4.0f,
138 (area.getHeight() / 2.0f) - (getBlackNoteWidth() / 12.0f)), 1.0f);
139 }
140}
141
143{
144 setOpaque (findColour (whiteNoteColourId).isOpaque());
145 repaint();
146}
147
148//==============================================================================
149MPEValue MPEKeyboardComponent::mousePositionToPitchbend (int initialNote, Point<float> mousePos)
150{
151 auto constrainedMousePos = [&]
152 {
153 auto horizontal = isHorizontal();
154
155 auto posToCheck = jlimit (0.0f,
156 horizontal ? (float) getWidth() - 1.0f : (float) getHeight(),
157 horizontal ? mousePos.x : mousePos.y);
158
159 auto bottomKeyRange = getRectangleForKey (jmax (getRangeStart(), initialNote - perNotePitchbendRange));
160 auto topKeyRange = getRectangleForKey (jmin (getRangeEnd(), initialNote + perNotePitchbendRange));
161
162 auto lowerLimit = horizontal ? bottomKeyRange.getCentreX()
163 : getOrientation() == Orientation::verticalKeyboardFacingRight ? topKeyRange.getCentreY()
164 : bottomKeyRange.getCentreY();
165
166 auto upperLimit = horizontal ? topKeyRange.getCentreX()
167 : getOrientation() == Orientation::verticalKeyboardFacingRight ? bottomKeyRange.getCentreY()
168 : topKeyRange.getCentreY();
169
171
172 return horizontal ? Point<float> (posToCheck, 0.0f)
173 : Point<float> (0.0f, posToCheck);
174 }();
175
176 auto note = getNoteAndVelocityAtPosition (constrainedMousePos, true).note;
177
178 if (note == -1)
179 {
181 return {};
182 }
183
184 auto fractionalSemitoneBend = [&]
185 {
186 auto noteRect = getRectangleForKey (note);
187
188 switch (getOrientation())
189 {
190 case horizontalKeyboard: return (constrainedMousePos.x - noteRect.getCentreX()) / noteRect.getWidth();
191 case verticalKeyboardFacingRight: return (noteRect.getCentreY() - constrainedMousePos.y) / noteRect.getHeight();
192 case verticalKeyboardFacingLeft: return (constrainedMousePos.y - noteRect.getCentreY()) / noteRect.getHeight();
193 }
194
196 return 0.0f;
197 }();
198
199 auto totalNumSemitones = ((float) note + fractionalSemitoneBend) - (float) initialNote;
200
201 return MPEValue::fromUnsignedFloat (jmap (totalNumSemitones, (float) -perNotePitchbendRange, (float) perNotePitchbendRange, 0.0f, 1.0f));
202}
203
204MPEValue MPEKeyboardComponent::mousePositionToTimbre (Point<float> mousePos)
205{
206 auto delta = [mousePos, this]
207 {
208 switch (getOrientation())
209 {
210 case horizontalKeyboard: return mousePos.y;
211 case verticalKeyboardFacingLeft: return (float) getWidth() - mousePos.x;
212 case verticalKeyboardFacingRight: return mousePos.x;
213 }
214
216 return 0.0f;
217 }();
218
219 return MPEValue::fromUnsignedFloat (jlimit (0.0f, 1.0f, 1.0f - (delta / getWhiteNoteLength())));
220}
221
223{
225
226 if (newNote >= 0)
227 {
228 auto channel = channelAssigner->findMidiChannelForNewNote (newNote);
229
230 instrument.noteOn (channel, newNote, MPEValue::fromUnsignedFloat (velocity));
231 sourceIDMap[e.source.getIndex()] = instrument.getNote (instrument.getNumPlayingNotes() - 1).noteID;
232
233 instrument.pitchbend (channel, MPEValue::centreValue());
234 instrument.timbre (channel, mousePositionToTimbre (e.position));
236 && useMouseSourcePressureForStrike ? e.pressure
237 : pressure));
238 }
239}
240
242{
243 auto noteID = sourceIDMap[e.source.getIndex()];
244 auto note = instrument.getNoteWithID (noteID);
245
246 if (! note.isValid())
247 return;
248
249 auto noteComponent = std::find_if (noteComponents.begin(),
250 noteComponents.end(),
251 [noteID] (auto& comp) { return comp->sourceID == noteID; });
252
253 if (noteComponent == noteComponents.end())
254 return;
255
256 if ((*noteComponent)->isLatched && std::abs (isHorizontal() ? e.getDistanceFromDragStartX()
258 {
259 (*noteComponent)->isLatched = false;
260 }
261
262 auto channel = channelAssigner->findMidiChannelForExistingNote (note.initialNote);
263
264 if (! (*noteComponent)->isLatched)
265 instrument.pitchbend (channel, mousePositionToPitchbend (note.initialNote, e.position));
266
267 instrument.timbre (channel, mousePositionToTimbre (e.position));
269 && useMouseSourcePressureForStrike ? e.pressure
270 : pressure));
271}
272
274{
275 auto note = instrument.getNoteWithID (sourceIDMap[e.source.getIndex()]);
276
277 if (! note.isValid())
278 return;
279
280 instrument.noteOff (channelAssigner->findMidiChannelForExistingNote (note.initialNote),
281 note.initialNote, MPEValue::fromUnsignedFloat (lift));
282 channelAssigner->noteOff (note.initialNote);
283 sourceIDMap.erase (e.source.getIndex());
284}
285
287{
288 for (auto& comp : noteComponents)
289 {
290 auto note = instrument.getNoteWithID (comp->sourceID);
291
292 if (note.isValid())
293 instrument.noteOff (channelAssigner->findMidiChannelForExistingNote (note.initialNote),
294 note.initialNote, MPEValue::fromUnsignedFloat (lift));
295 }
296}
297
298//==============================================================================
299void MPEKeyboardComponent::updateZoneLayout()
300{
301 {
302 const ScopedLock noteLock (activeNotesLock);
303 activeNotes.clear();
304 }
305
306 noteComponents.clear();
307
308 if (instrument.isLegacyModeEnabled())
309 {
310 channelAssigner = std::make_unique<MPEChannelAssigner> (instrument.getLegacyModeChannelRange());
311 perNotePitchbendRange = instrument.getLegacyModePitchbendRange();
312 }
313 else
314 {
315 auto layout = instrument.getZoneLayout();
316
317 if (layout.isActive())
318 {
319 auto zone = layout.getLowerZone().isActive() ? layout.getLowerZone()
320 : layout.getUpperZone();
321
322 channelAssigner = std::make_unique<MPEChannelAssigner> (zone);
323 perNotePitchbendRange = zone.perNotePitchbendRange;
324 }
325 else
326 {
327 channelAssigner.reset();
328 }
329 }
330}
331
332void MPEKeyboardComponent::addNewNote (MPENote note)
333{
334 noteComponents.push_back (std::make_unique<MPENoteComponent> (*this, note.noteID, note.initialNote,
335 note.noteOnVelocity.asUnsignedFloat(),
336 note.pressure.asUnsignedFloat()));
337 auto& comp = noteComponents.back();
338
339 addAndMakeVisible (*comp);
340 comp->toBack();
341}
342
343void MPEKeyboardComponent::handleNoteOns (std::set<MPENote>& notesToUpdate)
344{
345 for (auto& note : notesToUpdate)
346 {
347 if (! std::any_of (noteComponents.begin(),
348 noteComponents.end(),
349 [note] (auto& comp) { return comp->sourceID == note.noteID; }))
350 {
351 addNewNote (note);
352 }
353 }
354}
355
356void MPEKeyboardComponent::handleNoteOffs (std::set<MPENote>& notesToUpdate)
357{
359 {
360 return std::none_of (notesToUpdate.begin(),
361 notesToUpdate.end(),
362 [&comp] (auto& note) { return comp->sourceID == note.noteID; });
363 };
364
365 noteComponents.erase (std::remove_if (std::begin (noteComponents),
366 std::end (noteComponents),
368 std::end (noteComponents));
369
370 if (noteComponents.empty())
371 stopTimer();
372}
373
374void MPEKeyboardComponent::updateNoteComponentBounds (const MPENote& note, MPENoteComponent& noteComponent)
375{
376 auto xPos = [&]
377 {
378 const auto currentNote = note.initialNote + (float) note.totalPitchbendInSemitones;
380
381 const auto noteBounds = getRectangleForKey ((int) currentNote);
382 const auto nextNoteBounds = getRectangleForKey ((int) currentNote + 1);
383
384 const auto horizontal = isHorizontal();
385
386 const auto distance = noteBend * (horizontal ? nextNoteBounds.getCentreX() - noteBounds.getCentreX()
387 : nextNoteBounds.getCentreY() - noteBounds.getCentreY());
388
389 return (horizontal ? noteBounds.getCentreX() : noteBounds.getCentreY()) + distance;
390 }();
391
392 auto yPos = [&]
393 {
394 const auto currentOrientation = getOrientation();
395
396 const auto timbrePosition = (currentOrientation == horizontalKeyboard
397 || currentOrientation == verticalKeyboardFacingRight ? 1.0f - note.timbre.asUnsignedFloat()
398 : note.timbre.asUnsignedFloat());
399
401 }();
402
403 const auto centrePos = (isHorizontal() ? Point<float> (xPos, yPos)
404 : Point<float> (yPos, xPos));
405
406 const auto radius = jmax (noteComponent.getStrikeRadius(), noteComponent.getPressureRadius());
407
408 noteComponent.setBounds (Rectangle<float> (radius * 2.0f, radius * 2.0f)
409 .withCentre (centrePos)
410 .getSmallestIntegerContainer());
411}
412
413static bool operator< (const MPENote& n1, const MPENote& n2) noexcept { return n1.noteID < n2.noteID; }
414
415void MPEKeyboardComponent::updateNoteComponents()
416{
418
419 {
420 ScopedLock noteLock (activeNotesLock);
421
422 for (const auto& note : activeNotes)
423 if (note.second)
424 notesToUpdate.insert (note.first);
425 };
426
427 handleNoteOns (notesToUpdate);
428 handleNoteOffs (notesToUpdate);
429
430 for (auto& comp : noteComponents)
431 {
433 notesToUpdate.end(),
434 [&comp] (auto& note) { return note.noteID == comp->sourceID; });
435
436 if (noteForComponent != notesToUpdate.end())
437 {
438 comp->pressure = noteForComponent->pressure.asUnsignedFloat();
439 updateNoteComponentBounds (*noteForComponent, *comp);
440
441 comp->repaint();
442 }
443 }
444}
445
446void MPEKeyboardComponent::timerCallback()
447{
448 updateNoteComponents();
449}
450
451//==============================================================================
452void MPEKeyboardComponent::noteAdded (MPENote newNote)
453{
454 {
455 const ScopedLock noteLock (activeNotesLock);
456 activeNotes.push_back ({ newNote, true });
457 }
458
459 startTimerHz (30);
460}
461
462void MPEKeyboardComponent::updateNoteData (MPENote& changedNote)
463{
464 const ScopedLock noteLock (activeNotesLock);
465
466 for (auto& note : activeNotes)
467 {
468 if (note.first.noteID == changedNote.noteID)
469 {
470 note.first = changedNote;
471 note.second = true;
472 return;
473 }
474 }
475}
476
477void MPEKeyboardComponent::notePressureChanged (MPENote changedNote)
478{
479 updateNoteData (changedNote);
480}
481
482void MPEKeyboardComponent::notePitchbendChanged (MPENote changedNote)
483{
484 updateNoteData (changedNote);
485}
486
487void MPEKeyboardComponent::noteTimbreChanged (MPENote changedNote)
488{
489 updateNoteData (changedNote);
490}
491
492void MPEKeyboardComponent::noteReleased (MPENote finishedNote)
493{
494 const ScopedLock noteLock (activeNotesLock);
495
496 activeNotes.erase (std::remove_if (std::begin (activeNotes),
497 std::end (activeNotes),
498 [finishedNote] (auto& note) { return note.first.noteID == finishedNote.noteID; }),
499 std::end (activeNotes));
500}
501
502void MPEKeyboardComponent::zoneLayoutChanged()
503{
504 MessageManager::callAsync ([this] { updateZoneLayout(); });
505}
506
507} // namespace juce
T any_of(T... args)
T begin(T... args)
The base class for all JUCE user-interface objects.
bool isOpaque() const noexcept
Returns true if no parts of this component are transparent.
int getHeight() const noexcept
Returns the component's height in pixels.
void addAndMakeVisible(Component *child, int zOrder=-1)
Adds a child component to this one, and also makes the child visible if it isn't already.
FocusChangeType
Enumeration used by the focusGained() and focusLost() methods.
void setOpaque(bool shouldBeOpaque)
Indicates whether any parts of the component might be transparent.
Rectangle< int > getBounds() const noexcept
Returns this component's bounding box.
void repaint()
Marks the whole component as needing to be redrawn.
Colour findColour(int colourID, bool inheritFromParent=false) const
Looks for a colour that has been registered with the given colour ID number.
int getWidth() const noexcept
Returns the component's width in pixels.
Rectangle< int > getLocalBounds() const noexcept
Returns the component's bounds, relative to its own origin.
Automatically locks and unlocks a mutex object.
A graphics context, used for drawing a component or image.
void fillRect(Rectangle< int > rectangle) const
Fills a rectangle with the current colour or brush.
void drawEllipse(float x, float y, float width, float height, float lineThickness) const
Draws an elliptical stroke using the current colour or brush.
void setColour(Colour newColour)
Changes the current drawing colour.
void fillEllipse(float x, float y, float width, float height) const
Fills an ellipse with the current colour or brush.
@ centredRight
Indicates that the item should be centred vertically but placed on the right hand side.
@ centredBottom
Indicates that the item should be centred horizontally and placed at the bottom.
@ centredLeft
Indicates that the item should be centred vertically but placed on the left hand side.
A base class for drawing a custom MIDI keyboard component.
bool isHorizontal() const noexcept
Returns true if the keyboard's orientation is horizontal.
float getKeyWidth() const noexcept
Returns the width that was set by setKeyWidth().
float getBlackNoteWidth() const noexcept
Returns the absolute width of the black notes.
void setKeyWidth(float widthInPixels)
Changes the width used to draw the white keys.
int getRangeStart() const noexcept
Returns the first note in the available range.
int getRangeEnd() const noexcept
Returns the last note in the available range.
Rectangle< float > getRectangleForKey(int midiNoteNumber) const
Returns the rectangle for a given key.
int getOctaveForMiddleC() const noexcept
This returns the value set by setOctaveForMiddleC().
NoteAndVelocity getNoteAndVelocityAtPosition(Point< float > position, bool includeChildComponents=false)
Returns the note number and velocity for a given position within the component.
Orientation getOrientation() const noexcept
Returns the keyboard's current direction.
Orientation
The direction of the keyboard.
float getWhiteNoteLength() const noexcept
Returns the absolute length of the white notes.
This class represents an instrument handling MPE.
MPEZoneLayout getZoneLayout() const noexcept
Returns the current zone layout of the instrument.
virtual void pitchbend(int midiChannel, MPEValue pitchbend)
Request a pitchbend on the given channel with the given value (in units of MIDI pitchwheel position).
MPENote getNote(int index) const noexcept
Returns the note at the given index.
bool isLegacyModeEnabled() const noexcept
Returns true if the instrument is in legacy mode, false otherwise.
void addListener(Listener *listenerToAdd)
Adds a listener.
void removeListener(Listener *listenerToRemove)
Removes a listener.
MPENote getNoteWithID(uint16 noteID) const noexcept
Returns the note with a given ID.
int getNumPlayingNotes() const noexcept
Returns the number of MPE notes currently played by the instrument.
virtual void timbre(int midiChannel, MPEValue value)
Request a third dimension (timbre) change on the given channel with the given value.
Range< int > getLegacyModeChannelRange() const noexcept
Returns the range of MIDI channels (1-16) to be used for notes when in legacy mode.
virtual void noteOn(int midiChannel, int midiNoteNumber, MPEValue midiNoteOnVelocity)
Request a note-on on the given channel, with the given initial note number and velocity.
int getLegacyModePitchbendRange() const noexcept
Returns the pitchbend range in semitones (0-96) to be used for notes when in legacy mode.
virtual void noteOff(int midiChannel, int midiNoteNumber, MPEValue midiNoteOffVelocity)
Request a note-off.
virtual void pressure(int midiChannel, MPEValue value)
Request a pressure change on the given channel with the given value.
A component that displays an MPE-compatible keyboard, whose notes can be clicked on.
void mouseUp(const MouseEvent &) override
Called when a mouse button is released.
void mouseDrag(const MouseEvent &) override
Called when the mouse is moved while a button is held down.
MPEKeyboardComponent(MPEInstrument &instrument, Orientation orientation)
Creates an MPEKeyboardComponent.
void focusLost(FocusChangeType) override
Called to indicate that this component has just lost the keyboard focus.
void colourChanged() override
This method is called when a colour is changed by the setColour() method, or when the look-and-feel i...
virtual ~MPEKeyboardComponent() override
Destructor.
void mouseDown(const MouseEvent &) override
Called when a mouse button is pressed.
This class represents a single value for any of the MPE dimensions of control.
static MPEValue centreValue() noexcept
Constructs an MPEValue corresponding to the centre value.
static MPEValue fromUnsignedFloat(float value) noexcept
Constructs an MPEValue from a float between 0.0f and 1.0f.
MPEZone getLowerZone() const noexcept
Returns a struct representing the lower MPE zone.
static bool callAsync(std::function< void()> functionToCall)
Asynchronously invokes a function or C++11 lambda on the message thread.
static String getMidiNoteName(int noteNumber, bool useSharps, bool includeOctaveNumber, int octaveNumForMiddleC)
Returns the name of a midi note number.
Contains position and status information about a mouse event.
MouseInputSource source
The source device that generated this event.
const float pressure
The pressure of the touch or stylus for this event.
const Point< float > position
The position of the mouse when the event occurred.
bool isPressureValid() const noexcept
Returns true if the pressure value for this event is meaningful.
int getDistanceFromDragStartY() const noexcept
Returns the difference between the mouse's current y position and where it was when the button was la...
int getDistanceFromDragStartX() const noexcept
Returns the difference between the mouse's current x position and where it was when the button was la...
int getIndex() const noexcept
Returns this source's index in the global list of possible sources.
A pair of (x, y) coordinates.
Definition juce_Point.h:42
Manages a rectangle and allows geometric operations to be performed on it.
Point< ValueType > getCentre() const noexcept
Returns the centre point of the rectangle.
Rectangle< float > toFloat() const noexcept
Casts this rectangle to a Rectangle<float>.
void stopTimer() noexcept
Stops the timer.
void startTimerHz(int timerFrequencyHz) noexcept
Starts the timer with an interval specified in Hertz.
T distance(T... args)
T end(T... args)
T erase(T... args)
T find_if(T... args)
T floor(T... args)
#define jassertfalse
This will always cause an assertion failure.
typedef float
JUCE Namespace.
CriticalSection::ScopedLockType ScopedLock
Automatically locks and unlocks a CriticalSection object.
unsigned short uint16
A platform-independent 16-bit unsigned integer type.
constexpr Type jmap(Type value0To1, Type targetRangeMin, Type targetRangeMax)
Remaps a normalised value (between 0 and 1) to a target range.
constexpr Type jmin(Type a, Type b)
Returns the smaller of two values.
constexpr Type jmax(Type a, Type b)
Returns the larger of two values.
Type jlimit(Type lowerLimit, Type upperLimit, Type valueToConstrain) noexcept
Constrains a value to keep it within a given range.
Type unalignedPointerCast(void *ptr) noexcept
Casts a pointer to another type via void*, which suppresses the cast-align warning which sometimes ar...
Definition juce_Memory.h:88
@ press
Represents a "press" action.
unsigned char uint8
A platform-independent 8-bit unsigned integer type.
int roundToInt(const FloatType value) noexcept
Fast floating-point-to-integer conversion.
T remove_if(T... args)
void paint(Graphics &g) override
Components can override this method to draw their content.
uint16 noteID
A unique ID.