|
- /*
- ==============================================================================
-
- 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 * 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 = getWidth() * n / 128;
- auto y = 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 (AlertWindow::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 * (xPos)), 20,
- int (channelWidth * (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() * startChannel), 0,
- int (getChannelRectangleWidth() * 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()) / 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)
- };
|