1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

Add KeyboardComponentBase class for custom MIDI keyboard components and MPEKeyboardComponent class

This commit is contained in:
ed 2021-12-17 15:25:29 +00:00 committed by Tom Poole
parent 461192b355
commit e0e8e85d6b
28 changed files with 2219 additions and 1502 deletions

View file

@ -48,6 +48,7 @@
#pragma once #pragma once
//============================================================================== //==============================================================================
class ZoneColourPicker class ZoneColourPicker
{ {
@ -94,251 +95,12 @@ private:
}; };
//============================================================================== //==============================================================================
class NoteComponent : public Component class MPESetupComponent : public Component
{
public:
NoteComponent (const MPENote& n, Colour colourToUse)
: note (n), colour (colourToUse)
{}
//==============================================================================
void update (const MPENote& newNote, Point<float> newCentre)
{
note = newNote;
centre = newCentre;
setBounds (getSquareAroundCentre (jmax (getNoteOnRadius(), getNoteOffRadius(), getPressureRadius()))
.getUnion (getTextRectangle())
.getSmallestIntegerContainer()
.expanded (3));
repaint();
}
//==============================================================================
void paint (Graphics& g) override
{
if (note.keyState == MPENote::keyDown || note.keyState == MPENote::keyDownAndSustained)
drawPressedNoteCircle (g, colour);
else if (note.keyState == MPENote::sustained)
drawSustainedNoteCircle (g, colour);
else
return;
drawNoteLabel (g, colour);
}
//==============================================================================
MPENote note;
Colour colour;
Point<float> centre;
private:
//==============================================================================
void drawPressedNoteCircle (Graphics& g, Colour zoneColour)
{
g.setColour (zoneColour.withAlpha (0.3f));
g.fillEllipse (translateToLocalBounds (getSquareAroundCentre (getNoteOnRadius())));
g.setColour (zoneColour);
g.drawEllipse (translateToLocalBounds (getSquareAroundCentre (getPressureRadius())), 2.0f);
}
//==============================================================================
void drawSustainedNoteCircle (Graphics& g, Colour zoneColour)
{
g.setColour (zoneColour);
Path circle, dashedCircle;
circle.addEllipse (translateToLocalBounds (getSquareAroundCentre (getNoteOffRadius())));
float dashLengths[] = { 3.0f, 3.0f };
PathStrokeType (2.0, PathStrokeType::mitered).createDashedStroke (dashedCircle, circle, dashLengths, 2);
g.fillPath (dashedCircle);
}
//==============================================================================
void drawNoteLabel (Graphics& g, Colour /**zoneColour*/)
{
auto textBounds = translateToLocalBounds (getTextRectangle()).getSmallestIntegerContainer();
g.drawText ("+", textBounds, Justification::centred);
g.drawText (MidiMessage::getMidiNoteName (note.initialNote, true, true, 3), textBounds, Justification::centredBottom);
g.setFont (Font (22.0f, Font::bold));
g.drawText (String (note.midiChannel), textBounds, Justification::centredTop);
}
//==============================================================================
Rectangle<float> getSquareAroundCentre (float radius) const noexcept
{
return Rectangle<float> (radius * 2.0f, radius * 2.0f).withCentre (centre);
}
Rectangle<float> translateToLocalBounds (Rectangle<float> r) const noexcept
{
return r - getPosition().toFloat();
}
Rectangle<float> getTextRectangle() const noexcept
{
return Rectangle<float> (30.0f, 50.0f).withCentre (centre);
}
float getNoteOnRadius() const noexcept { return note.noteOnVelocity .asUnsignedFloat() * maxNoteRadius; }
float getNoteOffRadius() const noexcept { return note.noteOffVelocity.asUnsignedFloat() * maxNoteRadius; }
float getPressureRadius() const noexcept { return note.pressure .asUnsignedFloat() * maxNoteRadius; }
const float maxNoteRadius = 100.0f;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NoteComponent)
};
//==============================================================================
class Visualiser : public Component,
public MPEInstrument::Listener,
private AsyncUpdater
{ {
public: public:
//============================================================================== //==============================================================================
Visualiser (ZoneColourPicker& zoneColourPicker) MPESetupComponent (MPEInstrument& instr)
: colourPicker (zoneColourPicker) : instrument (instr)
{}
//==============================================================================
void paint (Graphics& g) override
{
g.fillAll (Colours::black);
auto noteDistance = float (getWidth()) / 128;
for (auto i = 0; i < 128; ++i)
{
auto x = noteDistance * (float) i;
auto noteHeight = int (MidiMessage::isMidiNoteBlack (i) ? 0.7 * getHeight() : getHeight());
g.setColour (MidiMessage::isMidiNoteBlack (i) ? Colours::white : Colours::grey);
g.drawLine (x, 0.0f, x, (float) noteHeight);
if (i > 0 && i % 12 == 0)
{
g.setColour (Colours::grey);
auto octaveNumber = (i / 12) - 2;
g.drawText ("C" + String (octaveNumber), (int) x - 15, getHeight() - 30, 30, 30, Justification::centredBottom);
}
}
}
//==============================================================================
void noteAdded (MPENote newNote) override
{
const ScopedLock sl (lock);
activeNotes.add (newNote);
triggerAsyncUpdate();
}
void notePressureChanged (MPENote note) override { noteChanged (note); }
void notePitchbendChanged (MPENote note) override { noteChanged (note); }
void noteTimbreChanged (MPENote note) override { noteChanged (note); }
void noteKeyStateChanged (MPENote note) override { noteChanged (note); }
void noteChanged (MPENote changedNote)
{
const ScopedLock sl (lock);
for (auto& note : activeNotes)
if (note.noteID == changedNote.noteID)
note = changedNote;
triggerAsyncUpdate();
}
void noteReleased (MPENote finishedNote) override
{
const ScopedLock sl (lock);
for (auto i = activeNotes.size(); --i >= 0;)
if (activeNotes.getReference(i).noteID == finishedNote.noteID)
activeNotes.remove (i);
triggerAsyncUpdate();
}
private:
//==============================================================================
const MPENote* findActiveNote (int noteID) const noexcept
{
for (auto& note : activeNotes)
if (note.noteID == noteID)
return &note;
return nullptr;
}
NoteComponent* findNoteComponent (int noteID) const noexcept
{
for (auto& noteComp : noteComponents)
if (noteComp->note.noteID == noteID)
return noteComp;
return nullptr;
}
//==============================================================================
void handleAsyncUpdate() override
{
const ScopedLock sl (lock);
for (auto i = noteComponents.size(); --i >= 0;)
if (findActiveNote (noteComponents.getUnchecked(i)->note.noteID) == nullptr)
noteComponents.remove (i);
for (auto& note : activeNotes)
if (findNoteComponent (note.noteID) == nullptr)
addAndMakeVisible (noteComponents.add (new NoteComponent (note, colourPicker.getColourForMidiChannel(note.midiChannel))));
for (auto& noteComp : noteComponents)
if (auto* noteInfo = findActiveNote (noteComp->note.noteID))
noteComp->update (*noteInfo, getCentrePositionForNote (*noteInfo));
}
//==============================================================================
Point<float> getCentrePositionForNote (MPENote note) const
{
auto n = float (note.initialNote) + float (note.totalPitchbendInSemitones);
auto x = (float) getWidth() * n / 128;
auto y = (float) getHeight() * (1 - note.timbre.asUnsignedFloat());
return { x, y };
}
//==============================================================================
OwnedArray<NoteComponent> noteComponents;
CriticalSection lock;
Array<MPENote> activeNotes;
ZoneColourPicker& colourPicker;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Visualiser)
};
//==============================================================================
class MPESetupComponent : public Component,
public ChangeBroadcaster
{
public:
//==============================================================================
class Listener
{
public:
virtual ~Listener() {}
virtual void zoneChanged (bool isLower, int numMemberChans, int perNotePb, int masterPb) = 0;
virtual void allZonesCleared() = 0;
virtual void legacyModeChanged (bool legacyModeEnabled, int pitchbendRange, Range<int> channelRange) = 0;
virtual void voiceStealingEnabledChanged (bool voiceStealingEnabled) = 0;
virtual void numberOfVoicesChanged (int numberOfVoices) = 0;
};
void addListener (Listener* listenerToAdd) { listeners.add (listenerToAdd); }
void removeListener (Listener* listenerToRemove) { listeners.remove (listenerToRemove); }
//==============================================================================
MPESetupComponent()
{ {
addAndMakeVisible (isLowerZoneButton); addAndMakeVisible (isLowerZoneButton);
isLowerZoneButton.setToggleState (true, NotificationType::dontSendNotification); isLowerZoneButton.setToggleState (true, NotificationType::dontSendNotification);
@ -353,10 +115,13 @@ public:
addAndMakeVisible (setZoneButton); addAndMakeVisible (setZoneButton);
setZoneButton.onClick = [this] { setZoneButtonClicked(); }; setZoneButton.onClick = [this] { setZoneButtonClicked(); };
addAndMakeVisible (clearAllZonesButton); addAndMakeVisible (clearAllZonesButton);
clearAllZonesButton.onClick = [this] { clearAllZonesButtonClicked(); }; clearAllZonesButton.onClick = [this] { clearAllZonesButtonClicked(); };
addAndMakeVisible (legacyModeEnabledToggle); addAndMakeVisible (legacyModeEnabledToggle);
legacyModeEnabledToggle.onClick = [this] { legacyModeEnabledToggleClicked(); }; legacyModeEnabledToggle.onClick = [this] { legacyModeEnabledToggleClicked(); };
addAndMakeVisible (voiceStealingEnabledToggle); addAndMakeVisible (voiceStealingEnabledToggle);
voiceStealingEnabledToggle.onClick = [this] { voiceStealingEnabledToggleClicked(); }; voiceStealingEnabledToggle.onClick = [this] { voiceStealingEnabledToggleClicked(); };
@ -402,6 +167,12 @@ public:
numberOfVoices.setBounds (r.removeFromTop (h)); numberOfVoices.setBounds (r.removeFromTop (h));
} }
//==============================================================================
bool isVoiceStealingEnabled() const { return voiceStealingEnabledToggle.getToggleState(); }
int getNumVoices() const { return numberOfVoices.getText().getIntValue(); }
std::function<void()> onSynthParametersChange;
private: private:
//============================================================================== //==============================================================================
void initialiseComboBoxWithConsecutiveIntegers (ComboBox& comboBox, Label& labelToAttach, void initialiseComboBoxWithConsecutiveIntegers (ComboBox& comboBox, Label& labelToAttach,
@ -435,22 +206,21 @@ private:
auto perNotePb = notePitchbendRange.getText().getIntValue(); auto perNotePb = notePitchbendRange.getText().getIntValue();
auto masterPb = masterPitchbendRange.getText().getIntValue(); auto masterPb = masterPitchbendRange.getText().getIntValue();
auto zoneLayout = instrument.getZoneLayout();
if (isLowerZone) if (isLowerZone)
zoneLayout.setLowerZone (numMemberChannels, perNotePb, masterPb); zoneLayout.setLowerZone (numMemberChannels, perNotePb, masterPb);
else else
zoneLayout.setUpperZone (numMemberChannels, perNotePb, masterPb); zoneLayout.setUpperZone (numMemberChannels, perNotePb, masterPb);
listeners.call ([&] (Listener& l) { l.zoneChanged (isLowerZone, numMemberChannels, perNotePb, masterPb); }); instrument.setZoneLayout (zoneLayout);
} }
//==============================================================================
void clearAllZonesButtonClicked() void clearAllZonesButtonClicked()
{ {
zoneLayout.clearAllZones(); instrument.setZoneLayout ({});
listeners.call ([] (Listener& l) { l.allZonesCleared(); });
} }
//==============================================================================
void legacyModeEnabledToggleClicked() void legacyModeEnabledToggleClicked()
{ {
auto legacyModeEnabled = legacyModeEnabledToggle.getToggleState(); auto legacyModeEnabled = legacyModeEnabledToggle.getToggleState();
@ -466,38 +236,32 @@ private:
legacyEndChannel .setVisible (legacyModeEnabled); legacyEndChannel .setVisible (legacyModeEnabled);
legacyPitchbendRange.setVisible (legacyModeEnabled); legacyPitchbendRange.setVisible (legacyModeEnabled);
if (areLegacyModeParametersValid()) if (legacyModeEnabled)
{ {
listeners.call ([&] (Listener& l) { l.legacyModeChanged (legacyModeEnabledToggle.getToggleState(), if (areLegacyModeParametersValid())
legacyPitchbendRange.getText().getIntValue(), {
getLegacyModeChannelRange()); }); instrument.enableLegacyMode();
instrument.setLegacyModeChannelRange (getLegacyModeChannelRange());
instrument.setLegacyModePitchbendRange (getLegacyModePitchbendRange());
}
else
{
handleInvalidLegacyModeParameters();
}
} }
else else
{ {
handleInvalidLegacyModeParameters(); instrument.setZoneLayout ({ MPEZone (MPEZone::Type::lower, 15) });
} }
} }
//============================================================================== //==============================================================================
void voiceStealingEnabledToggleClicked()
{
auto newState = voiceStealingEnabledToggle.getToggleState();
listeners.call ([=] (Listener& l) { l.voiceStealingEnabledChanged (newState); });
}
//==============================================================================
void numberOfVoicesChanged()
{
listeners.call ([this] (Listener& l) { l.numberOfVoicesChanged (numberOfVoices.getText().getIntValue()); });
}
void legacyModePitchbendRangeChanged() void legacyModePitchbendRangeChanged()
{ {
jassert (legacyModeEnabledToggle.getToggleState() == true); jassert (legacyModeEnabledToggle.getToggleState() == true);
listeners.call ([this] (Listener& l) { l.legacyModeChanged (true, instrument.setLegacyModePitchbendRange (getLegacyModePitchbendRange());
legacyPitchbendRange.getText().getIntValue(),
getLegacyModeChannelRange()); });
} }
void legacyModeChannelRangeChanged() void legacyModeChannelRangeChanged()
@ -505,18 +269,11 @@ private:
jassert (legacyModeEnabledToggle.getToggleState() == true); jassert (legacyModeEnabledToggle.getToggleState() == true);
if (areLegacyModeParametersValid()) if (areLegacyModeParametersValid())
{ instrument.setLegacyModeChannelRange (getLegacyModeChannelRange());
listeners.call ([this] (Listener& l) { l.legacyModeChanged (true,
legacyPitchbendRange.getText().getIntValue(),
getLegacyModeChannelRange()); });
}
else else
{
handleInvalidLegacyModeParameters(); handleInvalidLegacyModeParameters();
}
} }
//==============================================================================
bool areLegacyModeParametersValid() const bool areLegacyModeParametersValid() const
{ {
return legacyStartChannel.getText().getIntValue() <= legacyEndChannel.getText().getIntValue(); return legacyStartChannel.getText().getIntValue() <= legacyEndChannel.getText().getIntValue();
@ -531,15 +288,32 @@ private:
"Got it"); "Got it");
} }
//==============================================================================
Range<int> getLegacyModeChannelRange() const Range<int> getLegacyModeChannelRange() const
{ {
return { legacyStartChannel.getText().getIntValue(), return { legacyStartChannel.getText().getIntValue(),
legacyEndChannel.getText().getIntValue() + 1 }; legacyEndChannel.getText().getIntValue() + 1 };
} }
int getLegacyModePitchbendRange() const
{
return legacyPitchbendRange.getText().getIntValue();
}
//============================================================================== //==============================================================================
MPEZoneLayout zoneLayout; void voiceStealingEnabledToggleClicked()
{
jassert (onSynthParametersChange != nullptr);
onSynthParametersChange();
}
void numberOfVoicesChanged()
{
jassert (onSynthParametersChange != nullptr);
onSynthParametersChange();
}
//==============================================================================
MPEInstrument& instrument;
ComboBox memberChannels, masterPitchbendRange, notePitchbendRange; ComboBox memberChannels, masterPitchbendRange, notePitchbendRange;
@ -564,67 +338,49 @@ private:
ComboBox numberOfVoices; ComboBox numberOfVoices;
Label numberOfVoicesLabel { {}, "Number of synth voices"}; Label numberOfVoicesLabel { {}, "Number of synth voices"};
ListenerList<Listener> listeners; static constexpr int defaultMemberChannels = 15,
defaultMasterPitchbendRange = 2,
const int defaultMemberChannels = 15, defaultNotePitchbendRange = 48;
defaultMasterPitchbendRange = 2,
defaultNotePitchbendRange = 48;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPESetupComponent) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPESetupComponent)
}; };
//============================================================================== //==============================================================================
class ZoneLayoutComponent : public Component, class ZoneLayoutComponent : public Component,
public MPESetupComponent::Listener private MPEInstrument::Listener
{ {
public: public:
//============================================================================== //==============================================================================
ZoneLayoutComponent (const ZoneColourPicker& zoneColourPicker) ZoneLayoutComponent (MPEInstrument& instr, ZoneColourPicker& zoneColourPicker)
: colourPicker (zoneColourPicker) : instrument (instr),
{} colourPicker (zoneColourPicker)
{
instrument.addListener (this);
}
~ZoneLayoutComponent() override
{
instrument.removeListener (this);
}
//============================================================================== //==============================================================================
void paint (Graphics& g) override void paint (Graphics& g) override
{ {
paintBackground (g); paintBackground (g);
if (legacyModeEnabled) if (instrument.isLegacyModeEnabled())
paintLegacyMode (g); paintLegacyMode (g);
else else
paintZones (g); paintZones (g);
} }
//==============================================================================
void zoneChanged (bool isLowerZone, int numMemberChannels,
int perNotePitchbendRange, int masterPitchbendRange) override
{
if (isLowerZone)
zoneLayout.setLowerZone (numMemberChannels, perNotePitchbendRange, masterPitchbendRange);
else
zoneLayout.setUpperZone (numMemberChannels, perNotePitchbendRange, masterPitchbendRange);
repaint();
}
void allZonesCleared() override
{
zoneLayout.clearAllZones();
repaint();
}
void legacyModeChanged (bool legacyModeShouldBeEnabled, int pitchbendRange, Range<int> channelRange) override
{
legacyModeEnabled = legacyModeShouldBeEnabled;
legacyModePitchbendRange = pitchbendRange;
legacyModeChannelRange = channelRange;
repaint();
}
void voiceStealingEnabledChanged (bool) override { /* not interested in this change */ }
void numberOfVoicesChanged (int) override { /* not interested in this change */ }
private: private:
//==============================================================================
void zoneLayoutChanged() override
{
repaint();
}
//============================================================================== //==============================================================================
void paintBackground (Graphics& g) void paintBackground (Graphics& g)
{ {
@ -646,6 +402,8 @@ private:
{ {
auto channelWidth = getChannelRectangleWidth(); auto channelWidth = getChannelRectangleWidth();
auto zoneLayout = instrument.getZoneLayout();
Array<MPEZoneLayout::Zone> activeZones; Array<MPEZoneLayout::Zone> activeZones;
if (zoneLayout.getLowerZone().isActive()) activeZones.add (zoneLayout.getLowerZone()); if (zoneLayout.getLowerZone().isActive()) activeZones.add (zoneLayout.getLowerZone());
if (zoneLayout.getUpperZone().isActive()) activeZones.add (zoneLayout.getUpperZone()); if (zoneLayout.getUpperZone().isActive()) activeZones.add (zoneLayout.getUpperZone());
@ -676,9 +434,9 @@ private:
//============================================================================== //==============================================================================
void paintLegacyMode (Graphics& g) void paintLegacyMode (Graphics& g)
{ {
auto startChannel = legacyModeChannelRange.getStart() - 1; auto channelRange = instrument.getLegacyModeChannelRange();
auto numChannels = legacyModeChannelRange.getEnd() - startChannel - 1; auto startChannel = channelRange.getStart() - 1;
auto numChannels = channelRange.getEnd() - startChannel - 1;
Rectangle<int> zoneRect (int (getChannelRectangleWidth() * (float) startChannel), 0, Rectangle<int> zoneRect (int (getChannelRectangleWidth() * (float) startChannel), 0,
int (getChannelRectangleWidth() * (float) numChannels), getHeight()); int (getChannelRectangleWidth() * (float) numChannels), getHeight());
@ -688,7 +446,7 @@ private:
g.setColour (Colours::white); g.setColour (Colours::white);
g.drawRect (zoneRect, 3); g.drawRect (zoneRect, 3);
g.drawText ("LGCY", zoneRect.reduced (4, 4), Justification::topLeft, false); g.drawText ("LGCY", zoneRect.reduced (4, 4), Justification::topLeft, false);
g.drawText ("<>" + String (legacyModePitchbendRange), zoneRect.reduced (4, 4), Justification::bottomLeft, false); g.drawText ("<>" + String (instrument.getLegacyModePitchbendRange()), zoneRect.reduced (4, 4), Justification::bottomLeft, false);
} }
//============================================================================== //==============================================================================
@ -698,13 +456,10 @@ private:
} }
//============================================================================== //==============================================================================
MPEZoneLayout zoneLayout; static constexpr int numMidiChannels = 16;
const ZoneColourPicker& colourPicker;
bool legacyModeEnabled = false; MPEInstrument& instrument;
int legacyModePitchbendRange = 48; ZoneColourPicker& colourPicker;
Range<int> legacyModeChannelRange = { 1, 17 };
const int numMidiChannels = 16;
}; };
//============================================================================== //==============================================================================
@ -867,14 +622,11 @@ private:
class MPEDemo : public Component, class MPEDemo : public Component,
private AudioIODeviceCallback, private AudioIODeviceCallback,
private MidiInputCallback, private MidiInputCallback,
private MPESetupComponent::Listener private MPEInstrument::Listener
{ {
public: public:
//============================================================================== //==============================================================================
MPEDemo() MPEDemo()
: audioSetupComp (audioDeviceManager, 0, 0, 0, 256, true, true, true, false),
zoneLayoutComp (colourPicker),
visualiserComp (colourPicker)
{ {
#ifndef JUCE_DEMO_RUNNER #ifndef JUCE_DEMO_RUNNER
audioDeviceManager.initialise (0, 2, nullptr, true, {}, nullptr); audioDeviceManager.initialise (0, 2, nullptr, true, {}, nullptr);
@ -884,22 +636,33 @@ public:
audioDeviceManager.addAudioCallback (this); audioDeviceManager.addAudioCallback (this);
addAndMakeVisible (audioSetupComp); addAndMakeVisible (audioSetupComp);
addAndMakeVisible (MPESetupComp); addAndMakeVisible (mpeSetupComp);
addAndMakeVisible (zoneLayoutComp); addAndMakeVisible (zoneLayoutComp);
addAndMakeVisible (visualiserViewport); addAndMakeVisible (keyboardComponent);
visualiserViewport.setScrollBarsShown (false, true);
visualiserViewport.setViewedComponent (&visualiserComp, false);
visualiserViewport.setViewPositionProportionately (0.5, 0.0);
MPESetupComp.addListener (&zoneLayoutComp);
MPESetupComp.addListener (this);
visualiserInstrument.addListener (&visualiserComp);
synth.setVoiceStealingEnabled (false); synth.setVoiceStealingEnabled (false);
for (auto i = 0; i < 15; ++i) for (auto i = 0; i < 15; ++i)
synth.addVoice (new MPEDemoSynthVoice()); synth.addVoice (new MPEDemoSynthVoice());
mpeSetupComp.onSynthParametersChange = [this]
{
synth.setVoiceStealingEnabled (mpeSetupComp.isVoiceStealingEnabled());
auto numVoices = mpeSetupComp.getNumVoices();
if (numVoices < synth.getNumVoices())
{
synth.reduceNumVoices (numVoices);
}
else
{
while (synth.getNumVoices() < numVoices)
synth.addVoice (new MPEDemoSynthVoice());
}
};
instrument.addListener (this);
setSize (880, 720); setSize (880, 720);
} }
@ -912,20 +675,17 @@ public:
//============================================================================== //==============================================================================
void resized() override void resized() override
{ {
auto visualiserCompWidth = 2800;
auto visualiserCompHeight = 300;
auto zoneLayoutCompHeight = 60; auto zoneLayoutCompHeight = 60;
auto audioSetupCompRelativeWidth = 0.55f; auto audioSetupCompRelativeWidth = 0.55f;
auto r = getLocalBounds(); auto r = getLocalBounds();
visualiserViewport.setBounds (r.removeFromBottom (visualiserCompHeight)); keyboardComponent.setBounds (r.removeFromBottom (150));
visualiserComp .setBounds ({ visualiserCompWidth, r.reduce (10, 10);
visualiserViewport.getHeight() - visualiserViewport.getScrollBarThickness() });
zoneLayoutComp.setBounds (r.removeFromBottom (zoneLayoutCompHeight)); zoneLayoutComp.setBounds (r.removeFromBottom (zoneLayoutCompHeight));
audioSetupComp.setBounds (r.removeFromLeft (proportionOfWidth (audioSetupCompRelativeWidth))); audioSetupComp.setBounds (r.removeFromLeft (proportionOfWidth (audioSetupCompRelativeWidth)));
MPESetupComp .setBounds (r); mpeSetupComp .setBounds (r);
} }
//============================================================================== //==============================================================================
@ -955,75 +715,34 @@ private:
void handleIncomingMidiMessage (MidiInput* /*source*/, void handleIncomingMidiMessage (MidiInput* /*source*/,
const MidiMessage& message) override const MidiMessage& message) override
{ {
visualiserInstrument.processNextMidiEvent (message); instrument.processNextMidiEvent (message);
midiCollector.addMessageToQueue (message); midiCollector.addMessageToQueue (message);
} }
//============================================================================== //==============================================================================
void zoneChanged (bool isLowerZone, int numMemberChannels, void zoneLayoutChanged() override
int perNotePitchbendRange, int masterPitchbendRange) override
{ {
auto* midiOutput = audioDeviceManager.getDefaultMidiOutput(); if (instrument.isLegacyModeEnabled())
if (midiOutput != nullptr)
{ {
if (isLowerZone) colourPicker.setLegacyModeEnabled (true);
midiOutput->sendBlockOfMessagesNow (MPEMessages::setLowerZone (numMemberChannels, perNotePitchbendRange, masterPitchbendRange));
else
midiOutput->sendBlockOfMessagesNow (MPEMessages::setUpperZone (numMemberChannels, perNotePitchbendRange, masterPitchbendRange));
}
if (isLowerZone) synth.enableLegacyMode (instrument.getLegacyModePitchbendRange(),
zoneLayout.setLowerZone (numMemberChannels, perNotePitchbendRange, masterPitchbendRange); instrument.getLegacyModeChannelRange());
else
zoneLayout.setUpperZone (numMemberChannels, perNotePitchbendRange, masterPitchbendRange);
visualiserInstrument.setZoneLayout (zoneLayout);
synth.setZoneLayout (zoneLayout);
colourPicker.setZoneLayout (zoneLayout);
}
void allZonesCleared() override
{
auto* midiOutput = audioDeviceManager.getDefaultMidiOutput();
if (midiOutput != nullptr)
midiOutput->sendBlockOfMessagesNow (MPEMessages::clearAllZones());
zoneLayout.clearAllZones();
visualiserInstrument.setZoneLayout (zoneLayout);
synth.setZoneLayout (zoneLayout);
colourPicker.setZoneLayout (zoneLayout);
}
void legacyModeChanged (bool legacyModeShouldBeEnabled, int pitchbendRange, Range<int> channelRange) override
{
colourPicker.setLegacyModeEnabled (legacyModeShouldBeEnabled);
if (legacyModeShouldBeEnabled)
{
synth.enableLegacyMode (pitchbendRange, channelRange);
visualiserInstrument.enableLegacyMode (pitchbendRange, channelRange);
} }
else else
{ {
colourPicker.setLegacyModeEnabled (false);
auto zoneLayout = instrument.getZoneLayout();
if (auto* midiOutput = audioDeviceManager.getDefaultMidiOutput())
midiOutput->sendBlockOfMessagesNow (MPEMessages::setZoneLayout (zoneLayout));
synth.setZoneLayout (zoneLayout); synth.setZoneLayout (zoneLayout);
visualiserInstrument.setZoneLayout (zoneLayout); colourPicker.setZoneLayout (zoneLayout);
} }
} }
void voiceStealingEnabledChanged (bool voiceStealingEnabled) override
{
synth.setVoiceStealingEnabled (voiceStealingEnabled);
}
void numberOfVoicesChanged (int numberOfVoices) override
{
if (numberOfVoices < synth.getNumVoices())
synth.reduceNumVoices (numberOfVoices);
else
while (synth.getNumVoices() < numberOfVoices)
synth.addVoice (new MPEDemoSynthVoice());
}
//============================================================================== //==============================================================================
// if this PIP is running inside the demo runner, we'll use the shared device manager instead // if this PIP is running inside the demo runner, we'll use the shared device manager instead
#ifndef JUCE_DEMO_RUNNER #ifndef JUCE_DEMO_RUNNER
@ -1032,19 +751,18 @@ private:
AudioDeviceManager& audioDeviceManager { getSharedAudioDeviceManager (0, 2) }; AudioDeviceManager& audioDeviceManager { getSharedAudioDeviceManager (0, 2) };
#endif #endif
MPEZoneLayout zoneLayout; AudioDeviceSelectorComponent audioSetupComp { audioDeviceManager, 0, 0, 0, 256, true, true, true, false };
ZoneColourPicker colourPicker;
AudioDeviceSelectorComponent audioSetupComp;
MPESetupComponent MPESetupComp;
ZoneLayoutComponent zoneLayoutComp;
Visualiser visualiserComp;
Viewport visualiserViewport;
MPEInstrument visualiserInstrument;
MPESynthesiser synth;
MidiMessageCollector midiCollector; MidiMessageCollector midiCollector;
MPEInstrument instrument { MPEZone (MPEZone::Type::lower, 15) };
ZoneColourPicker colourPicker;
MPESetupComponent mpeSetupComp { instrument };
ZoneLayoutComponent zoneLayoutComp { instrument, colourPicker};
MPESynthesiser synth { instrument };
MPEKeyboardComponent keyboardComponent { instrument, MPEKeyboardComponent::horizontalKeyboard };
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPEDemo) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPEDemo)
}; };

