/* ============================================================================== This file is part of the JUCE 7 technical preview. Copyright (c) 2022 - Raw Material Software Limited You may use this code under the terms of the GPL v3 (see www.gnu.org/licenses). For the technical preview this file cannot be licensed commercially. 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