/* ============================================================================== 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 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 area) { g.fillAll (findColour (whiteNoteColourId)); auto width = area.getWidth(); auto height = area.getHeight(); auto currentOrientation = getOrientation(); Point 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 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 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 area) { drawWhiteNote (midiNoteNumber, g, area, state.isNoteOnForChannels (midiInChannelMask, midiNoteNumber), mouseOverNotes.contains (midiNoteNumber), findColour (keySeparatorLineColourId), findColour (textLabelColourId)); } void MidiKeyboardComponent::drawBlackKey (int midiNoteNumber, Graphics& g, Rectangle 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