View file

@ -605,22 +605,6 @@ private:
namespace juce namespace juce
{ {
bool operator== (const MPEZoneLayout& a, const MPEZoneLayout& b)
{
if (a.getLowerZone() != b.getLowerZone())
return false;
if (a.getUpperZone() != b.getUpperZone())
return false;
return true;
}
bool operator!= (const MPEZoneLayout& a, const MPEZoneLayout& b)
{
return ! (a == b);
}
template<> template<>
struct VariantConverter<LoopMode> struct VariantConverter<LoopMode>
{ {

View file

@ -43,16 +43,20 @@ MPEInstrument::MPEInstrument() noexcept
mpeInstrumentFill (isMemberChannelSustained, false); mpeInstrumentFill (isMemberChannelSustained, false);
pitchbendDimension.value = &MPENote::pitchbend; pitchbendDimension.value = &MPENote::pitchbend;
pressureDimension.value = &MPENote::pressure; pressureDimension.value = &MPENote::pressure;
timbreDimension.value = &MPENote::timbre; timbreDimension.value = &MPENote::timbre;
resetLastReceivedValues(); resetLastReceivedValues();
legacyMode.isEnabled = false;
legacyMode.pitchbendRange = 2;
legacyMode.channelRange = allChannels; legacyMode.channelRange = allChannels;
} }
MPEInstrument::MPEInstrument (MPEZoneLayout layout)
: MPEInstrument()
{
setZoneLayout (layout);
}
MPEInstrument::~MPEInstrument() = default; MPEInstrument::~MPEInstrument() = default;
//============================================================================== //==============================================================================
@ -84,21 +88,30 @@ void MPEInstrument::setZoneLayout (MPEZoneLayout newLayout)
const ScopedLock sl (lock); const ScopedLock sl (lock);
legacyMode.isEnabled = false; legacyMode.isEnabled = false;
zoneLayout = newLayout;
resetLastReceivedValues(); if (zoneLayout != newLayout)
{
zoneLayout = newLayout;
listeners.call ([=] (Listener& l) { l.zoneLayoutChanged(); });
}
} }
//============================================================================== //==============================================================================
void MPEInstrument::enableLegacyMode (int pitchbendRange, Range<int> channelRange) void MPEInstrument::enableLegacyMode (int pitchbendRange, Range<int> channelRange)
{ {
if (legacyMode.isEnabled)
return;
releaseAllNotes(); releaseAllNotes();
const ScopedLock sl (lock); const ScopedLock sl (lock);
legacyMode.isEnabled = true; legacyMode.isEnabled = true;
legacyMode.pitchbendRange = pitchbendRange; legacyMode.pitchbendRange = pitchbendRange;
legacyMode.channelRange = channelRange; legacyMode.channelRange = channelRange;
zoneLayout.clearAllZones(); zoneLayout.clearAllZones();
listeners.call ([=] (Listener& l) { l.zoneLayoutChanged(); });
} }
bool MPEInstrument::isLegacyModeEnabled() const noexcept bool MPEInstrument::isLegacyModeEnabled() const noexcept
@ -117,7 +130,12 @@ void MPEInstrument::setLegacyModeChannelRange (Range<int> channelRange)
releaseAllNotes(); releaseAllNotes();
const ScopedLock sl (lock); const ScopedLock sl (lock);
legacyMode.channelRange = channelRange;
if (legacyMode.channelRange != channelRange)
{
legacyMode.channelRange = channelRange;
listeners.call ([=] (Listener& l) { l.zoneLayoutChanged(); });
}
} }
int MPEInstrument::getLegacyModePitchbendRange() const noexcept int MPEInstrument::getLegacyModePitchbendRange() const noexcept
@ -131,7 +149,12 @@ void MPEInstrument::setLegacyModePitchbendRange (int pitchbendRange)
releaseAllNotes(); releaseAllNotes();
const ScopedLock sl (lock); const ScopedLock sl (lock);
legacyMode.pitchbendRange = pitchbendRange;
if (legacyMode.pitchbendRange != pitchbendRange)
{
legacyMode.pitchbendRange = pitchbendRange;
listeners.call ([=] (Listener& l) { l.zoneLayoutChanged(); });
}
} }
//============================================================================== //==============================================================================
@ -242,7 +265,7 @@ void MPEInstrument::processMidiResetAllControllersMessage (const MidiMessage& me
if (legacyMode.isEnabled && legacyMode.channelRange.contains (message.getChannel())) if (legacyMode.isEnabled && legacyMode.channelRange.contains (message.getChannel()))
{ {
for (auto i = notes.size(); --i >= 0;) for (int i = notes.size(); --i >= 0;)
{ {
auto& note = notes.getReference (i); auto& note = notes.getReference (i);
@ -260,7 +283,7 @@ void MPEInstrument::processMidiResetAllControllersMessage (const MidiMessage& me
auto zone = (message.getChannel() == 1 ? zoneLayout.getLowerZone() auto zone = (message.getChannel() == 1 ? zoneLayout.getLowerZone()
: zoneLayout.getUpperZone()); : zoneLayout.getUpperZone());
for (auto i = notes.size(); --i >= 0;) for (int i = notes.size(); --i >= 0;)
{ {
auto& note = notes.getReference (i); auto& note = notes.getReference (i);
@ -348,11 +371,11 @@ void MPEInstrument::noteOff (int midiChannel,
int midiNoteNumber, int midiNoteNumber,
MPEValue midiNoteOffVelocity) MPEValue midiNoteOffVelocity)
{ {
const ScopedLock sl (lock);
if (notes.isEmpty() || ! isUsingChannel (midiChannel)) if (notes.isEmpty() || ! isUsingChannel (midiChannel))
return; return;
const ScopedLock sl (lock);
if (auto* note = getNotePtr (midiChannel, midiNoteNumber)) if (auto* note = getNotePtr (midiChannel, midiNoteNumber))
{ {
note->keyState = (note->keyState == MPENote::keyDownAndSustained) ? MPENote::sustained : MPENote::off; note->keyState = (note->keyState == MPENote::keyDownAndSustained) ? MPENote::sustained : MPENote::off;
@ -401,7 +424,7 @@ void MPEInstrument::polyAftertouch (int midiChannel, int midiNoteNumber, MPEValu
{ {
const ScopedLock sl (lock); const ScopedLock sl (lock);
for (auto i = notes.size(); --i >= 0;) for (int i = notes.size(); --i >= 0;)
{ {
auto& note = notes.getReference (i); auto& note = notes.getReference (i);
@ -435,7 +458,7 @@ void MPEInstrument::updateDimension (int midiChannel, MPEDimension& dimension, M
{ {
if (dimension.trackingMode == allNotesOnChannel) if (dimension.trackingMode == allNotesOnChannel)
{ {
for (auto i = notes.size(); --i >= 0;) for (int i = notes.size(); --i >= 0;)
{ {
auto& note = notes.getReference (i); auto& note = notes.getReference (i);
@ -464,7 +487,7 @@ void MPEInstrument::updateDimensionMaster (bool isLowerZone, MPEDimension& dimen
if (! zone.isActive()) if (! zone.isActive())
return; return;
for (auto i = notes.size(); --i >= 0;) for (int i = notes.size(); --i >= 0;)
{ {
auto& note = notes.getReference (i); auto& note = notes.getReference (i);
@ -573,7 +596,7 @@ void MPEInstrument::handleSustainOrSostenuto (int midiChannel, bool isDown, bool
auto zone = (midiChannel == 1 ? zoneLayout.getLowerZone() auto zone = (midiChannel == 1 ? zoneLayout.getLowerZone()
: zoneLayout.getUpperZone()); : zoneLayout.getUpperZone());
for (auto i = notes.size(); --i >= 0;) for (int i = notes.size(); --i >= 0;)
{ {
auto& note = notes.getReference (i); auto& note = notes.getReference (i);
@ -605,11 +628,15 @@ void MPEInstrument::handleSustainOrSostenuto (int midiChannel, bool isDown, bool
if (! legacyMode.isEnabled) if (! legacyMode.isEnabled)
{ {
if (zone.isLowerZone()) if (zone.isLowerZone())
for (auto i = zone.getFirstMemberChannel(); i <= zone.getLastMemberChannel(); ++i) {
for (int i = zone.getFirstMemberChannel(); i <= zone.getLastMemberChannel(); ++i)
isMemberChannelSustained[i - 1] = isDown; isMemberChannelSustained[i - 1] = isDown;
}
else else
for (auto i = zone.getFirstMemberChannel(); i >= zone.getLastMemberChannel(); --i) {
for (int i = zone.getFirstMemberChannel(); i >= zone.getLastMemberChannel(); --i)
isMemberChannelSustained[i - 1] = isDown; isMemberChannelSustained[i - 1] = isDown;
}
} }
} }
} }
@ -664,6 +691,17 @@ MPENote MPEInstrument::getNote (int index) const noexcept
return notes[index]; return notes[index];
} }
MPENote MPEInstrument::getNoteWithID (uint16 noteID) const noexcept
{
const ScopedLock sl (lock);
for (auto& note : notes)
if (note.noteID == noteID)
return note;
return {};
}
//============================================================================== //==============================================================================
MPENote MPEInstrument::getMostRecentNote (int midiChannel) const noexcept MPENote MPEInstrument::getMostRecentNote (int midiChannel) const noexcept
{ {
@ -727,6 +765,8 @@ MPENote* MPEInstrument::getNotePtr (int midiChannel, TrackingMode mode) noexcept
//============================================================================== //==============================================================================
const MPENote* MPEInstrument::getLastNotePlayedPtr (int midiChannel) const noexcept const MPENote* MPEInstrument::getLastNotePlayedPtr (int midiChannel) const noexcept
{ {
const ScopedLock sl (lock);
for (auto i = notes.size(); --i >= 0;) for (auto i = notes.size(); --i >= 0;)
{ {
auto& note = notes.getReference (i); auto& note = notes.getReference (i);

View file

@ -38,10 +38,8 @@ namespace juce
MPE. If you pass it a message, it will know what notes on what MPE. If you pass it a message, it will know what notes on what
channels (if any) should be affected by that message. channels (if any) should be affected by that message.
The class has a Listener class with the three callbacks MPENoteAdded, The class has a Listener class that can be used to react to note and
MPENoteChanged, and MPENoteFinished. Implement such a state changes and trigger some functionality for your application.
Listener class to react to note changes and trigger some functionality for
your application that depends on the MPE note state.
For example, you can use this class to write an MPE visualiser. For example, you can use this class to write an MPE visualiser.
If you want to write a real-time audio synth with MPE functionality, If you want to write a real-time audio synth with MPE functionality,
@ -59,11 +57,14 @@ public:
This will construct an MPE instrument with inactive lower and upper zones. This will construct an MPE instrument with inactive lower and upper zones.
In order to process incoming MIDI, call setZoneLayout, define the layout In order to process incoming MIDI messages call setZoneLayout, use the MPEZoneLayout
via MIDI RPN messages, or set the instrument to legacy mode. constructor, define the layout via MIDI RPN messages, or set the instrument to legacy mode.
*/ */
MPEInstrument() noexcept; MPEInstrument() noexcept;
/** Constructs an MPE instrument with the specified zone layout. */
MPEInstrument (MPEZoneLayout layout);
/** Destructor. */ /** Destructor. */
virtual ~MPEInstrument(); virtual ~MPEInstrument();
@ -229,6 +230,9 @@ public:
*/ */
MPENote getNote (int midiChannel, int midiNoteNumber) const noexcept; MPENote getNote (int midiChannel, int midiNoteNumber) const noexcept;
/** Returns the note with a given ID. */
MPENote getNoteWithID (uint16 noteID) const noexcept;
/** Returns the most recent note that is playing on the given midiChannel /** Returns the most recent note that is playing on the given midiChannel
(this will be the note which has received the most recent note-on without (this will be the note which has received the most recent note-on without
a corresponding note-off), if there is such a note. Otherwise, this returns an a corresponding note-off), if there is such a note. Otherwise, this returns an
@ -244,8 +248,8 @@ public:
MPENote getMostRecentNoteOtherThan (MPENote otherThanThisNote) const noexcept; MPENote getMostRecentNoteOtherThan (MPENote otherThanThisNote) const noexcept;
//============================================================================== //==============================================================================
/** Derive from this class to be informed about any changes in the expressive /** Derive from this class to be informed about any changes in the MPE notes played
MIDI notes played by this instrument. by this instrument, and any changes to its zone layout.
Note: This listener type receives its callbacks immediately, and not Note: This listener type receives its callbacks immediately, and not
via the message thread (so you might be for example in the MIDI thread). via the message thread (so you might be for example in the MIDI thread).
@ -297,6 +301,11 @@ public:
and should therefore stop playing. and should therefore stop playing.
*/ */
virtual void noteReleased (MPENote finishedNote) { ignoreUnused (finishedNote); } virtual void noteReleased (MPENote finishedNote) { ignoreUnused (finishedNote); }
/** Implement this callback to be informed whenever the MPE zone layout
or legacy mode settings of this instrument have been changed.
*/
virtual void zoneLayoutChanged() {}
}; };
//============================================================================== //==============================================================================
@ -307,7 +316,9 @@ public:
void removeListener (Listener* listenerToRemove); void removeListener (Listener* listenerToRemove);
//============================================================================== //==============================================================================
/** Puts the instrument into legacy mode. /** Puts the instrument into legacy mode. If legacy mode is already enabled this method
does nothing.
As a side effect, this will discard all currently playing notes, As a side effect, this will discard all currently playing notes,
and call noteReleased for all of them. and call noteReleased for all of them.
@ -360,9 +371,9 @@ private:
struct LegacyMode struct LegacyMode
{ {
bool isEnabled; bool isEnabled = false;
Range<int> channelRange; Range<int> channelRange;
int pitchbendRange; int pitchbendRange = 2;
}; };
struct MPEDimension struct MPEDimension

View file

@ -115,7 +115,7 @@ struct JUCE_API MPENote
*/ */
MPEValue noteOnVelocity { MPEValue::minValue() }; MPEValue noteOnVelocity { MPEValue::minValue() };
/** Current per-note pitchbend of the note (in units of MIDI pitchwheel /** Current per-note pitchbend of the note (in units of MIDI pitchwheel
position). This dimension can be modulated while the note sounds. position). This dimension can be modulated while the note sounds.
Note: This value is not aware of the currently used pitchbend range, Note: This value is not aware of the currently used pitchbend range,

View file

@ -25,12 +25,10 @@ namespace juce
MPESynthesiser::MPESynthesiser() MPESynthesiser::MPESynthesiser()
{ {
MPEZoneLayout zoneLayout;
zoneLayout.setLowerZone (15);
setZoneLayout (zoneLayout);
} }
MPESynthesiser::MPESynthesiser (MPEInstrument* mpeInstrument) : MPESynthesiserBase (mpeInstrument) MPESynthesiser::MPESynthesiser (MPEInstrument& mpeInstrument)
: MPESynthesiserBase (mpeInstrument)
{ {
} }
@ -314,7 +312,7 @@ void MPESynthesiser::turnOffAllVoices (bool allowTailOff)
} }
// finally make sure the MPE Instrument also doesn't have any notes anymore. // finally make sure the MPE Instrument also doesn't have any notes anymore.
instrument->releaseAllNotes(); instrument.releaseAllNotes();
} }
//============================================================================== //==============================================================================

View file

@ -65,11 +65,10 @@ public:
/** Constructor to pass to the synthesiser a custom MPEInstrument object /** Constructor to pass to the synthesiser a custom MPEInstrument object
to handle the MPE note state, MIDI channel assignment etc. to handle the MPE note state, MIDI channel assignment etc.
(in case you need custom logic for this that goes beyond MIDI and MPE). (in case you need custom logic for this that goes beyond MIDI and MPE).
The synthesiser will take ownership of this object.
@see MPESynthesiserBase, MPEInstrument @see MPESynthesiserBase, MPEInstrument
*/ */
MPESynthesiser (MPEInstrument* instrumentToUse); MPESynthesiser (MPEInstrument& instrumentToUse);
/** Destructor. */ /** Destructor. */
~MPESynthesiser() override; ~MPESynthesiser() override;
@ -303,7 +302,7 @@ protected:
private: private:
//============================================================================== //==============================================================================
bool shouldStealVoices = false; std::atomic<bool> shouldStealVoices { false };
uint32 lastNoteOnCounter = 0; uint32 lastNoteOnCounter = 0;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPESynthesiser) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPESynthesiser)

View file

@ -24,80 +24,79 @@ namespace juce
{ {
MPESynthesiserBase::MPESynthesiserBase() MPESynthesiserBase::MPESynthesiserBase()
: instrument (new MPEInstrument) : instrument (defaultInstrument)
{ {
instrument->addListener (this); instrument.addListener (this);
} }
MPESynthesiserBase::MPESynthesiserBase (MPEInstrument* inst) MPESynthesiserBase::MPESynthesiserBase (MPEInstrument& inst)
: instrument (inst) : instrument (inst)
{ {
jassert (instrument != nullptr); instrument.addListener (this);
instrument->addListener (this);
} }
//============================================================================== //==============================================================================
MPEZoneLayout MPESynthesiserBase::getZoneLayout() const noexcept MPEZoneLayout MPESynthesiserBase::getZoneLayout() const noexcept
{ {
return instrument->getZoneLayout(); return instrument.getZoneLayout();
} }
void MPESynthesiserBase::setZoneLayout (MPEZoneLayout newLayout) void MPESynthesiserBase::setZoneLayout (MPEZoneLayout newLayout)
{ {
instrument->setZoneLayout (newLayout); instrument.setZoneLayout (newLayout);
} }
//============================================================================== //==============================================================================
void MPESynthesiserBase::enableLegacyMode (int pitchbendRange, Range<int> channelRange) void MPESynthesiserBase::enableLegacyMode (int pitchbendRange, Range<int> channelRange)
{ {
instrument->enableLegacyMode (pitchbendRange, channelRange); instrument.enableLegacyMode (pitchbendRange, channelRange);
} }
bool MPESynthesiserBase::isLegacyModeEnabled() const noexcept bool MPESynthesiserBase::isLegacyModeEnabled() const noexcept
{ {
return instrument->isLegacyModeEnabled(); return instrument.isLegacyModeEnabled();
} }
Range<int> MPESynthesiserBase::getLegacyModeChannelRange() const noexcept Range<int> MPESynthesiserBase::getLegacyModeChannelRange() const noexcept
{ {
return instrument->getLegacyModeChannelRange(); return instrument.getLegacyModeChannelRange();
} }
void MPESynthesiserBase::setLegacyModeChannelRange (Range<int> channelRange) void MPESynthesiserBase::setLegacyModeChannelRange (Range<int> channelRange)
{ {
instrument->setLegacyModeChannelRange (channelRange); instrument.setLegacyModeChannelRange (channelRange);
} }
int MPESynthesiserBase::getLegacyModePitchbendRange() const noexcept int MPESynthesiserBase::getLegacyModePitchbendRange() const noexcept
{ {
return instrument->getLegacyModePitchbendRange(); return instrument.getLegacyModePitchbendRange();
} }
void MPESynthesiserBase::setLegacyModePitchbendRange (int pitchbendRange) void MPESynthesiserBase::setLegacyModePitchbendRange (int pitchbendRange)
{ {
instrument->setLegacyModePitchbendRange (pitchbendRange); instrument.setLegacyModePitchbendRange (pitchbendRange);
} }
//============================================================================== //==============================================================================
void MPESynthesiserBase::setPressureTrackingMode (TrackingMode modeToUse) void MPESynthesiserBase::setPressureTrackingMode (TrackingMode modeToUse)
{ {
instrument->setPressureTrackingMode (modeToUse); instrument.setPressureTrackingMode (modeToUse);
} }
void MPESynthesiserBase::setPitchbendTrackingMode (TrackingMode modeToUse) void MPESynthesiserBase::setPitchbendTrackingMode (TrackingMode modeToUse)
{ {
instrument->setPitchbendTrackingMode (modeToUse); instrument.setPitchbendTrackingMode (modeToUse);
} }
void MPESynthesiserBase::setTimbreTrackingMode (TrackingMode modeToUse) void MPESynthesiserBase::setTimbreTrackingMode (TrackingMode modeToUse)
{ {
instrument->setTimbreTrackingMode (modeToUse); instrument.setTimbreTrackingMode (modeToUse);
} }
//============================================================================== //==============================================================================
void MPESynthesiserBase::handleMidiEvent (const MidiMessage& m) void MPESynthesiserBase::handleMidiEvent (const MidiMessage& m)
{ {
instrument->processNextMidiEvent (m); instrument.processNextMidiEvent (m);
} }
//============================================================================== //==============================================================================
@ -148,7 +147,7 @@ void MPESynthesiserBase::setCurrentPlaybackSampleRate (const double newRate)
if (sampleRate != newRate) if (sampleRate != newRate)
{ {
const ScopedLock sl (noteStateLock); const ScopedLock sl (noteStateLock);
instrument->releaseAllNotes(); instrument.releaseAllNotes();
sampleRate = newRate; sampleRate = newRate;
} }
} }

View file

@ -52,13 +52,12 @@ public:
/** Constructor. /** Constructor.
If you use this constructor, the synthesiser will take ownership of the If you use this constructor, the synthesiser will use the provided instrument
provided instrument object, and will use it internally to handle the object to handle the MPE note state logic.
MPE note state logic.
This is useful if you want to use an instance of your own class derived This is useful if you want to use an instance of your own class derived
from MPEInstrument for the MPE logic. from MPEInstrument for the MPE logic.
*/ */
MPESynthesiserBase (MPEInstrument* instrument); MPESynthesiserBase (MPEInstrument& instrument);
//============================================================================== //==============================================================================
/** Returns the synthesiser's internal MPE zone layout. /** Returns the synthesiser's internal MPE zone layout.
@ -200,10 +199,12 @@ protected:
protected: protected:
//============================================================================== //==============================================================================
/** @internal */ /** @internal */
std::unique_ptr<MPEInstrument> instrument; MPEInstrument& instrument;
private: private:
//============================================================================== //==============================================================================
MPEInstrument defaultInstrument { MPEZone (MPEZone::Type::lower, 15) };
CriticalSection noteStateLock; CriticalSection noteStateLock;
double sampleRate = 0.0; double sampleRate = 0.0;
int minimumSubBlockSize = 32; int minimumSubBlockSize = 32;

View file

@ -52,25 +52,25 @@ int MPEChannelAssigner::findMidiChannelForNewNote (int noteNumber) noexcept
if (numChannels <= 1) if (numChannels <= 1)
return firstChannel; return firstChannel;
for (auto ch = firstChannel; (isLegacy || zone->isLowerZone() ? ch <= lastChannel : ch >= lastChannel); ch += channelIncrement) for (int ch = firstChannel; (isLegacy || zone->isLowerZone() ? ch <= lastChannel : ch >= lastChannel); ch += channelIncrement)
{ {
if (midiChannels[ch].isFree() && midiChannels[ch].lastNotePlayed == noteNumber) if (midiChannels[(size_t) ch].isFree() && midiChannels[(size_t) ch].lastNotePlayed == noteNumber)
{ {
midiChannelLastAssigned = ch; midiChannelLastAssigned = ch;
midiChannels[ch].notes.add (noteNumber); midiChannels[(size_t) ch].notes.add (noteNumber);
return ch; return ch;
} }
} }
for (auto ch = midiChannelLastAssigned + channelIncrement; ; ch += channelIncrement) for (int ch = midiChannelLastAssigned + channelIncrement; ; ch += channelIncrement)
{ {
if (ch == lastChannel + channelIncrement) // loop wrap-around if (ch == lastChannel + channelIncrement) // loop wrap-around
ch = firstChannel; ch = firstChannel;
if (midiChannels[ch].isFree()) if (midiChannels[(size_t) ch].isFree())
{ {
midiChannelLastAssigned = ch; midiChannelLastAssigned = ch;
midiChannels[ch].notes.add (noteNumber); midiChannels[(size_t) ch].notes.add (noteNumber);
return ch; return ch;
} }
@ -79,11 +79,21 @@ int MPEChannelAssigner::findMidiChannelForNewNote (int noteNumber) noexcept
} }
midiChannelLastAssigned = findMidiChannelPlayingClosestNonequalNote (noteNumber); midiChannelLastAssigned = findMidiChannelPlayingClosestNonequalNote (noteNumber);
midiChannels[midiChannelLastAssigned].notes.add (noteNumber); midiChannels[(size_t) midiChannelLastAssigned].notes.add (noteNumber);
return midiChannelLastAssigned; return midiChannelLastAssigned;
} }
int MPEChannelAssigner::findMidiChannelForExistingNote (int noteNumber) noexcept
{
const auto iter = std::find_if (midiChannels.cbegin(), midiChannels.cend(), [&] (auto& ch)
{
return std::find (ch.notes.begin(), ch.notes.end(), noteNumber) != ch.notes.end();
});
return iter != midiChannels.cend() ? (int) std::distance (midiChannels.cbegin(), iter) : -1;
}
void MPEChannelAssigner::noteOff (int noteNumber, int midiChannel) void MPEChannelAssigner::noteOff (int noteNumber, int midiChannel)
{ {
const auto removeNote = [] (MidiChannel& ch, int noteNum) const auto removeNote = [] (MidiChannel& ch, int noteNum)
@ -99,7 +109,7 @@ void MPEChannelAssigner::noteOff (int noteNumber, int midiChannel)
if (midiChannel >= 0 && midiChannel <= 16) if (midiChannel >= 0 && midiChannel <= 16)
{ {
removeNote (midiChannels[midiChannel], noteNumber); removeNote (midiChannels[(size_t) midiChannel], noteNumber);
return; return;
} }
@ -126,9 +136,9 @@ int MPEChannelAssigner::findMidiChannelPlayingClosestNonequalNote (int noteNumbe
auto channelWithClosestNote = firstChannel; auto channelWithClosestNote = firstChannel;
int closestNoteDistance = 127; int closestNoteDistance = 127;
for (auto ch = firstChannel; (isLegacy || zone->isLowerZone() ? ch <= lastChannel : ch >= lastChannel); ch += channelIncrement) for (int ch = firstChannel; (isLegacy || zone->isLowerZone() ? ch <= lastChannel : ch >= lastChannel); ch += channelIncrement)
{ {
for (auto note : midiChannels[ch].notes) for (auto note : midiChannels[(size_t) ch].notes)
{ {
auto noteDistance = std::abs (note - noteNumber); auto noteDistance = std::abs (note - noteNumber);
@ -296,24 +306,35 @@ struct MPEUtilsUnitTests : public UnitTest
// check that channels are assigned in correct order // check that channels are assigned in correct order
int noteNum = 60; int noteNum = 60;
for (int ch = 2; ch <= 16; ++ch) for (int ch = 2; ch <= 16; ++ch)
expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum++), ch); {
expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum), ch);
expectEquals (channelAssigner.findMidiChannelForExistingNote (noteNum), ch);
++noteNum;
}
// check that note-offs are processed // check that note-offs are processed
channelAssigner.noteOff (60); channelAssigner.noteOff (60);
expectEquals (channelAssigner.findMidiChannelForNewNote (60), 2); expectEquals (channelAssigner.findMidiChannelForNewNote (60), 2);
expectEquals (channelAssigner.findMidiChannelForExistingNote (60), 2);
channelAssigner.noteOff (61); channelAssigner.noteOff (61);
expectEquals (channelAssigner.findMidiChannelForNewNote (61), 3); expectEquals (channelAssigner.findMidiChannelForNewNote (61), 3);
expectEquals (channelAssigner.findMidiChannelForExistingNote (61), 3);
// check that assigned channel was last to play note // check that assigned channel was last to play note
channelAssigner.noteOff (65); channelAssigner.noteOff (65);
channelAssigner.noteOff (66); channelAssigner.noteOff (66);
expectEquals (channelAssigner.findMidiChannelForNewNote (66), 8); expectEquals (channelAssigner.findMidiChannelForNewNote (66), 8);
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 7); expectEquals (channelAssigner.findMidiChannelForNewNote (65), 7);
expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 8);
expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 7);
// find closest channel playing nonequal note // find closest channel playing nonequal note
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16); expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 2); expectEquals (channelAssigner.findMidiChannelForNewNote (55), 2);
expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 16);
expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 2);
// all notes off // all notes off
channelAssigner.allNotesOff(); channelAssigner.allNotesOff();
@ -323,10 +344,16 @@ struct MPEUtilsUnitTests : public UnitTest
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 7); expectEquals (channelAssigner.findMidiChannelForNewNote (65), 7);
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16); expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 2); expectEquals (channelAssigner.findMidiChannelForNewNote (55), 2);
expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 8);
expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 7);
expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 16);
expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 2);
// normal assignment // normal assignment
expectEquals (channelAssigner.findMidiChannelForNewNote (101), 3); expectEquals (channelAssigner.findMidiChannelForNewNote (101), 3);
expectEquals (channelAssigner.findMidiChannelForNewNote (20), 4); expectEquals (channelAssigner.findMidiChannelForNewNote (20), 4);
expectEquals (channelAssigner.findMidiChannelForExistingNote (101), 3);
expectEquals (channelAssigner.findMidiChannelForExistingNote (20), 4);
} }
// upper // upper
@ -339,24 +366,35 @@ struct MPEUtilsUnitTests : public UnitTest
// check that channels are assigned in correct order // check that channels are assigned in correct order
int noteNum = 60; int noteNum = 60;
for (int ch = 15; ch >= 1; --ch) for (int ch = 15; ch >= 1; --ch)
expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum++), ch); {
expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum), ch);
expectEquals (channelAssigner.findMidiChannelForExistingNote (noteNum), ch);
++noteNum;
}
// check that note-offs are processed // check that note-offs are processed
channelAssigner.noteOff (60); channelAssigner.noteOff (60);
expectEquals (channelAssigner.findMidiChannelForNewNote (60), 15); expectEquals (channelAssigner.findMidiChannelForNewNote (60), 15);
expectEquals (channelAssigner.findMidiChannelForExistingNote (60), 15);
channelAssigner.noteOff (61); channelAssigner.noteOff (61);
expectEquals (channelAssigner.findMidiChannelForNewNote (61), 14); expectEquals (channelAssigner.findMidiChannelForNewNote (61), 14);
expectEquals (channelAssigner.findMidiChannelForExistingNote (61), 14);
// check that assigned channel was last to play note // check that assigned channel was last to play note
channelAssigner.noteOff (65); channelAssigner.noteOff (65);
channelAssigner.noteOff (66); channelAssigner.noteOff (66);
expectEquals (channelAssigner.findMidiChannelForNewNote (66), 9); expectEquals (channelAssigner.findMidiChannelForNewNote (66), 9);
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 10); expectEquals (channelAssigner.findMidiChannelForNewNote (65), 10);
expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 9);
expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 10);
// find closest channel playing nonequal note // find closest channel playing nonequal note
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 1); expectEquals (channelAssigner.findMidiChannelForNewNote (80), 1);
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 15); expectEquals (channelAssigner.findMidiChannelForNewNote (55), 15);
expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 1);
expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 15);
// all notes off // all notes off
channelAssigner.allNotesOff(); channelAssigner.allNotesOff();
@ -366,10 +404,16 @@ struct MPEUtilsUnitTests : public UnitTest
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 10); expectEquals (channelAssigner.findMidiChannelForNewNote (65), 10);
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 1); expectEquals (channelAssigner.findMidiChannelForNewNote (80), 1);
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 15); expectEquals (channelAssigner.findMidiChannelForNewNote (55), 15);
expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 9);
expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 10);
expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 1);
expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 15);
// normal assignment // normal assignment
expectEquals (channelAssigner.findMidiChannelForNewNote (101), 14); expectEquals (channelAssigner.findMidiChannelForNewNote (101), 14);
expectEquals (channelAssigner.findMidiChannelForNewNote (20), 13); expectEquals (channelAssigner.findMidiChannelForNewNote (20), 13);
expectEquals (channelAssigner.findMidiChannelForExistingNote (101), 14);
expectEquals (channelAssigner.findMidiChannelForExistingNote (20), 13);
} }
// legacy // legacy
@ -379,24 +423,35 @@ struct MPEUtilsUnitTests : public UnitTest
// check that channels are assigned in correct order // check that channels are assigned in correct order
int noteNum = 60; int noteNum = 60;
for (int ch = 1; ch <= 16; ++ch) for (int ch = 1; ch <= 16; ++ch)
expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum++), ch); {
expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum), ch);
expectEquals (channelAssigner.findMidiChannelForExistingNote (noteNum), ch);
++noteNum;
}
// check that note-offs are processed // check that note-offs are processed
channelAssigner.noteOff (60); channelAssigner.noteOff (60);
expectEquals (channelAssigner.findMidiChannelForNewNote (60), 1); expectEquals (channelAssigner.findMidiChannelForNewNote (60), 1);
expectEquals (channelAssigner.findMidiChannelForExistingNote (60), 1);
channelAssigner.noteOff (61); channelAssigner.noteOff (61);
expectEquals (channelAssigner.findMidiChannelForNewNote (61), 2); expectEquals (channelAssigner.findMidiChannelForNewNote (61), 2);
expectEquals (channelAssigner.findMidiChannelForExistingNote (61), 2);
// check that assigned channel was last to play note // check that assigned channel was last to play note
channelAssigner.noteOff (65); channelAssigner.noteOff (65);
channelAssigner.noteOff (66); channelAssigner.noteOff (66);
expectEquals (channelAssigner.findMidiChannelForNewNote (66), 7); expectEquals (channelAssigner.findMidiChannelForNewNote (66), 7);
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 6); expectEquals (channelAssigner.findMidiChannelForNewNote (65), 6);
expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 7);
expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 6);
// find closest channel playing nonequal note // find closest channel playing nonequal note
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16); expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 1); expectEquals (channelAssigner.findMidiChannelForNewNote (55), 1);
expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 16);
expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 1);
// all notes off // all notes off
channelAssigner.allNotesOff(); channelAssigner.allNotesOff();
@ -406,10 +461,16 @@ struct MPEUtilsUnitTests : public UnitTest
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 6); expectEquals (channelAssigner.findMidiChannelForNewNote (65), 6);
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16); expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 1); expectEquals (channelAssigner.findMidiChannelForNewNote (55), 1);
expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 7);
expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 6);
expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 16);
expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 1);
// normal assignment // normal assignment
expectEquals (channelAssigner.findMidiChannelForNewNote (101), 2); expectEquals (channelAssigner.findMidiChannelForNewNote (101), 2);
expectEquals (channelAssigner.findMidiChannelForNewNote (20), 3); expectEquals (channelAssigner.findMidiChannelForNewNote (20), 3);
expectEquals (channelAssigner.findMidiChannelForExistingNote (101), 2);
expectEquals (channelAssigner.findMidiChannelForExistingNote (20), 3);
} }
} }

