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 ¬e; 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