1051 lines
40 KiB
C
1051 lines
40 KiB
C
|
/*
|
||
|
==============================================================================
|
||
|
|
||
|
This file is part of the JUCE examples.
|
||
|
Copyright (c) 2020 - Raw Material Software Limited
|
||
|
|
||
|
The code included in this file is provided under the terms of the ISC license
|
||
|
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
|
||
|
To use, copy, modify, and/or distribute this software for any purpose with or
|
||
|
without fee is hereby granted provided that the above copyright notice and
|
||
|
this permission notice appear in all copies.
|
||
|
|
||
|
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
|
||
|
WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
|
||
|
PURPOSE, ARE DISCLAIMED.
|
||
|
|
||
|
==============================================================================
|
||
|
*/
|
||
|
|
||
|
/*******************************************************************************
|
||
|
The block below describes the properties of this PIP. A PIP is a short snippet
|
||
|
of code that can be read by the Projucer and used to generate a JUCE project.
|
||
|
|
||
|
BEGIN_JUCE_PIP_METADATA
|
||
|
|
||
|
name: MPEDemo
|
||
|
version: 1.0.0
|
||
|
vendor: JUCE
|
||
|
website: http://juce.com
|
||
|
description: Simple MPE synthesiser application.
|
||
|
|
||
|
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
|
||
|
juce_audio_processors, juce_audio_utils, juce_core,
|
||
|
juce_data_structures, juce_events, juce_graphics,
|
||
|
juce_gui_basics, juce_gui_extra
|
||
|
exporters: xcode_mac, vs2019, linux_make, androidstudio, xcode_iphone
|
||
|
|
||
|
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
||
|
|
||
|
type: Component
|
||
|
mainClass: MPEDemo
|
||
|
|
||
|
useLocalCopy: 1
|
||
|
|
||
|
END_JUCE_PIP_METADATA
|
||
|
|
||
|
*******************************************************************************/
|
||
|
|
||
|
#pragma once
|
||
|
|
||
|
//==============================================================================
|
||
|
class ZoneColourPicker
|
||
|
{
|
||
|
public:
|
||
|
ZoneColourPicker() {}
|
||
|
|
||
|
//==============================================================================
|
||
|
Colour getColourForMidiChannel (int midiChannel) noexcept
|
||
|
{
|
||
|
if (legacyModeEnabled)
|
||
|
return Colours::white;
|
||
|
|
||
|
if (zoneLayout.getLowerZone().isUsingChannelAsMemberChannel (midiChannel))
|
||
|
return getColourForZone (true);
|
||
|
|
||
|
if (zoneLayout.getUpperZone().isUsingChannelAsMemberChannel (midiChannel))
|
||
|
return getColourForZone (false);
|
||
|
|
||
|
return Colours::transparentBlack;
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
Colour getColourForZone (bool isLowerZone) const noexcept
|
||
|
{
|
||
|
if (legacyModeEnabled)
|
||
|
return Colours::white;
|
||
|
|
||
|
if (isLowerZone)
|
||
|
return Colours::blue;
|
||
|
|
||
|
return Colours::red;
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
void setZoneLayout (MPEZoneLayout layout) noexcept { zoneLayout = layout; }
|
||
|
void setLegacyModeEnabled (bool shouldBeEnabled) noexcept { legacyModeEnabled = shouldBeEnabled; }
|
||
|
|
||
|
private:
|
||
|
//==============================================================================
|
||
|
MPEZoneLayout zoneLayout;
|
||
|
bool legacyModeEnabled = false;
|
||
|
|
||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ZoneColourPicker)
|
||
|
};
|
||
|
|
||
|
//==============================================================================
|
||
|
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
|
||
|
{
|
||
|
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()
|
||
|
{
|
||
|
addAndMakeVisible (isLowerZoneButton);
|
||
|
isLowerZoneButton.setToggleState (true, NotificationType::dontSendNotification);
|
||
|
|
||
|
initialiseComboBoxWithConsecutiveIntegers (memberChannels, memberChannelsLabel, 0, 16, defaultMemberChannels);
|
||
|
initialiseComboBoxWithConsecutiveIntegers (masterPitchbendRange, masterPitchbendRangeLabel, 0, 96, defaultMasterPitchbendRange);
|
||
|
initialiseComboBoxWithConsecutiveIntegers (notePitchbendRange, notePitchbendRangeLabel, 0, 96, defaultNotePitchbendRange);
|
||
|
|
||
|
initialiseComboBoxWithConsecutiveIntegers (legacyStartChannel, legacyStartChannelLabel, 1, 16, 1, false);
|
||
|
initialiseComboBoxWithConsecutiveIntegers (legacyEndChannel, legacyEndChannelLabel, 1, 16, 16, false);
|
||
|
initialiseComboBoxWithConsecutiveIntegers (legacyPitchbendRange, legacyPitchbendRangeLabel, 0, 96, 2, false);
|
||
|
|
||
|
addAndMakeVisible (setZoneButton);
|
||
|
setZoneButton.onClick = [this] { setZoneButtonClicked(); };
|
||
|
addAndMakeVisible (clearAllZonesButton);
|
||
|
clearAllZonesButton.onClick = [this] { clearAllZonesButtonClicked(); };
|
||
|
addAndMakeVisible (legacyModeEnabledToggle);
|
||
|
legacyModeEnabledToggle.onClick = [this] { legacyModeEnabledToggleClicked(); };
|
||
|
addAndMakeVisible (voiceStealingEnabledToggle);
|
||
|
voiceStealingEnabledToggle.onClick = [this] { voiceStealingEnabledToggleClicked(); };
|
||
|
|
||
|
initialiseComboBoxWithConsecutiveIntegers (numberOfVoices, numberOfVoicesLabel, 1, 20, 15);
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
void resized() override
|
||
|
{
|
||
|
Rectangle<int> r (proportionOfWidth (0.65f), 15, proportionOfWidth (0.25f), 3000);
|
||
|
auto h = 24;
|
||
|
auto hspace = 6;
|
||
|
auto hbigspace = 18;
|
||
|
|
||
|
isLowerZoneButton.setBounds (r.removeFromTop (h));
|
||
|
r.removeFromTop (hspace);
|
||
|
memberChannels.setBounds (r.removeFromTop (h));
|
||
|
r.removeFromTop (hspace);
|
||
|
notePitchbendRange.setBounds (r.removeFromTop (h));
|
||
|
r.removeFromTop (hspace);
|
||
|
masterPitchbendRange.setBounds (r.removeFromTop (h));
|
||
|
|
||
|
legacyStartChannel .setBounds (isLowerZoneButton .getBounds());
|
||
|
legacyEndChannel .setBounds (memberChannels .getBounds());
|
||
|
legacyPitchbendRange.setBounds (notePitchbendRange.getBounds());
|
||
|
|
||
|
r.removeFromTop (hbigspace);
|
||
|
|
||
|
auto buttonLeft = proportionOfWidth (0.5f);
|
||
|
|
||
|
setZoneButton.setBounds (r.removeFromTop (h).withLeft (buttonLeft));
|
||
|
r.removeFromTop (hspace);
|
||
|
clearAllZonesButton.setBounds (r.removeFromTop (h).withLeft (buttonLeft));
|
||
|
|
||
|
r.removeFromTop (hbigspace);
|
||
|
|
||
|
auto toggleLeft = proportionOfWidth (0.25f);
|
||
|
|
||
|
legacyModeEnabledToggle.setBounds (r.removeFromTop (h).withLeft (toggleLeft));
|
||
|
r.removeFromTop (hspace);
|
||
|
voiceStealingEnabledToggle.setBounds (r.removeFromTop (h).withLeft (toggleLeft));
|
||
|
r.removeFromTop (hspace);
|
||
|
numberOfVoices.setBounds (r.removeFromTop (h));
|
||
|
}
|
||
|
|
||
|
private:
|
||
|
//==============================================================================
|
||
|
void initialiseComboBoxWithConsecutiveIntegers (ComboBox& comboBox, Label& labelToAttach,
|
||
|
int firstValue, int numValues, int valueToSelect,
|
||
|
bool makeVisible = true)
|
||
|
{
|
||
|
for (auto i = 0; i < numValues; ++i)
|
||
|
comboBox.addItem (String (i + firstValue), i + 1);
|
||
|
|
||
|
comboBox.setSelectedId (valueToSelect - firstValue + 1);
|
||
|
labelToAttach.attachToComponent (&comboBox, true);
|
||
|
|
||
|
if (makeVisible)
|
||
|
addAndMakeVisible (comboBox);
|
||
|
else
|
||
|
addChildComponent (comboBox);
|
||
|
|
||
|
if (&comboBox == &numberOfVoices)
|
||
|
comboBox.onChange = [this] { numberOfVoicesChanged(); };
|
||
|
else if (&comboBox == &legacyPitchbendRange)
|
||
|
comboBox.onChange = [this] { if (legacyModeEnabledToggle.getToggleState()) legacyModePitchbendRangeChanged(); };
|
||
|
else if (&comboBox == &legacyStartChannel || &comboBox == &legacyEndChannel)
|
||
|
comboBox.onChange = [this] { if (legacyModeEnabledToggle.getToggleState()) legacyModeChannelRangeChanged(); };
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
void setZoneButtonClicked()
|
||
|
{
|
||
|
auto isLowerZone = isLowerZoneButton.getToggleState();
|
||
|
auto numMemberChannels = memberChannels.getText().getIntValue();
|
||
|
auto perNotePb = notePitchbendRange.getText().getIntValue();
|
||
|
auto masterPb = masterPitchbendRange.getText().getIntValue();
|
||
|
|
||
|
if (isLowerZone)
|
||
|
zoneLayout.setLowerZone (numMemberChannels, perNotePb, masterPb);
|
||
|
else
|
||
|
zoneLayout.setUpperZone (numMemberChannels, perNotePb, masterPb);
|
||
|
|
||
|
listeners.call ([&] (Listener& l) { l.zoneChanged (isLowerZone, numMemberChannels, perNotePb, masterPb); });
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
void clearAllZonesButtonClicked()
|
||
|
{
|
||
|
zoneLayout.clearAllZones();
|
||
|
listeners.call ([] (Listener& l) { l.allZonesCleared(); });
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
void legacyModeEnabledToggleClicked()
|
||
|
{
|
||
|
auto legacyModeEnabled = legacyModeEnabledToggle.getToggleState();
|
||
|
|
||
|
isLowerZoneButton .setVisible (! legacyModeEnabled);
|
||
|
memberChannels .setVisible (! legacyModeEnabled);
|
||
|
notePitchbendRange .setVisible (! legacyModeEnabled);
|
||
|
masterPitchbendRange.setVisible (! legacyModeEnabled);
|
||
|
setZoneButton .setVisible (! legacyModeEnabled);
|
||
|
clearAllZonesButton .setVisible (! legacyModeEnabled);
|
||
|
|
||
|
legacyStartChannel .setVisible (legacyModeEnabled);
|
||
|
legacyEndChannel .setVisible (legacyModeEnabled);
|
||
|
legacyPitchbendRange.setVisible (legacyModeEnabled);
|
||
|
|
||
|
if (areLegacyModeParametersValid())
|
||
|
{
|
||
|
listeners.call ([&] (Listener& l) { l.legacyModeChanged (legacyModeEnabledToggle.getToggleState(),
|
||
|
legacyPitchbendRange.getText().getIntValue(),
|
||
|
getLegacyModeChannelRange()); });
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
handleInvalidLegacyModeParameters();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
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()); });
|
||
|
}
|
||
|
|
||
|
void legacyModeChannelRangeChanged()
|
||
|
{
|
||
|
jassert (legacyModeEnabledToggle.getToggleState() == true);
|
||
|
|
||
|
if (areLegacyModeParametersValid())
|
||
|
{
|
||
|
listeners.call ([this] (Listener& l) { l.legacyModeChanged (true,
|
||
|
legacyPitchbendRange.getText().getIntValue(),
|
||
|
getLegacyModeChannelRange()); });
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
handleInvalidLegacyModeParameters();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
bool areLegacyModeParametersValid() const
|
||
|
{
|
||
|
return legacyStartChannel.getText().getIntValue() <= legacyEndChannel.getText().getIntValue();
|
||
|
}
|
||
|
|
||
|
void handleInvalidLegacyModeParameters() const
|
||
|
{
|
||
|
AlertWindow::showMessageBoxAsync (MessageBoxIconType::WarningIcon,
|
||
|
"Invalid legacy mode channel layout",
|
||
|
"Cannot set legacy mode start/end channel:\n"
|
||
|
"The end channel must not be less than the start channel!",
|
||
|
"Got it");
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
Range<int> getLegacyModeChannelRange() const
|
||
|
{
|
||
|
return { legacyStartChannel.getText().getIntValue(),
|
||
|
legacyEndChannel.getText().getIntValue() + 1 };
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
MPEZoneLayout zoneLayout;
|
||
|
|
||
|
ComboBox memberChannels, masterPitchbendRange, notePitchbendRange;
|
||
|
|
||
|
ToggleButton isLowerZoneButton { "Lower zone" };
|
||
|
|
||
|
Label memberChannelsLabel { {}, "Nr. of member channels:" };
|
||
|
Label masterPitchbendRangeLabel { {}, "Master pitchbend range (semitones):" };
|
||
|
Label notePitchbendRangeLabel { {}, "Note pitchbend range (semitones):" };
|
||
|
|
||
|
TextButton setZoneButton { "Set zone" };
|
||
|
TextButton clearAllZonesButton { "Clear all zones" };
|
||
|
|
||
|
ComboBox legacyStartChannel, legacyEndChannel, legacyPitchbendRange;
|
||
|
|
||
|
Label legacyStartChannelLabel { {}, "First channel:" };
|
||
|
Label legacyEndChannelLabel { {}, "Last channel:" };
|
||
|
Label legacyPitchbendRangeLabel { {}, "Pitchbend range (semitones):"};
|
||
|
|
||
|
ToggleButton legacyModeEnabledToggle { "Enable Legacy Mode" };
|
||
|
ToggleButton voiceStealingEnabledToggle { "Enable synth voice stealing" };
|
||
|
|
||
|
ComboBox numberOfVoices;
|
||
|
Label numberOfVoicesLabel { {}, "Number of synth voices"};
|
||
|
|
||
|
ListenerList<Listener> listeners;
|
||
|
|
||
|
const int defaultMemberChannels = 15,
|
||
|
defaultMasterPitchbendRange = 2,
|
||
|
defaultNotePitchbendRange = 48;
|
||
|
|
||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPESetupComponent)
|
||
|
};
|
||
|
|
||
|
//==============================================================================
|
||
|
class ZoneLayoutComponent : public Component,
|
||
|
public MPESetupComponent::Listener
|
||
|
{
|
||
|
public:
|
||
|
//==============================================================================
|
||
|
ZoneLayoutComponent (const ZoneColourPicker& zoneColourPicker)
|
||
|
: colourPicker (zoneColourPicker)
|
||
|
{}
|
||
|
|
||
|
//==============================================================================
|
||
|
void paint (Graphics& g) override
|
||
|
{
|
||
|
paintBackground (g);
|
||
|
|
||
|
if (legacyModeEnabled)
|
||
|
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 paintBackground (Graphics& g)
|
||
|
{
|
||
|
g.setColour (Colours::black);
|
||
|
auto channelWidth = getChannelRectangleWidth();
|
||
|
|
||
|
for (auto i = 0; i < numMidiChannels; ++i)
|
||
|
{
|
||
|
auto x = float (i) * channelWidth;
|
||
|
Rectangle<int> channelArea ((int) x, 0, (int) channelWidth, getHeight());
|
||
|
|
||
|
g.drawLine ({ x, 0.0f, x, float (getHeight()) });
|
||
|
g.drawText (String (i + 1), channelArea.reduced (4, 4), Justification::topLeft, false);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
void paintZones (Graphics& g)
|
||
|
{
|
||
|
auto channelWidth = getChannelRectangleWidth();
|
||
|
|
||
|
Array<MPEZoneLayout::Zone> activeZones;
|
||
|
if (zoneLayout.getLowerZone().isActive()) activeZones.add (zoneLayout.getLowerZone());
|
||
|
if (zoneLayout.getUpperZone().isActive()) activeZones.add (zoneLayout.getUpperZone());
|
||
|
|
||
|
for (auto zone : activeZones)
|
||
|
{
|
||
|
auto zoneColour = colourPicker.getColourForZone (zone.isLowerZone());
|
||
|
|
||
|
auto xPos = zone.isLowerZone() ? 0 : zone.getLastMemberChannel() - 1;
|
||
|
|
||
|
Rectangle<int> zoneRect { int (channelWidth * (float) xPos), 20,
|
||
|
int (channelWidth * (float) (zone.numMemberChannels + 1)), getHeight() - 20 };
|
||
|
|
||
|
g.setColour (zoneColour);
|
||
|
g.drawRect (zoneRect, 3);
|
||
|
|
||
|
auto masterRect = zone.isLowerZone() ? zoneRect.removeFromLeft ((int) channelWidth) : zoneRect.removeFromRight ((int) channelWidth);
|
||
|
|
||
|
g.setColour (zoneColour.withAlpha (0.3f));
|
||
|
g.fillRect (masterRect);
|
||
|
|
||
|
g.setColour (zoneColour.contrasting());
|
||
|
g.drawText ("<>" + String (zone.masterPitchbendRange), masterRect.reduced (4), Justification::top, false);
|
||
|
g.drawText ("<>" + String (zone.perNotePitchbendRange), masterRect.reduced (4), Justification::bottom, false);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
void paintLegacyMode (Graphics& g)
|
||
|
{
|
||
|
auto startChannel = legacyModeChannelRange.getStart() - 1;
|
||
|
auto numChannels = legacyModeChannelRange.getEnd() - startChannel - 1;
|
||
|
|
||
|
|
||
|
Rectangle<int> zoneRect (int (getChannelRectangleWidth() * (float) startChannel), 0,
|
||
|
int (getChannelRectangleWidth() * (float) numChannels), getHeight());
|
||
|
|
||
|
zoneRect.removeFromTop (20);
|
||
|
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
float getChannelRectangleWidth() const noexcept
|
||
|
{
|
||
|
return (float) getWidth() / (float) numMidiChannels;
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
MPEZoneLayout zoneLayout;
|
||
|
const ZoneColourPicker& colourPicker;
|
||
|
|
||
|
bool legacyModeEnabled = false;
|
||
|
int legacyModePitchbendRange = 48;
|
||
|
Range<int> legacyModeChannelRange = { 1, 17 };
|
||
|
const int numMidiChannels = 16;
|
||
|
};
|
||
|
|
||
|
//==============================================================================
|
||
|
class MPEDemoSynthVoice : public MPESynthesiserVoice
|
||
|
{
|
||
|
public:
|
||
|
//==============================================================================
|
||
|
MPEDemoSynthVoice() {}
|
||
|
|
||
|
//==============================================================================
|
||
|
void noteStarted() override
|
||
|
{
|
||
|
jassert (currentlyPlayingNote.isValid());
|
||
|
jassert (currentlyPlayingNote.keyState == MPENote::keyDown
|
||
|
|| currentlyPlayingNote.keyState == MPENote::keyDownAndSustained);
|
||
|
|
||
|
level .setTargetValue (currentlyPlayingNote.pressure.asUnsignedFloat());
|
||
|
frequency.setTargetValue (currentlyPlayingNote.getFrequencyInHertz());
|
||
|
timbre .setTargetValue (currentlyPlayingNote.timbre.asUnsignedFloat());
|
||
|
|
||
|
phase = 0.0;
|
||
|
auto cyclesPerSample = frequency.getNextValue() / currentSampleRate;
|
||
|
phaseDelta = MathConstants<double>::twoPi * cyclesPerSample;
|
||
|
|
||
|
tailOff = 0.0;
|
||
|
}
|
||
|
|
||
|
void noteStopped (bool allowTailOff) override
|
||
|
{
|
||
|
jassert (currentlyPlayingNote.keyState == MPENote::off);
|
||
|
|
||
|
if (allowTailOff)
|
||
|
{
|
||
|
// start a tail-off by setting this flag. The render callback will pick up on
|
||
|
// this and do a fade out, calling clearCurrentNote() when it's finished.
|
||
|
|
||
|
if (tailOff == 0.0) // we only need to begin a tail-off if it's not already doing so - the
|
||
|
// stopNote method could be called more than once.
|
||
|
tailOff = 1.0;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// we're being told to stop playing immediately, so reset everything..
|
||
|
clearCurrentNote();
|
||
|
phaseDelta = 0.0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void notePressureChanged() override
|
||
|
{
|
||
|
level.setTargetValue (currentlyPlayingNote.pressure.asUnsignedFloat());
|
||
|
}
|
||
|
|
||
|
void notePitchbendChanged() override
|
||
|
{
|
||
|
frequency.setTargetValue (currentlyPlayingNote.getFrequencyInHertz());
|
||
|
}
|
||
|
|
||
|
void noteTimbreChanged() override
|
||
|
{
|
||
|
timbre.setTargetValue (currentlyPlayingNote.timbre.asUnsignedFloat());
|
||
|
}
|
||
|
|
||
|
void noteKeyStateChanged() override {}
|
||
|
|
||
|
void setCurrentSampleRate (double newRate) override
|
||
|
{
|
||
|
if (currentSampleRate != newRate)
|
||
|
{
|
||
|
noteStopped (false);
|
||
|
currentSampleRate = newRate;
|
||
|
|
||
|
level .reset (currentSampleRate, smoothingLengthInSeconds);
|
||
|
timbre .reset (currentSampleRate, smoothingLengthInSeconds);
|
||
|
frequency.reset (currentSampleRate, smoothingLengthInSeconds);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
virtual void renderNextBlock (AudioBuffer<float>& outputBuffer,
|
||
|
int startSample,
|
||
|
int numSamples) override
|
||
|
{
|
||
|
if (phaseDelta != 0.0)
|
||
|
{
|
||
|
if (tailOff > 0.0)
|
||
|
{
|
||
|
while (--numSamples >= 0)
|
||
|
{
|
||
|
auto currentSample = getNextSample() * (float) tailOff;
|
||
|
|
||
|
for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
|
||
|
outputBuffer.addSample (i, startSample, currentSample);
|
||
|
|
||
|
++startSample;
|
||
|
|
||
|
tailOff *= 0.99;
|
||
|
|
||
|
if (tailOff <= 0.005)
|
||
|
{
|
||
|
clearCurrentNote();
|
||
|
|
||
|
phaseDelta = 0.0;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
while (--numSamples >= 0)
|
||
|
{
|
||
|
auto currentSample = getNextSample();
|
||
|
|
||
|
for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
|
||
|
outputBuffer.addSample (i, startSample, currentSample);
|
||
|
|
||
|
++startSample;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
using MPESynthesiserVoice::renderNextBlock;
|
||
|
|
||
|
private:
|
||
|
//==============================================================================
|
||
|
float getNextSample() noexcept
|
||
|
{
|
||
|
auto levelDb = (level.getNextValue() - 1.0) * maxLevelDb;
|
||
|
auto amplitude = pow (10.0f, 0.05f * levelDb) * maxLevel;
|
||
|
|
||
|
// timbre is used to blend between a sine and a square.
|
||
|
auto f1 = std::sin (phase);
|
||
|
auto f2 = copysign (1.0, f1);
|
||
|
auto a2 = timbre.getNextValue();
|
||
|
auto a1 = 1.0 - a2;
|
||
|
|
||
|
auto nextSample = float (amplitude * ((a1 * f1) + (a2 * f2)));
|
||
|
|
||
|
auto cyclesPerSample = frequency.getNextValue() / currentSampleRate;
|
||
|
phaseDelta = MathConstants<double>::twoPi * cyclesPerSample;
|
||
|
phase = std::fmod (phase + phaseDelta, MathConstants<double>::twoPi);
|
||
|
|
||
|
return nextSample;
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
SmoothedValue<double> level, timbre, frequency;
|
||
|
|
||
|
double phase = 0.0;
|
||
|
double phaseDelta = 0.0;
|
||
|
double tailOff = 0.0;
|
||
|
|
||
|
const double maxLevel = 0.05;
|
||
|
const double maxLevelDb = 31.0;
|
||
|
const double smoothingLengthInSeconds = 0.01;
|
||
|
};
|
||
|
|
||
|
//==============================================================================
|
||
|
class MPEDemo : public Component,
|
||
|
private AudioIODeviceCallback,
|
||
|
private MidiInputCallback,
|
||
|
private MPESetupComponent::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);
|
||
|
#endif
|
||
|
|
||
|
audioDeviceManager.addMidiInputDeviceCallback ({}, this);
|
||
|
audioDeviceManager.addAudioCallback (this);
|
||
|
|
||
|
addAndMakeVisible (audioSetupComp);
|
||
|
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);
|
||
|
|
||
|
synth.setVoiceStealingEnabled (false);
|
||
|
for (auto i = 0; i < 15; ++i)
|
||
|
synth.addVoice (new MPEDemoSynthVoice());
|
||
|
|
||
|
setSize (880, 720);
|
||
|
}
|
||
|
|
||
|
~MPEDemo() override
|
||
|
{
|
||
|
audioDeviceManager.removeMidiInputDeviceCallback ({}, this);
|
||
|
audioDeviceManager.removeAudioCallback (this);
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
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() });
|
||
|
|
||
|
zoneLayoutComp.setBounds (r.removeFromBottom (zoneLayoutCompHeight));
|
||
|
audioSetupComp.setBounds (r.removeFromLeft (proportionOfWidth (audioSetupCompRelativeWidth)));
|
||
|
MPESetupComp .setBounds (r);
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
void audioDeviceIOCallback (const float** /*inputChannelData*/, int /*numInputChannels*/,
|
||
|
float** outputChannelData, int numOutputChannels,
|
||
|
int numSamples) override
|
||
|
{
|
||
|
AudioBuffer<float> buffer (outputChannelData, numOutputChannels, numSamples);
|
||
|
buffer.clear();
|
||
|
|
||
|
MidiBuffer incomingMidi;
|
||
|
midiCollector.removeNextBlockOfMessages (incomingMidi, numSamples);
|
||
|
synth.renderNextBlock (buffer, incomingMidi, 0, numSamples);
|
||
|
}
|
||
|
|
||
|
void audioDeviceAboutToStart (AudioIODevice* device) override
|
||
|
{
|
||
|
auto sampleRate = device->getCurrentSampleRate();
|
||
|
midiCollector.reset (sampleRate);
|
||
|
synth.setCurrentPlaybackSampleRate (sampleRate);
|
||
|
}
|
||
|
|
||
|
void audioDeviceStopped() override {}
|
||
|
|
||
|
private:
|
||
|
//==============================================================================
|
||
|
void handleIncomingMidiMessage (MidiInput* /*source*/,
|
||
|
const MidiMessage& message) override
|
||
|
{
|
||
|
visualiserInstrument.processNextMidiEvent (message);
|
||
|
midiCollector.addMessageToQueue (message);
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
void zoneChanged (bool isLowerZone, int numMemberChannels,
|
||
|
int perNotePitchbendRange, int masterPitchbendRange) override
|
||
|
{
|
||
|
auto* midiOutput = audioDeviceManager.getDefaultMidiOutput();
|
||
|
if (midiOutput != nullptr)
|
||
|
{
|
||
|
if (isLowerZone)
|
||
|
midiOutput->sendBlockOfMessagesNow (MPEMessages::setLowerZone (numMemberChannels, perNotePitchbendRange, masterPitchbendRange));
|
||
|
else
|
||
|
midiOutput->sendBlockOfMessagesNow (MPEMessages::setUpperZone (numMemberChannels, perNotePitchbendRange, masterPitchbendRange));
|
||
|
}
|
||
|
|
||
|
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);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
synth.setZoneLayout (zoneLayout);
|
||
|
visualiserInstrument.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
|
||
|
AudioDeviceManager audioDeviceManager;
|
||
|
#else
|
||
|
AudioDeviceManager& audioDeviceManager { getSharedAudioDeviceManager (0, 2) };
|
||
|
#endif
|
||
|
|
||
|
MPEZoneLayout zoneLayout;
|
||
|
ZoneColourPicker colourPicker;
|
||
|
|
||
|
AudioDeviceSelectorComponent audioSetupComp;
|
||
|
MPESetupComponent MPESetupComp;
|
||
|
ZoneLayoutComponent zoneLayoutComp;
|
||
|
|
||
|
Visualiser visualiserComp;
|
||
|
Viewport visualiserViewport;
|
||
|
MPEInstrument visualiserInstrument;
|
||
|
|
||
|
MPESynthesiser synth;
|
||
|
MidiMessageCollector midiCollector;
|
||
|
|
||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPEDemo)
|
||
|
};
|