1 /*
2   ==============================================================================
3 
4    This file is part of the JUCE examples.
5    Copyright (c) 2020 - Raw Material Software Limited
6 
7    The code included in this file is provided under the terms of the ISC license
8    http://www.isc.org/downloads/software-support-policy/isc-license. Permission
9    To use, copy, modify, and/or distribute this software for any purpose with or
10    without fee is hereby granted provided that the above copyright notice and
11    this permission notice appear in all copies.
12 
13    THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
14    WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
15    PURPOSE, ARE DISCLAIMED.
16 
17   ==============================================================================
18 */
19 
20 /*******************************************************************************
21  The block below describes the properties of this PIP. A PIP is a short snippet
22  of code that can be read by the Projucer and used to generate a JUCE project.
23 
24  BEGIN_JUCE_PIP_METADATA
25 
26  name:             MPEDemo
27  version:          1.0.0
28  vendor:           JUCE
29  website:          http://juce.com
30  description:      Simple MPE synthesiser application.
31 
32  dependencies:     juce_audio_basics, juce_audio_devices, juce_audio_formats,
33                    juce_audio_processors, juce_audio_utils, juce_core,
34                    juce_data_structures, juce_events, juce_graphics,
35                    juce_gui_basics, juce_gui_extra
36  exporters:        xcode_mac, vs2019, linux_make, androidstudio, xcode_iphone
37 
38  moduleFlags:      JUCE_STRICT_REFCOUNTEDPOINTER=1
39 
40  type:             Component
41  mainClass:        MPEDemo
42 
43  useLocalCopy:     1
44 
45  END_JUCE_PIP_METADATA
46 
47 *******************************************************************************/
48 
49 #pragma once
50 
51 //==============================================================================
52 class ZoneColourPicker
53 {
54 public:
ZoneColourPicker()55     ZoneColourPicker() {}
56 
57     //==============================================================================
getColourForMidiChannel(int midiChannel)58     Colour getColourForMidiChannel (int midiChannel) noexcept
59     {
60         if (legacyModeEnabled)
61             return Colours::white;
62 
63         if (zoneLayout.getLowerZone().isUsingChannelAsMemberChannel (midiChannel))
64             return getColourForZone (true);
65 
66         if (zoneLayout.getUpperZone().isUsingChannelAsMemberChannel (midiChannel))
67             return getColourForZone (false);
68 
69         return Colours::transparentBlack;
70     }
71 
72     //==============================================================================
getColourForZone(bool isLowerZone)73     Colour getColourForZone (bool isLowerZone) const noexcept
74     {
75         if (legacyModeEnabled)
76             return Colours::white;
77 
78         if (isLowerZone)
79             return Colours::blue;
80 
81         return Colours::red;
82     }
83 
84     //==============================================================================
setZoneLayout(MPEZoneLayout layout)85     void setZoneLayout (MPEZoneLayout layout) noexcept          { zoneLayout = layout; }
setLegacyModeEnabled(bool shouldBeEnabled)86     void setLegacyModeEnabled (bool shouldBeEnabled) noexcept   { legacyModeEnabled = shouldBeEnabled; }
87 
88 private:
89     //==============================================================================
90     MPEZoneLayout zoneLayout;
91     bool legacyModeEnabled = false;
92 
93     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ZoneColourPicker)
94 };
95 
96 //==============================================================================
97 class NoteComponent : public Component
98 {
99 public:
NoteComponent(const MPENote & n,Colour colourToUse)100     NoteComponent (const MPENote& n, Colour colourToUse)
101         : note (n), colour (colourToUse)
102     {}
103 
104     //==============================================================================
update(const MPENote & newNote,Point<float> newCentre)105     void update (const MPENote& newNote, Point<float> newCentre)
106     {
107         note = newNote;
108         centre = newCentre;
109 
110         setBounds (getSquareAroundCentre (jmax (getNoteOnRadius(), getNoteOffRadius(), getPressureRadius()))
111                      .getUnion (getTextRectangle())
112                      .getSmallestIntegerContainer()
113                      .expanded (3));
114 
115         repaint();
116     }
117 
118     //==============================================================================
paint(Graphics & g)119     void paint (Graphics& g) override
120     {
121         if (note.keyState == MPENote::keyDown || note.keyState == MPENote::keyDownAndSustained)
122             drawPressedNoteCircle (g, colour);
123         else if (note.keyState == MPENote::sustained)
124             drawSustainedNoteCircle (g, colour);
125         else
126             return;
127 
128         drawNoteLabel (g, colour);
129     }
130 
131     //==============================================================================
132     MPENote note;
133     Colour colour;
134     Point<float> centre;
135 
136 private:
137     //==============================================================================
drawPressedNoteCircle(Graphics & g,Colour zoneColour)138     void drawPressedNoteCircle (Graphics& g, Colour zoneColour)
139     {
140         g.setColour (zoneColour.withAlpha (0.3f));
141         g.fillEllipse (translateToLocalBounds (getSquareAroundCentre (getNoteOnRadius())));
142         g.setColour (zoneColour);
143         g.drawEllipse (translateToLocalBounds (getSquareAroundCentre (getPressureRadius())), 2.0f);
144     }
145 
146     //==============================================================================
drawSustainedNoteCircle(Graphics & g,Colour zoneColour)147     void drawSustainedNoteCircle (Graphics& g, Colour zoneColour)
148     {
149         g.setColour (zoneColour);
150         Path circle, dashedCircle;
151         circle.addEllipse (translateToLocalBounds (getSquareAroundCentre (getNoteOffRadius())));
152         float dashLengths[] = { 3.0f, 3.0f };
153         PathStrokeType (2.0, PathStrokeType::mitered).createDashedStroke (dashedCircle, circle, dashLengths, 2);
154         g.fillPath (dashedCircle);
155     }
156 
157     //==============================================================================
drawNoteLabel(Graphics & g,Colour)158     void drawNoteLabel (Graphics& g, Colour /**zoneColour*/)
159     {
160         auto textBounds = translateToLocalBounds (getTextRectangle()).getSmallestIntegerContainer();
161 
162         g.drawText ("+", textBounds, Justification::centred);
163         g.drawText (MidiMessage::getMidiNoteName (note.initialNote, true, true, 3), textBounds, Justification::centredBottom);
164         g.setFont (Font (22.0f, Font::bold));
165         g.drawText (String (note.midiChannel), textBounds, Justification::centredTop);
166     }
167 
168     //==============================================================================
getSquareAroundCentre(float radius)169     Rectangle<float> getSquareAroundCentre (float radius) const noexcept
170     {
171         return Rectangle<float> (radius * 2.0f, radius * 2.0f).withCentre (centre);
172     }
173 
translateToLocalBounds(Rectangle<float> r)174     Rectangle<float> translateToLocalBounds (Rectangle<float> r) const noexcept
175     {
176         return r - getPosition().toFloat();
177     }
178 
getTextRectangle()179     Rectangle<float> getTextRectangle() const noexcept
180     {
181         return Rectangle<float> (30.0f, 50.0f).withCentre (centre);
182     }
183 
getNoteOnRadius()184     float getNoteOnRadius()   const noexcept   { return note.noteOnVelocity .asUnsignedFloat() * maxNoteRadius; }
getNoteOffRadius()185     float getNoteOffRadius()  const noexcept   { return note.noteOffVelocity.asUnsignedFloat() * maxNoteRadius; }
getPressureRadius()186     float getPressureRadius() const noexcept   { return note.pressure       .asUnsignedFloat() * maxNoteRadius; }
187 
188     const float maxNoteRadius = 100.0f;
189 
190     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NoteComponent)
191 };
192 
193 //==============================================================================
194 class Visualiser : public Component,
195                    public MPEInstrument::Listener,
196                    private AsyncUpdater
197 {
198 public:
199     //==============================================================================
Visualiser(ZoneColourPicker & zoneColourPicker)200     Visualiser (ZoneColourPicker& zoneColourPicker)
201         : colourPicker (zoneColourPicker)
202     {}
203 
204     //==============================================================================
paint(Graphics & g)205     void paint (Graphics& g) override
206     {
207         g.fillAll (Colours::black);
208 
209         auto noteDistance = float (getWidth()) / 128;
210         for (auto i = 0; i < 128; ++i)
211         {
212             auto x = noteDistance * (float) i;
213             auto noteHeight = int (MidiMessage::isMidiNoteBlack (i) ? 0.7 * getHeight() : getHeight());
214 
215             g.setColour (MidiMessage::isMidiNoteBlack (i) ? Colours::white : Colours::grey);
216             g.drawLine (x, 0.0f, x, (float) noteHeight);
217 
218             if (i > 0 && i % 12 == 0)
219             {
220                 g.setColour (Colours::grey);
221                 auto octaveNumber = (i / 12) - 2;
222                 g.drawText ("C" + String (octaveNumber), (int) x - 15, getHeight() - 30, 30, 30, Justification::centredBottom);
223             }
224         }
225     }
226 
227     //==============================================================================
noteAdded(MPENote newNote)228     void noteAdded (MPENote newNote) override
229     {
230         const ScopedLock sl (lock);
231         activeNotes.add (newNote);
232         triggerAsyncUpdate();
233     }
234 
notePressureChanged(MPENote note)235     void notePressureChanged  (MPENote note) override { noteChanged (note); }
notePitchbendChanged(MPENote note)236     void notePitchbendChanged (MPENote note) override { noteChanged (note); }
noteTimbreChanged(MPENote note)237     void noteTimbreChanged    (MPENote note) override { noteChanged (note); }
noteKeyStateChanged(MPENote note)238     void noteKeyStateChanged  (MPENote note) override { noteChanged (note); }
239 
noteChanged(MPENote changedNote)240     void noteChanged (MPENote changedNote)
241     {
242         const ScopedLock sl (lock);
243 
244         for (auto& note : activeNotes)
245             if (note.noteID == changedNote.noteID)
246                 note = changedNote;
247 
248         triggerAsyncUpdate();
249     }
250 
noteReleased(MPENote finishedNote)251     void noteReleased (MPENote finishedNote) override
252     {
253         const ScopedLock sl (lock);
254 
255         for (auto i = activeNotes.size(); --i >= 0;)
256             if (activeNotes.getReference(i).noteID == finishedNote.noteID)
257                 activeNotes.remove (i);
258 
259         triggerAsyncUpdate();
260     }
261 
262 
263 private:
264     //==============================================================================
findActiveNote(int noteID)265     const MPENote* findActiveNote (int noteID) const noexcept
266     {
267         for (auto& note : activeNotes)
268             if (note.noteID == noteID)
269                 return &note;
270 
271         return nullptr;
272     }
273 
findNoteComponent(int noteID)274     NoteComponent* findNoteComponent (int noteID) const noexcept
275     {
276         for (auto& noteComp : noteComponents)
277             if (noteComp->note.noteID == noteID)
278                 return noteComp;
279 
280         return nullptr;
281     }
282 
283     //==============================================================================
handleAsyncUpdate()284     void handleAsyncUpdate() override
285     {
286         const ScopedLock sl (lock);
287 
288         for (auto i = noteComponents.size(); --i >= 0;)
289             if (findActiveNote (noteComponents.getUnchecked(i)->note.noteID) == nullptr)
290                 noteComponents.remove (i);
291 
292         for (auto& note : activeNotes)
293             if (findNoteComponent (note.noteID) == nullptr)
294                 addAndMakeVisible (noteComponents.add (new NoteComponent (note, colourPicker.getColourForMidiChannel(note.midiChannel))));
295 
296         for (auto& noteComp : noteComponents)
297             if (auto* noteInfo = findActiveNote (noteComp->note.noteID))
298                 noteComp->update (*noteInfo, getCentrePositionForNote (*noteInfo));
299     }
300 
301     //==============================================================================
getCentrePositionForNote(MPENote note)302     Point<float> getCentrePositionForNote (MPENote note) const
303     {
304         auto n = float (note.initialNote) + float (note.totalPitchbendInSemitones);
305         auto x = (float) getWidth() * n / 128;
306         auto y = (float) getHeight() * (1 - note.timbre.asUnsignedFloat());
307 
308         return { x, y };
309     }
310 
311     //==============================================================================
312     OwnedArray<NoteComponent> noteComponents;
313     CriticalSection lock;
314     Array<MPENote> activeNotes;
315     ZoneColourPicker& colourPicker;
316 
317     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Visualiser)
318 };
319 
320 //==============================================================================
321 class MPESetupComponent : public Component,
322                           public ChangeBroadcaster
323 {
324 public:
325     //==============================================================================
326     class Listener
327     {
328     public:
~Listener()329         virtual ~Listener() {}
330         virtual void zoneChanged (bool isLower, int numMemberChans, int perNotePb, int masterPb) = 0;
331         virtual void allZonesCleared() = 0;
332         virtual void legacyModeChanged (bool legacyModeEnabled, int pitchbendRange, Range<int> channelRange) = 0;
333         virtual void voiceStealingEnabledChanged (bool voiceStealingEnabled) = 0;
334         virtual void numberOfVoicesChanged (int numberOfVoices) = 0;
335     };
336 
addListener(Listener * listenerToAdd)337     void addListener (Listener* listenerToAdd)         { listeners.add (listenerToAdd); }
removeListener(Listener * listenerToRemove)338     void removeListener (Listener* listenerToRemove)   { listeners.remove (listenerToRemove); }
339 
340     //==============================================================================
MPESetupComponent()341     MPESetupComponent()
342     {
343         addAndMakeVisible (isLowerZoneButton);
344         isLowerZoneButton.setToggleState (true, NotificationType::dontSendNotification);
345 
346         initialiseComboBoxWithConsecutiveIntegers (memberChannels, memberChannelsLabel, 0, 16, defaultMemberChannels);
347         initialiseComboBoxWithConsecutiveIntegers (masterPitchbendRange, masterPitchbendRangeLabel, 0, 96, defaultMasterPitchbendRange);
348         initialiseComboBoxWithConsecutiveIntegers (notePitchbendRange, notePitchbendRangeLabel, 0, 96, defaultNotePitchbendRange);
349 
350         initialiseComboBoxWithConsecutiveIntegers (legacyStartChannel, legacyStartChannelLabel, 1, 16, 1, false);
351         initialiseComboBoxWithConsecutiveIntegers (legacyEndChannel, legacyEndChannelLabel, 1, 16, 16, false);
352         initialiseComboBoxWithConsecutiveIntegers (legacyPitchbendRange, legacyPitchbendRangeLabel, 0, 96, 2, false);
353 
354         addAndMakeVisible (setZoneButton);
355         setZoneButton.onClick = [this] { setZoneButtonClicked(); };
356         addAndMakeVisible (clearAllZonesButton);
357         clearAllZonesButton.onClick = [this] { clearAllZonesButtonClicked(); };
358         addAndMakeVisible (legacyModeEnabledToggle);
359         legacyModeEnabledToggle.onClick = [this] { legacyModeEnabledToggleClicked(); };
360         addAndMakeVisible (voiceStealingEnabledToggle);
361         voiceStealingEnabledToggle.onClick = [this] { voiceStealingEnabledToggleClicked(); };
362 
363         initialiseComboBoxWithConsecutiveIntegers (numberOfVoices, numberOfVoicesLabel, 1, 20, 15);
364     }
365 
366     //==============================================================================
resized()367     void resized() override
368     {
369         Rectangle<int> r (proportionOfWidth (0.65f), 15, proportionOfWidth (0.25f), 3000);
370         auto h = 24;
371         auto hspace = 6;
372         auto hbigspace = 18;
373 
374         isLowerZoneButton.setBounds (r.removeFromTop (h));
375         r.removeFromTop (hspace);
376         memberChannels.setBounds (r.removeFromTop (h));
377         r.removeFromTop (hspace);
378         notePitchbendRange.setBounds (r.removeFromTop (h));
379         r.removeFromTop (hspace);
380         masterPitchbendRange.setBounds (r.removeFromTop (h));
381 
382         legacyStartChannel  .setBounds (isLowerZoneButton .getBounds());
383         legacyEndChannel    .setBounds (memberChannels    .getBounds());
384         legacyPitchbendRange.setBounds (notePitchbendRange.getBounds());
385 
386         r.removeFromTop (hbigspace);
387 
388         auto buttonLeft = proportionOfWidth (0.5f);
389 
390         setZoneButton.setBounds (r.removeFromTop (h).withLeft (buttonLeft));
391         r.removeFromTop (hspace);
392         clearAllZonesButton.setBounds (r.removeFromTop (h).withLeft (buttonLeft));
393 
394         r.removeFromTop (hbigspace);
395 
396         auto toggleLeft = proportionOfWidth (0.25f);
397 
398         legacyModeEnabledToggle.setBounds (r.removeFromTop (h).withLeft (toggleLeft));
399         r.removeFromTop (hspace);
400         voiceStealingEnabledToggle.setBounds (r.removeFromTop (h).withLeft (toggleLeft));
401         r.removeFromTop (hspace);
402         numberOfVoices.setBounds (r.removeFromTop (h));
403     }
404 
405 private:
406     //==============================================================================
407     void initialiseComboBoxWithConsecutiveIntegers (ComboBox& comboBox, Label& labelToAttach,
408                                                     int firstValue, int numValues, int valueToSelect,
409                                                     bool makeVisible = true)
410     {
411         for (auto i = 0; i < numValues; ++i)
412             comboBox.addItem (String (i + firstValue), i + 1);
413 
414         comboBox.setSelectedId (valueToSelect - firstValue + 1);
415         labelToAttach.attachToComponent (&comboBox, true);
416 
417         if (makeVisible)
418             addAndMakeVisible (comboBox);
419         else
420             addChildComponent (comboBox);
421 
422         if (&comboBox == &numberOfVoices)
423             comboBox.onChange = [this] { numberOfVoicesChanged(); };
424         else if (&comboBox == &legacyPitchbendRange)
425             comboBox.onChange = [this] { if (legacyModeEnabledToggle.getToggleState()) legacyModePitchbendRangeChanged(); };
426         else if (&comboBox == &legacyStartChannel || &comboBox == &legacyEndChannel)
427             comboBox.onChange = [this] { if (legacyModeEnabledToggle.getToggleState()) legacyModeChannelRangeChanged(); };
428     }
429 
430     //==============================================================================
setZoneButtonClicked()431     void setZoneButtonClicked()
432     {
433         auto isLowerZone = isLowerZoneButton.getToggleState();
434         auto numMemberChannels = memberChannels.getText().getIntValue();
435         auto perNotePb = notePitchbendRange.getText().getIntValue();
436         auto masterPb = masterPitchbendRange.getText().getIntValue();
437 
438         if (isLowerZone)
439             zoneLayout.setLowerZone (numMemberChannels, perNotePb, masterPb);
440         else
441             zoneLayout.setUpperZone (numMemberChannels, perNotePb, masterPb);
442 
443         listeners.call ([&] (Listener& l) { l.zoneChanged (isLowerZone, numMemberChannels, perNotePb, masterPb); });
444     }
445 
446     //==============================================================================
clearAllZonesButtonClicked()447     void clearAllZonesButtonClicked()
448     {
449         zoneLayout.clearAllZones();
450         listeners.call ([] (Listener& l) { l.allZonesCleared(); });
451     }
452 
453     //==============================================================================
legacyModeEnabledToggleClicked()454     void legacyModeEnabledToggleClicked()
455     {
456         auto legacyModeEnabled = legacyModeEnabledToggle.getToggleState();
457 
458         isLowerZoneButton   .setVisible (! legacyModeEnabled);
459         memberChannels      .setVisible (! legacyModeEnabled);
460         notePitchbendRange  .setVisible (! legacyModeEnabled);
461         masterPitchbendRange.setVisible (! legacyModeEnabled);
462         setZoneButton       .setVisible (! legacyModeEnabled);
463         clearAllZonesButton .setVisible (! legacyModeEnabled);
464 
465         legacyStartChannel  .setVisible (legacyModeEnabled);
466         legacyEndChannel    .setVisible (legacyModeEnabled);
467         legacyPitchbendRange.setVisible (legacyModeEnabled);
468 
469         if (areLegacyModeParametersValid())
470         {
471             listeners.call ([&] (Listener& l) { l.legacyModeChanged (legacyModeEnabledToggle.getToggleState(),
472                                                                      legacyPitchbendRange.getText().getIntValue(),
473                                                                      getLegacyModeChannelRange()); });
474         }
475         else
476         {
477             handleInvalidLegacyModeParameters();
478         }
479     }
480 
481     //==============================================================================
voiceStealingEnabledToggleClicked()482     void voiceStealingEnabledToggleClicked()
483     {
484         auto newState = voiceStealingEnabledToggle.getToggleState();
485         listeners.call ([=] (Listener& l) { l.voiceStealingEnabledChanged (newState); });
486     }
487 
488     //==============================================================================
numberOfVoicesChanged()489     void numberOfVoicesChanged()
490     {
491         listeners.call ([this] (Listener& l) { l.numberOfVoicesChanged (numberOfVoices.getText().getIntValue()); });
492     }
493 
legacyModePitchbendRangeChanged()494     void legacyModePitchbendRangeChanged()
495     {
496         jassert (legacyModeEnabledToggle.getToggleState() == true);
497 
498         listeners.call ([this] (Listener& l) { l.legacyModeChanged (true,
499                                                                     legacyPitchbendRange.getText().getIntValue(),
500                                                                     getLegacyModeChannelRange()); });
501     }
502 
legacyModeChannelRangeChanged()503     void legacyModeChannelRangeChanged()
504     {
505         jassert (legacyModeEnabledToggle.getToggleState() == true);
506 
507         if (areLegacyModeParametersValid())
508         {
509             listeners.call ([this] (Listener& l) { l.legacyModeChanged (true,
510                                                                         legacyPitchbendRange.getText().getIntValue(),
511                                                                         getLegacyModeChannelRange()); });
512         }
513         else
514         {
515             handleInvalidLegacyModeParameters();
516         }
517     }
518 
519     //==============================================================================
areLegacyModeParametersValid()520     bool areLegacyModeParametersValid() const
521     {
522         return legacyStartChannel.getText().getIntValue() <= legacyEndChannel.getText().getIntValue();
523     }
524 
handleInvalidLegacyModeParameters()525     void handleInvalidLegacyModeParameters() const
526     {
527         AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
528                                           "Invalid legacy mode channel layout",
529                                           "Cannot set legacy mode start/end channel:\n"
530                                           "The end channel must not be less than the start channel!",
531                                           "Got it");
532     }
533 
534     //==============================================================================
getLegacyModeChannelRange()535     Range<int> getLegacyModeChannelRange() const
536     {
537         return { legacyStartChannel.getText().getIntValue(),
538                  legacyEndChannel.getText().getIntValue() + 1 };
539     }
540 
541     //==============================================================================
542     MPEZoneLayout zoneLayout;
543 
544     ComboBox memberChannels, masterPitchbendRange, notePitchbendRange;
545 
546     ToggleButton isLowerZoneButton  { "Lower zone" };
547 
548     Label memberChannelsLabel       { {}, "Nr. of member channels:" };
549     Label masterPitchbendRangeLabel { {}, "Master pitchbend range (semitones):" };
550     Label notePitchbendRangeLabel   { {}, "Note pitchbend range (semitones):" };
551 
552     TextButton setZoneButton        { "Set zone" };
553     TextButton clearAllZonesButton  { "Clear all zones" };
554 
555     ComboBox legacyStartChannel, legacyEndChannel, legacyPitchbendRange;
556 
557     Label legacyStartChannelLabel   { {}, "First channel:" };
558     Label legacyEndChannelLabel     { {}, "Last channel:" };
559     Label legacyPitchbendRangeLabel { {}, "Pitchbend range (semitones):"};
560 
561     ToggleButton legacyModeEnabledToggle    { "Enable Legacy Mode" };
562     ToggleButton voiceStealingEnabledToggle { "Enable synth voice stealing" };
563 
564     ComboBox numberOfVoices;
565     Label numberOfVoicesLabel { {}, "Number of synth voices"};
566 
567     ListenerList<Listener> listeners;
568 
569     const int defaultMemberChannels       = 15,
570               defaultMasterPitchbendRange = 2,
571               defaultNotePitchbendRange   = 48;
572 
573     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPESetupComponent)
574 };
575 
576 //==============================================================================
577 class ZoneLayoutComponent : public Component,
578                             public MPESetupComponent::Listener
579 {
580 public:
581     //==============================================================================
ZoneLayoutComponent(const ZoneColourPicker & zoneColourPicker)582     ZoneLayoutComponent (const ZoneColourPicker& zoneColourPicker)
583         : colourPicker (zoneColourPicker)
584     {}
585 
586     //==============================================================================
paint(Graphics & g)587     void paint (Graphics& g) override
588     {
589         paintBackground (g);
590 
591         if (legacyModeEnabled)
592             paintLegacyMode (g);
593         else
594             paintZones (g);
595     }
596 
597     //==============================================================================
zoneChanged(bool isLowerZone,int numMemberChannels,int perNotePitchbendRange,int masterPitchbendRange)598     void zoneChanged (bool isLowerZone, int numMemberChannels,
599                       int perNotePitchbendRange, int masterPitchbendRange) override
600     {
601         if (isLowerZone)
602             zoneLayout.setLowerZone (numMemberChannels, perNotePitchbendRange, masterPitchbendRange);
603         else
604             zoneLayout.setUpperZone (numMemberChannels, perNotePitchbendRange, masterPitchbendRange);
605 
606         repaint();
607     }
608 
allZonesCleared()609     void allZonesCleared() override
610     {
611         zoneLayout.clearAllZones();
612         repaint();
613     }
614 
legacyModeChanged(bool legacyModeShouldBeEnabled,int pitchbendRange,Range<int> channelRange)615     void legacyModeChanged (bool legacyModeShouldBeEnabled, int pitchbendRange, Range<int> channelRange) override
616     {
617         legacyModeEnabled = legacyModeShouldBeEnabled;
618         legacyModePitchbendRange = pitchbendRange;
619         legacyModeChannelRange = channelRange;
620 
621         repaint();
622     }
623 
voiceStealingEnabledChanged(bool)624     void voiceStealingEnabledChanged (bool) override   { /* not interested in this change */ }
numberOfVoicesChanged(int)625     void numberOfVoicesChanged (int) override          { /* not interested in this change */ }
626 
627 private:
628     //==============================================================================
paintBackground(Graphics & g)629     void paintBackground (Graphics& g)
630     {
631         g.setColour (Colours::black);
632         auto channelWidth = getChannelRectangleWidth();
633 
634         for (auto i = 0; i < numMidiChannels; ++i)
635         {
636             auto x = float (i) * channelWidth;
637             Rectangle<int> channelArea ((int) x, 0, (int) channelWidth, getHeight());
638 
639             g.drawLine ({ x, 0.0f, x, float (getHeight()) });
640             g.drawText (String (i + 1), channelArea.reduced (4, 4), Justification::topLeft, false);
641         }
642     }
643 
644     //==============================================================================
paintZones(Graphics & g)645     void paintZones (Graphics& g)
646     {
647         auto channelWidth = getChannelRectangleWidth();
648 
649         Array<MPEZoneLayout::Zone> activeZones;
650         if (zoneLayout.getLowerZone().isActive())  activeZones.add (zoneLayout.getLowerZone());
651         if (zoneLayout.getUpperZone().isActive())  activeZones.add (zoneLayout.getUpperZone());
652 
653         for (auto zone : activeZones)
654         {
655             auto zoneColour = colourPicker.getColourForZone (zone.isLowerZone());
656 
657             auto xPos = zone.isLowerZone() ? 0 : zone.getLastMemberChannel() - 1;
658 
659             Rectangle<int> zoneRect { int (channelWidth * (float) xPos), 20,
660                                       int (channelWidth * (float) (zone.numMemberChannels + 1)), getHeight() - 20 };
661 
662             g.setColour (zoneColour);
663             g.drawRect (zoneRect, 3);
664 
665             auto masterRect = zone.isLowerZone() ? zoneRect.removeFromLeft ((int) channelWidth) : zoneRect.removeFromRight ((int) channelWidth);
666 
667             g.setColour (zoneColour.withAlpha (0.3f));
668             g.fillRect (masterRect);
669 
670             g.setColour (zoneColour.contrasting());
671             g.drawText ("<>" + String (zone.masterPitchbendRange),  masterRect.reduced (4), Justification::top,    false);
672             g.drawText ("<>" + String (zone.perNotePitchbendRange), masterRect.reduced (4), Justification::bottom, false);
673         }
674     }
675 
676     //==============================================================================
paintLegacyMode(Graphics & g)677     void paintLegacyMode (Graphics& g)
678     {
679         auto startChannel = legacyModeChannelRange.getStart() - 1;
680         auto numChannels  = legacyModeChannelRange.getEnd() - startChannel - 1;
681 
682 
683         Rectangle<int> zoneRect (int (getChannelRectangleWidth() * (float) startChannel), 0,
684                                  int (getChannelRectangleWidth() * (float) numChannels), getHeight());
685 
686         zoneRect.removeFromTop (20);
687 
688         g.setColour (Colours::white);
689         g.drawRect (zoneRect, 3);
690         g.drawText ("LGCY", zoneRect.reduced (4, 4), Justification::topLeft, false);
691         g.drawText ("<>" + String (legacyModePitchbendRange), zoneRect.reduced (4, 4), Justification::bottomLeft, false);
692     }
693 
694     //==============================================================================
getChannelRectangleWidth()695     float getChannelRectangleWidth() const noexcept
696     {
697         return (float) getWidth() / (float) numMidiChannels;
698     }
699 
700     //==============================================================================
701     MPEZoneLayout zoneLayout;
702     const ZoneColourPicker& colourPicker;
703 
704     bool legacyModeEnabled = false;
705     int legacyModePitchbendRange = 48;
706     Range<int> legacyModeChannelRange = { 1, 17 };
707     const int numMidiChannels = 16;
708 };
709 
710 //==============================================================================
711 class MPEDemoSynthVoice : public MPESynthesiserVoice
712 {
713 public:
714     //==============================================================================
MPEDemoSynthVoice()715     MPEDemoSynthVoice() {}
716 
717     //==============================================================================
noteStarted()718     void noteStarted() override
719     {
720         jassert (currentlyPlayingNote.isValid());
721         jassert (currentlyPlayingNote.keyState == MPENote::keyDown
722                  || currentlyPlayingNote.keyState == MPENote::keyDownAndSustained);
723 
724         level    .setTargetValue (currentlyPlayingNote.pressure.asUnsignedFloat());
725         frequency.setTargetValue (currentlyPlayingNote.getFrequencyInHertz());
726         timbre   .setTargetValue (currentlyPlayingNote.timbre.asUnsignedFloat());
727 
728         phase = 0.0;
729         auto cyclesPerSample = frequency.getNextValue() / currentSampleRate;
730         phaseDelta = MathConstants<double>::twoPi * cyclesPerSample;
731 
732         tailOff = 0.0;
733     }
734 
noteStopped(bool allowTailOff)735     void noteStopped (bool allowTailOff) override
736     {
737         jassert (currentlyPlayingNote.keyState == MPENote::off);
738 
739         if (allowTailOff)
740         {
741             // start a tail-off by setting this flag. The render callback will pick up on
742             // this and do a fade out, calling clearCurrentNote() when it's finished.
743 
744             if (tailOff == 0.0) // we only need to begin a tail-off if it's not already doing so - the
745                                 // stopNote method could be called more than once.
746                 tailOff = 1.0;
747         }
748         else
749         {
750             // we're being told to stop playing immediately, so reset everything..
751             clearCurrentNote();
752             phaseDelta = 0.0;
753         }
754     }
755 
notePressureChanged()756     void notePressureChanged() override
757     {
758         level.setTargetValue (currentlyPlayingNote.pressure.asUnsignedFloat());
759     }
760 
notePitchbendChanged()761     void notePitchbendChanged() override
762     {
763         frequency.setTargetValue (currentlyPlayingNote.getFrequencyInHertz());
764     }
765 
noteTimbreChanged()766     void noteTimbreChanged() override
767     {
768         timbre.setTargetValue (currentlyPlayingNote.timbre.asUnsignedFloat());
769     }
770 
noteKeyStateChanged()771     void noteKeyStateChanged() override {}
772 
setCurrentSampleRate(double newRate)773     void setCurrentSampleRate (double newRate) override
774     {
775         if (currentSampleRate != newRate)
776         {
777             noteStopped (false);
778             currentSampleRate = newRate;
779 
780             level    .reset (currentSampleRate, smoothingLengthInSeconds);
781             timbre   .reset (currentSampleRate, smoothingLengthInSeconds);
782             frequency.reset (currentSampleRate, smoothingLengthInSeconds);
783         }
784     }
785 
786     //==============================================================================
renderNextBlock(AudioBuffer<float> & outputBuffer,int startSample,int numSamples)787     virtual void renderNextBlock (AudioBuffer<float>& outputBuffer,
788                                   int startSample,
789                                   int numSamples) override
790     {
791         if (phaseDelta != 0.0)
792         {
793             if (tailOff > 0.0)
794             {
795                 while (--numSamples >= 0)
796                 {
797                     auto currentSample = getNextSample() * (float) tailOff;
798 
799                     for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
800                         outputBuffer.addSample (i, startSample, currentSample);
801 
802                     ++startSample;
803 
804                     tailOff *= 0.99;
805 
806                     if (tailOff <= 0.005)
807                     {
808                         clearCurrentNote();
809 
810                         phaseDelta = 0.0;
811                         break;
812                     }
813                 }
814             }
815             else
816             {
817                 while (--numSamples >= 0)
818                 {
819                     auto currentSample = getNextSample();
820 
821                     for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
822                         outputBuffer.addSample (i, startSample, currentSample);
823 
824                     ++startSample;
825                 }
826             }
827         }
828     }
829 
830     using MPESynthesiserVoice::renderNextBlock;
831 
832 private:
833     //==============================================================================
getNextSample()834     float getNextSample() noexcept
835     {
836         auto levelDb = (level.getNextValue() - 1.0) * maxLevelDb;
837         auto amplitude = pow (10.0f, 0.05f * levelDb) * maxLevel;
838 
839         // timbre is used to blend between a sine and a square.
840         auto f1 = std::sin (phase);
841         auto f2 = copysign (1.0, f1);
842         auto a2 = timbre.getNextValue();
843         auto a1 = 1.0 - a2;
844 
845         auto nextSample = float (amplitude * ((a1 * f1) + (a2 * f2)));
846 
847         auto cyclesPerSample = frequency.getNextValue() / currentSampleRate;
848         phaseDelta = MathConstants<double>::twoPi * cyclesPerSample;
849         phase = std::fmod (phase + phaseDelta, MathConstants<double>::twoPi);
850 
851         return nextSample;
852     }
853 
854     //==============================================================================
855     SmoothedValue<double> level, timbre, frequency;
856 
857     double phase      = 0.0;
858     double phaseDelta = 0.0;
859     double tailOff    = 0.0;
860 
861     const double maxLevel   = 0.05;
862     const double maxLevelDb = 31.0;
863     const double smoothingLengthInSeconds = 0.01;
864 };
865 
866 //==============================================================================
867 class MPEDemo : public Component,
868                 private AudioIODeviceCallback,
869                 private MidiInputCallback,
870                 private MPESetupComponent::Listener
871 {
872 public:
873     //==============================================================================
MPEDemo()874     MPEDemo()
875         : audioSetupComp (audioDeviceManager, 0, 0, 0, 256, true, true, true, false),
876           zoneLayoutComp (colourPicker),
877           visualiserComp (colourPicker)
878     {
879        #ifndef JUCE_DEMO_RUNNER
880         audioDeviceManager.initialise (0, 2, nullptr, true, {}, nullptr);
881        #endif
882 
883         audioDeviceManager.addMidiInputDeviceCallback ({}, this);
884         audioDeviceManager.addAudioCallback (this);
885 
886         addAndMakeVisible (audioSetupComp);
887         addAndMakeVisible (MPESetupComp);
888         addAndMakeVisible (zoneLayoutComp);
889         addAndMakeVisible (visualiserViewport);
890 
891         visualiserViewport.setScrollBarsShown (false, true);
892         visualiserViewport.setViewedComponent (&visualiserComp, false);
893         visualiserViewport.setViewPositionProportionately (0.5, 0.0);
894 
895         MPESetupComp.addListener (&zoneLayoutComp);
896         MPESetupComp.addListener (this);
897         visualiserInstrument.addListener (&visualiserComp);
898 
899         synth.setVoiceStealingEnabled (false);
900         for (auto i = 0; i < 15; ++i)
901             synth.addVoice (new MPEDemoSynthVoice());
902 
903         setSize (880, 720);
904     }
905 
~MPEDemo()906     ~MPEDemo() override
907     {
908         audioDeviceManager.removeMidiInputDeviceCallback ({}, this);
909         audioDeviceManager.removeAudioCallback (this);
910     }
911 
912     //==============================================================================
resized()913     void resized() override
914     {
915         auto visualiserCompWidth  = 2800;
916         auto visualiserCompHeight = 300;
917         auto zoneLayoutCompHeight = 60;
918         auto audioSetupCompRelativeWidth = 0.55f;
919 
920         auto r = getLocalBounds();
921 
922         visualiserViewport.setBounds (r.removeFromBottom (visualiserCompHeight));
923         visualiserComp    .setBounds ({ visualiserCompWidth,
924                                         visualiserViewport.getHeight() - visualiserViewport.getScrollBarThickness() });
925 
926         zoneLayoutComp.setBounds (r.removeFromBottom (zoneLayoutCompHeight));
927         audioSetupComp.setBounds (r.removeFromLeft (proportionOfWidth (audioSetupCompRelativeWidth)));
928         MPESetupComp  .setBounds (r);
929     }
930 
931     //==============================================================================
audioDeviceIOCallback(const float **,int,float ** outputChannelData,int numOutputChannels,int numSamples)932     void audioDeviceIOCallback (const float** /*inputChannelData*/, int /*numInputChannels*/,
933                                 float** outputChannelData, int numOutputChannels,
934                                 int numSamples) override
935     {
936         AudioBuffer<float> buffer (outputChannelData, numOutputChannels, numSamples);
937         buffer.clear();
938 
939         MidiBuffer incomingMidi;
940         midiCollector.removeNextBlockOfMessages (incomingMidi, numSamples);
941         synth.renderNextBlock (buffer, incomingMidi, 0, numSamples);
942     }
943 
audioDeviceAboutToStart(AudioIODevice * device)944     void audioDeviceAboutToStart (AudioIODevice* device) override
945     {
946         auto sampleRate = device->getCurrentSampleRate();
947         midiCollector.reset (sampleRate);
948         synth.setCurrentPlaybackSampleRate (sampleRate);
949     }
950 
audioDeviceStopped()951     void audioDeviceStopped() override {}
952 
953 private:
954     //==============================================================================
handleIncomingMidiMessage(MidiInput *,const MidiMessage & message)955     void handleIncomingMidiMessage (MidiInput* /*source*/,
956                                     const MidiMessage& message) override
957     {
958         visualiserInstrument.processNextMidiEvent (message);
959         midiCollector.addMessageToQueue (message);
960     }
961 
962     //==============================================================================
zoneChanged(bool isLowerZone,int numMemberChannels,int perNotePitchbendRange,int masterPitchbendRange)963     void zoneChanged (bool isLowerZone, int numMemberChannels,
964                       int perNotePitchbendRange, int masterPitchbendRange) override
965     {
966         auto* midiOutput = audioDeviceManager.getDefaultMidiOutput();
967         if (midiOutput != nullptr)
968         {
969             if (isLowerZone)
970                 midiOutput->sendBlockOfMessagesNow (MPEMessages::setLowerZone (numMemberChannels, perNotePitchbendRange, masterPitchbendRange));
971             else
972                 midiOutput->sendBlockOfMessagesNow (MPEMessages::setUpperZone (numMemberChannels, perNotePitchbendRange, masterPitchbendRange));
973         }
974 
975         if (isLowerZone)
976             zoneLayout.setLowerZone (numMemberChannels, perNotePitchbendRange, masterPitchbendRange);
977         else
978             zoneLayout.setUpperZone (numMemberChannels, perNotePitchbendRange, masterPitchbendRange);
979 
980         visualiserInstrument.setZoneLayout (zoneLayout);
981         synth.setZoneLayout (zoneLayout);
982         colourPicker.setZoneLayout (zoneLayout);
983     }
984 
allZonesCleared()985     void allZonesCleared() override
986     {
987         auto* midiOutput = audioDeviceManager.getDefaultMidiOutput();
988         if (midiOutput != nullptr)
989             midiOutput->sendBlockOfMessagesNow (MPEMessages::clearAllZones());
990 
991         zoneLayout.clearAllZones();
992         visualiserInstrument.setZoneLayout (zoneLayout);
993         synth.setZoneLayout (zoneLayout);
994         colourPicker.setZoneLayout (zoneLayout);
995     }
996 
legacyModeChanged(bool legacyModeShouldBeEnabled,int pitchbendRange,Range<int> channelRange)997     void legacyModeChanged (bool legacyModeShouldBeEnabled, int pitchbendRange, Range<int> channelRange) override
998     {
999         colourPicker.setLegacyModeEnabled (legacyModeShouldBeEnabled);
1000 
1001         if (legacyModeShouldBeEnabled)
1002         {
1003             synth.enableLegacyMode (pitchbendRange, channelRange);
1004             visualiserInstrument.enableLegacyMode (pitchbendRange, channelRange);
1005         }
1006         else
1007         {
1008             synth.setZoneLayout (zoneLayout);
1009             visualiserInstrument.setZoneLayout (zoneLayout);
1010         }
1011     }
1012 
voiceStealingEnabledChanged(bool voiceStealingEnabled)1013     void voiceStealingEnabledChanged (bool voiceStealingEnabled) override
1014     {
1015         synth.setVoiceStealingEnabled (voiceStealingEnabled);
1016     }
1017 
numberOfVoicesChanged(int numberOfVoices)1018     void numberOfVoicesChanged (int numberOfVoices) override
1019     {
1020         if (numberOfVoices < synth.getNumVoices())
1021             synth.reduceNumVoices (numberOfVoices);
1022         else
1023             while (synth.getNumVoices() < numberOfVoices)
1024                 synth.addVoice (new MPEDemoSynthVoice());
1025     }
1026 
1027     //==============================================================================
1028     // if this PIP is running inside the demo runner, we'll use the shared device manager instead
1029    #ifndef JUCE_DEMO_RUNNER
1030     AudioDeviceManager audioDeviceManager;
1031    #else
1032     AudioDeviceManager& audioDeviceManager { getSharedAudioDeviceManager (0, 2) };
1033    #endif
1034 
1035     MPEZoneLayout zoneLayout;
1036     ZoneColourPicker colourPicker;
1037 
1038     AudioDeviceSelectorComponent audioSetupComp;
1039     MPESetupComponent MPESetupComp;
1040     ZoneLayoutComponent zoneLayoutComp;
1041 
1042     Visualiser visualiserComp;
1043     Viewport visualiserViewport;
1044     MPEInstrument visualiserInstrument;
1045 
1046     MPESynthesiser synth;
1047     MidiMessageCollector midiCollector;
1048 
1049     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPEDemo)
1050 };
1051