View file

@ -63,6 +63,11 @@ public:
*/ */
int findMidiChannelForNewNote (int noteNumber) noexcept; int findMidiChannelForNewNote (int noteNumber) noexcept;
/** If a note has been added using findMidiChannelForNewNote() this will return the channel
to which it was assigned, otherwise it will return -1.
*/
int findMidiChannelForExistingNote (int initialNoteOnNumber) noexcept;
/** You must call this method for all note-offs that you receive so that this class /** You must call this method for all note-offs that you receive so that this class
can keep track of the currently playing notes internally. can keep track of the currently playing notes internally.
@ -86,7 +91,7 @@ private:
int lastNotePlayed = -1; int lastNotePlayed = -1;
bool isFree() const noexcept { return notes.isEmpty(); } bool isFree() const noexcept { return notes.isEmpty(); }
}; };
MidiChannel midiChannels[17]; std::array<MidiChannel, 17> midiChannels;
//============================================================================== //==============================================================================
int findMidiChannelPlayingClosestNonequalNote (int noteNumber) noexcept; int findMidiChannelPlayingClosestNonequalNote (int noteNumber) noexcept;

View file

@ -43,6 +43,18 @@ MPEValue MPEValue::from14BitInt (int value) noexcept
return { value }; return { value };
} }
MPEValue MPEValue::fromUnsignedFloat (float value) noexcept
{
jassert (0.0f <= value && value <= 1.0f);
return { roundToInt (value * 16383.0f) };
}
MPEValue MPEValue::fromSignedFloat (float value) noexcept
{
jassert (-1.0f <= value && value <= 1.0f);
return { roundToInt (((value + 1.0f) * 16383.0f) / 2.0f) };
}
//============================================================================== //==============================================================================
MPEValue MPEValue::minValue() noexcept { return MPEValue::from7BitInt (0); } MPEValue MPEValue::minValue() noexcept { return MPEValue::from7BitInt (0); }
MPEValue MPEValue::centreValue() noexcept { return MPEValue::from7BitInt (64); } MPEValue MPEValue::centreValue() noexcept { return MPEValue::from7BitInt (64); }
@ -121,26 +133,34 @@ public:
beginTest ("zero/minimum value"); beginTest ("zero/minimum value");
{ {
expectValuesConsistent (MPEValue::from7BitInt (0), 0, 0, -1.0f, 0.0f); expectValuesConsistent (MPEValue::from7BitInt (0), 0, 0, -1.0f, 0.0f);
expectValuesConsistent (MPEValue::from14BitInt (0), 0, 0, -1.0f, 0.0f); expectValuesConsistent (MPEValue::from14BitInt (0), 0, 0, -1.0f, 0.0f);
expectValuesConsistent (MPEValue::fromUnsignedFloat (0.0f), 0, 0, -1.0f, 0.0f);
expectValuesConsistent (MPEValue::fromSignedFloat (-1.0f), 0, 0, -1.0f, 0.0f);
} }
beginTest ("maximum value"); beginTest ("maximum value");
{ {
expectValuesConsistent (MPEValue::from7BitInt (127), 127, 16383, 1.0f, 1.0f); expectValuesConsistent (MPEValue::from7BitInt (127), 127, 16383, 1.0f, 1.0f);
expectValuesConsistent (MPEValue::from14BitInt (16383), 127, 16383, 1.0f, 1.0f); expectValuesConsistent (MPEValue::from14BitInt (16383), 127, 16383, 1.0f, 1.0f);
expectValuesConsistent (MPEValue::fromUnsignedFloat (1.0f), 127, 16383, 1.0f, 1.0f);
expectValuesConsistent (MPEValue::fromSignedFloat (1.0f), 127, 16383, 1.0f, 1.0f);
} }
beginTest ("centre value"); beginTest ("centre value");
{ {
expectValuesConsistent (MPEValue::from7BitInt (64), 64, 8192, 0.0f, 0.5f); expectValuesConsistent (MPEValue::from7BitInt (64), 64, 8192, 0.0f, 0.5f);
expectValuesConsistent (MPEValue::from14BitInt (8192), 64, 8192, 0.0f, 0.5f); expectValuesConsistent (MPEValue::from14BitInt (8192), 64, 8192, 0.0f, 0.5f);
expectValuesConsistent (MPEValue::fromUnsignedFloat (0.5f), 64, 8192, 0.0f, 0.5f);
expectValuesConsistent (MPEValue::fromSignedFloat (0.0f), 64, 8192, 0.0f, 0.5f);
} }
beginTest ("value halfway between min and centre"); beginTest ("value halfway between min and centre");
{ {
expectValuesConsistent (MPEValue::from7BitInt (32), 32, 4096, -0.5f, 0.25f); expectValuesConsistent (MPEValue::from7BitInt (32), 32, 4096, -0.5f, 0.25f);
expectValuesConsistent (MPEValue::from14BitInt (4096), 32, 4096, -0.5f, 0.25f); expectValuesConsistent (MPEValue::from14BitInt (4096), 32, 4096, -0.5f, 0.25f);
expectValuesConsistent (MPEValue::fromUnsignedFloat (0.25f), 32, 4096, -0.5f, 0.25f);
expectValuesConsistent (MPEValue::fromSignedFloat (-0.5f), 32, 4096, -0.5f, 0.25f);
} }
} }

View file

@ -53,6 +53,12 @@ public:
*/ */
static MPEValue from14BitInt (int value) noexcept; static MPEValue from14BitInt (int value) noexcept;
/** Constructs an MPEValue from a float between 0.0f and 1.0f. */
static MPEValue fromUnsignedFloat (float value) noexcept;
/** Constructs an MPEValue from a float between -1.0f and 1.0f. */
static MPEValue fromSignedFloat (float value) noexcept;
/** Constructs an MPEValue corresponding to the centre value. */ /** Constructs an MPEValue corresponding to the centre value. */
static MPEValue centreValue() noexcept; static MPEValue centreValue() noexcept;

