508 lines
18 KiB
C++
508 lines
18 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
|
|
{
|
|
|
|
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<float> 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<float> area)
|
|
{
|
|
g.setColour (findColour (whiteNoteColourId));
|
|
g.fillRect (area);
|
|
}
|
|
|
|
void MPEKeyboardComponent::drawWhiteKey (int midiNoteNumber, Graphics& g, Rectangle<float> 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<float> 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<float> 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<float> (posToCheck, 0.0f)
|
|
: Point<float> (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<float> 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<MPEChannelAssigner> (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<MPEChannelAssigner> (zone);
|
|
perNotePitchbendRange = zone.perNotePitchbendRange;
|
|
}
|
|
else
|
|
{
|
|
channelAssigner.reset();
|
|
}
|
|
}
|
|
}
|
|
|
|
void MPEKeyboardComponent::addNewNote (MPENote note)
|
|
{
|
|
noteComponents.push_back (std::make_unique<MPENoteComponent> (*this, note.noteID, note.initialNote,
|
|
note.noteOnVelocity.asUnsignedFloat(),
|
|
note.pressure.asUnsignedFloat()));
|
|
auto& comp = noteComponents.back();
|
|
|
|
addAndMakeVisible (*comp);
|
|
comp->toBack();
|
|
}
|
|
|
|
void MPEKeyboardComponent::handleNoteOns (std::set<MPENote>& 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<MPENote>& notesToUpdate)
|
|
{
|
|
auto removePredicate = [¬esToUpdate] (std::unique_ptr<MPENoteComponent>& 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<float> (xPos, yPos)
|
|
: Point<float> (yPos, xPos));
|
|
|
|
const auto radius = jmax (noteComponent.getStrikeRadius(), noteComponent.getPressureRadius());
|
|
|
|
noteComponent.setBounds (Rectangle<float> (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<MPENote> 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
|