/* ============================================================================== 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 { struct MPEKeyboardComponent::MPENoteComponent : public Component { MPENoteComponent (MPEKeyboardComponent& o, uint16 sID, uint8 initial, float noteOnVel, float press) : owner (o), radiusScale (owner.getKeyWidth() / 1.5f), noteOnVelocity (noteOnVel), pressure (press), sourceID (sID), initialNote (initial) { } float getStrikeRadius() const { return 5.0f + getNoteOnVelocity() * radiusScale * 2.0f; } float getPressureRadius() const { return 5.0f + getPressure() * radiusScale * 2.0f; } float getNoteOnVelocity() const { return noteOnVelocity; } float getPressure() const { return pressure; } Point getCentrePos() const { return getBounds().toFloat().getCentre(); } void paint (Graphics& g) override { auto strikeSize = getStrikeRadius() * 2.0f; auto pressSize = getPressureRadius() * 2.0f; auto bounds = getLocalBounds().toFloat(); g.setColour (owner.findColour (noteCircleFillColourId)); g.fillEllipse (bounds.withSizeKeepingCentre (strikeSize, strikeSize)); g.setColour (owner.findColour (noteCircleOutlineColourId)); g.drawEllipse (bounds.withSizeKeepingCentre (pressSize, pressSize), 1.0f); } //========================================================================== MPEKeyboardComponent& owner; float radiusScale = 0.0f, noteOnVelocity = 0.0f, pressure = 0.5f; uint16 sourceID = 0; uint8 initialNote = 0; bool isLatched = true; }; //============================================================================== MPEKeyboardComponent::MPEKeyboardComponent (MPEInstrument& instr, Orientation orientationToUse) : KeyboardComponentBase (orientationToUse), instrument (instr) { updateZoneLayout(); colourChanged(); setKeyWidth (25.0f); instrument.addListener (this); } MPEKeyboardComponent::~MPEKeyboardComponent() { instrument.removeListener (this); } //============================================================================== void MPEKeyboardComponent::drawKeyboardBackground (Graphics& g, Rectangle area) { g.setColour (findColour (whiteNoteColourId)); g.fillRect (area); } void MPEKeyboardComponent::drawWhiteKey (int midiNoteNumber, Graphics& g, Rectangle area) { if (midiNoteNumber % 12 == 0) { auto fontHeight = jmin (12.0f, getKeyWidth() * 0.9f); auto text = MidiMessage::getMidiNoteName (midiNoteNumber, true, true, getOctaveForMiddleC()); g.setColour (findColour (textLabelColourId)); g.setFont (Font (fontHeight).withHorizontalScale (0.8f)); switch (getOrientation()) { 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; } } } void MPEKeyboardComponent::drawBlackKey (int /*midiNoteNumber*/, Graphics& g, Rectangle area) { g.setColour (findColour (whiteNoteColourId)); g.fillRect (area); g.setColour (findColour (blackNoteColourId)); if (isHorizontal()) { g.fillRoundedRectangle (area.toFloat().reduced ((area.getWidth() / 2.0f) - (getBlackNoteWidth() / 12.0f), area.getHeight() / 4.0f), 1.0f); } else { g.fillRoundedRectangle (area.toFloat().reduced (area.getWidth() / 4.0f, (area.getHeight() / 2.0f) - (getBlackNoteWidth() / 12.0f)), 1.0f); } } void MPEKeyboardComponent::colourChanged() { setOpaque (findColour (whiteNoteColourId).isOpaque()); repaint(); } //========================================================================== MPEValue MPEKeyboardComponent::mousePositionToPitchbend (int initialNote, Point mousePos) { auto constrainedMousePos = [&] { auto horizontal = isHorizontal(); auto posToCheck = jlimit (0.0f, horizontal ? (float) getWidth() - 1.0f : (float) getHeight(), horizontal ? mousePos.x : mousePos.y); auto bottomKeyRange = getRectangleForKey (jmax (getRangeStart(), initialNote - perNotePitchbendRange)); auto topKeyRange = getRectangleForKey (jmin (getRangeEnd(), initialNote + perNotePitchbendRange)); auto lowerLimit = horizontal ? bottomKeyRange.getCentreX() : getOrientation() == Orientation::verticalKeyboardFacingRight ? topKeyRange.getCentreY() : bottomKeyRange.getCentreY(); auto upperLimit = horizontal ? topKeyRange.getCentreX() : getOrientation() == Orientation::verticalKeyboardFacingRight ? bottomKeyRange.getCentreY() : topKeyRange.getCentreY(); posToCheck = jlimit (lowerLimit, upperLimit, posToCheck); return horizontal ? Point (posToCheck, 0.0f) : Point (0.0f, posToCheck); }(); auto note = getNoteAndVelocityAtPosition (constrainedMousePos, true).note; if (note == -1) { jassertfalse; return {}; } auto fractionalSemitoneBend = [&] { auto noteRect = getRectangleForKey (note); switch (getOrientation()) { case horizontalKeyboard: return (constrainedMousePos.x - noteRect.getCentreX()) / noteRect.getWidth(); case verticalKeyboardFacingRight: return (noteRect.getCentreY() - constrainedMousePos.y) / noteRect.getHeight(); case verticalKeyboardFacingLeft: return (constrainedMousePos.y - noteRect.getCentreY()) / noteRect.getHeight(); } jassertfalse; return 0.0f; }(); auto totalNumSemitones = ((float) note + fractionalSemitoneBend) - (float) initialNote; return MPEValue::fromUnsignedFloat (jmap (totalNumSemitones, (float) -perNotePitchbendRange, (float) perNotePitchbendRange, 0.0f, 1.0f)); } MPEValue MPEKeyboardComponent::mousePositionToTimbre (Point mousePos) { auto delta = [mousePos, this] { switch (getOrientation()) { case horizontalKeyboard: return mousePos.y; case verticalKeyboardFacingLeft: return (float) getWidth() - mousePos.x; case verticalKeyboardFacingRight: return mousePos.x; } jassertfalse; return 0.0f; }(); return MPEValue::fromUnsignedFloat (jlimit (0.0f, 1.0f, 1.0f - (delta / getWhiteNoteLength()))); } void MPEKeyboardComponent::mouseDown (const MouseEvent& e) { auto newNote = getNoteAndVelocityAtPosition (e.position).note; if (newNote >= 0) { auto channel = channelAssigner->findMidiChannelForNewNote (newNote); instrument.noteOn (channel, newNote, MPEValue::fromUnsignedFloat (velocity)); sourceIDMap[e.source.getIndex()] = instrument.getNote (instrument.getNumPlayingNotes() - 1).noteID; instrument.pitchbend (channel, MPEValue::centreValue()); instrument.timbre (channel, mousePositionToTimbre (e.position)); instrument.pressure (channel, MPEValue::fromUnsignedFloat (e.isPressureValid() && useMouseSourcePressureForStrike ? e.pressure : pressure)); } } void MPEKeyboardComponent::mouseDrag (const MouseEvent& e) { auto noteID = sourceIDMap[e.source.getIndex()]; auto note = instrument.getNoteWithID (noteID); if (! note.isValid()) return; auto noteComponent = std::find_if (noteComponents.begin(), noteComponents.end(), [noteID] (auto& comp) { return comp->sourceID == noteID; }); if (noteComponent == noteComponents.end()) return; if ((*noteComponent)->isLatched && std::abs (isHorizontal() ? e.getDistanceFromDragStartX() : e.getDistanceFromDragStartY()) > roundToInt (getKeyWidth() / 4.0f)) { (*noteComponent)->isLatched = false; } auto channel = channelAssigner->findMidiChannelForExistingNote (note.initialNote); if (! (*noteComponent)->isLatched) instrument.pitchbend (channel, mousePositionToPitchbend (note.initialNote, e.position)); instrument.timbre (channel, mousePositionToTimbre (e.position)); instrument.pressure (channel, MPEValue::fromUnsignedFloat (e.isPressureValid() && useMouseSourcePressureForStrike ? e.pressure : pressure)); } void MPEKeyboardComponent::mouseUp (const MouseEvent& e) { auto note = instrument.getNoteWithID (sourceIDMap[e.source.getIndex()]); if (! note.isValid()) return; instrument.noteOff (channelAssigner->findMidiChannelForExistingNote (note.initialNote), note.initialNote, MPEValue::fromUnsignedFloat (lift)); channelAssigner->noteOff (note.initialNote); sourceIDMap.erase (e.source.getIndex()); } void MPEKeyboardComponent::focusLost (FocusChangeType) { for (auto& comp : noteComponents) { auto note = instrument.getNoteWithID (comp->sourceID); if (note.isValid()) instrument.noteOff (channelAssigner->findMidiChannelForExistingNote (note.initialNote), note.initialNote, MPEValue::fromUnsignedFloat (lift)); } } //============================================================================== void MPEKeyboardComponent::updateZoneLayout() { { const ScopedLock noteLock (activeNotesLock); activeNotes.clear(); } noteComponents.clear(); if (instrument.isLegacyModeEnabled()) { channelAssigner = std::make_unique (instrument.getLegacyModeChannelRange()); perNotePitchbendRange = instrument.getLegacyModePitchbendRange(); } else { auto layout = instrument.getZoneLayout(); if (layout.isActive()) { auto zone = layout.getLowerZone().isActive() ? layout.getLowerZone() : layout.getUpperZone(); channelAssigner = std::make_unique (zone); perNotePitchbendRange = zone.perNotePitchbendRange; } else { channelAssigner.reset(); } } } void MPEKeyboardComponent::addNewNote (MPENote note) { noteComponents.push_back (std::make_unique (*this, note.noteID, note.initialNote, note.noteOnVelocity.asUnsignedFloat(), note.pressure.asUnsignedFloat())); auto& comp = noteComponents.back(); addAndMakeVisible (*comp); comp->toBack(); } void MPEKeyboardComponent::handleNoteOns (std::set& notesToUpdate) { for (auto& note : notesToUpdate) { if (! std::any_of (noteComponents.begin(), noteComponents.end(), [note] (auto& comp) { return comp->sourceID == note.noteID; })) { addNewNote (note); } } } void MPEKeyboardComponent::handleNoteOffs (std::set& notesToUpdate) { auto removePredicate = [¬esToUpdate] (std::unique_ptr& comp) { return std::none_of (notesToUpdate.begin(), notesToUpdate.end(), [&comp] (auto& note) { return comp->sourceID == note.noteID; }); }; noteComponents.erase (std::remove_if (std::begin (noteComponents), std::end (noteComponents), removePredicate), std::end (noteComponents)); if (noteComponents.empty()) stopTimer(); } void MPEKeyboardComponent::updateNoteComponentBounds (const MPENote& note, MPENoteComponent& noteComponent) { auto xPos = [&] { const auto currentNote = note.initialNote + (float) note.totalPitchbendInSemitones; const auto noteBend = currentNote - std::floor (currentNote); const auto noteBounds = getRectangleForKey ((int) currentNote); const auto nextNoteBounds = getRectangleForKey ((int) currentNote + 1); const auto horizontal = isHorizontal(); const auto distance = noteBend * (horizontal ? nextNoteBounds.getCentreX() - noteBounds.getCentreX() : nextNoteBounds.getCentreY() - noteBounds.getCentreY()); return (horizontal ? noteBounds.getCentreX() : noteBounds.getCentreY()) + distance; }(); auto yPos = [&] { const auto currentOrientation = getOrientation(); const auto timbrePosition = (currentOrientation == horizontalKeyboard || currentOrientation == verticalKeyboardFacingRight ? 1.0f - note.timbre.asUnsignedFloat() : note.timbre.asUnsignedFloat()); return timbrePosition * getWhiteNoteLength(); }(); const auto centrePos = (isHorizontal() ? Point (xPos, yPos) : Point (yPos, xPos)); const auto radius = jmax (noteComponent.getStrikeRadius(), noteComponent.getPressureRadius()); noteComponent.setBounds (Rectangle (radius * 2.0f, radius * 2.0f) .withCentre (centrePos) .getSmallestIntegerContainer()); } static bool operator< (const MPENote& n1, const MPENote& n2) noexcept { return n1.noteID < n2.noteID; } void MPEKeyboardComponent::updateNoteComponents() { std::set notesToUpdate; { ScopedLock noteLock (activeNotesLock); for (const auto& note : activeNotes) if (note.second) notesToUpdate.insert (note.first); }; handleNoteOns (notesToUpdate); handleNoteOffs (notesToUpdate); for (auto& comp : noteComponents) { auto noteForComponent = std::find_if (notesToUpdate.begin(), notesToUpdate.end(), [&comp] (auto& note) { return note.noteID == comp->sourceID; }); if (noteForComponent != notesToUpdate.end()) { comp->pressure = noteForComponent->pressure.asUnsignedFloat(); updateNoteComponentBounds (*noteForComponent, *comp); comp->repaint(); } } } void MPEKeyboardComponent::timerCallback() { updateNoteComponents(); } //============================================================================== void MPEKeyboardComponent::noteAdded (MPENote newNote) { { const ScopedLock noteLock (activeNotesLock); activeNotes.push_back ({ newNote, true }); } startTimerHz (30); } void MPEKeyboardComponent::updateNoteData (MPENote& changedNote) { const ScopedLock noteLock (activeNotesLock); for (auto& note : activeNotes) { if (note.first.noteID == changedNote.noteID) { note.first = changedNote; note.second = true; return; } } } void MPEKeyboardComponent::notePressureChanged (MPENote changedNote) { updateNoteData (changedNote); } void MPEKeyboardComponent::notePitchbendChanged (MPENote changedNote) { updateNoteData (changedNote); } void MPEKeyboardComponent::noteTimbreChanged (MPENote changedNote) { updateNoteData (changedNote); } void MPEKeyboardComponent::noteReleased (MPENote finishedNote) { const ScopedLock noteLock (activeNotesLock); activeNotes.erase (std::remove_if (std::begin (activeNotes), std::end (activeNotes), [finishedNote] (auto& note) { return note.first.noteID == finishedNote.noteID; }), std::end (activeNotes)); } void MPEKeyboardComponent::zoneLayoutChanged() { MessageManager::callAsync ([this] { updateZoneLayout(); }); } } // namespace juce