View file

@ -23,7 +23,17 @@
namespace juce namespace juce
{ {
MPEZoneLayout::MPEZoneLayout() noexcept {} MPEZoneLayout::MPEZoneLayout (MPEZone lower, MPEZone upper)
: lowerZone (lower), upperZone (upper)
{
}
MPEZoneLayout::MPEZoneLayout (MPEZone zone)
: lowerZone (zone.isLowerZone() ? zone : MPEZone()),
upperZone (! zone.isLowerZone() ? zone : MPEZone())
{
}
MPEZoneLayout::MPEZoneLayout (const MPEZoneLayout& other) MPEZoneLayout::MPEZoneLayout (const MPEZoneLayout& other)
: lowerZone (other.lowerZone), : lowerZone (other.lowerZone),
@ -54,9 +64,9 @@ void MPEZoneLayout::setZone (bool isLower, int numMemberChannels, int perNotePit
checkAndLimitZoneParameters (0, 96, masterPitchbendRange); checkAndLimitZoneParameters (0, 96, masterPitchbendRange);
if (isLower) if (isLower)
lowerZone = { true, numMemberChannels, perNotePitchbendRange, masterPitchbendRange }; lowerZone = { MPEZone::Type::lower, numMemberChannels, perNotePitchbendRange, masterPitchbendRange };
else else
upperZone = { false, numMemberChannels, perNotePitchbendRange, masterPitchbendRange }; upperZone = { MPEZone::Type::upper, numMemberChannels, perNotePitchbendRange, masterPitchbendRange };
if (numMemberChannels > 0) if (numMemberChannels > 0)
{ {
@ -86,8 +96,8 @@ void MPEZoneLayout::setUpperZone (int numMemberChannels, int perNotePitchbendRan
void MPEZoneLayout::clearAllZones() void MPEZoneLayout::clearAllZones()
{ {
lowerZone = { true, 0 }; lowerZone = { MPEZone::Type::lower, 0 };
upperZone = { false, 0 }; upperZone = { MPEZone::Type::upper, 0 };
sendLayoutChangeMessage(); sendLayoutChangeMessage();
} }
@ -128,7 +138,7 @@ void MPEZoneLayout::processZoneLayoutRpnMessage (MidiRPNMessage rpn)
} }
} }
void MPEZoneLayout::updateMasterPitchbend (Zone& zone, int value) void MPEZoneLayout::updateMasterPitchbend (MPEZone& zone, int value)
{ {
if (zone.masterPitchbendRange != value) if (zone.masterPitchbendRange != value)
{ {
@ -138,7 +148,7 @@ void MPEZoneLayout::updateMasterPitchbend (Zone& zone, int value)
} }
} }
void MPEZoneLayout::updatePerNotePitchbendRange (Zone& zone, int value) void MPEZoneLayout::updatePerNotePitchbendRange (MPEZone& zone, int value)
{ {
if (zone.perNotePitchbendRange != value) if (zone.perNotePitchbendRange != value)
{ {

View file

@ -23,6 +23,83 @@
namespace juce namespace juce
{ {
//==============================================================================
/**
This struct represents an MPE zone.
It can either be a lower or an upper zone, where:
- A lower zone encompasses master channel 1 and an arbitrary number of ascending
MIDI channels, increasing from channel 2.
- An upper zone encompasses master channel 16 and an arbitrary number of descending
MIDI channels, decreasing from channel 15.
It also defines a pitchbend range (in semitones) to be applied for per-note pitchbends and
master pitchbends, respectively.
*/
struct MPEZone
{
enum class Type { lower, upper };
MPEZone() = default;
MPEZone (const MPEZone& other) = default;
MPEZone (Type type, int memberChannels = 0, int perNotePitchbend = 48, int masterPitchbend = 2)
: zoneType (type),
numMemberChannels (memberChannels),
perNotePitchbendRange (perNotePitchbend),
masterPitchbendRange (masterPitchbend)
{}
bool isLowerZone() const noexcept { return zoneType == Type::lower; }
bool isUpperZone() const noexcept { return zoneType == Type::upper; }
bool isActive() const noexcept { return numMemberChannels > 0; }
int getMasterChannel() const noexcept { return isLowerZone() ? lowerZoneMasterChannel : upperZoneMasterChannel; }
int getFirstMemberChannel() const noexcept { return isLowerZone() ? lowerZoneMasterChannel + 1 : upperZoneMasterChannel - 1; }
int getLastMemberChannel() const noexcept { return isLowerZone() ? (lowerZoneMasterChannel + numMemberChannels)
: (upperZoneMasterChannel - numMemberChannels); }
bool isUsingChannelAsMemberChannel (int channel) const noexcept
{
return isLowerZone() ? (lowerZoneMasterChannel < channel && channel <= getLastMemberChannel())
: (channel < upperZoneMasterChannel && getLastMemberChannel() <= channel);
}
bool isUsing (int channel) const noexcept
{
return isUsingChannelAsMemberChannel (channel) || channel == getMasterChannel();
}
static auto tie (const MPEZone& z)
{
return std::tie (z.zoneType,
z.numMemberChannels,
z.perNotePitchbendRange,
z.masterPitchbendRange);
}
bool operator== (const MPEZone& other) const
{
return tie (*this) == tie (other);
}
bool operator!= (const MPEZone& other) const
{
return tie (*this) != tie (other);
}
//==============================================================================
static constexpr int lowerZoneMasterChannel = 1,
upperZoneMasterChannel = 16;
Type zoneType = Type::lower;
int numMemberChannels = 0;
int perNotePitchbendRange = 48;
int masterPitchbendRange = 2;
};
//============================================================================== //==============================================================================
/** /**
This class represents the current MPE zone layout of a device capable of handling MPE. This class represents the current MPE zone layout of a device capable of handling MPE.
@ -44,89 +121,28 @@ namespace juce
class JUCE_API MPEZoneLayout class JUCE_API MPEZoneLayout
{ {
public: public:
/** Default constructor. //==============================================================================
/** Creates a layout with inactive upper and lower zones. */
MPEZoneLayout() = default;
This will create a layout with inactive lower and upper zones, representing /** Creates a layout with the given upper and lower zones. */
a device with MPE mode disabled. MPEZoneLayout (MPEZone lower, MPEZone upper);
You can set the lower or upper MPE zones using the setZone() method. /** Creates a layout with a single upper or lower zone, leaving the other zone uninitialised. */
MPEZoneLayout (MPEZone singleZone);
@see setZone
*/
MPEZoneLayout() noexcept;
/** Copy constuctor.
This will not copy the listeners registered to the MPEZoneLayout.
*/
MPEZoneLayout (const MPEZoneLayout& other); MPEZoneLayout (const MPEZoneLayout& other);
/** Copy assignment operator.
This will not copy the listeners registered to the MPEZoneLayout.
*/
MPEZoneLayout& operator= (const MPEZoneLayout& other); MPEZoneLayout& operator= (const MPEZoneLayout& other);
bool operator== (const MPEZoneLayout& other) const { return lowerZone == other.lowerZone && upperZone == other.upperZone; }
bool operator!= (const MPEZoneLayout& other) const { return ! operator== (other); }
//============================================================================== //==============================================================================
/** /** Returns a struct representing the lower MPE zone. */
This struct represents an MPE zone. MPEZone getLowerZone() const noexcept { return lowerZone; }
It can either be a lower or an upper zone, where: /** Returns a struct representing the upper MPE zone. */
- A lower zone encompasses master channel 1 and an arbitrary number of ascending MPEZone getUpperZone() const noexcept { return upperZone; }
MIDI channels, increasing from channel 2.
- An upper zone encompasses master channel 16 and an arbitrary number of descending
MIDI channels, decreasing from channel 15.
It also defines a pitchbend range (in semitones) to be applied for per-note pitchbends and
master pitchbends, respectively.
*/
struct Zone
{
Zone (const Zone& other) = default;
bool isLowerZone() const noexcept { return lowerZone; }
bool isUpperZone() const noexcept { return ! lowerZone; }
bool isActive() const noexcept { return numMemberChannels > 0; }
int getMasterChannel() const noexcept { return lowerZone ? 1 : 16; }
int getFirstMemberChannel() const noexcept { return lowerZone ? 2 : 15; }
int getLastMemberChannel() const noexcept { return lowerZone ? (1 + numMemberChannels)
: (16 - numMemberChannels); }
bool isUsingChannelAsMemberChannel (int channel) const noexcept
{
return lowerZone ? (channel > 1 && channel <= 1 + numMemberChannels)
: (channel < 16 && channel >= 16 - numMemberChannels);
}
bool isUsing (int channel) const noexcept
{
return isUsingChannelAsMemberChannel (channel) || channel == getMasterChannel();
}
bool operator== (const Zone& other) const noexcept { return lowerZone == other.lowerZone
&& numMemberChannels == other.numMemberChannels
&& perNotePitchbendRange == other.perNotePitchbendRange
&& masterPitchbendRange == other.masterPitchbendRange; }
bool operator!= (const Zone& other) const noexcept { return ! operator== (other); }
int numMemberChannels;
int perNotePitchbendRange;
int masterPitchbendRange;
private:
friend class MPEZoneLayout;
Zone (bool lower, int memberChans = 0, int perNotePb = 48, int masterPb = 2) noexcept
: numMemberChannels (memberChans),
perNotePitchbendRange (perNotePb),
masterPitchbendRange (masterPb),
lowerZone (lower)
{
}
bool lowerZone;
};
/** Sets the lower zone of this layout. */ /** Sets the lower zone of this layout. */
void setLowerZone (int numMemberChannels = 0, void setLowerZone (int numMemberChannels = 0,
@ -138,17 +154,14 @@ public:
int perNotePitchbendRange = 48, int perNotePitchbendRange = 48,
int masterPitchbendRange = 2) noexcept; int masterPitchbendRange = 2) noexcept;
/** Returns a struct representing the lower MPE zone. */
const Zone getLowerZone() const noexcept { return lowerZone; }
/** Returns a struct representing the upper MPE zone. */
const Zone getUpperZone() const noexcept { return upperZone; }
/** Clears the lower and upper zones of this layout, making them both inactive /** Clears the lower and upper zones of this layout, making them both inactive
and disabling MPE mode. and disabling MPE mode.
*/ */
void clearAllZones(); void clearAllZones();
/** Returns true if either of the zones are active. */
bool isActive() const { return lowerZone.isActive() || upperZone.isActive(); }
//============================================================================== //==============================================================================
/** Pass incoming MIDI messages to an object of this class if you want the /** Pass incoming MIDI messages to an object of this class if you want the
zone layout to properly react to MPE RPN messages like an zone layout to properly react to MPE RPN messages like an
@ -200,10 +213,14 @@ public:
/** Removes a listener. */ /** Removes a listener. */
void removeListener (Listener* const listenerToRemove) noexcept; void removeListener (Listener* const listenerToRemove) noexcept;
#ifndef DOXYGEN
using Zone = MPEZone;
#endif
private: private:
//============================================================================== //==============================================================================
Zone lowerZone { true, 0 }; MPEZone lowerZone { MPEZone::Type::lower, 0 };
Zone upperZone { false, 0 }; MPEZone upperZone { MPEZone::Type::upper, 0 };
MidiRPNDetector rpnDetector; MidiRPNDetector rpnDetector;
ListenerList<Listener> listeners; ListenerList<Listener> listeners;
@ -215,8 +232,8 @@ private:
void processZoneLayoutRpnMessage (MidiRPNMessage); void processZoneLayoutRpnMessage (MidiRPNMessage);
void processPitchbendRangeRpnMessage (MidiRPNMessage); void processPitchbendRangeRpnMessage (MidiRPNMessage);
void updateMasterPitchbend (Zone&, int); void updateMasterPitchbend (MPEZone&, int);
void updatePerNotePitchbendRange (Zone&, int); void updatePerNotePitchbendRange (MPEZone&, int);
void sendLayoutChangeMessage(); void sendLayoutChangeMessage();
void checkAndLimitZoneParameters (int, int, int&) noexcept; void checkAndLimitZoneParameters (int, int, int&) noexcept;

View file

@ -40,7 +40,6 @@
#include "juce_audio_processors.h" #include "juce_audio_processors.h"
#include <juce_gui_extra/juce_gui_extra.h> #include <juce_gui_extra/juce_gui_extra.h>
#include <set>
//============================================================================== //==============================================================================
#if JUCE_MAC #if JUCE_MAC

View file

@ -0,0 +1,452 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - 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 6 End-User License
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
End User License Agreement: www.juce.com/juce-6-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
{
constexpr uint8 whiteNotes[] = { 0, 2, 4, 5, 7, 9, 11 };
constexpr uint8 blackNotes[] = { 1, 3, 6, 8, 10 };
//==============================================================================
struct KeyboardComponentBase::UpDownButton : public Button
{
UpDownButton (KeyboardComponentBase& c, int d)
: Button ({}), owner (c), delta (d)
{
}
void clicked() override
{
auto note = owner.getLowestVisibleKey();
note = delta < 0 ? (note - 1) / 12 : note / 12 + 1;
owner.setLowestVisibleKey (note * 12);
}
using Button::clicked;
void paintButton (Graphics& g, bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override
{
owner.drawUpDownButton (g, getWidth(), getHeight(),
shouldDrawButtonAsHighlighted, shouldDrawButtonAsDown,
delta > 0);
}
private:
KeyboardComponentBase& owner;
int delta;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UpDownButton)
};
//==============================================================================
KeyboardComponentBase::KeyboardComponentBase (Orientation o) : orientation (o)
{
scrollDown = std::make_unique<UpDownButton> (*this, -1);
scrollUp = std::make_unique<UpDownButton> (*this, 1);
addChildComponent (*scrollDown);
addChildComponent (*scrollUp);
colourChanged();
}
//==============================================================================
void KeyboardComponentBase::setKeyWidth (float widthInPixels)
{
jassert (widthInPixels > 0);
if (keyWidth != widthInPixels) // Prevent infinite recursion if the width is being computed in a 'resized()' callback
{
keyWidth = widthInPixels;
resized();
}
}
void KeyboardComponentBase::setScrollButtonWidth (int widthInPixels)
{
jassert (widthInPixels > 0);
if (scrollButtonWidth != widthInPixels)
{
scrollButtonWidth = widthInPixels;
resized();
}
}
void KeyboardComponentBase::setOrientation (Orientation newOrientation)
{
if (orientation != newOrientation)
{
orientation = newOrientation;
resized();
}
}
void KeyboardComponentBase::setAvailableRange (int lowestNote, int highestNote)
{
jassert (lowestNote >= 0 && lowestNote <= 127);
jassert (highestNote >= 0 && highestNote <= 127);
jassert (lowestNote <= highestNote);
if (rangeStart != lowestNote || rangeEnd != highestNote)
{
rangeStart = jlimit (0, 127, lowestNote);
rangeEnd = jlimit (0, 127, highestNote);
firstKey = jlimit ((float) rangeStart, (float) rangeEnd, firstKey);
resized();
}
}
void KeyboardComponentBase::setLowestVisibleKey (int noteNumber)
{
setLowestVisibleKeyFloat ((float) noteNumber);
}
void KeyboardComponentBase::setLowestVisibleKeyFloat (float noteNumber)
{
noteNumber = jlimit ((float) rangeStart, (float) rangeEnd, noteNumber);
if (noteNumber != firstKey)
{
bool hasMoved = (((int) firstKey) != (int) noteNumber);
firstKey = noteNumber;
if (hasMoved)
sendChangeMessage();
resized();
}
}
float KeyboardComponentBase::getWhiteNoteLength() const noexcept
{
return (orientation == horizontalKeyboard) ? (float) getHeight() : (float) getWidth();
}
void KeyboardComponentBase::setBlackNoteLengthProportion (float ratio) noexcept
{
jassert (ratio >= 0.0f && ratio <= 1.0f);
if (blackNoteLengthRatio != ratio)
{
blackNoteLengthRatio = ratio;
resized();
}
}
float KeyboardComponentBase::getBlackNoteLength() const noexcept
{
auto whiteNoteLength = orientation == horizontalKeyboard ? getHeight() : getWidth();
return (float) whiteNoteLength * blackNoteLengthRatio;
}
void KeyboardComponentBase::setBlackNoteWidthProportion (float ratio) noexcept
{
jassert (ratio >= 0.0f && ratio <= 1.0f);
if (blackNoteWidthRatio != ratio)
{
blackNoteWidthRatio = ratio;
resized();
}
}
void KeyboardComponentBase::setScrollButtonsVisible (bool newCanScroll)
{
if (canScroll != newCanScroll)
{
canScroll = newCanScroll;
resized();
}
}
//==============================================================================
Range<float> KeyboardComponentBase::getKeyPos (int midiNoteNumber) const
{
return getKeyPosition (midiNoteNumber, keyWidth)
- xOffset
- getKeyPosition (rangeStart, keyWidth).getStart();
}
float KeyboardComponentBase::getKeyStartPosition (int midiNoteNumber) const
{
return getKeyPos (midiNoteNumber).getStart();
}
float KeyboardComponentBase::getTotalKeyboardWidth() const noexcept
{
return getKeyPos (rangeEnd).getEnd();
}
KeyboardComponentBase::NoteAndVelocity KeyboardComponentBase::getNoteAndVelocityAtPosition (Point<float> pos, bool children)
{
if (! reallyContains (pos, children))
return { -1, 0.0f };
auto p = pos;
if (orientation != horizontalKeyboard)
{
p = { p.y, p.x };
if (orientation == verticalKeyboardFacingLeft)
p = { p.x, (float) getWidth() - p.y };
else
p = { (float) getHeight() - p.x, p.y };
}
return remappedXYToNote (p + Point<float> (xOffset, 0));
}
KeyboardComponentBase::NoteAndVelocity KeyboardComponentBase::remappedXYToNote (Point<float> pos) const
{
auto blackNoteLength = getBlackNoteLength();
if (pos.getY() < blackNoteLength)
{
for (int octaveStart = 12 * (rangeStart / 12); octaveStart <= rangeEnd; octaveStart += 12)
{
for (int i = 0; i < 5; ++i)
{
auto note = octaveStart + blackNotes[i];
if (rangeStart <= note && note <= rangeEnd)
{
if (getKeyPos (note).contains (pos.x - xOffset))
{
return { note, jmax (0.0f, pos.y / blackNoteLength) };
}
}
}
}
}
for (int octaveStart = 12 * (rangeStart / 12); octaveStart <= rangeEnd; octaveStart += 12)
{
for (int i = 0; i < 7; ++i)
{
auto note = octaveStart + whiteNotes[i];
if (note >= rangeStart && note <= rangeEnd)
{
if (getKeyPos (note).contains (pos.x - xOffset))
{
auto whiteNoteLength = (orientation == horizontalKeyboard) ? getHeight() : getWidth();
return { note, jmax (0.0f, pos.y / (float) whiteNoteLength) };
}
}
}
}
return { -1, 0 };
}
Rectangle<float> KeyboardComponentBase::getRectangleForKey (int note) const
{
jassert (note >= rangeStart && note <= rangeEnd);
auto pos = getKeyPos (note);
auto x = pos.getStart();
auto w = pos.getLength();
if (MidiMessage::isMidiNoteBlack (note))
{
auto blackNoteLength = getBlackNoteLength();
switch (orientation)
{
case horizontalKeyboard: return { x, 0, w, blackNoteLength };
case verticalKeyboardFacingLeft: return { (float) getWidth() - blackNoteLength, x, blackNoteLength, w };
case verticalKeyboardFacingRight: return { 0, (float) getHeight() - x - w, blackNoteLength, w };
default: jassertfalse; break;
}
}
else
{
switch (orientation)
{
case horizontalKeyboard: return { x, 0, w, (float) getHeight() };
case verticalKeyboardFacingLeft: return { 0, x, (float) getWidth(), w };
case verticalKeyboardFacingRight: return { 0, (float) getHeight() - x - w, (float) getWidth(), w };
default: jassertfalse; break;
}
}
return {};
}
//==============================================================================
void KeyboardComponentBase::setOctaveForMiddleC (int octaveNum)
{
octaveNumForMiddleC = octaveNum;
repaint();
}
//==============================================================================
void KeyboardComponentBase::drawUpDownButton (Graphics& g, int w, int h, bool mouseOver, bool buttonDown, bool movesOctavesUp)
{
g.fillAll (findColour (upDownButtonBackgroundColourId));
float angle = 0;
switch (getOrientation())
{
case horizontalKeyboard: angle = movesOctavesUp ? 0.0f : 0.5f; break;
case verticalKeyboardFacingLeft: angle = movesOctavesUp ? 0.25f : 0.75f; break;
case verticalKeyboardFacingRight: angle = movesOctavesUp ? 0.75f : 0.25f; break;
default: jassertfalse; break;
}
Path path;
path.addTriangle (0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.5f);
path.applyTransform (AffineTransform::rotation (MathConstants<float>::twoPi * angle, 0.5f, 0.5f));
g.setColour (findColour (upDownButtonArrowColourId)
.withAlpha (buttonDown ? 1.0f : (mouseOver ? 0.6f : 0.4f)));
g.fillPath (path, path.getTransformToScaleToFit (1.0f, 1.0f, (float) w - 2.0f, (float) h - 2.0f, true));
}
Range<float> KeyboardComponentBase::getKeyPosition (int midiNoteNumber, float targetKeyWidth) const
{
auto ratio = getBlackNoteWidthProportion();
static const float notePos[] = { 0.0f, 1 - ratio * 0.6f,
1.0f, 2 - ratio * 0.4f,
2.0f,
3.0f, 4 - ratio * 0.7f,
4.0f, 5 - ratio * 0.5f,
5.0f, 6 - ratio * 0.3f,
6.0f };
auto octave = midiNoteNumber / 12;
auto note = midiNoteNumber % 12;
auto start = (float) octave * 7.0f * targetKeyWidth + notePos[note] * targetKeyWidth;
auto width = MidiMessage::isMidiNoteBlack (note) ? blackNoteWidthRatio * targetKeyWidth : targetKeyWidth;
return { start, start + width };
}
//==============================================================================
void KeyboardComponentBase::paint (Graphics& g)
{
drawKeyboardBackground (g, getLocalBounds().toFloat());
for (int octaveBase = 0; octaveBase < 128; octaveBase += 12)
{
for (auto noteNum : whiteNotes)
drawWhiteKey (octaveBase + noteNum, g, getRectangleForKey (octaveBase + noteNum));
for (auto noteNum : blackNotes)
drawBlackKey (octaveBase + noteNum, g, getRectangleForKey (octaveBase + noteNum));
}
}
void KeyboardComponentBase::resized()
{
auto w = getWidth();
auto h = getHeight();
if (w > 0 && h > 0)
{
if (orientation != horizontalKeyboard)
std::swap (w, h);
auto kx2 = getKeyPos (rangeEnd).getEnd();
if ((int) firstKey != rangeStart)
{
auto kx1 = getKeyPos (rangeStart).getStart();
if (kx2 - kx1 <= (float) w)
{
firstKey = (float) rangeStart;
sendChangeMessage();
repaint();
}
}
scrollDown->setVisible (canScroll && firstKey > (float) rangeStart);
xOffset = 0;
if (canScroll)
{
auto scrollButtonW = jmin (scrollButtonWidth, w / 2);
auto r = getLocalBounds();
if (orientation == horizontalKeyboard)
{
scrollDown->setBounds (r.removeFromLeft (scrollButtonW));
scrollUp ->setBounds (r.removeFromRight (scrollButtonW));
}
else if (orientation == verticalKeyboardFacingLeft)
{
scrollDown->setBounds (r.removeFromTop (scrollButtonW));
scrollUp ->setBounds (r.removeFromBottom (scrollButtonW));
}
else
{
scrollDown->setBounds (r.removeFromBottom (scrollButtonW));
scrollUp ->setBounds (r.removeFromTop (scrollButtonW));
}
auto endOfLastKey = getKeyPos (rangeEnd).getEnd();
auto spaceAvailable = w;
auto lastStartKey = remappedXYToNote ({ endOfLastKey - (float) spaceAvailable, 0 }).note + 1;
if (lastStartKey >= 0 && ((int) firstKey) > lastStartKey)
{
firstKey = (float) jlimit (rangeStart, rangeEnd, lastStartKey);
sendChangeMessage();
}
xOffset = getKeyPos ((int) firstKey).getStart();
}
else
{
firstKey = (float) rangeStart;
}
scrollUp->setVisible (canScroll && getKeyPos (rangeEnd).getStart() > (float) w);
repaint();
}
}
//==============================================================================
void KeyboardComponentBase::mouseWheelMove (const MouseEvent&, const MouseWheelDetails& wheel)
{
auto amount = (orientation == horizontalKeyboard && wheel.deltaX != 0)
? wheel.deltaX : (orientation == verticalKeyboardFacingLeft ? wheel.deltaY
: -wheel.deltaY);
setLowestVisibleKeyFloat (firstKey - amount * keyWidth);
}
} // namespace juce

View file

@ -0,0 +1,295 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - 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 6 End-User License
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
End User License Agreement: www.juce.com/juce-6-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
{
//==============================================================================
/**
A base class for drawing a custom MIDI keyboard component.
Implement the drawKeyboardBackground(), drawWhiteKey(), and drawBlackKey() methods
to draw your content and this class will handle the underlying keyboard logic.
The component is a ChangeBroadcaster, so if you want to be informed when the
keyboard is scrolled, you can register a ChangeListener for callbacks.
@tags{Audio}
*/
class JUCE_API KeyboardComponentBase : public Component,
public ChangeBroadcaster
{
public:
//==============================================================================
/** The direction of the keyboard.
@see setOrientation
*/
enum Orientation
{
horizontalKeyboard,
verticalKeyboardFacingLeft,
verticalKeyboardFacingRight,
};
//==============================================================================
/** Constructor.
@param orientation whether the keyboard is horizontal or vertical
*/
explicit KeyboardComponentBase (Orientation orientation);
/** Destructor. */
~KeyboardComponentBase() override = default;
//==============================================================================
/** Changes the width used to draw the white keys. */
void setKeyWidth (float widthInPixels);
/** Returns the width that was set by setKeyWidth(). */
float getKeyWidth() const noexcept { return keyWidth; }
/** Changes the width used to draw the buttons that scroll the keyboard up/down in octaves. */
void setScrollButtonWidth (int widthInPixels);
/** Returns the width that was set by setScrollButtonWidth(). */
int getScrollButtonWidth() const noexcept { return scrollButtonWidth; }
/** Changes the keyboard's current direction. */
void setOrientation (Orientation newOrientation);
/** Returns the keyboard's current direction. */
Orientation getOrientation() const noexcept { return orientation; }
/** Returns true if the keyboard's orientation is horizontal. */
bool isHorizontal() const noexcept { return orientation == horizontalKeyboard; }
/** Sets the range of midi notes that the keyboard will be limited to.
By default the range is 0 to 127 (inclusive), but you can limit this if you
only want a restricted set of the keys to be shown.
Note that the values here are inclusive and must be between 0 and 127.
*/
void setAvailableRange (int lowestNote, int highestNote);
/** Returns the first note in the available range.
@see setAvailableRange
*/
int getRangeStart() const noexcept { return rangeStart; }
/** Returns the last note in the available range.
@see setAvailableRange
*/
int getRangeEnd() const noexcept { return rangeEnd; }
/** If the keyboard extends beyond the size of the component, this will scroll
it to show the given key at the start.
Whenever the keyboard's position is changed, this will use the ChangeBroadcaster
base class to send a callback to any ChangeListeners that have been registered.
*/
void setLowestVisibleKey (int noteNumber);
/** Returns the number of the first key shown in the component.
@see setLowestVisibleKey
*/
int getLowestVisibleKey() const noexcept { return (int) firstKey; }
/** Returns the absolute length of the white notes.
This will be their vertical or horizontal length, depending on the keyboard's orientation.
*/
float getWhiteNoteLength() const noexcept;
/** Sets the length of the black notes as a proportion of the white note length. */
void setBlackNoteLengthProportion (float ratio) noexcept;
/** Returns the length of the black notes as a proportion of the white note length. */
float getBlackNoteLengthProportion() const noexcept { return blackNoteLengthRatio; }
/** Returns the absolute length of the black notes.
This will be their vertical or horizontal length, depending on the keyboard's orientation.
*/
float getBlackNoteLength() const noexcept;
/** Sets the width of the black notes as a proportion of the white note width. */
void setBlackNoteWidthProportion (float ratio) noexcept;
/** Returns the width of the black notes as a proportion of the white note width. */
float getBlackNoteWidthProportion() const noexcept { return blackNoteWidthRatio; }
/** Returns the absolute width of the black notes.
This will be their vertical or horizontal width, depending on the keyboard's orientation.
*/
float getBlackNoteWidth() const noexcept { return keyWidth * blackNoteWidthRatio; }
/** If set to true, then scroll buttons will appear at either end of the keyboard
if there are too many notes to fit them all in the component at once.
*/
void setScrollButtonsVisible (bool canScroll);
//==============================================================================
/** Colour IDs to use to change the colour of the octave scroll buttons.
These constants can be used either via the Component::setColour(), or LookAndFeel::setColour()
methods.
@see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour
*/
enum ColourIds
{
upDownButtonBackgroundColourId = 0x1004000,
upDownButtonArrowColourId = 0x1004001
};
/** Returns the position within the component of the left-hand edge of a key.
Depending on the keyboard's orientation, this may be a horizontal or vertical
distance, in either direction.
*/
float getKeyStartPosition (int midiNoteNumber) const;
/** Returns the total width needed to fit all the keys in the available range. */
float getTotalKeyboardWidth() const noexcept;
/** This structure is returned by the getNoteAndVelocityAtPosition() method.
*/
struct JUCE_API NoteAndVelocity
{
int note;
float velocity;
};
/** Returns the note number and velocity for a given position within the component.
If includeChildComponents is true then this will return a key obscured by any child
components.
*/
NoteAndVelocity getNoteAndVelocityAtPosition (Point<float> position, bool includeChildComponents = false);
#ifndef DOXYGEN
/** Returns the key at a given coordinate, or -1 if the position does not intersect a key. */
[[deprecated ("This method has been deprecated in favour of getNoteAndVelocityAtPosition.")]]
int getNoteAtPosition (Point<float> p) { return getNoteAndVelocityAtPosition (p).note; }
#endif
/** Returns the rectangle for a given key. */
Rectangle<float> getRectangleForKey (int midiNoteNumber) const;
//==============================================================================
/** This sets the octave number which is shown as the octave number for middle C.
This affects only the default implementation of getWhiteNoteText(), which
passes this octave number to MidiMessage::getMidiNoteName() in order to
get the note text. See MidiMessage::getMidiNoteName() for more info about
the parameter.
By default this value is set to 3.
@see getOctaveForMiddleC
*/
void setOctaveForMiddleC (int octaveNumForMiddleC);
/** This returns the value set by setOctaveForMiddleC().
@see setOctaveForMiddleC
*/
int getOctaveForMiddleC() const noexcept { return octaveNumForMiddleC; }
//==============================================================================
/** Use this method to draw the background of the keyboard that will be drawn under
the white and black notes. This can also be used to draw any shadow or outline effects.
*/
virtual void drawKeyboardBackground (Graphics& g, Rectangle<float> area) = 0;
/** Use this method to draw a white key of the keyboard in a given rectangle.
When doing this, be sure to note the keyboard's orientation.
*/
virtual void drawWhiteKey (int midiNoteNumber, Graphics& g, Rectangle<float> area) = 0;
/** Use this method to draw a black key of the keyboard in a given rectangle.
When doing this, be sure to note the keyboard's orientation.
*/
virtual void drawBlackKey (int midiNoteNumber, Graphics& g, Rectangle<float> area) = 0;
/** This can be overridden to draw the up and down buttons that scroll the keyboard
up/down in octaves.
*/
virtual void drawUpDownButton (Graphics& g, int w, int h, bool isMouseOver, bool isButtonPressed, bool movesOctavesUp);
/** Calculates the position of a given midi-note.
This can be overridden to create layouts with custom key-widths.
@param midiNoteNumber the note to find
@param keyWidth the desired width in pixels of one key - see setKeyWidth()
@returns the start and length of the key along the axis of the keyboard
*/
virtual Range<float> getKeyPosition (int midiNoteNumber, float keyWidth) const;
//==============================================================================
/** @internal */
void paint (Graphics&) override;
/** @internal */
void resized() override;
/** @internal */
void mouseWheelMove (const MouseEvent&, const MouseWheelDetails&) override;
private:
//==============================================================================
struct UpDownButton;
Range<float> getKeyPos (int midiNoteNumber) const;
NoteAndVelocity remappedXYToNote (Point<float>) const;
void setLowestVisibleKeyFloat (float noteNumber);
//==============================================================================
Orientation orientation;
float blackNoteLengthRatio = 0.7f, blackNoteWidthRatio = 0.7f;
float xOffset = 0.0f;
float keyWidth = 16.0f;
float firstKey = 12 * 4.0f;
int scrollButtonWidth = 12;
int rangeStart = 0, rangeEnd = 127;
int octaveNumForMiddleC = 3;
bool canScroll = true;
std::unique_ptr<Button> scrollDown, scrollUp;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (KeyboardComponentBase)
};
} // namespace juce

View file

@ -0,0 +1,507 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - 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 6 End-User License
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
End User License Agreement: www.juce.com/juce-6-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

View file

@ -0,0 +1,153 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - 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 6 End-User License
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
End User License Agreement: www.juce.com/juce-6-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
{
//==============================================================================
/**
A component that displays an MPE-compatible keyboard, whose notes can be clicked on.
This component will mimic a physical MPE-compatible keyboard, showing the current state
of an MPEInstrument object. When the on-screen keys are clicked on, it will play these
notes by calling the noteOn() and noteOff() methods of its MPEInstrument object. Moving
the mouse will update the pitchbend and timbre dimensions of the MPEInstrument.
@see MPEInstrument
@tags{Audio}
*/
class JUCE_API MPEKeyboardComponent : public KeyboardComponentBase,
private MPEInstrument::Listener,
private Timer
{
public:
//==============================================================================
/** Creates an MPEKeyboardComponent.
@param instrument the MPEInstrument that this component represents
@param orientation whether the keyboard is horizontal or vertical
*/
MPEKeyboardComponent (MPEInstrument& instrument, Orientation orientation);
/** Destructor. */
virtual ~MPEKeyboardComponent() override;
//==============================================================================
/** Sets the note-on velocity, or "strike", value that will be used when triggering new notes. */
void setVelocity (float newVelocity) { velocity = jlimit (newVelocity, 0.0f, 1.0f); }
/** Sets the pressure value that will be used for new notes. */
void setPressure (float newPressure) { pressure = jlimit (newPressure, 0.0f, 1.0f); }
/** Sets the note-off velocity, or "lift", value that will be used when notes are released. */
void setLift (float newLift) { lift = jlimit (newLift, 0.0f, 1.0f); }
/** Use this to enable the mouse source pressure to be used for the initial note-on
velocity, or "strike", value if the mouse source supports it.
*/
void setUseMouseSourcePressureForStrike (bool usePressureForStrike) { useMouseSourcePressureForStrike = usePressureForStrike; }
//==============================================================================
/** A set of colour IDs to use to change the colour of various aspects of the keyboard.
These constants can be used either via the Component::setColour(), or LookAndFeel::setColour()
methods.
@see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour
*/
enum ColourIds
{
whiteNoteColourId = 0x1006000,
blackNoteColourId = 0x1006001,
textLabelColourId = 0x1006002,
noteCircleFillColourId = 0x1006003,
noteCircleOutlineColourId = 0x1006004
};
//==============================================================================
/** @internal */
void mouseDrag (const MouseEvent&) override;
/** @internal */
void mouseDown (const MouseEvent&) override;
/** @internal */
void mouseUp (const MouseEvent&) override;
/** @internal */
void focusLost (FocusChangeType) override;
/** @internal */
void colourChanged() override;
private:
//==========================================================================
struct MPENoteComponent;
//==============================================================================
void drawKeyboardBackground (Graphics& g, Rectangle<float> area) override;
void drawWhiteKey (int midiNoteNumber, Graphics& g, Rectangle<float> area) override;
void drawBlackKey (int midiNoteNumber, Graphics& g, Rectangle<float> area) override;
void updateNoteData (MPENote&);
void noteAdded (MPENote) override;
void notePressureChanged (MPENote) override;
void notePitchbendChanged (MPENote) override;
void noteTimbreChanged (MPENote) override;
void noteReleased (MPENote) override;
void zoneLayoutChanged() override;
void timerCallback() override;
//==============================================================================
MPEValue mousePositionToPitchbend (int, Point<float>);
MPEValue mousePositionToTimbre (Point<float>);
void addNewNote (MPENote);
void removeNote (MPENote);
void handleNoteOns (std::set<MPENote>&);
void handleNoteOffs (std::set<MPENote>&);
void updateNoteComponentBounds (const MPENote&, MPENoteComponent&);
void updateNoteComponents();
void updateZoneLayout();
//==============================================================================
MPEInstrument& instrument;
std::unique_ptr<MPEChannelAssigner> channelAssigner;
CriticalSection activeNotesLock;
std::vector<std::pair<MPENote, bool>> activeNotes;
std::vector<std::unique_ptr<MPENoteComponent>> noteComponents;
std::map<int, uint16> sourceIDMap;
float velocity = 0.7f, pressure = 1.0f, lift = 0.0f;
bool useMouseSourcePressureForStrike = false;
int perNotePitchbendRange = 48;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPEKeyboardComponent)
};
} // namespace juce

View file

@ -26,60 +26,17 @@
namespace juce namespace juce
{ {
static const uint8 whiteNotes[] = { 0, 2, 4, 5, 7, 9, 11 };
static const uint8 blackNotes[] = { 1, 3, 6, 8, 10 };
struct MidiKeyboardComponent::UpDownButton : public Button
{
UpDownButton (MidiKeyboardComponent& c, int d)
: Button ({}), owner (c), delta (d)
{
}
void clicked() override
{
auto note = owner.getLowestVisibleKey();
if (delta < 0)
note = (note - 1) / 12;
else
note = note / 12 + 1;
owner.setLowestVisibleKey (note * 12);
}
using Button::clicked;
void paintButton (Graphics& g, bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override
{
owner.drawUpDownButton (g, getWidth(), getHeight(),
shouldDrawButtonAsHighlighted, shouldDrawButtonAsDown,
delta > 0);
}
private:
MidiKeyboardComponent& owner;
const int delta;
JUCE_DECLARE_NON_COPYABLE (UpDownButton)
};
//============================================================================== //==============================================================================
MidiKeyboardComponent::MidiKeyboardComponent (MidiKeyboardState& s, Orientation o) MidiKeyboardComponent::MidiKeyboardComponent (MidiKeyboardState& stateToUse, Orientation orientationToUse)
: state (s), orientation (o) : KeyboardComponentBase (orientationToUse), state (stateToUse)
{ {
scrollDown.reset (new UpDownButton (*this, -1)); state.addListener (this);
scrollUp .reset (new UpDownButton (*this, 1));
addChildComponent (scrollDown.get());
addChildComponent (scrollUp.get());
// initialise with a default set of qwerty key-mappings.. // initialise with a default set of qwerty key-mappings..
int note = 0; int note = 0;
for (char c : "awsedftgyhujkolp;") for (char c : "awsedftgyhujkolp;")
setKeyPressForNote (KeyPress (c, 0, 0), note++); setKeyPressForNote ({ c, 0, 0 }, note++);
mouseOverNotes.insertMultiple (0, -1, 32); mouseOverNotes.insertMultiple (0, -1, 32);
mouseDownNotes.insertMultiple (0, -1, 32); mouseDownNotes.insertMultiple (0, -1, 32);
@ -87,8 +44,6 @@ MidiKeyboardComponent::MidiKeyboardComponent (MidiKeyboardState& s, Orientation
colourChanged(); colourChanged();
setWantsKeyboardFocus (true); setWantsKeyboardFocus (true);
state.addListener (this);
startTimerHz (20); startTimerHz (20);
} }
@ -98,86 +53,10 @@ MidiKeyboardComponent::~MidiKeyboardComponent()
} }
//============================================================================== //==============================================================================
void MidiKeyboardComponent::setKeyWidth (float widthInPixels) void MidiKeyboardComponent::setVelocity (float v, bool useMousePosition)
{ {
jassert (widthInPixels > 0); velocity = v;
useMousePositionForVelocity = useMousePosition;
if (keyWidth != widthInPixels) // Prevent infinite recursion if the width is being computed in a 'resized()' call-back
{
keyWidth = widthInPixels;
resized();
}
}
void MidiKeyboardComponent::setScrollButtonWidth (int widthInPixels)
{
jassert (widthInPixels > 0);
if (scrollButtonWidth != widthInPixels)
{
scrollButtonWidth = widthInPixels;
resized();
}
}
void MidiKeyboardComponent::setOrientation (Orientation newOrientation)
{
if (orientation != newOrientation)
{
orientation = newOrientation;
resized();
}
}
void MidiKeyboardComponent::setAvailableRange (int lowestNote, int highestNote)
{
jassert (lowestNote >= 0 && lowestNote <= 127);
jassert (highestNote >= 0 && highestNote <= 127);
jassert (lowestNote <= highestNote);
if (rangeStart != lowestNote || rangeEnd != highestNote)
{
rangeStart = jlimit (0, 127, lowestNote);
rangeEnd = jlimit (0, 127, highestNote);
firstKey = jlimit ((float) rangeStart, (float) rangeEnd, firstKey);
resized();
}
}
void MidiKeyboardComponent::setLowestVisibleKey (int noteNumber)
{
setLowestVisibleKeyFloat ((float) noteNumber);
}
void MidiKeyboardComponent::setLowestVisibleKeyFloat (float noteNumber)
{
noteNumber = jlimit ((float) rangeStart, (float) rangeEnd, noteNumber);
if (noteNumber != firstKey)
{
bool hasMoved = (((int) firstKey) != (int) noteNumber);
firstKey = noteNumber;
if (hasMoved)
sendChangeMessage();
resized();
}
}
void MidiKeyboardComponent::setScrollButtonsVisible (bool newCanScroll)
{
if (canScroll != newCanScroll)
{
canScroll = newCanScroll;
resized();
}
}
void MidiKeyboardComponent::colourChanged()
{
setOpaque (findColour (whiteNoteColourId).isOpaque());
repaint();
} }
//============================================================================== //==============================================================================
@ -198,477 +77,39 @@ void MidiKeyboardComponent::setMidiChannelsToDisplay (int midiChannelMask)
noPendingUpdates.store (false); noPendingUpdates.store (false);
} }
void MidiKeyboardComponent::setVelocity (float v, bool useMousePosition)
{
velocity = jlimit (0.0f, 1.0f, v);
useMousePositionForVelocity = useMousePosition;
}
//============================================================================== //==============================================================================
Range<float> MidiKeyboardComponent::getKeyPosition (int midiNoteNumber, float targetKeyWidth) const void MidiKeyboardComponent::clearKeyMappings()
{ {
jassert (midiNoteNumber >= 0 && midiNoteNumber < 128); resetAnyKeysInUse();
keyPressNotes.clear();
static const float notePos[] = { 0.0f, 1 - blackNoteWidthRatio * 0.6f, keyPresses.clear();
1.0f, 2 - blackNoteWidthRatio * 0.4f,
2.0f,
3.0f, 4 - blackNoteWidthRatio * 0.7f,
4.0f, 5 - blackNoteWidthRatio * 0.5f,
5.0f, 6 - blackNoteWidthRatio * 0.3f,
6.0f };
auto octave = midiNoteNumber / 12;
auto note = midiNoteNumber % 12;
auto start = (float) octave * 7.0f * targetKeyWidth + notePos[note] * targetKeyWidth;
auto width = MidiMessage::isMidiNoteBlack (note) ? blackNoteWidthRatio * targetKeyWidth : targetKeyWidth;
return { start, start + width };
} }
Range<float> MidiKeyboardComponent::getKeyPos (int midiNoteNumber) const void MidiKeyboardComponent::setKeyPressForNote (const KeyPress& key, int midiNoteOffsetFromC)
{ {
return getKeyPosition (midiNoteNumber, keyWidth) removeKeyPressForNote (midiNoteOffsetFromC);
- xOffset
- getKeyPosition (rangeStart, keyWidth).getStart(); keyPressNotes.add (midiNoteOffsetFromC);
keyPresses.add (key);
} }
Rectangle<float> MidiKeyboardComponent::getRectangleForKey (int note) const void MidiKeyboardComponent::removeKeyPressForNote (int midiNoteOffsetFromC)
{ {
jassert (note >= rangeStart && note <= rangeEnd); for (int i = keyPressNotes.size(); --i >= 0;)
auto pos = getKeyPos (note);
auto x = pos.getStart();
auto w = pos.getLength();
if (MidiMessage::isMidiNoteBlack (note))
{ {
auto blackNoteLength = getBlackNoteLength(); if (keyPressNotes.getUnchecked (i) == midiNoteOffsetFromC)
switch (orientation)
{ {
case horizontalKeyboard: return { x, 0, w, blackNoteLength }; keyPressNotes.remove (i);
case verticalKeyboardFacingLeft: return { (float) getWidth() - blackNoteLength, x, blackNoteLength, w }; keyPresses.remove (i);
case verticalKeyboardFacingRight: return { 0, (float) getHeight() - x - w, blackNoteLength, w };
default: jassertfalse; break;
}
}
else
{
switch (orientation)
{
case horizontalKeyboard: return { x, 0, w, (float) getHeight() };
case verticalKeyboardFacingLeft: return { 0, x, (float) getWidth(), w };
case verticalKeyboardFacingRight: return { 0, (float) getHeight() - x - w, (float) getWidth(), w };
default: jassertfalse; break;
}
}
return {};
}
float MidiKeyboardComponent::getKeyStartPosition (int midiNoteNumber) const
{
return getKeyPos (midiNoteNumber).getStart();
}
float MidiKeyboardComponent::getTotalKeyboardWidth() const noexcept
{
return getKeyPos (rangeEnd).getEnd();
}
int MidiKeyboardComponent::getNoteAtPosition (Point<float> p)
{
return xyToNote (p).note;
}
MidiKeyboardComponent::NoteAndVelocity MidiKeyboardComponent::xyToNote (Point<float> pos)
{
if (! reallyContains (pos, false))
return { -1, 0.0f };
auto p = pos;
if (orientation != horizontalKeyboard)
{
p = { p.y, p.x };
if (orientation == verticalKeyboardFacingLeft)
p = { p.x, (float) getWidth() - p.y };
else
p = { (float) getHeight() - p.x, p.y };
}
return remappedXYToNote (p + Point<float> (xOffset, 0));
}
MidiKeyboardComponent::NoteAndVelocity MidiKeyboardComponent::remappedXYToNote (Point<float> pos) const
{
auto blackNoteLength = getBlackNoteLength();
if (pos.getY() < blackNoteLength)
{
for (int octaveStart = 12 * (rangeStart / 12); octaveStart <= rangeEnd; octaveStart += 12)
{
for (int i = 0; i < 5; ++i)
{
auto note = octaveStart + blackNotes[i];
if (rangeStart <= note && note <= rangeEnd)
{
if (getKeyPos (note).contains (pos.x - xOffset))
{
return { note, jmax (0.0f, pos.y / blackNoteLength) };
}
}
}
}
}
for (int octaveStart = 12 * (rangeStart / 12); octaveStart <= rangeEnd; octaveStart += 12)
{
for (int i = 0; i < 7; ++i)
{
auto note = octaveStart + whiteNotes[i];
if (note >= rangeStart && note <= rangeEnd)
{
if (getKeyPos (note).contains (pos.x - xOffset))
{
auto whiteNoteLength = (orientation == horizontalKeyboard) ? getHeight() : getWidth();
return { note, jmax (0.0f, pos.y / (float) whiteNoteLength) };
}
}
}
}
return { -1, 0 };
}
//==============================================================================
void MidiKeyboardComponent::repaintNote (int noteNum)
{
if (noteNum >= rangeStart && noteNum <= rangeEnd)
repaint (getRectangleForKey (noteNum).getSmallestIntegerContainer());
}
void MidiKeyboardComponent::paint (Graphics& g)
{
g.fillAll (findColour (whiteNoteColourId));
auto lineColour = findColour (keySeparatorLineColourId);
auto textColour = findColour (textLabelColourId);
for (int octave = 0; octave < 128; octave += 12)
{
for (int white = 0; white < 7; ++white)
{
auto noteNum = octave + whiteNotes[white];
if (noteNum >= rangeStart && noteNum <= rangeEnd)
drawWhiteNote (noteNum, g, getRectangleForKey (noteNum),
state.isNoteOnForChannels (midiInChannelMask, noteNum),
mouseOverNotes.contains (noteNum), lineColour, textColour);
}
}
float x1 = 0.0f, y1 = 0.0f, x2 = 0.0f, y2 = 0.0f;
auto width = getWidth();
auto height = getHeight();
if (orientation == verticalKeyboardFacingLeft)
{
x1 = (float) width - 1.0f;
x2 = (float) width - 5.0f;
}
else if (orientation == verticalKeyboardFacingRight)
x2 = 5.0f;
else
y2 = 5.0f;
auto x = getKeyPos (rangeEnd).getEnd();
auto shadowCol = findColour (shadowColourId);
if (! shadowCol.isTransparent())
{
g.setGradientFill (ColourGradient (shadowCol, x1, y1, shadowCol.withAlpha (0.0f), x2, y2, false));
switch (orientation)
{
case horizontalKeyboard: g.fillRect (0.0f, 0.0f, x, 5.0f); break;
case verticalKeyboardFacingLeft: g.fillRect ((float) width - 5.0f, 0.0f, 5.0f, x); break;
case verticalKeyboardFacingRight: g.fillRect (0.0f, 0.0f, 5.0f, x); break;
default: break;
}
}
if (! lineColour.isTransparent())
{
g.setColour (lineColour);
switch (orientation)
{
case horizontalKeyboard: g.fillRect (0.0f, (float) height - 1.0f, x, 1.0f); break;
case verticalKeyboardFacingLeft: g.fillRect (0.0f, 0.0f, 1.0f, x); break;
case verticalKeyboardFacingRight: g.fillRect ((float) width - 1.0f, 0.0f, 1.0f, x); break;
default: break;
}
}
auto blackNoteColour = findColour (blackNoteColourId);
for (int octave = 0; octave < 128; octave += 12)
{
for (int black = 0; black < 5; ++black)
{
auto noteNum = octave + blackNotes[black];
if (noteNum >= rangeStart && noteNum <= rangeEnd)
drawBlackNote (noteNum, g, getRectangleForKey (noteNum),
state.isNoteOnForChannels (midiInChannelMask, noteNum),
mouseOverNotes.contains (noteNum), blackNoteColour);
} }
} }
} }
void MidiKeyboardComponent::drawWhiteNote (int midiNoteNumber, Graphics& g, Rectangle<float> area, void MidiKeyboardComponent::setKeyPressBaseOctave (int newOctaveNumber)
bool isDown, bool isOver, Colour lineColour, Colour textColour)
{ {
auto c = Colours::transparentWhite; jassert (newOctaveNumber >= 0 && newOctaveNumber <= 10);
if (isDown) c = findColour (keyDownOverlayColourId); keyMappingOctave = newOctaveNumber;
if (isOver) c = c.overlaidWith (findColour (mouseOverKeyOverlayColourId));
g.setColour (c);
g.fillRect (area);
auto text = getWhiteNoteText (midiNoteNumber);
if (text.isNotEmpty())
{
auto fontHeight = jmin (12.0f, keyWidth * 0.9f);
g.setColour (textColour);
g.setFont (Font (fontHeight).withHorizontalScale (0.8f));
switch (orientation)
{
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 (orientation)
{
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 == rangeEnd)
{
switch (orientation)
{
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<float> 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 (orientation)
{
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;
}
}
}
void MidiKeyboardComponent::setOctaveForMiddleC (int octaveNum)
{
octaveNumForMiddleC = octaveNum;
repaint();
}
String MidiKeyboardComponent::getWhiteNoteText (int midiNoteNumber)
{
if (midiNoteNumber % 12 == 0)
return MidiMessage::getMidiNoteName (midiNoteNumber, true, true, octaveNumForMiddleC);
return {};
}
void MidiKeyboardComponent::drawUpDownButton (Graphics& g, int w, int h,
bool mouseOver,
bool buttonDown,
bool movesOctavesUp)
{
g.fillAll (findColour (upDownButtonBackgroundColourId));
float angle = 0;
switch (orientation)
{
case horizontalKeyboard: angle = movesOctavesUp ? 0.0f : 0.5f; break;
case verticalKeyboardFacingLeft: angle = movesOctavesUp ? 0.25f : 0.75f; break;
case verticalKeyboardFacingRight: angle = movesOctavesUp ? 0.75f : 0.25f; break;
default: jassertfalse; break;
}
Path path;
path.addTriangle (0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.5f);
path.applyTransform (AffineTransform::rotation (MathConstants<float>::twoPi * angle, 0.5f, 0.5f));
g.setColour (findColour (upDownButtonArrowColourId)
.withAlpha (buttonDown ? 1.0f : (mouseOver ? 0.6f : 0.4f)));
g.fillPath (path, path.getTransformToScaleToFit (1.0f, 1.0f, (float) w - 2.0f, (float) h - 2.0f, true));
}
void MidiKeyboardComponent::setBlackNoteLengthProportion (float ratio) noexcept
{
jassert (ratio >= 0.0f && ratio <= 1.0f);
if (blackNoteLengthRatio != ratio)
{
blackNoteLengthRatio = ratio;
resized();
}
}
float MidiKeyboardComponent::getBlackNoteLength() const noexcept
{
auto whiteNoteLength = orientation == horizontalKeyboard ? getHeight() : getWidth();
return (float) whiteNoteLength * blackNoteLengthRatio;
}
void MidiKeyboardComponent::setBlackNoteWidthProportion (float ratio) noexcept
{
jassert (ratio >= 0.0f && ratio <= 1.0f);
if (blackNoteWidthRatio != ratio)
{
blackNoteWidthRatio = ratio;
resized();
}
}
void MidiKeyboardComponent::resized()
{
auto w = getWidth();
auto h = getHeight();
if (w > 0 && h > 0)
{
if (orientation != horizontalKeyboard)
std::swap (w, h);
auto kx2 = getKeyPos (rangeEnd).getEnd();
if ((int) firstKey != rangeStart)
{
auto kx1 = getKeyPos (rangeStart).getStart();
if (kx2 - kx1 <= (float) w)
{
firstKey = (float) rangeStart;
sendChangeMessage();
repaint();
}
}
scrollDown->setVisible (canScroll && firstKey > (float) rangeStart);
xOffset = 0;
if (canScroll)
{
auto scrollButtonW = jmin (scrollButtonWidth, w / 2);
auto r = getLocalBounds();
if (orientation == horizontalKeyboard)
{
scrollDown->setBounds (r.removeFromLeft (scrollButtonW));
scrollUp ->setBounds (r.removeFromRight (scrollButtonW));
}
else if (orientation == verticalKeyboardFacingLeft)
{
scrollDown->setBounds (r.removeFromTop (scrollButtonW));
scrollUp ->setBounds (r.removeFromBottom (scrollButtonW));
}
else
{
scrollDown->setBounds (r.removeFromBottom (scrollButtonW));
scrollUp ->setBounds (r.removeFromTop (scrollButtonW));
}
auto endOfLastKey = getKeyPos (rangeEnd).getEnd();
auto spaceAvailable = w;
auto lastStartKey = remappedXYToNote ({ endOfLastKey - (float) spaceAvailable, 0 }).note + 1;
if (lastStartKey >= 0 && ((int) firstKey) > lastStartKey)
{
firstKey = (float) jlimit (rangeStart, rangeEnd, lastStartKey);
sendChangeMessage();
}
xOffset = getKeyPos ((int) firstKey).getStart();
}
else
{
firstKey = (float) rangeStart;
}
scrollUp->setVisible (canScroll && getKeyPos (rangeEnd).getStart() > (float) w);
repaint();
}
}
//==============================================================================
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);
} }
//============================================================================== //==============================================================================
@ -685,7 +126,7 @@ void MidiKeyboardComponent::resetAnyKeysInUse()
for (int i = mouseDownNotes.size(); --i >= 0;) for (int i = mouseDownNotes.size(); --i >= 0;)
{ {
auto noteDown = mouseDownNotes.getUnchecked(i); auto noteDown = mouseDownNotes.getUnchecked (i);
if (noteDown >= 0) if (noteDown >= 0)
{ {
@ -704,7 +145,7 @@ void MidiKeyboardComponent::updateNoteUnderMouse (const MouseEvent& e, bool isDo
void MidiKeyboardComponent::updateNoteUnderMouse (Point<float> pos, bool isDown, int fingerNum) void MidiKeyboardComponent::updateNoteUnderMouse (Point<float> pos, bool isDown, int fingerNum)
{ {
const auto noteInfo = xyToNote (pos); const auto noteInfo = getNoteAndVelocityAtPosition (pos);
const auto newNote = noteInfo.note; const auto newNote = noteInfo.note;
const auto oldNote = mouseOverNotes.getUnchecked (fingerNum); const auto oldNote = mouseOverNotes.getUnchecked (fingerNum);
const auto oldNoteDown = mouseDownNotes.getUnchecked (fingerNum); const auto oldNoteDown = mouseDownNotes.getUnchecked (fingerNum);
@ -745,6 +186,13 @@ void MidiKeyboardComponent::updateNoteUnderMouse (Point<float> pos, bool isDown,
} }
} }
void MidiKeyboardComponent::repaintNote (int noteNum)
{
if (getRangeStart() <= noteNum && noteNum <= getRangeEnd())
repaint (getRectangleForKey (noteNum).getSmallestIntegerContainer());
}
void MidiKeyboardComponent::mouseMove (const MouseEvent& e) void MidiKeyboardComponent::mouseMove (const MouseEvent& e)
{ {
updateNoteUnderMouse (e, false); updateNoteUnderMouse (e, false);
@ -752,19 +200,15 @@ void MidiKeyboardComponent::mouseMove (const MouseEvent& e)
void MidiKeyboardComponent::mouseDrag (const MouseEvent& e) void MidiKeyboardComponent::mouseDrag (const MouseEvent& e)
{ {
auto newNote = xyToNote (e.position).note; auto newNote = getNoteAndVelocityAtPosition (e.position).note;
if (newNote >= 0 && mouseDraggedToKey (newNote, e)) if (newNote >= 0 && mouseDraggedToKey (newNote, e))
updateNoteUnderMouse (e, true); updateNoteUnderMouse (e, true);
} }
bool MidiKeyboardComponent::mouseDownOnKey (int, const MouseEvent&) { return true; }
bool MidiKeyboardComponent::mouseDraggedToKey (int, const MouseEvent&) { return true; }
void MidiKeyboardComponent::mouseUpOnKey (int, const MouseEvent&) {}
void MidiKeyboardComponent::mouseDown (const MouseEvent& e) void MidiKeyboardComponent::mouseDown (const MouseEvent& e)
{ {
auto newNote = xyToNote (e.position).note; auto newNote = getNoteAndVelocityAtPosition (e.position).note;
if (newNote >= 0 && mouseDownOnKey (newNote, e)) if (newNote >= 0 && mouseDownOnKey (newNote, e))
updateNoteUnderMouse (e, true); updateNoteUnderMouse (e, true);
@ -774,7 +218,7 @@ void MidiKeyboardComponent::mouseUp (const MouseEvent& e)
{ {
updateNoteUnderMouse (e, false); updateNoteUnderMouse (e, false);
auto note = xyToNote (e.position).note; auto note = getNoteAndVelocityAtPosition (e.position).note;
if (note >= 0) if (note >= 0)
mouseUpOnKey (note, e); mouseUpOnKey (note, e);
@ -790,23 +234,14 @@ void MidiKeyboardComponent::mouseExit (const MouseEvent& e)
updateNoteUnderMouse (e, false); updateNoteUnderMouse (e, false);
} }
void MidiKeyboardComponent::mouseWheelMove (const MouseEvent&, const MouseWheelDetails& wheel)
{
auto amount = (orientation == horizontalKeyboard && wheel.deltaX != 0)
? wheel.deltaX : (orientation == verticalKeyboardFacingLeft ? wheel.deltaY
: -wheel.deltaY);
setLowestVisibleKeyFloat (firstKey - amount * keyWidth);
}
void MidiKeyboardComponent::timerCallback() void MidiKeyboardComponent::timerCallback()
{ {
if (noPendingUpdates.exchange (true)) if (noPendingUpdates.exchange (true))
return; return;
for (int i = rangeStart; i <= rangeEnd; ++i) for (auto i = getRangeStart(); i <= getRangeEnd(); ++i)
{ {
bool isOn = state.isNoteOnForChannels (midiInChannelMask, i); const auto isOn = state.isNoteOnForChannels (midiInChannelMask, i);
if (keysCurrentlyDrawnDown[i] != isOn) if (keysCurrentlyDrawnDown[i] != isOn)
{ {
@ -816,41 +251,6 @@ void MidiKeyboardComponent::timerCallback()
} }
} }
//==============================================================================
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;
}
bool MidiKeyboardComponent::keyStateChanged (bool /*isKeyDown*/) bool MidiKeyboardComponent::keyStateChanged (bool /*isKeyDown*/)
{ {
bool keyPressUsed = false; bool keyPressUsed = false;
@ -892,4 +292,190 @@ void MidiKeyboardComponent::focusLost (FocusChangeType)
resetAnyKeysInUse(); resetAnyKeysInUse();
} }
//==============================================================================
void MidiKeyboardComponent::drawKeyboardBackground (Graphics& g, Rectangle<float> area)
{
g.fillAll (findColour (whiteNoteColourId));
auto width = area.getWidth();
auto height = area.getHeight();
auto currentOrientation = getOrientation();
Point<float> 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<float> 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<float> 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<float> area)
{
drawWhiteNote (midiNoteNumber, g, area, state.isNoteOnForChannels (midiInChannelMask, midiNoteNumber),
mouseOverNotes.contains (midiNoteNumber), findColour (keySeparatorLineColourId), findColour (textLabelColourId));
}
void MidiKeyboardComponent::drawBlackKey (int midiNoteNumber, Graphics& g, Rectangle<float> 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 } // namespace juce

View file

@ -46,30 +46,18 @@ namespace juce
@tags{Audio} @tags{Audio}
*/ */
class JUCE_API MidiKeyboardComponent : public Component, class JUCE_API MidiKeyboardComponent : public KeyboardComponentBase,
public MidiKeyboardState::Listener, private MidiKeyboardState::Listener,
public ChangeBroadcaster,
private Timer private Timer
{ {
public: public:
//============================================================================== //==============================================================================
/** The direction of the keyboard.
@see setOrientation
*/
enum Orientation
{
horizontalKeyboard,
verticalKeyboardFacingLeft,
verticalKeyboardFacingRight,
};
/** Creates a MidiKeyboardComponent. /** Creates a MidiKeyboardComponent.
@param state the midi keyboard model that this component will represent @param state the midi keyboard model that this component will represent
@param orientation whether the keyboard is horizontal or vertical @param orientation whether the keyboard is horizontal or vertical
*/ */
MidiKeyboardComponent (MidiKeyboardState& state, MidiKeyboardComponent (MidiKeyboardState& state, Orientation orientation);
Orientation orientation);
/** Destructor. */ /** Destructor. */
~MidiKeyboardComponent() override; ~MidiKeyboardComponent() override;
@ -84,6 +72,7 @@ public:
*/ */
void setVelocity (float velocity, bool useMousePositionForVelocity); void setVelocity (float velocity, bool useMousePositionForVelocity);
//==============================================================================
/** Changes the midi channel number that will be used for events triggered by clicking /** Changes the midi channel number that will be used for events triggered by clicking
on the component. on the component.
@ -100,7 +89,7 @@ public:
/** Returns the midi channel that the keyboard is using for midi messages. /** Returns the midi channel that the keyboard is using for midi messages.
@see setMidiChannel @see setMidiChannel
*/ */
int getMidiChannel() const noexcept { return midiChannel; } int getMidiChannel() const noexcept { return midiChannel; }
/** Sets a mask to indicate which incoming midi channels should be represented by /** Sets a mask to indicate which incoming midi channels should be represented by
key movements. key movements.
@ -119,86 +108,41 @@ public:
/** Returns the current set of midi channels represented by the component. /** Returns the current set of midi channels represented by the component.
This is the value that was set with setMidiChannelsToDisplay(). This is the value that was set with setMidiChannelsToDisplay().
*/ */
int getMidiChannelsToDisplay() const noexcept { return midiInChannelMask; } int getMidiChannelsToDisplay() const noexcept { return midiInChannelMask; }
//============================================================================== //==============================================================================
/** Changes the width used to draw the white keys. */ /** Deletes all key-mappings.
void setKeyWidth (float widthInPixels);
/** Returns the width that was set by setKeyWidth(). */ @see setKeyPressForNote
float getKeyWidth() const noexcept { return keyWidth; }
/** Changes the width used to draw the buttons that scroll the keyboard up/down in octaves. */
void setScrollButtonWidth (int widthInPixels);
/** Returns the width that was set by setScrollButtonWidth(). */
int getScrollButtonWidth() const noexcept { return scrollButtonWidth; }
/** Changes the keyboard's current direction. */
void setOrientation (Orientation newOrientation);
/** Returns the keyboard's current direction. */
Orientation getOrientation() const noexcept { return orientation; }
/** Sets the range of midi notes that the keyboard will be limited to.
By default the range is 0 to 127 (inclusive), but you can limit this if you
only want a restricted set of the keys to be shown.
Note that the values here are inclusive and must be between 0 and 127.
*/ */
void setAvailableRange (int lowestNote, void clearKeyMappings();
int highestNote);
/** Returns the first note in the available range. /** Maps a key-press to a given note.
@see setAvailableRange
@param key the key that should trigger the note
@param midiNoteOffsetFromC how many semitones above C the triggered note should
be. The actual midi note that gets played will be
this value + (12 * the current base octave). To change
the base octave, see setKeyPressBaseOctave()
*/ */
int getRangeStart() const noexcept { return rangeStart; } void setKeyPressForNote (const KeyPress& key, int midiNoteOffsetFromC);
/** Returns the last note in the available range. /** Removes any key-mappings for a given note.
@see setAvailableRange
For a description of what the note number means, see setKeyPressForNote().
*/ */
int getRangeEnd() const noexcept { return rangeEnd; } void removeKeyPressForNote (int midiNoteOffsetFromC);
/** If the keyboard extends beyond the size of the component, this will scroll /** Changes the base note above which key-press-triggered notes are played.
it to show the given key at the start.
Whenever the keyboard's position is changed, this will use the ChangeBroadcaster The set of key-mappings that trigger notes can be moved up and down to cover
base class to send a callback to any ChangeListeners that have been registered. the entire scale using this method.
The value passed in is an octave number between 0 and 10 (inclusive), and
indicates which C is the base note to which the key-mapped notes are
relative.
*/ */
void setLowestVisibleKey (int noteNumber); void setKeyPressBaseOctave (int newOctaveNumber);
/** Returns the number of the first key shown in the component.
@see setLowestVisibleKey
*/
int getLowestVisibleKey() const noexcept { return (int) firstKey; }
/** Sets the length of the black notes as a proportion of the white note length. */
void setBlackNoteLengthProportion (float ratio) noexcept;
/** Returns the length of the black notes as a proportion of the white note length. */
float getBlackNoteLengthProportion() const noexcept { return blackNoteLengthRatio; }
/** Returns the absolute length of the black notes.
This will be their vertical or horizontal length, depending on the keyboard's orientation.
*/
float getBlackNoteLength() const noexcept;
/** Sets the width of the black notes as a proportion of the white note width. */
void setBlackNoteWidthProportion (float ratio) noexcept;
/** Returns the width of the black notes as a proportion of the white note width. */
float getBlackNoteWidthProportion() const noexcept { return blackNoteWidthRatio; }
/** Returns the absolute width of the black notes.
This will be their vertical or horizontal width, depending on the keyboard's orientation.
*/
float getBlackNoteWidth() const noexcept { return keyWidth * blackNoteWidthRatio; }
/** If set to true, then scroll buttons will appear at either end of the keyboard
if there are too many notes to fit them all in the component at once.
*/
void setScrollButtonsVisible (bool canScroll);
//============================================================================== //==============================================================================
/** A set of colour IDs to use to change the colour of various aspects of the keyboard. /** A set of colour IDs to use to change the colour of various aspects of the keyboard.
@ -216,81 +160,66 @@ public:
mouseOverKeyOverlayColourId = 0x1005003, /**< This colour will be overlaid on the normal note colour. */ mouseOverKeyOverlayColourId = 0x1005003, /**< This colour will be overlaid on the normal note colour. */
keyDownOverlayColourId = 0x1005004, /**< This colour will be overlaid on the normal note colour. */ keyDownOverlayColourId = 0x1005004, /**< This colour will be overlaid on the normal note colour. */
textLabelColourId = 0x1005005, textLabelColourId = 0x1005005,
upDownButtonBackgroundColourId = 0x1005006, shadowColourId = 0x1005006
upDownButtonArrowColourId = 0x1005007,
shadowColourId = 0x1005008
}; };
/** Returns the position within the component of the left-hand edge of a key.
Depending on the keyboard's orientation, this may be a horizontal or vertical
distance, in either direction.
*/
float getKeyStartPosition (int midiNoteNumber) const;
/** Returns the total width needed to fit all the keys in the available range. */
float getTotalKeyboardWidth() const noexcept;
/** Returns the key at a given coordinate. */
int getNoteAtPosition (Point<float> position);
//============================================================================== //==============================================================================
/** Deletes all key-mappings. /** Use this method to draw a white note of the keyboard in a given rectangle.
@see setKeyPressForNote
isOver indicates whether the mouse is over the key, isDown indicates whether the key is
currently pressed down.
When doing this, be sure to note the keyboard's orientation.
*/ */
void clearKeyMappings(); virtual void drawWhiteNote (int midiNoteNumber, Graphics& g, Rectangle<float> area,
bool isDown, bool isOver, Colour lineColour, Colour textColour);
/** Maps a key-press to a given note. /** Use this method to draw a black note of the keyboard in a given rectangle.
@param key the key that should trigger the note isOver indicates whether the mouse is over the key, isDown indicates whether the key is
@param midiNoteOffsetFromC how many semitones above C the triggered note should currently pressed down.
be. The actual midi note that gets played will be
this value + (12 * the current base octave). To change When doing this, be sure to note the keyboard's orientation.
the base octave, see setKeyPressBaseOctave()
*/ */
void setKeyPressForNote (const KeyPress& key, virtual void drawBlackNote (int midiNoteNumber, Graphics& g, Rectangle<float> area,
int midiNoteOffsetFromC); bool isDown, bool isOver, Colour noteFillColour);
/** Removes any key-mappings for a given note. /** Callback when the mouse is clicked on a key.
For a description of what the note number means, see setKeyPressForNote().
You could use this to do things like handle right-clicks on keys, etc.
Return true if you want the click to trigger the note, or false if you
want to handle it yourself and not have the note played.
@see mouseDraggedToKey
*/ */
void removeKeyPressForNote (int midiNoteOffsetFromC); virtual bool mouseDownOnKey (int midiNoteNumber, const MouseEvent& e) { ignoreUnused (midiNoteNumber, e); return true; }
/** Changes the base note above which key-press-triggered notes are played. /** Callback when the mouse is dragged from one key onto another.
The set of key-mappings that trigger notes can be moved up and down to cover Return true if you want the drag to trigger the new note, or false if you
the entire scale using this method. want to handle it yourself and not have the note played.
The value passed in is an octave number between 0 and 10 (inclusive), and @see mouseDownOnKey
indicates which C is the base note to which the key-mapped notes are
relative.
*/ */
void setKeyPressBaseOctave (int newOctaveNumber); virtual bool mouseDraggedToKey (int midiNoteNumber, const MouseEvent& e) { ignoreUnused (midiNoteNumber, e); return true; }
/** This sets the octave number which is shown as the octave number for middle C. /** Callback when the mouse is released from a key.
This affects only the default implementation of getWhiteNoteText(), which @see mouseDownOnKey
passes this octave number to MidiMessage::getMidiNoteName() in order to
get the note text. See MidiMessage::getMidiNoteName() for more info about
the parameter.
By default this value is set to 3.
@see getOctaveForMiddleC
*/ */
void setOctaveForMiddleC (int octaveNumForMiddleC); virtual void mouseUpOnKey (int midiNoteNumber, const MouseEvent& e) { ignoreUnused (midiNoteNumber, e); }
/** Allows text to be drawn on the white notes.
By default this is used to label the C in each octave, but could be used for other things.
/** This returns the value set by setOctaveForMiddleC().
@see setOctaveForMiddleC @see setOctaveForMiddleC
*/ */
int getOctaveForMiddleC() const noexcept { return octaveNumForMiddleC; } virtual String getWhiteNoteText (int midiNoteNumber);
//============================================================================== //==============================================================================
/** @internal */ /** @internal */
void paint (Graphics&) override;
/** @internal */
void resized() override;
/** @internal */
void mouseMove (const MouseEvent&) override; void mouseMove (const MouseEvent&) override;
/** @internal */ /** @internal */
void mouseDrag (const MouseEvent&) override; void mouseDrag (const MouseEvent&) override;
@ -303,8 +232,6 @@ public:
/** @internal */ /** @internal */
void mouseExit (const MouseEvent&) override; void mouseExit (const MouseEvent&) override;
/** @internal */ /** @internal */
void mouseWheelMove (const MouseEvent&, const MouseWheelDetails&) override;
/** @internal */
void timerCallback() override; void timerCallback() override;
/** @internal */ /** @internal */
bool keyStateChanged (bool isKeyDown) override; bool keyStateChanged (bool isKeyDown) override;
@ -313,127 +240,39 @@ public:
/** @internal */ /** @internal */
void focusLost (FocusChangeType) override; void focusLost (FocusChangeType) override;
/** @internal */ /** @internal */
void handleNoteOn (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override;
/** @internal */
void handleNoteOff (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override;
/** @internal */
void colourChanged() override; void colourChanged() override;
protected:
//==============================================================================
/** Draws a white note in the given rectangle.
isOver indicates whether the mouse is over the key, isDown indicates whether the key is
currently pressed down.
When doing this, be sure to note the keyboard's orientation.
*/
virtual void drawWhiteNote (int midiNoteNumber,
Graphics& g, Rectangle<float> area,
bool isDown, bool isOver,
Colour lineColour, Colour textColour);
/** Draws a black note in the given rectangle.
isOver indicates whether the mouse is over the key, isDown indicates whether the key is
currently pressed down.
When doing this, be sure to note the keyboard's orientation.
*/
virtual void drawBlackNote (int midiNoteNumber,
Graphics& g, Rectangle<float> area,
bool isDown, bool isOver,
Colour noteFillColour);
/** Allows text to be drawn on the white notes.
By default this is used to label the C in each octave, but could be used for other things.
@see setOctaveForMiddleC
*/
virtual String getWhiteNoteText (int midiNoteNumber);
/** Draws the up and down buttons that scroll the keyboard up/down in octaves. */
virtual void drawUpDownButton (Graphics& g, int w, int h,
bool isMouseOver,
bool isButtonPressed,
bool movesOctavesUp);
/** Callback when the mouse is clicked on a key.
You could use this to do things like handle right-clicks on keys, etc.
Return true if you want the click to trigger the note, or false if you
want to handle it yourself and not have the note played.
@see mouseDraggedToKey
*/
virtual bool mouseDownOnKey (int midiNoteNumber, const MouseEvent& e);
/** Callback when the mouse is dragged from one key onto another.
Return true if you want the drag to trigger the new note, or false if you
want to handle it yourself and not have the note played.
@see mouseDownOnKey
*/
virtual bool mouseDraggedToKey (int midiNoteNumber, const MouseEvent& e);
/** Callback when the mouse is released from a key.
@see mouseDownOnKey
*/
virtual void mouseUpOnKey (int midiNoteNumber, const MouseEvent& e);
/** Calculates the position of a given midi-note.
This can be overridden to create layouts with custom key-widths.
@param midiNoteNumber the note to find
@param keyWidth the desired width in pixels of one key - see setKeyWidth()
@returns the start and length of the key along the axis of the keyboard
*/
virtual Range<float> getKeyPosition (int midiNoteNumber, float keyWidth) const;
/** Returns the rectangle for a given key if within the displayable range */
Rectangle<float> getRectangleForKey (int midiNoteNumber) const;
private: private:
//============================================================================== //==============================================================================
struct UpDownButton; void drawKeyboardBackground (Graphics& g, Rectangle<float> area) override final;
struct NoteAndVelocity { int note; float velocity; }; void drawWhiteKey (int midiNoteNumber, Graphics& g, Rectangle<float> area) override final;
void drawBlackKey (int midiNoteNumber, Graphics& g, Rectangle<float> area) override final;
MidiKeyboardState& state; void handleNoteOn (MidiKeyboardState*, int, int, float) override;
float blackNoteLengthRatio = 0.7f; void handleNoteOff (MidiKeyboardState*, int, int, float) override;
float blackNoteWidthRatio = 0.7f;
float xOffset = 0;
float keyWidth = 16.0f;
int scrollButtonWidth = 12;
Orientation orientation;
int midiChannel = 1, midiInChannelMask = 0xffff; //==============================================================================
float velocity = 1.0f;
Array<int> mouseOverNotes, mouseDownNotes;
BigInteger keysPressed, keysCurrentlyDrawnDown;
std::atomic<bool> noPendingUpdates { true };
int rangeStart = 0, rangeEnd = 127;
float firstKey = 12 * 4.0f;
bool canScroll = true, useMousePositionForVelocity = true;
std::unique_ptr<Button> scrollDown, scrollUp;
Array<KeyPress> keyPresses;
Array<int> keyPressNotes;
int keyMappingOctave = 6, octaveNumForMiddleC = 3;
Range<float> getKeyPos (int midiNoteNumber) const;
NoteAndVelocity xyToNote (Point<float>);
NoteAndVelocity remappedXYToNote (Point<float>) const;
void resetAnyKeysInUse(); void resetAnyKeysInUse();
void updateNoteUnderMouse (Point<float>, bool isDown, int fingerNum); void updateNoteUnderMouse (Point<float>, bool isDown, int fingerNum);
void updateNoteUnderMouse (const MouseEvent&, bool isDown); void updateNoteUnderMouse (const MouseEvent&, bool isDown);
void repaintNote (int midiNoteNumber); void repaintNote (int midiNoteNumber);
void setLowestVisibleKeyFloat (float noteNumber);
//==============================================================================
MidiKeyboardState& state;
int midiChannel = 1, midiInChannelMask = 0xffff;
int keyMappingOctave = 6;
float velocity = 1.0f;
bool useMousePositionForVelocity = true;
Array<int> mouseOverNotes, mouseDownNotes;
Array<KeyPress> keyPresses;
Array<int> keyPressNotes;
BigInteger keysPressed, keysCurrentlyDrawnDown;
std::atomic<bool> noPendingUpdates { true };
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiKeyboardComponent) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiKeyboardComponent)
}; };

View file

@ -58,7 +58,9 @@
#include "gui/juce_AudioThumbnail.cpp" #include "gui/juce_AudioThumbnail.cpp"
#include "gui/juce_AudioThumbnailCache.cpp" #include "gui/juce_AudioThumbnailCache.cpp"
#include "gui/juce_AudioVisualiserComponent.cpp" #include "gui/juce_AudioVisualiserComponent.cpp"
#include "gui/juce_KeyboardComponentBase.cpp"
#include "gui/juce_MidiKeyboardComponent.cpp" #include "gui/juce_MidiKeyboardComponent.cpp"
#include "gui/juce_MPEKeyboardComponent.cpp"
#include "gui/juce_AudioAppComponent.cpp" #include "gui/juce_AudioAppComponent.cpp"
#include "players/juce_SoundPlayer.cpp" #include "players/juce_SoundPlayer.cpp"
#include "players/juce_AudioProcessorPlayer.cpp" #include "players/juce_AudioProcessorPlayer.cpp"

View file

@ -80,7 +80,9 @@
#include "gui/juce_AudioThumbnail.h" #include "gui/juce_AudioThumbnail.h"
#include "gui/juce_AudioThumbnailCache.h" #include "gui/juce_AudioThumbnailCache.h"
#include "gui/juce_AudioVisualiserComponent.h" #include "gui/juce_AudioVisualiserComponent.h"
#include "gui/juce_KeyboardComponentBase.h"
#include "gui/juce_MidiKeyboardComponent.h" #include "gui/juce_MidiKeyboardComponent.h"
#include "gui/juce_MPEKeyboardComponent.h"
#include "gui/juce_AudioAppComponent.h" #include "gui/juce_AudioAppComponent.h"
#include "gui/juce_BluetoothMidiDevicePairingDialogue.h" #include "gui/juce_BluetoothMidiDevicePairingDialogue.h"
#include "players/juce_SoundPlayer.h" #include "players/juce_SoundPlayer.h"

View file

@ -64,6 +64,7 @@
#include <typeindex> #include <typeindex>
#include <unordered_set> #include <unordered_set>
#include <vector> #include <vector>
#include <set>
//============================================================================== //==============================================================================
#include "juce_CompilerSupport.h" #include "juce_CompilerSupport.h"

View file

@ -94,8 +94,6 @@
#endif #endif
#endif #endif
#include <set>
//============================================================================== //==============================================================================
#define JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED_OR_OFFSCREEN \ #define JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED_OR_OFFSCREEN \
jassert ((MessageManager::getInstanceWithoutCreating() != nullptr \ jassert ((MessageManager::getInstanceWithoutCreating() != nullptr \

View file

@ -188,15 +188,22 @@ LookAndFeel_V2::LookAndFeel_V2()
0x1000440, /*LassoComponent::lassoFillColourId*/ 0x66dddddd, 0x1000440, /*LassoComponent::lassoFillColourId*/ 0x66dddddd,
0x1000441, /*LassoComponent::lassoOutlineColourId*/ 0x99111111, 0x1000441, /*LassoComponent::lassoOutlineColourId*/ 0x99111111,
0x1004000, /*KeyboardComponentBase::upDownButtonBackgroundColourId*/ 0xffd3d3d3,
0x1004001, /*KeyboardComponentBase::upDownButtonArrowColourId*/ 0xff000000,
0x1005000, /*MidiKeyboardComponent::whiteNoteColourId*/ 0xffffffff, 0x1005000, /*MidiKeyboardComponent::whiteNoteColourId*/ 0xffffffff,
0x1005001, /*MidiKeyboardComponent::blackNoteColourId*/ 0xff000000, 0x1005001, /*MidiKeyboardComponent::blackNoteColourId*/ 0xff000000,
0x1005002, /*MidiKeyboardComponent::keySeparatorLineColourId*/ 0x66000000, 0x1005002, /*MidiKeyboardComponent::keySeparatorLineColourId*/ 0x66000000,
0x1005003, /*MidiKeyboardComponent::mouseOverKeyOverlayColourId*/ 0x80ffff00, 0x1005003, /*MidiKeyboardComponent::mouseOverKeyOverlayColourId*/ 0x80ffff00,
0x1005004, /*MidiKeyboardComponent::keyDownOverlayColourId*/ 0xffb6b600, 0x1005004, /*MidiKeyboardComponent::keyDownOverlayColourId*/ 0xffb6b600,
0x1005005, /*MidiKeyboardComponent::textLabelColourId*/ 0xff000000, 0x1005005, /*MidiKeyboardComponent::textLabelColourId*/ 0xff000000,
0x1005006, /*MidiKeyboardComponent::upDownButtonBackgroundColourId*/ 0xffd3d3d3, 0x1005006, /*MidiKeyboardComponent::shadowColourId*/ 0x4c000000,
0x1005007, /*MidiKeyboardComponent::upDownButtonArrowColourId*/ 0xff000000,
0x1005008, /*MidiKeyboardComponent::shadowColourId*/ 0x4c000000, 0x1006000, /*MPEKeyboardComponent::whiteNoteColourId*/ 0xff1a1c27,
0x1006001, /*MPEKeyboardComponent::blackNoteColourId*/ 0x99f1f1f1,
0x1006002, /*MPEKeyboardComponent::textLabelColourId*/ 0xfff1f1f1,
0x1006003, /*MPEKeyboardComponent::noteCircleFillColourId*/ 0x99ba00ff,
0x1006004, /*MPEKeyboardComponent::noteCircleOutlineColourId*/ 0xfff1f1f1,
0x1004500, /*CodeEditorComponent::backgroundColourId*/ 0xffffffff, 0x1004500, /*CodeEditorComponent::backgroundColourId*/ 0xffffffff,
0x1004502, /*CodeEditorComponent::highlightColourId*/ textHighlightColour, 0x1004502, /*CodeEditorComponent::highlightColourId*/ textHighlightColour,

View file

@ -1438,15 +1438,22 @@ void LookAndFeel_V4::initialiseColours()
0x1000440, /*LassoComponent::lassoFillColourId*/ currentColourScheme.getUIColour (ColourScheme::UIColour::defaultFill).getARGB(), 0x1000440, /*LassoComponent::lassoFillColourId*/ currentColourScheme.getUIColour (ColourScheme::UIColour::defaultFill).getARGB(),
0x1000441, /*LassoComponent::lassoOutlineColourId*/ currentColourScheme.getUIColour (ColourScheme::UIColour::outline).getARGB(), 0x1000441, /*LassoComponent::lassoOutlineColourId*/ currentColourScheme.getUIColour (ColourScheme::UIColour::outline).getARGB(),
0x1004000, /*KeyboardComponentBase::upDownButtonBackgroundColourId*/ 0xffd3d3d3,
0x1004001, /*KeyboardComponentBase::upDownButtonArrowColourId*/ 0xff000000,
0x1005000, /*MidiKeyboardComponent::whiteNoteColourId*/ 0xffffffff, 0x1005000, /*MidiKeyboardComponent::whiteNoteColourId*/ 0xffffffff,
0x1005001, /*MidiKeyboardComponent::blackNoteColourId*/ 0xff000000, 0x1005001, /*MidiKeyboardComponent::blackNoteColourId*/ 0xff000000,
0x1005002, /*MidiKeyboardComponent::keySeparatorLineColourId*/ 0x66000000, 0x1005002, /*MidiKeyboardComponent::keySeparatorLineColourId*/ 0x66000000,
0x1005003, /*MidiKeyboardComponent::mouseOverKeyOverlayColourId*/ 0x80ffff00, 0x1005003, /*MidiKeyboardComponent::mouseOverKeyOverlayColourId*/ 0x80ffff00,
0x1005004, /*MidiKeyboardComponent::keyDownOverlayColourId*/ 0xffb6b600, 0x1005004, /*MidiKeyboardComponent::keyDownOverlayColourId*/ 0xffb6b600,
0x1005005, /*MidiKeyboardComponent::textLabelColourId*/ 0xff000000, 0x1005005, /*MidiKeyboardComponent::textLabelColourId*/ 0xff000000,
0x1005006, /*MidiKeyboardComponent::upDownButtonBackgroundColourId*/ 0xffd3d3d3, 0x1005006, /*MidiKeyboardComponent::shadowColourId*/ 0x4c000000,
0x1005007, /*MidiKeyboardComponent::upDownButtonArrowColourId*/ 0xff000000,
0x1005008, /*MidiKeyboardComponent::shadowColourId*/ 0x4c000000, 0x1006000, /*MPEKeyboardComponent::whiteNoteColourId*/ 0xff1a1c27,
0x1006001, /*MPEKeyboardComponent::blackNoteColourId*/ 0x99f1f1f1,
0x1006002, /*MPEKeyboardComponent::textLabelColourId*/ 0xfff1f1f1,
0x1006003, /*MPEKeyboardComponent::noteCircleFillColourId*/ 0x99ba00ff,
0x1006004, /*MPEKeyboardComponent::noteCircleOutlineColourId*/ 0xfff1f1f1,
0x1004500, /*CodeEditorComponent::backgroundColourId*/ currentColourScheme.getUIColour (ColourScheme::UIColour::widgetBackground).getARGB(), 0x1004500, /*CodeEditorComponent::backgroundColourId*/ currentColourScheme.getUIColour (ColourScheme::UIColour::widgetBackground).getARGB(),
0x1004502, /*CodeEditorComponent::highlightColourId*/ currentColourScheme.getUIColour (ColourScheme::UIColour::defaultFill).withAlpha (0.4f).getARGB(), 0x1004502, /*CodeEditorComponent::highlightColourId*/ currentColourScheme.getUIColour (ColourScheme::UIColour::defaultFill).withAlpha (0.4f).getARGB(),