paulxstretch/deps/juce/modules/juce_audio_utils/gui/juce_MidiKeyboardComponent.cpp

482 lines
15 KiB
C++

/*
==============================================================================
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