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

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 = [&notesToUpdate] (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