2022-11-04 22:11:33 +00:00
|
|
|
/*
|
|
|
|
==============================================================================
|
|
|
|
|
|
|
|
This file is part of the JUCE library.
|
|
|
|
Copyright (c) 2022 - Raw Material Software Limited
|
|
|
|
|
|
|
|
JUCE is an open source library subject to commercial or open-source
|
|
|
|
licensing.
|
|
|
|
|
|
|
|
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
|
|
|
Agreement and JUCE Privacy Policy.
|
|
|
|
|
|
|
|
End User License Agreement: www.juce.com/juce-7-licence
|
|
|
|
Privacy Policy: www.juce.com/juce-privacy-policy
|
|
|
|
|
|
|
|
Or: You may also use this code under the terms of the GPL v3 (see
|
|
|
|
www.gnu.org/licenses).
|
|
|
|
|
|
|
|
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
|
|
|
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
|
|
|
DISCLAIMED.
|
|
|
|
|
|
|
|
==============================================================================
|
|
|
|
*/
|
|
|
|
|
|
|
|
namespace juce
|
|
|
|
{
|
|
|
|
|
|
|
|
//==============================================================================
|
|
|
|
MidiKeyboardComponent::MidiKeyboardComponent (MidiKeyboardState& stateToUse, Orientation orientationToUse)
|
|
|
|
: KeyboardComponentBase (orientationToUse), state (stateToUse)
|
|
|
|
{
|
|
|
|
state.addListener (this);
|
|
|
|
|
|
|
|
// initialise with a default set of qwerty key-mappings..
|
|
|
|
int note = 0;
|
|
|
|
|
|
|
|
for (char c : "awsedftgyhujkolp;")
|
|
|
|
setKeyPressForNote ({ c, 0, 0 }, note++);
|
|
|
|
|
|
|
|
mouseOverNotes.insertMultiple (0, -1, 32);
|
|
|
|
mouseDownNotes.insertMultiple (0, -1, 32);
|
|
|
|
|
|
|
|
colourChanged();
|
|
|
|
setWantsKeyboardFocus (true);
|
|
|
|
|
|
|
|
startTimerHz (20);
|
|
|
|
}
|
|
|
|
|
|
|
|
MidiKeyboardComponent::~MidiKeyboardComponent()
|
|
|
|
{
|
|
|
|
state.removeListener (this);
|
|
|
|
}
|
|
|
|
|
|
|
|
//==============================================================================
|
|
|
|
void MidiKeyboardComponent::setVelocity (float v, bool useMousePosition)
|
|
|
|
{
|
|
|
|
velocity = v;
|
|
|
|
useMousePositionForVelocity = useMousePosition;
|
|
|
|
}
|
|
|
|
|
|
|
|
//==============================================================================
|
|
|
|
void MidiKeyboardComponent::setMidiChannel (int midiChannelNumber)
|
|
|
|
{
|
|
|
|
jassert (midiChannelNumber > 0 && midiChannelNumber <= 16);
|
|
|
|
|
|
|
|
if (midiChannel != midiChannelNumber)
|
|
|
|
{
|
|
|
|
resetAnyKeysInUse();
|
|
|
|
midiChannel = jlimit (1, 16, midiChannelNumber);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void MidiKeyboardComponent::setMidiChannelsToDisplay (int midiChannelMask)
|
|
|
|
{
|
|
|
|
midiInChannelMask = midiChannelMask;
|
|
|
|
noPendingUpdates.store (false);
|
|
|
|
}
|
|
|
|
|
|
|
|
//==============================================================================
|
|
|
|
void MidiKeyboardComponent::clearKeyMappings()
|
|
|
|
{
|
|
|
|
resetAnyKeysInUse();
|
|
|
|
keyPressNotes.clear();
|
|
|
|
keyPresses.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
void MidiKeyboardComponent::setKeyPressForNote (const KeyPress& key, int midiNoteOffsetFromC)
|
|
|
|
{
|
|
|
|
removeKeyPressForNote (midiNoteOffsetFromC);
|
|
|
|
|
|
|
|
keyPressNotes.add (midiNoteOffsetFromC);
|
|
|
|
keyPresses.add (key);
|
|
|
|
}
|
|
|
|
|
|
|
|
void MidiKeyboardComponent::removeKeyPressForNote (int midiNoteOffsetFromC)
|
|
|
|
{
|
|
|
|
for (int i = keyPressNotes.size(); --i >= 0;)
|
|
|
|
{
|
|
|
|
if (keyPressNotes.getUnchecked (i) == midiNoteOffsetFromC)
|
|
|
|
{
|
|
|
|
keyPressNotes.remove (i);
|
|
|
|
keyPresses.remove (i);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void MidiKeyboardComponent::setKeyPressBaseOctave (int newOctaveNumber)
|
|
|
|
{
|
|
|
|
jassert (newOctaveNumber >= 0 && newOctaveNumber <= 10);
|
|
|
|
|
|
|
|
keyMappingOctave = newOctaveNumber;
|
|
|
|
}
|
|
|
|
|
|
|
|
//==============================================================================
|
|
|
|
void MidiKeyboardComponent::resetAnyKeysInUse()
|
|
|
|
{
|
|
|
|
if (! keysPressed.isZero())
|
|
|
|
{
|
|
|
|
for (int i = 128; --i >= 0;)
|
|
|
|
if (keysPressed[i])
|
|
|
|
state.noteOff (midiChannel, i, 0.0f);
|
|
|
|
|
|
|
|
keysPressed.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
for (int i = mouseDownNotes.size(); --i >= 0;)
|
|
|
|
{
|
|
|
|
auto noteDown = mouseDownNotes.getUnchecked (i);
|
|
|
|
|
|
|
|
if (noteDown >= 0)
|
|
|
|
{
|
|
|
|
state.noteOff (midiChannel, noteDown, 0.0f);
|
|
|
|
mouseDownNotes.set (i, -1);
|
|
|
|
}
|
|
|
|
|
|
|
|
mouseOverNotes.set (i, -1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void MidiKeyboardComponent::updateNoteUnderMouse (const MouseEvent& e, bool isDown)
|
|
|
|
{
|
|
|
|
updateNoteUnderMouse (e.getEventRelativeTo (this).position, isDown, e.source.getIndex());
|
|
|
|
}
|
|
|
|
|
|
|
|
void MidiKeyboardComponent::updateNoteUnderMouse (Point<float> pos, bool isDown, int fingerNum)
|
|
|
|
{
|
|
|
|
const auto noteInfo = getNoteAndVelocityAtPosition (pos);
|
|
|
|
const auto newNote = noteInfo.note;
|
|
|
|
const auto oldNote = mouseOverNotes.getUnchecked (fingerNum);
|
|
|
|
const auto oldNoteDown = mouseDownNotes.getUnchecked (fingerNum);
|
|
|
|
const auto eventVelocity = useMousePositionForVelocity ? noteInfo.velocity * velocity : velocity;
|
|
|
|
|
|
|
|
if (oldNote != newNote)
|
|
|
|
{
|
|
|
|
repaintNote (oldNote);
|
|
|
|
repaintNote (newNote);
|
|
|
|
mouseOverNotes.set (fingerNum, newNote);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isDown)
|
|
|
|
{
|
|
|
|
if (newNote != oldNoteDown)
|
|
|
|
{
|
|
|
|
if (oldNoteDown >= 0)
|
|
|
|
{
|
|
|
|
mouseDownNotes.set (fingerNum, -1);
|
|
|
|
|
|
|
|
if (! mouseDownNotes.contains (oldNoteDown))
|
|
|
|
state.noteOff (midiChannel, oldNoteDown, eventVelocity);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (newNote >= 0 && ! mouseDownNotes.contains (newNote))
|
|
|
|
{
|
|
|
|
state.noteOn (midiChannel, newNote, eventVelocity);
|
|
|
|
mouseDownNotes.set (fingerNum, newNote);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (oldNoteDown >= 0)
|
|
|
|
{
|
|
|
|
mouseDownNotes.set (fingerNum, -1);
|
|
|
|
|
|
|
|
if (! mouseDownNotes.contains (oldNoteDown))
|
|
|
|
state.noteOff (midiChannel, oldNoteDown, eventVelocity);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void MidiKeyboardComponent::repaintNote (int noteNum)
|
|
|
|
{
|
|
|
|
if (getRangeStart() <= noteNum && noteNum <= getRangeEnd())
|
|
|
|
repaint (getRectangleForKey (noteNum).getSmallestIntegerContainer());
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void MidiKeyboardComponent::mouseMove (const MouseEvent& e)
|
|
|
|
{
|
|
|
|
updateNoteUnderMouse (e, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
void MidiKeyboardComponent::mouseDrag (const MouseEvent& e)
|
|
|
|
{
|
|
|
|
auto newNote = getNoteAndVelocityAtPosition (e.position).note;
|
|
|
|
|
|
|
|
if (newNote >= 0 && mouseDraggedToKey (newNote, e))
|
|
|
|
updateNoteUnderMouse (e, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
void MidiKeyboardComponent::mouseDown (const MouseEvent& e)
|
|
|
|
{
|
|
|
|
auto newNote = getNoteAndVelocityAtPosition (e.position).note;
|
|
|
|
|
|
|
|
if (newNote >= 0 && mouseDownOnKey (newNote, e))
|
|
|
|
updateNoteUnderMouse (e, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
void MidiKeyboardComponent::mouseUp (const MouseEvent& e)
|
|
|
|
{
|
|
|
|
updateNoteUnderMouse (e, false);
|
|
|
|
|
|
|
|
auto note = getNoteAndVelocityAtPosition (e.position).note;
|
|
|
|
|
|
|
|
if (note >= 0)
|
|
|
|
mouseUpOnKey (note, e);
|
|
|
|
}
|
|
|
|
|
|
|
|
void MidiKeyboardComponent::mouseEnter (const MouseEvent& e)
|
|
|
|
{
|
|
|
|
updateNoteUnderMouse (e, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
void MidiKeyboardComponent::mouseExit (const MouseEvent& e)
|
|
|
|
{
|
|
|
|
updateNoteUnderMouse (e, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
void MidiKeyboardComponent::timerCallback()
|
|
|
|
{
|
|
|
|
if (noPendingUpdates.exchange (true))
|
|
|
|
return;
|
|
|
|
|
|
|
|
for (auto i = getRangeStart(); i <= getRangeEnd(); ++i)
|
|
|
|
{
|
|
|
|
const auto isOn = state.isNoteOnForChannels (midiInChannelMask, i);
|
|
|
|
|
|
|
|
if (keysCurrentlyDrawnDown[i] != isOn)
|
|
|
|
{
|
|
|
|
keysCurrentlyDrawnDown.setBit (i, isOn);
|
|
|
|
repaintNote (i);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool MidiKeyboardComponent::keyStateChanged (bool /*isKeyDown*/)
|
|
|
|
{
|
|
|
|
bool keyPressUsed = false;
|
|
|
|
|
|
|
|
for (int i = keyPresses.size(); --i >= 0;)
|
|
|
|
{
|
|
|
|
auto note = 12 * keyMappingOctave + keyPressNotes.getUnchecked (i);
|
|
|
|
|
|
|
|
if (keyPresses.getReference(i).isCurrentlyDown())
|
|
|
|
{
|
|
|
|
if (! keysPressed[note])
|
|
|
|
{
|
|
|
|
keysPressed.setBit (note);
|
|
|
|
state.noteOn (midiChannel, note, velocity);
|
|
|
|
keyPressUsed = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if (keysPressed[note])
|
|
|
|
{
|
|
|
|
keysPressed.clearBit (note);
|
|
|
|
state.noteOff (midiChannel, note, 0.0f);
|
|
|
|
keyPressUsed = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return keyPressUsed;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool MidiKeyboardComponent::keyPressed (const KeyPress& key)
|
|
|
|
{
|
|
|
|
return keyPresses.contains (key);
|
|
|
|
}
|
|
|
|
|
|
|
|
void MidiKeyboardComponent::focusLost (FocusChangeType)
|
|
|
|
{
|
|
|
|
resetAnyKeysInUse();
|
|
|
|
}
|
|
|
|
|
|
|
|
//==============================================================================
|
|
|
|
void MidiKeyboardComponent::drawKeyboardBackground (Graphics& g, Rectangle<float> area)
|
|
|
|
{
|
|
|
|
g.fillAll (findColour (whiteNoteColourId));
|
|
|
|
|
|
|
|
auto width = area.getWidth();
|
|
|
|
auto height = area.getHeight();
|
|
|
|
auto currentOrientation = getOrientation();
|
|
|
|
Point<float> shadowGradientStart, shadowGradientEnd;
|
|
|
|
|
|
|
|
if (currentOrientation == verticalKeyboardFacingLeft)
|
|
|
|
{
|
|
|
|
shadowGradientStart.x = width - 1.0f;
|
|
|
|
shadowGradientEnd.x = width - 5.0f;
|
|
|
|
}
|
|
|
|
else if (currentOrientation == verticalKeyboardFacingRight)
|
|
|
|
{
|
|
|
|
shadowGradientEnd.x = 5.0f;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
shadowGradientEnd.y = 5.0f;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto keyboardWidth = getRectangleForKey (getRangeEnd()).getRight();
|
|
|
|
auto shadowColour = findColour (shadowColourId);
|
|
|
|
|
|
|
|
if (! shadowColour.isTransparent())
|
|
|
|
{
|
|
|
|
g.setGradientFill ({ shadowColour, shadowGradientStart,
|
|
|
|
shadowColour.withAlpha (0.0f), shadowGradientEnd,
|
|
|
|
false });
|
|
|
|
|
|
|
|
switch (currentOrientation)
|
|
|
|
{
|
|
|
|
case horizontalKeyboard: g.fillRect (0.0f, 0.0f, keyboardWidth, 5.0f); break;
|
|
|
|
case verticalKeyboardFacingLeft: g.fillRect (width - 5.0f, 0.0f, 5.0f, keyboardWidth); break;
|
|
|
|
case verticalKeyboardFacingRight: g.fillRect (0.0f, 0.0f, 5.0f, keyboardWidth); break;
|
|
|
|
default: break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
auto lineColour = findColour (keySeparatorLineColourId);
|
|
|
|
|
|
|
|
if (! lineColour.isTransparent())
|
|
|
|
{
|
|
|
|
g.setColour (lineColour);
|
|
|
|
|
|
|
|
switch (currentOrientation)
|
|
|
|
{
|
|
|
|
case horizontalKeyboard: g.fillRect (0.0f, height - 1.0f, keyboardWidth, 1.0f); break;
|
|
|
|
case verticalKeyboardFacingLeft: g.fillRect (0.0f, 0.0f, 1.0f, keyboardWidth); break;
|
|
|
|
case verticalKeyboardFacingRight: g.fillRect (width - 1.0f, 0.0f, 1.0f, keyboardWidth); break;
|
|
|
|
default: break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void MidiKeyboardComponent::drawWhiteNote (int midiNoteNumber, Graphics& g, Rectangle<float> area,
|
|
|
|
bool isDown, bool isOver, Colour lineColour, Colour textColour)
|
|
|
|
{
|
|
|
|
auto c = Colours::transparentWhite;
|
|
|
|
|
|
|
|
if (isDown) c = findColour (keyDownOverlayColourId);
|
|
|
|
if (isOver) c = c.overlaidWith (findColour (mouseOverKeyOverlayColourId));
|
|
|
|
|
|
|
|
g.setColour (c);
|
|
|
|
g.fillRect (area);
|
|
|
|
|
|
|
|
const auto currentOrientation = getOrientation();
|
|
|
|
|
|
|
|
auto text = getWhiteNoteText (midiNoteNumber);
|
|
|
|
|
|
|
|
if (text.isNotEmpty())
|
|
|
|
{
|
|
|
|
auto fontHeight = jmin (12.0f, getKeyWidth() * 0.9f);
|
|
|
|
|
|
|
|
g.setColour (textColour);
|
|
|
|
g.setFont (Font (fontHeight).withHorizontalScale (0.8f));
|
|
|
|
|
|
|
|
switch (currentOrientation)
|
|
|
|
{
|
|
|
|
case horizontalKeyboard: g.drawText (text, area.withTrimmedLeft (1.0f).withTrimmedBottom (2.0f), Justification::centredBottom, false); break;
|
|
|
|
case verticalKeyboardFacingLeft: g.drawText (text, area.reduced (2.0f), Justification::centredLeft, false); break;
|
|
|
|
case verticalKeyboardFacingRight: g.drawText (text, area.reduced (2.0f), Justification::centredRight, false); break;
|
|
|
|
default: break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (! lineColour.isTransparent())
|
|
|
|
{
|
|
|
|
g.setColour (lineColour);
|
|
|
|
|
|
|
|
switch (currentOrientation)
|
|
|
|
{
|
|
|
|
case horizontalKeyboard: g.fillRect (area.withWidth (1.0f)); break;
|
|
|
|
case verticalKeyboardFacingLeft: g.fillRect (area.withHeight (1.0f)); break;
|
|
|
|
case verticalKeyboardFacingRight: g.fillRect (area.removeFromBottom (1.0f)); break;
|
|
|
|
default: break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (midiNoteNumber == getRangeEnd())
|
|
|
|
{
|
|
|
|
switch (currentOrientation)
|
|
|
|
{
|
|
|
|
case horizontalKeyboard: g.fillRect (area.expanded (1.0f, 0).removeFromRight (1.0f)); break;
|
|
|
|
case verticalKeyboardFacingLeft: g.fillRect (area.expanded (0, 1.0f).removeFromBottom (1.0f)); break;
|
|
|
|
case verticalKeyboardFacingRight: g.fillRect (area.expanded (0, 1.0f).removeFromTop (1.0f)); break;
|
|
|
|
default: break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void MidiKeyboardComponent::drawBlackNote (int /*midiNoteNumber*/, Graphics& g, Rectangle<float> area,
|
|
|
|
bool isDown, bool isOver, Colour noteFillColour)
|
|
|
|
{
|
|
|
|
auto c = noteFillColour;
|
|
|
|
|
|
|
|
if (isDown) c = c.overlaidWith (findColour (keyDownOverlayColourId));
|
|
|
|
if (isOver) c = c.overlaidWith (findColour (mouseOverKeyOverlayColourId));
|
|
|
|
|
|
|
|
g.setColour (c);
|
|
|
|
g.fillRect (area);
|
|
|
|
|
|
|
|
if (isDown)
|
|
|
|
{
|
|
|
|
g.setColour (noteFillColour);
|
|
|
|
g.drawRect (area);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
g.setColour (c.brighter());
|
|
|
|
auto sideIndent = 1.0f / 8.0f;
|
|
|
|
auto topIndent = 7.0f / 8.0f;
|
|
|
|
auto w = area.getWidth();
|
|
|
|
auto h = area.getHeight();
|
|
|
|
|
|
|
|
switch (getOrientation())
|
|
|
|
{
|
|
|
|
case horizontalKeyboard: g.fillRect (area.reduced (w * sideIndent, 0).removeFromTop (h * topIndent)); break;
|
|
|
|
case verticalKeyboardFacingLeft: g.fillRect (area.reduced (0, h * sideIndent).removeFromRight (w * topIndent)); break;
|
|
|
|
case verticalKeyboardFacingRight: g.fillRect (area.reduced (0, h * sideIndent).removeFromLeft (w * topIndent)); break;
|
|
|
|
default: break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
String MidiKeyboardComponent::getWhiteNoteText (int midiNoteNumber)
|
|
|
|
{
|
|
|
|
if (midiNoteNumber % 12 == 0)
|
|
|
|
return MidiMessage::getMidiNoteName (midiNoteNumber, true, true, getOctaveForMiddleC());
|
|
|
|
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
void MidiKeyboardComponent::colourChanged()
|
|
|
|
{
|
|
|
|
setOpaque (findColour (whiteNoteColourId).isOpaque());
|
|
|
|
repaint();
|
|
|
|
}
|
|
|
|
|
|
|
|
//==============================================================================
|
|
|
|
void MidiKeyboardComponent::drawWhiteKey (int midiNoteNumber, Graphics& g, Rectangle<float> area)
|
|
|
|
{
|
|
|
|
drawWhiteNote (midiNoteNumber, g, area, state.isNoteOnForChannels (midiInChannelMask, midiNoteNumber),
|
|
|
|
mouseOverNotes.contains (midiNoteNumber), findColour (keySeparatorLineColourId), findColour (textLabelColourId));
|
|
|
|
}
|
|
|
|
|
|
|
|
void MidiKeyboardComponent::drawBlackKey (int midiNoteNumber, Graphics& g, Rectangle<float> area)
|
|
|
|
{
|
|
|
|
drawBlackNote (midiNoteNumber, g, area, state.isNoteOnForChannels (midiInChannelMask, midiNoteNumber),
|
|
|
|
mouseOverNotes.contains (midiNoteNumber), findColour (blackNoteColourId));
|
|
|
|
}
|
|
|
|
|
|
|
|
//==============================================================================
|
|
|
|
void MidiKeyboardComponent::handleNoteOn (MidiKeyboardState*, int /*midiChannel*/, int /*midiNoteNumber*/, float /*velocity*/)
|
|
|
|
{
|
|
|
|
noPendingUpdates.store (false);
|
|
|
|
}
|
|
|
|
|
|
|
|
void MidiKeyboardComponent::handleNoteOff (MidiKeyboardState*, int /*midiChannel*/, int /*midiNoteNumber*/, float /*velocity*/)
|
|
|
|
{
|
|
|
|
noPendingUpdates.store (false);
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace juce
|