mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-09 23:34:20 +00:00
Add KeyboardComponentBase class for custom MIDI keyboard components and MPEKeyboardComponent class
This commit is contained in:
parent
461192b355
commit
e0e8e85d6b
28 changed files with 2219 additions and 1502 deletions
|
|
@ -48,6 +48,7 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
|
||||
//==============================================================================
|
||||
class ZoneColourPicker
|
||||
{
|
||||
|
|
@ -94,251 +95,12 @@ private:
|
|||
};
|
||||
|
||||
//==============================================================================
|
||||
class NoteComponent : 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
|
||||
class MPESetupComponent : public Component
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
Visualiser (ZoneColourPicker& zoneColourPicker)
|
||||
: colourPicker (zoneColourPicker)
|
||||
{}
|
||||
|
||||
//==============================================================================
|
||||
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 ¬e;
|
||||
|
||||
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()
|
||||
MPESetupComponent (MPEInstrument& instr)
|
||||
: instrument (instr)
|
||||
{
|
||||
addAndMakeVisible (isLowerZoneButton);
|
||||
isLowerZoneButton.setToggleState (true, NotificationType::dontSendNotification);
|
||||
|
|
@ -353,10 +115,13 @@ public:
|
|||
|
||||
addAndMakeVisible (setZoneButton);
|
||||
setZoneButton.onClick = [this] { setZoneButtonClicked(); };
|
||||
|
||||
addAndMakeVisible (clearAllZonesButton);
|
||||
clearAllZonesButton.onClick = [this] { clearAllZonesButtonClicked(); };
|
||||
|
||||
addAndMakeVisible (legacyModeEnabledToggle);
|
||||
legacyModeEnabledToggle.onClick = [this] { legacyModeEnabledToggleClicked(); };
|
||||
|
||||
addAndMakeVisible (voiceStealingEnabledToggle);
|
||||
voiceStealingEnabledToggle.onClick = [this] { voiceStealingEnabledToggleClicked(); };
|
||||
|
||||
|
|
@ -402,6 +167,12 @@ public:
|
|||
numberOfVoices.setBounds (r.removeFromTop (h));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool isVoiceStealingEnabled() const { return voiceStealingEnabledToggle.getToggleState(); }
|
||||
int getNumVoices() const { return numberOfVoices.getText().getIntValue(); }
|
||||
|
||||
std::function<void()> onSynthParametersChange;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
void initialiseComboBoxWithConsecutiveIntegers (ComboBox& comboBox, Label& labelToAttach,
|
||||
|
|
@ -435,22 +206,21 @@ private:
|
|||
auto perNotePb = notePitchbendRange.getText().getIntValue();
|
||||
auto masterPb = masterPitchbendRange.getText().getIntValue();
|
||||
|
||||
auto zoneLayout = instrument.getZoneLayout();
|
||||
|
||||
if (isLowerZone)
|
||||
zoneLayout.setLowerZone (numMemberChannels, perNotePb, masterPb);
|
||||
else
|
||||
zoneLayout.setUpperZone (numMemberChannels, perNotePb, masterPb);
|
||||
|
||||
listeners.call ([&] (Listener& l) { l.zoneChanged (isLowerZone, numMemberChannels, perNotePb, masterPb); });
|
||||
instrument.setZoneLayout (zoneLayout);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void clearAllZonesButtonClicked()
|
||||
{
|
||||
zoneLayout.clearAllZones();
|
||||
listeners.call ([] (Listener& l) { l.allZonesCleared(); });
|
||||
instrument.setZoneLayout ({});
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void legacyModeEnabledToggleClicked()
|
||||
{
|
||||
auto legacyModeEnabled = legacyModeEnabledToggle.getToggleState();
|
||||
|
|
@ -466,38 +236,32 @@ private:
|
|||
legacyEndChannel .setVisible (legacyModeEnabled);
|
||||
legacyPitchbendRange.setVisible (legacyModeEnabled);
|
||||
|
||||
if (areLegacyModeParametersValid())
|
||||
if (legacyModeEnabled)
|
||||
{
|
||||
listeners.call ([&] (Listener& l) { l.legacyModeChanged (legacyModeEnabledToggle.getToggleState(),
|
||||
legacyPitchbendRange.getText().getIntValue(),
|
||||
getLegacyModeChannelRange()); });
|
||||
if (areLegacyModeParametersValid())
|
||||
{
|
||||
instrument.enableLegacyMode();
|
||||
|
||||
instrument.setLegacyModeChannelRange (getLegacyModeChannelRange());
|
||||
instrument.setLegacyModePitchbendRange (getLegacyModePitchbendRange());
|
||||
}
|
||||
else
|
||||
{
|
||||
handleInvalidLegacyModeParameters();
|
||||
}
|
||||
}
|
||||
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()
|
||||
{
|
||||
jassert (legacyModeEnabledToggle.getToggleState() == true);
|
||||
|
||||
listeners.call ([this] (Listener& l) { l.legacyModeChanged (true,
|
||||
legacyPitchbendRange.getText().getIntValue(),
|
||||
getLegacyModeChannelRange()); });
|
||||
instrument.setLegacyModePitchbendRange (getLegacyModePitchbendRange());
|
||||
}
|
||||
|
||||
void legacyModeChannelRangeChanged()
|
||||
|
|
@ -505,18 +269,11 @@ private:
|
|||
jassert (legacyModeEnabledToggle.getToggleState() == true);
|
||||
|
||||
if (areLegacyModeParametersValid())
|
||||
{
|
||||
listeners.call ([this] (Listener& l) { l.legacyModeChanged (true,
|
||||
legacyPitchbendRange.getText().getIntValue(),
|
||||
getLegacyModeChannelRange()); });
|
||||
}
|
||||
instrument.setLegacyModeChannelRange (getLegacyModeChannelRange());
|
||||
else
|
||||
{
|
||||
handleInvalidLegacyModeParameters();
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool areLegacyModeParametersValid() const
|
||||
{
|
||||
return legacyStartChannel.getText().getIntValue() <= legacyEndChannel.getText().getIntValue();
|
||||
|
|
@ -531,15 +288,32 @@ private:
|
|||
"Got it");
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
Range<int> getLegacyModeChannelRange() const
|
||||
{
|
||||
return { legacyStartChannel.getText().getIntValue(),
|
||||
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;
|
||||
|
||||
|
|
@ -564,67 +338,49 @@ private:
|
|||
ComboBox numberOfVoices;
|
||||
Label numberOfVoicesLabel { {}, "Number of synth voices"};
|
||||
|
||||
ListenerList<Listener> listeners;
|
||||
|
||||
const int defaultMemberChannels = 15,
|
||||
defaultMasterPitchbendRange = 2,
|
||||
defaultNotePitchbendRange = 48;
|
||||
static constexpr int defaultMemberChannels = 15,
|
||||
defaultMasterPitchbendRange = 2,
|
||||
defaultNotePitchbendRange = 48;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPESetupComponent)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class ZoneLayoutComponent : public Component,
|
||||
public MPESetupComponent::Listener
|
||||
private MPEInstrument::Listener
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
ZoneLayoutComponent (const ZoneColourPicker& zoneColourPicker)
|
||||
: colourPicker (zoneColourPicker)
|
||||
{}
|
||||
ZoneLayoutComponent (MPEInstrument& instr, ZoneColourPicker& zoneColourPicker)
|
||||
: instrument (instr),
|
||||
colourPicker (zoneColourPicker)
|
||||
{
|
||||
instrument.addListener (this);
|
||||
}
|
||||
|
||||
~ZoneLayoutComponent() override
|
||||
{
|
||||
instrument.removeListener (this);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
paintBackground (g);
|
||||
|
||||
if (legacyModeEnabled)
|
||||
if (instrument.isLegacyModeEnabled())
|
||||
paintLegacyMode (g);
|
||||
else
|
||||
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:
|
||||
//==============================================================================
|
||||
void zoneLayoutChanged() override
|
||||
{
|
||||
repaint();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void paintBackground (Graphics& g)
|
||||
{
|
||||
|
|
@ -646,6 +402,8 @@ private:
|
|||
{
|
||||
auto channelWidth = getChannelRectangleWidth();
|
||||
|
||||
auto zoneLayout = instrument.getZoneLayout();
|
||||
|
||||
Array<MPEZoneLayout::Zone> activeZones;
|
||||
if (zoneLayout.getLowerZone().isActive()) activeZones.add (zoneLayout.getLowerZone());
|
||||
if (zoneLayout.getUpperZone().isActive()) activeZones.add (zoneLayout.getUpperZone());
|
||||
|
|
@ -676,9 +434,9 @@ private:
|
|||
//==============================================================================
|
||||
void paintLegacyMode (Graphics& g)
|
||||
{
|
||||
auto startChannel = legacyModeChannelRange.getStart() - 1;
|
||||
auto numChannels = legacyModeChannelRange.getEnd() - startChannel - 1;
|
||||
|
||||
auto channelRange = instrument.getLegacyModeChannelRange();
|
||||
auto startChannel = channelRange.getStart() - 1;
|
||||
auto numChannels = channelRange.getEnd() - startChannel - 1;
|
||||
|
||||
Rectangle<int> zoneRect (int (getChannelRectangleWidth() * (float) startChannel), 0,
|
||||
int (getChannelRectangleWidth() * (float) numChannels), getHeight());
|
||||
|
|
@ -688,7 +446,7 @@ private:
|
|||
g.setColour (Colours::white);
|
||||
g.drawRect (zoneRect, 3);
|
||||
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;
|
||||
const ZoneColourPicker& colourPicker;
|
||||
static constexpr int numMidiChannels = 16;
|
||||
|
||||
bool legacyModeEnabled = false;
|
||||
int legacyModePitchbendRange = 48;
|
||||
Range<int> legacyModeChannelRange = { 1, 17 };
|
||||
const int numMidiChannels = 16;
|
||||
MPEInstrument& instrument;
|
||||
ZoneColourPicker& colourPicker;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -867,14 +622,11 @@ private:
|
|||
class MPEDemo : public Component,
|
||||
private AudioIODeviceCallback,
|
||||
private MidiInputCallback,
|
||||
private MPESetupComponent::Listener
|
||||
private MPEInstrument::Listener
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
MPEDemo()
|
||||
: audioSetupComp (audioDeviceManager, 0, 0, 0, 256, true, true, true, false),
|
||||
zoneLayoutComp (colourPicker),
|
||||
visualiserComp (colourPicker)
|
||||
{
|
||||
#ifndef JUCE_DEMO_RUNNER
|
||||
audioDeviceManager.initialise (0, 2, nullptr, true, {}, nullptr);
|
||||
|
|
@ -884,22 +636,33 @@ public:
|
|||
audioDeviceManager.addAudioCallback (this);
|
||||
|
||||
addAndMakeVisible (audioSetupComp);
|
||||
addAndMakeVisible (MPESetupComp);
|
||||
addAndMakeVisible (mpeSetupComp);
|
||||
addAndMakeVisible (zoneLayoutComp);
|
||||
addAndMakeVisible (visualiserViewport);
|
||||
|
||||
visualiserViewport.setScrollBarsShown (false, true);
|
||||
visualiserViewport.setViewedComponent (&visualiserComp, false);
|
||||
visualiserViewport.setViewPositionProportionately (0.5, 0.0);
|
||||
|
||||
MPESetupComp.addListener (&zoneLayoutComp);
|
||||
MPESetupComp.addListener (this);
|
||||
visualiserInstrument.addListener (&visualiserComp);
|
||||
addAndMakeVisible (keyboardComponent);
|
||||
|
||||
synth.setVoiceStealingEnabled (false);
|
||||
for (auto i = 0; i < 15; ++i)
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -912,20 +675,17 @@ public:
|
|||
//==============================================================================
|
||||
void resized() override
|
||||
{
|
||||
auto visualiserCompWidth = 2800;
|
||||
auto visualiserCompHeight = 300;
|
||||
auto zoneLayoutCompHeight = 60;
|
||||
auto audioSetupCompRelativeWidth = 0.55f;
|
||||
|
||||
auto r = getLocalBounds();
|
||||
|
||||
visualiserViewport.setBounds (r.removeFromBottom (visualiserCompHeight));
|
||||
visualiserComp .setBounds ({ visualiserCompWidth,
|
||||
visualiserViewport.getHeight() - visualiserViewport.getScrollBarThickness() });
|
||||
keyboardComponent.setBounds (r.removeFromBottom (150));
|
||||
r.reduce (10, 10);
|
||||
|
||||
zoneLayoutComp.setBounds (r.removeFromBottom (zoneLayoutCompHeight));
|
||||
audioSetupComp.setBounds (r.removeFromLeft (proportionOfWidth (audioSetupCompRelativeWidth)));
|
||||
MPESetupComp .setBounds (r);
|
||||
mpeSetupComp .setBounds (r);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -955,75 +715,34 @@ private:
|
|||
void handleIncomingMidiMessage (MidiInput* /*source*/,
|
||||
const MidiMessage& message) override
|
||||
{
|
||||
visualiserInstrument.processNextMidiEvent (message);
|
||||
instrument.processNextMidiEvent (message);
|
||||
midiCollector.addMessageToQueue (message);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void zoneChanged (bool isLowerZone, int numMemberChannels,
|
||||
int perNotePitchbendRange, int masterPitchbendRange) override
|
||||
void zoneLayoutChanged() override
|
||||
{
|
||||
auto* midiOutput = audioDeviceManager.getDefaultMidiOutput();
|
||||
if (midiOutput != nullptr)
|
||||
if (instrument.isLegacyModeEnabled())
|
||||
{
|
||||
if (isLowerZone)
|
||||
midiOutput->sendBlockOfMessagesNow (MPEMessages::setLowerZone (numMemberChannels, perNotePitchbendRange, masterPitchbendRange));
|
||||
else
|
||||
midiOutput->sendBlockOfMessagesNow (MPEMessages::setUpperZone (numMemberChannels, perNotePitchbendRange, masterPitchbendRange));
|
||||
}
|
||||
colourPicker.setLegacyModeEnabled (true);
|
||||
|
||||
if (isLowerZone)
|
||||
zoneLayout.setLowerZone (numMemberChannels, perNotePitchbendRange, masterPitchbendRange);
|
||||
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);
|
||||
synth.enableLegacyMode (instrument.getLegacyModePitchbendRange(),
|
||||
instrument.getLegacyModeChannelRange());
|
||||
}
|
||||
else
|
||||
{
|
||||
colourPicker.setLegacyModeEnabled (false);
|
||||
|
||||
auto zoneLayout = instrument.getZoneLayout();
|
||||
|
||||
if (auto* midiOutput = audioDeviceManager.getDefaultMidiOutput())
|
||||
midiOutput->sendBlockOfMessagesNow (MPEMessages::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
|
||||
#ifndef JUCE_DEMO_RUNNER
|
||||
|
|
@ -1032,19 +751,18 @@ private:
|
|||
AudioDeviceManager& audioDeviceManager { getSharedAudioDeviceManager (0, 2) };
|
||||
#endif
|
||||
|
||||
MPEZoneLayout zoneLayout;
|
||||
ZoneColourPicker colourPicker;
|
||||
|
||||
AudioDeviceSelectorComponent audioSetupComp;
|
||||
MPESetupComponent MPESetupComp;
|
||||
ZoneLayoutComponent zoneLayoutComp;
|
||||
|
||||
Visualiser visualiserComp;
|
||||
Viewport visualiserViewport;
|
||||
MPEInstrument visualiserInstrument;
|
||||
|
||||
MPESynthesiser synth;
|
||||
AudioDeviceSelectorComponent audioSetupComp { audioDeviceManager, 0, 0, 0, 256, true, true, true, false };
|
||||
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)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -605,22 +605,6 @@ private:
|
|||
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<>
|
||||
struct VariantConverter<LoopMode>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -43,16 +43,20 @@ MPEInstrument::MPEInstrument() noexcept
|
|||
mpeInstrumentFill (isMemberChannelSustained, false);
|
||||
|
||||
pitchbendDimension.value = &MPENote::pitchbend;
|
||||
pressureDimension.value = &MPENote::pressure;
|
||||
timbreDimension.value = &MPENote::timbre;
|
||||
pressureDimension.value = &MPENote::pressure;
|
||||
timbreDimension.value = &MPENote::timbre;
|
||||
|
||||
resetLastReceivedValues();
|
||||
|
||||
legacyMode.isEnabled = false;
|
||||
legacyMode.pitchbendRange = 2;
|
||||
legacyMode.channelRange = allChannels;
|
||||
}
|
||||
|
||||
MPEInstrument::MPEInstrument (MPEZoneLayout layout)
|
||||
: MPEInstrument()
|
||||
{
|
||||
setZoneLayout (layout);
|
||||
}
|
||||
|
||||
MPEInstrument::~MPEInstrument() = default;
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -84,21 +88,30 @@ void MPEInstrument::setZoneLayout (MPEZoneLayout newLayout)
|
|||
|
||||
const ScopedLock sl (lock);
|
||||
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)
|
||||
{
|
||||
if (legacyMode.isEnabled)
|
||||
return;
|
||||
|
||||
releaseAllNotes();
|
||||
|
||||
const ScopedLock sl (lock);
|
||||
|
||||
legacyMode.isEnabled = true;
|
||||
legacyMode.pitchbendRange = pitchbendRange;
|
||||
legacyMode.channelRange = channelRange;
|
||||
|
||||
zoneLayout.clearAllZones();
|
||||
listeners.call ([=] (Listener& l) { l.zoneLayoutChanged(); });
|
||||
}
|
||||
|
||||
bool MPEInstrument::isLegacyModeEnabled() const noexcept
|
||||
|
|
@ -117,7 +130,12 @@ void MPEInstrument::setLegacyModeChannelRange (Range<int> channelRange)
|
|||
|
||||
releaseAllNotes();
|
||||
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
|
||||
|
|
@ -131,7 +149,12 @@ void MPEInstrument::setLegacyModePitchbendRange (int pitchbendRange)
|
|||
|
||||
releaseAllNotes();
|
||||
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()))
|
||||
{
|
||||
for (auto i = notes.size(); --i >= 0;)
|
||||
for (int i = notes.size(); --i >= 0;)
|
||||
{
|
||||
auto& note = notes.getReference (i);
|
||||
|
||||
|
|
@ -260,7 +283,7 @@ void MPEInstrument::processMidiResetAllControllersMessage (const MidiMessage& me
|
|||
auto zone = (message.getChannel() == 1 ? zoneLayout.getLowerZone()
|
||||
: zoneLayout.getUpperZone());
|
||||
|
||||
for (auto i = notes.size(); --i >= 0;)
|
||||
for (int i = notes.size(); --i >= 0;)
|
||||
{
|
||||
auto& note = notes.getReference (i);
|
||||
|
||||
|
|
@ -348,11 +371,11 @@ void MPEInstrument::noteOff (int midiChannel,
|
|||
int midiNoteNumber,
|
||||
MPEValue midiNoteOffVelocity)
|
||||
{
|
||||
const ScopedLock sl (lock);
|
||||
|
||||
if (notes.isEmpty() || ! isUsingChannel (midiChannel))
|
||||
return;
|
||||
|
||||
const ScopedLock sl (lock);
|
||||
|
||||
if (auto* note = getNotePtr (midiChannel, midiNoteNumber))
|
||||
{
|
||||
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);
|
||||
|
||||
for (auto i = notes.size(); --i >= 0;)
|
||||
for (int i = notes.size(); --i >= 0;)
|
||||
{
|
||||
auto& note = notes.getReference (i);
|
||||
|
||||
|
|
@ -435,7 +458,7 @@ void MPEInstrument::updateDimension (int midiChannel, MPEDimension& dimension, M
|
|||
{
|
||||
if (dimension.trackingMode == allNotesOnChannel)
|
||||
{
|
||||
for (auto i = notes.size(); --i >= 0;)
|
||||
for (int i = notes.size(); --i >= 0;)
|
||||
{
|
||||
auto& note = notes.getReference (i);
|
||||
|
||||
|
|
@ -464,7 +487,7 @@ void MPEInstrument::updateDimensionMaster (bool isLowerZone, MPEDimension& dimen
|
|||
if (! zone.isActive())
|
||||
return;
|
||||
|
||||
for (auto i = notes.size(); --i >= 0;)
|
||||
for (int i = notes.size(); --i >= 0;)
|
||||
{
|
||||
auto& note = notes.getReference (i);
|
||||
|
||||
|
|
@ -573,7 +596,7 @@ void MPEInstrument::handleSustainOrSostenuto (int midiChannel, bool isDown, bool
|
|||
auto zone = (midiChannel == 1 ? zoneLayout.getLowerZone()
|
||||
: zoneLayout.getUpperZone());
|
||||
|
||||
for (auto i = notes.size(); --i >= 0;)
|
||||
for (int i = notes.size(); --i >= 0;)
|
||||
{
|
||||
auto& note = notes.getReference (i);
|
||||
|
||||
|
|
@ -605,11 +628,15 @@ void MPEInstrument::handleSustainOrSostenuto (int midiChannel, bool isDown, bool
|
|||
if (! legacyMode.isEnabled)
|
||||
{
|
||||
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;
|
||||
}
|
||||
else
|
||||
for (auto i = zone.getFirstMemberChannel(); i >= zone.getLastMemberChannel(); --i)
|
||||
{
|
||||
for (int i = zone.getFirstMemberChannel(); i >= zone.getLastMemberChannel(); --i)
|
||||
isMemberChannelSustained[i - 1] = isDown;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -664,6 +691,17 @@ MPENote MPEInstrument::getNote (int index) const noexcept
|
|||
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
|
||||
{
|
||||
|
|
@ -727,6 +765,8 @@ MPENote* MPEInstrument::getNotePtr (int midiChannel, TrackingMode mode) noexcept
|
|||
//==============================================================================
|
||||
const MPENote* MPEInstrument::getLastNotePlayedPtr (int midiChannel) const noexcept
|
||||
{
|
||||
const ScopedLock sl (lock);
|
||||
|
||||
for (auto i = notes.size(); --i >= 0;)
|
||||
{
|
||||
auto& note = notes.getReference (i);
|
||||
|
|
|
|||
|
|
@ -38,10 +38,8 @@ namespace juce
|
|||
MPE. If you pass it a message, it will know what notes on what
|
||||
channels (if any) should be affected by that message.
|
||||
|
||||
The class has a Listener class with the three callbacks MPENoteAdded,
|
||||
MPENoteChanged, and MPENoteFinished. Implement such a
|
||||
Listener class to react to note changes and trigger some functionality for
|
||||
your application that depends on the MPE note state.
|
||||
The class has a Listener class that can be used to react to note and
|
||||
state changes and trigger some functionality for your application.
|
||||
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,
|
||||
|
|
@ -59,11 +57,14 @@ public:
|
|||
|
||||
This will construct an MPE instrument with inactive lower and upper zones.
|
||||
|
||||
In order to process incoming MIDI, call setZoneLayout, define the layout
|
||||
via MIDI RPN messages, or set the instrument to legacy mode.
|
||||
In order to process incoming MIDI messages call setZoneLayout, use the MPEZoneLayout
|
||||
constructor, define the layout via MIDI RPN messages, or set the instrument to legacy mode.
|
||||
*/
|
||||
MPEInstrument() noexcept;
|
||||
|
||||
/** Constructs an MPE instrument with the specified zone layout. */
|
||||
MPEInstrument (MPEZoneLayout layout);
|
||||
|
||||
/** Destructor. */
|
||||
virtual ~MPEInstrument();
|
||||
|
||||
|
|
@ -229,6 +230,9 @@ public:
|
|||
*/
|
||||
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
|
||||
(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
|
||||
|
|
@ -244,8 +248,8 @@ public:
|
|||
MPENote getMostRecentNoteOtherThan (MPENote otherThanThisNote) const noexcept;
|
||||
|
||||
//==============================================================================
|
||||
/** Derive from this class to be informed about any changes in the expressive
|
||||
MIDI notes played by this instrument.
|
||||
/** Derive from this class to be informed about any changes in the MPE notes played
|
||||
by this instrument, and any changes to its zone layout.
|
||||
|
||||
Note: This listener type receives its callbacks immediately, and not
|
||||
via the message thread (so you might be for example in the MIDI thread).
|
||||
|
|
@ -297,6 +301,11 @@ public:
|
|||
and should therefore stop playing.
|
||||
*/
|
||||
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);
|
||||
|
||||
//==============================================================================
|
||||
/** 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,
|
||||
and call noteReleased for all of them.
|
||||
|
||||
|
|
@ -360,9 +371,9 @@ private:
|
|||
|
||||
struct LegacyMode
|
||||
{
|
||||
bool isEnabled;
|
||||
bool isEnabled = false;
|
||||
Range<int> channelRange;
|
||||
int pitchbendRange;
|
||||
int pitchbendRange = 2;
|
||||
};
|
||||
|
||||
struct MPEDimension
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ struct JUCE_API MPENote
|
|||
*/
|
||||
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.
|
||||
|
||||
Note: This value is not aware of the currently used pitchbend range,
|
||||
|
|
|
|||
|
|
@ -25,12 +25,10 @@ namespace juce
|
|||
|
||||
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.
|
||||
instrument->releaseAllNotes();
|
||||
instrument.releaseAllNotes();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
|
|||
|
|
@ -65,11 +65,10 @@ public:
|
|||
/** Constructor to pass to the synthesiser a custom MPEInstrument object
|
||||
to handle the MPE note state, MIDI channel assignment etc.
|
||||
(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
|
||||
*/
|
||||
MPESynthesiser (MPEInstrument* instrumentToUse);
|
||||
MPESynthesiser (MPEInstrument& instrumentToUse);
|
||||
|
||||
/** Destructor. */
|
||||
~MPESynthesiser() override;
|
||||
|
|
@ -303,7 +302,7 @@ protected:
|
|||
|
||||
private:
|
||||
//==============================================================================
|
||||
bool shouldStealVoices = false;
|
||||
std::atomic<bool> shouldStealVoices { false };
|
||||
uint32 lastNoteOnCounter = 0;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPESynthesiser)
|
||||
|
|
|
|||
|
|
@ -24,80 +24,79 @@ namespace juce
|
|||
{
|
||||
|
||||
MPESynthesiserBase::MPESynthesiserBase()
|
||||
: instrument (new MPEInstrument)
|
||||
: instrument (defaultInstrument)
|
||||
{
|
||||
instrument->addListener (this);
|
||||
instrument.addListener (this);
|
||||
}
|
||||
|
||||
MPESynthesiserBase::MPESynthesiserBase (MPEInstrument* inst)
|
||||
MPESynthesiserBase::MPESynthesiserBase (MPEInstrument& inst)
|
||||
: instrument (inst)
|
||||
{
|
||||
jassert (instrument != nullptr);
|
||||
instrument->addListener (this);
|
||||
instrument.addListener (this);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
MPEZoneLayout MPESynthesiserBase::getZoneLayout() const noexcept
|
||||
{
|
||||
return instrument->getZoneLayout();
|
||||
return instrument.getZoneLayout();
|
||||
}
|
||||
|
||||
void MPESynthesiserBase::setZoneLayout (MPEZoneLayout newLayout)
|
||||
{
|
||||
instrument->setZoneLayout (newLayout);
|
||||
instrument.setZoneLayout (newLayout);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void MPESynthesiserBase::enableLegacyMode (int pitchbendRange, Range<int> channelRange)
|
||||
{
|
||||
instrument->enableLegacyMode (pitchbendRange, channelRange);
|
||||
instrument.enableLegacyMode (pitchbendRange, channelRange);
|
||||
}
|
||||
|
||||
bool MPESynthesiserBase::isLegacyModeEnabled() const noexcept
|
||||
{
|
||||
return instrument->isLegacyModeEnabled();
|
||||
return instrument.isLegacyModeEnabled();
|
||||
}
|
||||
|
||||
Range<int> MPESynthesiserBase::getLegacyModeChannelRange() const noexcept
|
||||
{
|
||||
return instrument->getLegacyModeChannelRange();
|
||||
return instrument.getLegacyModeChannelRange();
|
||||
}
|
||||
|
||||
void MPESynthesiserBase::setLegacyModeChannelRange (Range<int> channelRange)
|
||||
{
|
||||
instrument->setLegacyModeChannelRange (channelRange);
|
||||
instrument.setLegacyModeChannelRange (channelRange);
|
||||
}
|
||||
|
||||
int MPESynthesiserBase::getLegacyModePitchbendRange() const noexcept
|
||||
{
|
||||
return instrument->getLegacyModePitchbendRange();
|
||||
return instrument.getLegacyModePitchbendRange();
|
||||
}
|
||||
|
||||
void MPESynthesiserBase::setLegacyModePitchbendRange (int pitchbendRange)
|
||||
{
|
||||
instrument->setLegacyModePitchbendRange (pitchbendRange);
|
||||
instrument.setLegacyModePitchbendRange (pitchbendRange);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void MPESynthesiserBase::setPressureTrackingMode (TrackingMode modeToUse)
|
||||
{
|
||||
instrument->setPressureTrackingMode (modeToUse);
|
||||
instrument.setPressureTrackingMode (modeToUse);
|
||||
}
|
||||
|
||||
void MPESynthesiserBase::setPitchbendTrackingMode (TrackingMode modeToUse)
|
||||
{
|
||||
instrument->setPitchbendTrackingMode (modeToUse);
|
||||
instrument.setPitchbendTrackingMode (modeToUse);
|
||||
}
|
||||
|
||||
void MPESynthesiserBase::setTimbreTrackingMode (TrackingMode modeToUse)
|
||||
{
|
||||
instrument->setTimbreTrackingMode (modeToUse);
|
||||
instrument.setTimbreTrackingMode (modeToUse);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
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)
|
||||
{
|
||||
const ScopedLock sl (noteStateLock);
|
||||
instrument->releaseAllNotes();
|
||||
instrument.releaseAllNotes();
|
||||
sampleRate = newRate;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,13 +52,12 @@ public:
|
|||
|
||||
/** Constructor.
|
||||
|
||||
If you use this constructor, the synthesiser will take ownership of the
|
||||
provided instrument object, and will use it internally to handle the
|
||||
MPE note state logic.
|
||||
If you use this constructor, the synthesiser will use the provided instrument
|
||||
object to handle the MPE note state logic.
|
||||
This is useful if you want to use an instance of your own class derived
|
||||
from MPEInstrument for the MPE logic.
|
||||
*/
|
||||
MPESynthesiserBase (MPEInstrument* instrument);
|
||||
MPESynthesiserBase (MPEInstrument& instrument);
|
||||
|
||||
//==============================================================================
|
||||
/** Returns the synthesiser's internal MPE zone layout.
|
||||
|
|
@ -200,10 +199,12 @@ protected:
|
|||
protected:
|
||||
//==============================================================================
|
||||
/** @internal */
|
||||
std::unique_ptr<MPEInstrument> instrument;
|
||||
MPEInstrument& instrument;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
MPEInstrument defaultInstrument { MPEZone (MPEZone::Type::lower, 15) };
|
||||
|
||||
CriticalSection noteStateLock;
|
||||
double sampleRate = 0.0;
|
||||
int minimumSubBlockSize = 32;
|
||||
|
|
|
|||
|
|
@ -52,25 +52,25 @@ int MPEChannelAssigner::findMidiChannelForNewNote (int noteNumber) noexcept
|
|||
if (numChannels <= 1)
|
||||
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;
|
||||
midiChannels[ch].notes.add (noteNumber);
|
||||
midiChannels[(size_t) ch].notes.add (noteNumber);
|
||||
return ch;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto ch = midiChannelLastAssigned + channelIncrement; ; ch += channelIncrement)
|
||||
for (int ch = midiChannelLastAssigned + channelIncrement; ; ch += channelIncrement)
|
||||
{
|
||||
if (ch == lastChannel + channelIncrement) // loop wrap-around
|
||||
ch = firstChannel;
|
||||
|
||||
if (midiChannels[ch].isFree())
|
||||
if (midiChannels[(size_t) ch].isFree())
|
||||
{
|
||||
midiChannelLastAssigned = ch;
|
||||
midiChannels[ch].notes.add (noteNumber);
|
||||
midiChannels[(size_t) ch].notes.add (noteNumber);
|
||||
return ch;
|
||||
}
|
||||
|
||||
|
|
@ -79,11 +79,21 @@ int MPEChannelAssigner::findMidiChannelForNewNote (int noteNumber) noexcept
|
|||
}
|
||||
|
||||
midiChannelLastAssigned = findMidiChannelPlayingClosestNonequalNote (noteNumber);
|
||||
midiChannels[midiChannelLastAssigned].notes.add (noteNumber);
|
||||
midiChannels[(size_t) midiChannelLastAssigned].notes.add (noteNumber);
|
||||
|
||||
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)
|
||||
{
|
||||
const auto removeNote = [] (MidiChannel& ch, int noteNum)
|
||||
|
|
@ -99,7 +109,7 @@ void MPEChannelAssigner::noteOff (int noteNumber, int midiChannel)
|
|||
|
||||
if (midiChannel >= 0 && midiChannel <= 16)
|
||||
{
|
||||
removeNote (midiChannels[midiChannel], noteNumber);
|
||||
removeNote (midiChannels[(size_t) midiChannel], noteNumber);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -126,9 +136,9 @@ int MPEChannelAssigner::findMidiChannelPlayingClosestNonequalNote (int noteNumbe
|
|||
auto channelWithClosestNote = firstChannel;
|
||||
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);
|
||||
|
||||
|
|
@ -296,24 +306,35 @@ struct MPEUtilsUnitTests : public UnitTest
|
|||
// check that channels are assigned in correct order
|
||||
int noteNum = 60;
|
||||
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
|
||||
channelAssigner.noteOff (60);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (60), 2);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (60), 2);
|
||||
|
||||
channelAssigner.noteOff (61);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (61), 3);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (61), 3);
|
||||
|
||||
// check that assigned channel was last to play note
|
||||
channelAssigner.noteOff (65);
|
||||
channelAssigner.noteOff (66);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (66), 8);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 7);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 8);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 7);
|
||||
|
||||
// find closest channel playing nonequal note
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 2);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 16);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 2);
|
||||
|
||||
// all notes off
|
||||
channelAssigner.allNotesOff();
|
||||
|
|
@ -323,10 +344,16 @@ struct MPEUtilsUnitTests : public UnitTest
|
|||
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 7);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
|
||||
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
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (101), 3);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (20), 4);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (101), 3);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (20), 4);
|
||||
}
|
||||
|
||||
// upper
|
||||
|
|
@ -339,24 +366,35 @@ struct MPEUtilsUnitTests : public UnitTest
|
|||
// check that channels are assigned in correct order
|
||||
int noteNum = 60;
|
||||
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
|
||||
channelAssigner.noteOff (60);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (60), 15);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (60), 15);
|
||||
|
||||
channelAssigner.noteOff (61);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (61), 14);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (61), 14);
|
||||
|
||||
// check that assigned channel was last to play note
|
||||
channelAssigner.noteOff (65);
|
||||
channelAssigner.noteOff (66);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (66), 9);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 10);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 9);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 10);
|
||||
|
||||
// find closest channel playing nonequal note
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 1);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 15);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 1);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 15);
|
||||
|
||||
// all notes off
|
||||
channelAssigner.allNotesOff();
|
||||
|
|
@ -366,10 +404,16 @@ struct MPEUtilsUnitTests : public UnitTest
|
|||
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 10);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 1);
|
||||
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
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (101), 14);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (20), 13);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (101), 14);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (20), 13);
|
||||
}
|
||||
|
||||
// legacy
|
||||
|
|
@ -379,24 +423,35 @@ struct MPEUtilsUnitTests : public UnitTest
|
|||
// check that channels are assigned in correct order
|
||||
int noteNum = 60;
|
||||
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
|
||||
channelAssigner.noteOff (60);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (60), 1);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (60), 1);
|
||||
|
||||
channelAssigner.noteOff (61);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (61), 2);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (61), 2);
|
||||
|
||||
// check that assigned channel was last to play note
|
||||
channelAssigner.noteOff (65);
|
||||
channelAssigner.noteOff (66);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (66), 7);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 6);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 7);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 6);
|
||||
|
||||
// find closest channel playing nonequal note
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 1);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 16);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 1);
|
||||
|
||||
// all notes off
|
||||
channelAssigner.allNotesOff();
|
||||
|
|
@ -406,10 +461,16 @@ struct MPEUtilsUnitTests : public UnitTest
|
|||
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 6);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
|
||||
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
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (101), 2);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (20), 3);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (101), 2);
|
||||
expectEquals (channelAssigner.findMidiChannelForExistingNote (20), 3);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,11 @@ public:
|
|||
*/
|
||||
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
|
||||
can keep track of the currently playing notes internally.
|
||||
|
||||
|
|
@ -86,7 +91,7 @@ private:
|
|||
int lastNotePlayed = -1;
|
||||
bool isFree() const noexcept { return notes.isEmpty(); }
|
||||
};
|
||||
MidiChannel midiChannels[17];
|
||||
std::array<MidiChannel, 17> midiChannels;
|
||||
|
||||
//==============================================================================
|
||||
int findMidiChannelPlayingClosestNonequalNote (int noteNumber) noexcept;
|
||||
|
|
|
|||
|
|
@ -43,6 +43,18 @@ MPEValue MPEValue::from14BitInt (int value) noexcept
|
|||
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::centreValue() noexcept { return MPEValue::from7BitInt (64); }
|
||||
|
|
@ -121,26 +133,34 @@ public:
|
|||
|
||||
beginTest ("zero/minimum value");
|
||||
{
|
||||
expectValuesConsistent (MPEValue::from7BitInt (0), 0, 0, -1.0f, 0.0f);
|
||||
expectValuesConsistent (MPEValue::from14BitInt (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::fromUnsignedFloat (0.0f), 0, 0, -1.0f, 0.0f);
|
||||
expectValuesConsistent (MPEValue::fromSignedFloat (-1.0f), 0, 0, -1.0f, 0.0f);
|
||||
}
|
||||
|
||||
beginTest ("maximum value");
|
||||
{
|
||||
expectValuesConsistent (MPEValue::from7BitInt (127), 127, 16383, 1.0f, 1.0f);
|
||||
expectValuesConsistent (MPEValue::from14BitInt (16383), 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::fromUnsignedFloat (1.0f), 127, 16383, 1.0f, 1.0f);
|
||||
expectValuesConsistent (MPEValue::fromSignedFloat (1.0f), 127, 16383, 1.0f, 1.0f);
|
||||
}
|
||||
|
||||
beginTest ("centre value");
|
||||
{
|
||||
expectValuesConsistent (MPEValue::from7BitInt (64), 64, 8192, 0.0f, 0.5f);
|
||||
expectValuesConsistent (MPEValue::from14BitInt (8192), 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::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");
|
||||
{
|
||||
expectValuesConsistent (MPEValue::from7BitInt (32), 32, 4096, -0.5f, 0.25f);
|
||||
expectValuesConsistent (MPEValue::from14BitInt (4096), 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::fromUnsignedFloat (0.25f), 32, 4096, -0.5f, 0.25f);
|
||||
expectValuesConsistent (MPEValue::fromSignedFloat (-0.5f), 32, 4096, -0.5f, 0.25f);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,12 @@ public:
|
|||
*/
|
||||
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. */
|
||||
static MPEValue centreValue() noexcept;
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,17 @@
|
|||
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)
|
||||
: lowerZone (other.lowerZone),
|
||||
|
|
@ -54,9 +64,9 @@ void MPEZoneLayout::setZone (bool isLower, int numMemberChannels, int perNotePit
|
|||
checkAndLimitZoneParameters (0, 96, masterPitchbendRange);
|
||||
|
||||
if (isLower)
|
||||
lowerZone = { true, numMemberChannels, perNotePitchbendRange, masterPitchbendRange };
|
||||
lowerZone = { MPEZone::Type::lower, numMemberChannels, perNotePitchbendRange, masterPitchbendRange };
|
||||
else
|
||||
upperZone = { false, numMemberChannels, perNotePitchbendRange, masterPitchbendRange };
|
||||
upperZone = { MPEZone::Type::upper, numMemberChannels, perNotePitchbendRange, masterPitchbendRange };
|
||||
|
||||
if (numMemberChannels > 0)
|
||||
{
|
||||
|
|
@ -86,8 +96,8 @@ void MPEZoneLayout::setUpperZone (int numMemberChannels, int perNotePitchbendRan
|
|||
|
||||
void MPEZoneLayout::clearAllZones()
|
||||
{
|
||||
lowerZone = { true, 0 };
|
||||
upperZone = { false, 0 };
|
||||
lowerZone = { MPEZone::Type::lower, 0 };
|
||||
upperZone = { MPEZone::Type::upper, 0 };
|
||||
|
||||
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)
|
||||
{
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -23,6 +23,83 @@
|
|||
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.
|
||||
|
|
@ -44,89 +121,28 @@ namespace juce
|
|||
class JUCE_API MPEZoneLayout
|
||||
{
|
||||
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
|
||||
a device with MPE mode disabled.
|
||||
/** Creates a layout with the given upper and lower zones. */
|
||||
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);
|
||||
|
||||
/** Copy assignment operator.
|
||||
This will not copy the listeners registered to the MPEZoneLayout.
|
||||
*/
|
||||
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); }
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
This struct represents an MPE zone.
|
||||
/** Returns a struct representing the lower MPE zone. */
|
||||
MPEZone getLowerZone() const noexcept { return lowerZone; }
|
||||
|
||||
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 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;
|
||||
};
|
||||
/** Returns a struct representing the upper MPE zone. */
|
||||
MPEZone getUpperZone() const noexcept { return upperZone; }
|
||||
|
||||
/** Sets the lower zone of this layout. */
|
||||
void setLowerZone (int numMemberChannels = 0,
|
||||
|
|
@ -138,17 +154,14 @@ public:
|
|||
int perNotePitchbendRange = 48,
|
||||
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
|
||||
and disabling MPE mode.
|
||||
*/
|
||||
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
|
||||
zone layout to properly react to MPE RPN messages like an
|
||||
|
|
@ -200,10 +213,14 @@ public:
|
|||
/** Removes a listener. */
|
||||
void removeListener (Listener* const listenerToRemove) noexcept;
|
||||
|
||||
#ifndef DOXYGEN
|
||||
using Zone = MPEZone;
|
||||
#endif
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
Zone lowerZone { true, 0 };
|
||||
Zone upperZone { false, 0 };
|
||||
MPEZone lowerZone { MPEZone::Type::lower, 0 };
|
||||
MPEZone upperZone { MPEZone::Type::upper, 0 };
|
||||
|
||||
MidiRPNDetector rpnDetector;
|
||||
ListenerList<Listener> listeners;
|
||||
|
|
@ -215,8 +232,8 @@ private:
|
|||
void processZoneLayoutRpnMessage (MidiRPNMessage);
|
||||
void processPitchbendRangeRpnMessage (MidiRPNMessage);
|
||||
|
||||
void updateMasterPitchbend (Zone&, int);
|
||||
void updatePerNotePitchbendRange (Zone&, int);
|
||||
void updateMasterPitchbend (MPEZone&, int);
|
||||
void updatePerNotePitchbendRange (MPEZone&, int);
|
||||
|
||||
void sendLayoutChangeMessage();
|
||||
void checkAndLimitZoneParameters (int, int, int&) noexcept;
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@
|
|||
|
||||
#include "juce_audio_processors.h"
|
||||
#include <juce_gui_extra/juce_gui_extra.h>
|
||||
#include <set>
|
||||
|
||||
//==============================================================================
|
||||
#if JUCE_MAC
|
||||
|
|
|
|||
452
modules/juce_audio_utils/gui/juce_KeyboardComponentBase.cpp
Normal file
452
modules/juce_audio_utils/gui/juce_KeyboardComponentBase.cpp
Normal 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
|
||||
295
modules/juce_audio_utils/gui/juce_KeyboardComponentBase.h
Normal file
295
modules/juce_audio_utils/gui/juce_KeyboardComponentBase.h
Normal 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
|
||||
507
modules/juce_audio_utils/gui/juce_MPEKeyboardComponent.cpp
Normal file
507
modules/juce_audio_utils/gui/juce_MPEKeyboardComponent.cpp
Normal 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 = [¬esToUpdate] (std::unique_ptr<MPENoteComponent>& comp)
|
||||
{
|
||||
return std::none_of (notesToUpdate.begin(),
|
||||
notesToUpdate.end(),
|
||||
[&comp] (auto& note) { return comp->sourceID == note.noteID; });
|
||||
};
|
||||
|
||||
noteComponents.erase (std::remove_if (std::begin (noteComponents),
|
||||
std::end (noteComponents),
|
||||
removePredicate),
|
||||
std::end (noteComponents));
|
||||
|
||||
if (noteComponents.empty())
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
void MPEKeyboardComponent::updateNoteComponentBounds (const MPENote& note, MPENoteComponent& noteComponent)
|
||||
{
|
||||
auto xPos = [&]
|
||||
{
|
||||
const auto currentNote = note.initialNote + (float) note.totalPitchbendInSemitones;
|
||||
const auto noteBend = currentNote - std::floor (currentNote);
|
||||
|
||||
const auto noteBounds = getRectangleForKey ((int) currentNote);
|
||||
const auto nextNoteBounds = getRectangleForKey ((int) currentNote + 1);
|
||||
|
||||
const auto horizontal = isHorizontal();
|
||||
|
||||
const auto distance = noteBend * (horizontal ? nextNoteBounds.getCentreX() - noteBounds.getCentreX()
|
||||
: nextNoteBounds.getCentreY() - noteBounds.getCentreY());
|
||||
|
||||
return (horizontal ? noteBounds.getCentreX() : noteBounds.getCentreY()) + distance;
|
||||
}();
|
||||
|
||||
auto yPos = [&]
|
||||
{
|
||||
const auto currentOrientation = getOrientation();
|
||||
|
||||
const auto timbrePosition = (currentOrientation == horizontalKeyboard
|
||||
|| currentOrientation == verticalKeyboardFacingRight ? 1.0f - note.timbre.asUnsignedFloat()
|
||||
: note.timbre.asUnsignedFloat());
|
||||
|
||||
return timbrePosition * getWhiteNoteLength();
|
||||
}();
|
||||
|
||||
const auto centrePos = (isHorizontal() ? Point<float> (xPos, yPos)
|
||||
: Point<float> (yPos, xPos));
|
||||
|
||||
const auto radius = jmax (noteComponent.getStrikeRadius(), noteComponent.getPressureRadius());
|
||||
|
||||
noteComponent.setBounds (Rectangle<float> (radius * 2.0f, radius * 2.0f)
|
||||
.withCentre (centrePos)
|
||||
.getSmallestIntegerContainer());
|
||||
}
|
||||
|
||||
static bool operator< (const MPENote& n1, const MPENote& n2) noexcept { return n1.noteID < n2.noteID; }
|
||||
|
||||
void MPEKeyboardComponent::updateNoteComponents()
|
||||
{
|
||||
std::set<MPENote> notesToUpdate;
|
||||
|
||||
{
|
||||
ScopedLock noteLock (activeNotesLock);
|
||||
|
||||
for (const auto& note : activeNotes)
|
||||
if (note.second)
|
||||
notesToUpdate.insert (note.first);
|
||||
};
|
||||
|
||||
handleNoteOns (notesToUpdate);
|
||||
handleNoteOffs (notesToUpdate);
|
||||
|
||||
for (auto& comp : noteComponents)
|
||||
{
|
||||
auto noteForComponent = std::find_if (notesToUpdate.begin(),
|
||||
notesToUpdate.end(),
|
||||
[&comp] (auto& note) { return note.noteID == comp->sourceID; });
|
||||
|
||||
if (noteForComponent != notesToUpdate.end())
|
||||
{
|
||||
comp->pressure = noteForComponent->pressure.asUnsignedFloat();
|
||||
updateNoteComponentBounds (*noteForComponent, *comp);
|
||||
|
||||
comp->repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MPEKeyboardComponent::timerCallback()
|
||||
{
|
||||
updateNoteComponents();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void MPEKeyboardComponent::noteAdded (MPENote newNote)
|
||||
{
|
||||
{
|
||||
const ScopedLock noteLock (activeNotesLock);
|
||||
activeNotes.push_back ({ newNote, true });
|
||||
}
|
||||
|
||||
startTimerHz (30);
|
||||
}
|
||||
|
||||
void MPEKeyboardComponent::updateNoteData (MPENote& changedNote)
|
||||
{
|
||||
const ScopedLock noteLock (activeNotesLock);
|
||||
|
||||
for (auto& note : activeNotes)
|
||||
{
|
||||
if (note.first.noteID == changedNote.noteID)
|
||||
{
|
||||
note.first = changedNote;
|
||||
note.second = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MPEKeyboardComponent::notePressureChanged (MPENote changedNote)
|
||||
{
|
||||
updateNoteData (changedNote);
|
||||
}
|
||||
|
||||
void MPEKeyboardComponent::notePitchbendChanged (MPENote changedNote)
|
||||
{
|
||||
updateNoteData (changedNote);
|
||||
}
|
||||
|
||||
void MPEKeyboardComponent::noteTimbreChanged (MPENote changedNote)
|
||||
{
|
||||
updateNoteData (changedNote);
|
||||
}
|
||||
|
||||
void MPEKeyboardComponent::noteReleased (MPENote finishedNote)
|
||||
{
|
||||
const ScopedLock noteLock (activeNotesLock);
|
||||
|
||||
activeNotes.erase (std::remove_if (std::begin (activeNotes),
|
||||
std::end (activeNotes),
|
||||
[finishedNote] (auto& note) { return note.first.noteID == finishedNote.noteID; }),
|
||||
std::end (activeNotes));
|
||||
}
|
||||
|
||||
void MPEKeyboardComponent::zoneLayoutChanged()
|
||||
{
|
||||
MessageManager::callAsync ([this] { updateZoneLayout(); });
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
153
modules/juce_audio_utils/gui/juce_MPEKeyboardComponent.h
Normal file
153
modules/juce_audio_utils/gui/juce_MPEKeyboardComponent.h
Normal 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
|
||||
|
|
@ -26,60 +26,17 @@
|
|||
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)
|
||||
: state (s), orientation (o)
|
||||
MidiKeyboardComponent::MidiKeyboardComponent (MidiKeyboardState& stateToUse, Orientation orientationToUse)
|
||||
: KeyboardComponentBase (orientationToUse), state (stateToUse)
|
||||
{
|
||||
scrollDown.reset (new UpDownButton (*this, -1));
|
||||
scrollUp .reset (new UpDownButton (*this, 1));
|
||||
|
||||
addChildComponent (scrollDown.get());
|
||||
addChildComponent (scrollUp.get());
|
||||
state.addListener (this);
|
||||
|
||||
// initialise with a default set of qwerty key-mappings..
|
||||
int note = 0;
|
||||
|
||||
for (char c : "awsedftgyhujkolp;")
|
||||
setKeyPressForNote (KeyPress (c, 0, 0), note++);
|
||||
setKeyPressForNote ({ c, 0, 0 }, note++);
|
||||
|
||||
mouseOverNotes.insertMultiple (0, -1, 32);
|
||||
mouseDownNotes.insertMultiple (0, -1, 32);
|
||||
|
|
@ -87,8 +44,6 @@ MidiKeyboardComponent::MidiKeyboardComponent (MidiKeyboardState& s, Orientation
|
|||
colourChanged();
|
||||
setWantsKeyboardFocus (true);
|
||||
|
||||
state.addListener (this);
|
||||
|
||||
startTimerHz (20);
|
||||
}
|
||||
|
||||
|
|
@ -98,86 +53,10 @@ MidiKeyboardComponent::~MidiKeyboardComponent()
|
|||
}
|
||||
|
||||
//==============================================================================
|
||||
void MidiKeyboardComponent::setKeyWidth (float widthInPixels)
|
||||
void MidiKeyboardComponent::setVelocity (float v, bool useMousePosition)
|
||||
{
|
||||
jassert (widthInPixels > 0);
|
||||
|
||||
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();
|
||||
velocity = v;
|
||||
useMousePositionForVelocity = useMousePosition;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -198,477 +77,39 @@ void MidiKeyboardComponent::setMidiChannelsToDisplay (int midiChannelMask)
|
|||
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);
|
||||
|
||||
static const float notePos[] = { 0.0f, 1 - blackNoteWidthRatio * 0.6f,
|
||||
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 };
|
||||
resetAnyKeysInUse();
|
||||
keyPressNotes.clear();
|
||||
keyPresses.clear();
|
||||
}
|
||||
|
||||
Range<float> MidiKeyboardComponent::getKeyPos (int midiNoteNumber) const
|
||||
void MidiKeyboardComponent::setKeyPressForNote (const KeyPress& key, int midiNoteOffsetFromC)
|
||||
{
|
||||
return getKeyPosition (midiNoteNumber, keyWidth)
|
||||
- xOffset
|
||||
- getKeyPosition (rangeStart, keyWidth).getStart();
|
||||
removeKeyPressForNote (midiNoteOffsetFromC);
|
||||
|
||||
keyPressNotes.add (midiNoteOffsetFromC);
|
||||
keyPresses.add (key);
|
||||
}
|
||||
|
||||
Rectangle<float> MidiKeyboardComponent::getRectangleForKey (int note) const
|
||||
void MidiKeyboardComponent::removeKeyPressForNote (int midiNoteOffsetFromC)
|
||||
{
|
||||
jassert (note >= rangeStart && note <= rangeEnd);
|
||||
|
||||
auto pos = getKeyPos (note);
|
||||
auto x = pos.getStart();
|
||||
auto w = pos.getLength();
|
||||
|
||||
if (MidiMessage::isMidiNoteBlack (note))
|
||||
for (int i = keyPressNotes.size(); --i >= 0;)
|
||||
{
|
||||
auto blackNoteLength = getBlackNoteLength();
|
||||
|
||||
switch (orientation)
|
||||
if (keyPressNotes.getUnchecked (i) == midiNoteOffsetFromC)
|
||||
{
|
||||
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 {};
|
||||
}
|
||||
|
||||
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);
|
||||
keyPressNotes.remove (i);
|
||||
keyPresses.remove (i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MidiKeyboardComponent::drawWhiteNote (int midiNoteNumber, Graphics& g, Rectangle<float> area,
|
||||
bool isDown, bool isOver, Colour lineColour, Colour textColour)
|
||||
void MidiKeyboardComponent::setKeyPressBaseOctave (int newOctaveNumber)
|
||||
{
|
||||
auto c = Colours::transparentWhite;
|
||||
jassert (newOctaveNumber >= 0 && newOctaveNumber <= 10);
|
||||
|
||||
if (isDown) c = findColour (keyDownOverlayColourId);
|
||||
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);
|
||||
keyMappingOctave = newOctaveNumber;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -685,7 +126,7 @@ void MidiKeyboardComponent::resetAnyKeysInUse()
|
|||
|
||||
for (int i = mouseDownNotes.size(); --i >= 0;)
|
||||
{
|
||||
auto noteDown = mouseDownNotes.getUnchecked(i);
|
||||
auto noteDown = mouseDownNotes.getUnchecked (i);
|
||||
|
||||
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)
|
||||
{
|
||||
const auto noteInfo = xyToNote (pos);
|
||||
const auto noteInfo = getNoteAndVelocityAtPosition (pos);
|
||||
const auto newNote = noteInfo.note;
|
||||
const auto oldNote = mouseOverNotes.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)
|
||||
{
|
||||
updateNoteUnderMouse (e, false);
|
||||
|
|
@ -752,19 +200,15 @@ void MidiKeyboardComponent::mouseMove (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))
|
||||
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)
|
||||
{
|
||||
auto newNote = xyToNote (e.position).note;
|
||||
auto newNote = getNoteAndVelocityAtPosition (e.position).note;
|
||||
|
||||
if (newNote >= 0 && mouseDownOnKey (newNote, e))
|
||||
updateNoteUnderMouse (e, true);
|
||||
|
|
@ -774,7 +218,7 @@ void MidiKeyboardComponent::mouseUp (const MouseEvent& e)
|
|||
{
|
||||
updateNoteUnderMouse (e, false);
|
||||
|
||||
auto note = xyToNote (e.position).note;
|
||||
auto note = getNoteAndVelocityAtPosition (e.position).note;
|
||||
|
||||
if (note >= 0)
|
||||
mouseUpOnKey (note, e);
|
||||
|
|
@ -790,23 +234,14 @@ void MidiKeyboardComponent::mouseExit (const MouseEvent& e)
|
|||
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()
|
||||
{
|
||||
if (noPendingUpdates.exchange (true))
|
||||
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)
|
||||
{
|
||||
|
|
@ -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 keyPressUsed = false;
|
||||
|
|
@ -892,4 +292,190 @@ void MidiKeyboardComponent::focusLost (FocusChangeType)
|
|||
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
|
||||
|
|
|
|||
|
|
@ -46,30 +46,18 @@ namespace juce
|
|||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class JUCE_API MidiKeyboardComponent : public Component,
|
||||
public MidiKeyboardState::Listener,
|
||||
public ChangeBroadcaster,
|
||||
class JUCE_API MidiKeyboardComponent : public KeyboardComponentBase,
|
||||
private MidiKeyboardState::Listener,
|
||||
private Timer
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** The direction of the keyboard.
|
||||
@see setOrientation
|
||||
*/
|
||||
enum Orientation
|
||||
{
|
||||
horizontalKeyboard,
|
||||
verticalKeyboardFacingLeft,
|
||||
verticalKeyboardFacingRight,
|
||||
};
|
||||
|
||||
/** Creates a MidiKeyboardComponent.
|
||||
|
||||
@param state the midi keyboard model that this component will represent
|
||||
@param orientation whether the keyboard is horizontal or vertical
|
||||
*/
|
||||
MidiKeyboardComponent (MidiKeyboardState& state,
|
||||
Orientation orientation);
|
||||
MidiKeyboardComponent (MidiKeyboardState& state, Orientation orientation);
|
||||
|
||||
/** Destructor. */
|
||||
~MidiKeyboardComponent() override;
|
||||
|
|
@ -84,6 +72,7 @@ public:
|
|||
*/
|
||||
void setVelocity (float velocity, bool useMousePositionForVelocity);
|
||||
|
||||
//==============================================================================
|
||||
/** Changes the midi channel number that will be used for events triggered by clicking
|
||||
on the component.
|
||||
|
||||
|
|
@ -100,7 +89,7 @@ public:
|
|||
/** Returns the midi channel that the keyboard is using for midi messages.
|
||||
@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
|
||||
key movements.
|
||||
|
|
@ -119,86 +108,41 @@ public:
|
|||
/** Returns the current set of midi channels represented by the component.
|
||||
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. */
|
||||
void setKeyWidth (float widthInPixels);
|
||||
/** Deletes all key-mappings.
|
||||
|
||||
/** 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; }
|
||||
|
||||
/** 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.
|
||||
@see setKeyPressForNote
|
||||
*/
|
||||
void setAvailableRange (int lowestNote,
|
||||
int highestNote);
|
||||
void clearKeyMappings();
|
||||
|
||||
/** Returns the first note in the available range.
|
||||
@see setAvailableRange
|
||||
/** Maps a key-press to a given note.
|
||||
|
||||
@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.
|
||||
@see setAvailableRange
|
||||
/** Removes any key-mappings for a given note.
|
||||
|
||||
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
|
||||
it to show the given key at the start.
|
||||
/** Changes the base note above which key-press-triggered notes are played.
|
||||
|
||||
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.
|
||||
The set of key-mappings that trigger notes can be moved up and down to cover
|
||||
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);
|
||||
|
||||
/** 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);
|
||||
void setKeyPressBaseOctave (int newOctaveNumber);
|
||||
|
||||
//==============================================================================
|
||||
/** 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. */
|
||||
keyDownOverlayColourId = 0x1005004, /**< This colour will be overlaid on the normal note colour. */
|
||||
textLabelColourId = 0x1005005,
|
||||
upDownButtonBackgroundColourId = 0x1005006,
|
||||
upDownButtonArrowColourId = 0x1005007,
|
||||
shadowColourId = 0x1005008
|
||||
shadowColourId = 0x1005006
|
||||
};
|
||||
|
||||
/** 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.
|
||||
@see setKeyPressForNote
|
||||
/** Use this method to draw a white note of the keyboard in a 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.
|
||||
*/
|
||||
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
|
||||
@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()
|
||||
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 setKeyPressForNote (const KeyPress& key,
|
||||
int midiNoteOffsetFromC);
|
||||
virtual void drawBlackNote (int midiNoteNumber, Graphics& g, Rectangle<float> area,
|
||||
bool isDown, bool isOver, Colour noteFillColour);
|
||||
|
||||
/** Removes any key-mappings for a given note.
|
||||
For a description of what the note number means, see setKeyPressForNote().
|
||||
/** 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
|
||||
*/
|
||||
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
|
||||
the entire scale using this method.
|
||||
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.
|
||||
|
||||
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.
|
||||
@see mouseDownOnKey
|
||||
*/
|
||||
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
|
||||
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
|
||||
@see mouseDownOnKey
|
||||
*/
|
||||
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
|
||||
*/
|
||||
int getOctaveForMiddleC() const noexcept { return octaveNumForMiddleC; }
|
||||
virtual String getWhiteNoteText (int midiNoteNumber);
|
||||
|
||||
//==============================================================================
|
||||
/** @internal */
|
||||
void paint (Graphics&) override;
|
||||
/** @internal */
|
||||
void resized() override;
|
||||
/** @internal */
|
||||
void mouseMove (const MouseEvent&) override;
|
||||
/** @internal */
|
||||
void mouseDrag (const MouseEvent&) override;
|
||||
|
|
@ -303,8 +232,6 @@ public:
|
|||
/** @internal */
|
||||
void mouseExit (const MouseEvent&) override;
|
||||
/** @internal */
|
||||
void mouseWheelMove (const MouseEvent&, const MouseWheelDetails&) override;
|
||||
/** @internal */
|
||||
void timerCallback() override;
|
||||
/** @internal */
|
||||
bool keyStateChanged (bool isKeyDown) override;
|
||||
|
|
@ -313,127 +240,39 @@ public:
|
|||
/** @internal */
|
||||
void focusLost (FocusChangeType) override;
|
||||
/** @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;
|
||||
|
||||
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:
|
||||
//==============================================================================
|
||||
struct UpDownButton;
|
||||
struct NoteAndVelocity { int note; float velocity; };
|
||||
void drawKeyboardBackground (Graphics& g, Rectangle<float> area) override final;
|
||||
void drawWhiteKey (int midiNoteNumber, Graphics& g, Rectangle<float> area) override final;
|
||||
void drawBlackKey (int midiNoteNumber, Graphics& g, Rectangle<float> area) override final;
|
||||
|
||||
MidiKeyboardState& state;
|
||||
float blackNoteLengthRatio = 0.7f;
|
||||
float blackNoteWidthRatio = 0.7f;
|
||||
float xOffset = 0;
|
||||
float keyWidth = 16.0f;
|
||||
int scrollButtonWidth = 12;
|
||||
Orientation orientation;
|
||||
void handleNoteOn (MidiKeyboardState*, int, int, float) override;
|
||||
void handleNoteOff (MidiKeyboardState*, int, int, float) override;
|
||||
|
||||
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 updateNoteUnderMouse (Point<float>, bool isDown, int fingerNum);
|
||||
void updateNoteUnderMouse (const MouseEvent&, bool isDown);
|
||||
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)
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,9 @@
|
|||
#include "gui/juce_AudioThumbnail.cpp"
|
||||
#include "gui/juce_AudioThumbnailCache.cpp"
|
||||
#include "gui/juce_AudioVisualiserComponent.cpp"
|
||||
#include "gui/juce_KeyboardComponentBase.cpp"
|
||||
#include "gui/juce_MidiKeyboardComponent.cpp"
|
||||
#include "gui/juce_MPEKeyboardComponent.cpp"
|
||||
#include "gui/juce_AudioAppComponent.cpp"
|
||||
#include "players/juce_SoundPlayer.cpp"
|
||||
#include "players/juce_AudioProcessorPlayer.cpp"
|
||||
|
|
|
|||
|
|
@ -80,7 +80,9 @@
|
|||
#include "gui/juce_AudioThumbnail.h"
|
||||
#include "gui/juce_AudioThumbnailCache.h"
|
||||
#include "gui/juce_AudioVisualiserComponent.h"
|
||||
#include "gui/juce_KeyboardComponentBase.h"
|
||||
#include "gui/juce_MidiKeyboardComponent.h"
|
||||
#include "gui/juce_MPEKeyboardComponent.h"
|
||||
#include "gui/juce_AudioAppComponent.h"
|
||||
#include "gui/juce_BluetoothMidiDevicePairingDialogue.h"
|
||||
#include "players/juce_SoundPlayer.h"
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@
|
|||
#include <typeindex>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#include <set>
|
||||
|
||||
//==============================================================================
|
||||
#include "juce_CompilerSupport.h"
|
||||
|
|
|
|||
|
|
@ -94,8 +94,6 @@
|
|||
#endif
|
||||
#endif
|
||||
|
||||
#include <set>
|
||||
|
||||
//==============================================================================
|
||||
#define JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED_OR_OFFSCREEN \
|
||||
jassert ((MessageManager::getInstanceWithoutCreating() != nullptr \
|
||||
|
|
|
|||
|
|
@ -188,15 +188,22 @@ LookAndFeel_V2::LookAndFeel_V2()
|
|||
0x1000440, /*LassoComponent::lassoFillColourId*/ 0x66dddddd,
|
||||
0x1000441, /*LassoComponent::lassoOutlineColourId*/ 0x99111111,
|
||||
|
||||
0x1004000, /*KeyboardComponentBase::upDownButtonBackgroundColourId*/ 0xffd3d3d3,
|
||||
0x1004001, /*KeyboardComponentBase::upDownButtonArrowColourId*/ 0xff000000,
|
||||
|
||||
0x1005000, /*MidiKeyboardComponent::whiteNoteColourId*/ 0xffffffff,
|
||||
0x1005001, /*MidiKeyboardComponent::blackNoteColourId*/ 0xff000000,
|
||||
0x1005002, /*MidiKeyboardComponent::keySeparatorLineColourId*/ 0x66000000,
|
||||
0x1005003, /*MidiKeyboardComponent::mouseOverKeyOverlayColourId*/ 0x80ffff00,
|
||||
0x1005004, /*MidiKeyboardComponent::keyDownOverlayColourId*/ 0xffb6b600,
|
||||
0x1005005, /*MidiKeyboardComponent::textLabelColourId*/ 0xff000000,
|
||||
0x1005006, /*MidiKeyboardComponent::upDownButtonBackgroundColourId*/ 0xffd3d3d3,
|
||||
0x1005007, /*MidiKeyboardComponent::upDownButtonArrowColourId*/ 0xff000000,
|
||||
0x1005008, /*MidiKeyboardComponent::shadowColourId*/ 0x4c000000,
|
||||
0x1005006, /*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,
|
||||
0x1004502, /*CodeEditorComponent::highlightColourId*/ textHighlightColour,
|
||||
|
|
|
|||
|
|
@ -1438,15 +1438,22 @@ void LookAndFeel_V4::initialiseColours()
|
|||
0x1000440, /*LassoComponent::lassoFillColourId*/ currentColourScheme.getUIColour (ColourScheme::UIColour::defaultFill).getARGB(),
|
||||
0x1000441, /*LassoComponent::lassoOutlineColourId*/ currentColourScheme.getUIColour (ColourScheme::UIColour::outline).getARGB(),
|
||||
|
||||
0x1004000, /*KeyboardComponentBase::upDownButtonBackgroundColourId*/ 0xffd3d3d3,
|
||||
0x1004001, /*KeyboardComponentBase::upDownButtonArrowColourId*/ 0xff000000,
|
||||
|
||||
0x1005000, /*MidiKeyboardComponent::whiteNoteColourId*/ 0xffffffff,
|
||||
0x1005001, /*MidiKeyboardComponent::blackNoteColourId*/ 0xff000000,
|
||||
0x1005002, /*MidiKeyboardComponent::keySeparatorLineColourId*/ 0x66000000,
|
||||
0x1005003, /*MidiKeyboardComponent::mouseOverKeyOverlayColourId*/ 0x80ffff00,
|
||||
0x1005004, /*MidiKeyboardComponent::keyDownOverlayColourId*/ 0xffb6b600,
|
||||
0x1005005, /*MidiKeyboardComponent::textLabelColourId*/ 0xff000000,
|
||||
0x1005006, /*MidiKeyboardComponent::upDownButtonBackgroundColourId*/ 0xffd3d3d3,
|
||||
0x1005007, /*MidiKeyboardComponent::upDownButtonArrowColourId*/ 0xff000000,
|
||||
0x1005008, /*MidiKeyboardComponent::shadowColourId*/ 0x4c000000,
|
||||
0x1005006, /*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(),
|
||||
0x1004502, /*CodeEditorComponent::highlightColourId*/ currentColourScheme.getUIColour (ColourScheme::UIColour::defaultFill).withAlpha (0.4f).getARGB(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue