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: BlocksSynthDemo 27 version: 1.0.0 28 vendor: JUCE 29 website: http://juce.com 30 description: Blocks synthesiser application. 31 32 dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats, 33 juce_audio_processors, juce_audio_utils, juce_blocks_basics, 34 juce_core, juce_data_structures, juce_events, juce_graphics, 35 juce_gui_basics, juce_gui_extra 36 exporters: xcode_mac, vs2019, linux_make, xcode_iphone 37 38 moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1 39 40 type: Component 41 mainClass: BlocksSynthDemo 42 43 useLocalCopy: 1 44 45 END_JUCE_PIP_METADATA 46 47 *******************************************************************************/ 48 49 #pragma once 50 51 52 //============================================================================== 53 /** 54 Base class for oscillators 55 */ 56 class OscillatorBase : public SynthesiserVoice 57 { 58 public: OscillatorBase()59 OscillatorBase() 60 { 61 amplitude.reset (getSampleRate(), 0.1); 62 phaseIncrement.reset (getSampleRate(), 0.1); 63 } 64 startNote(int midiNoteNumber,float velocity,SynthesiserSound *,int)65 void startNote (int midiNoteNumber, float velocity, SynthesiserSound*, int) override 66 { 67 frequency = MidiMessage::getMidiNoteInHertz (midiNoteNumber); 68 phaseIncrement.setTargetValue (((MathConstants<double>::twoPi) * frequency) / sampleRate); 69 amplitude.setTargetValue (velocity); 70 71 // Store the initial note and work out the maximum frequency deviations for pitch bend 72 initialNote = midiNoteNumber; 73 maxFreq = MidiMessage::getMidiNoteInHertz (initialNote + 4) - frequency; 74 minFreq = frequency - MidiMessage::getMidiNoteInHertz (initialNote - 4); 75 } 76 stopNote(float,bool)77 void stopNote (float, bool) override 78 { 79 clearCurrentNote(); 80 amplitude.setTargetValue (0.0); 81 } 82 pitchWheelMoved(int newValue)83 void pitchWheelMoved (int newValue) override 84 { 85 // Change the phase increment based on pitch bend amount 86 auto frequencyOffset = ((newValue > 0 ? maxFreq : minFreq) * (newValue / 127.0)); 87 phaseIncrement.setTargetValue (((MathConstants<double>::twoPi) * (frequency + frequencyOffset)) / sampleRate); 88 } 89 controllerMoved(int,int)90 void controllerMoved (int, int) override {} 91 channelPressureChanged(int newChannelPressureValue)92 void channelPressureChanged (int newChannelPressureValue) override 93 { 94 // Set the amplitude based on pressure value 95 amplitude.setTargetValue (newChannelPressureValue / 127.0); 96 } 97 renderNextBlock(AudioBuffer<float> & outputBuffer,int startSample,int numSamples)98 void renderNextBlock (AudioBuffer<float>& outputBuffer, int startSample, int numSamples) override 99 { 100 while (--numSamples >= 0) 101 { 102 auto output = getSample() * amplitude.getNextValue(); 103 104 for (auto i = outputBuffer.getNumChannels(); --i >= 0;) 105 outputBuffer.addSample (i, startSample, static_cast<float> (output)); 106 107 ++startSample; 108 } 109 } 110 111 using SynthesiserVoice::renderNextBlock; 112 113 /** Returns the next sample */ getSample()114 double getSample() 115 { 116 auto output = renderWaveShape (phasePos); 117 118 phasePos += phaseIncrement.getNextValue(); 119 120 if (phasePos > MathConstants<double>::twoPi) 121 phasePos -= MathConstants<double>::twoPi; 122 123 return output; 124 } 125 126 /** Subclasses should override this to say whether they can play the given sound */ 127 bool canPlaySound (SynthesiserSound*) override = 0; 128 129 /** Subclasses should override this to render a waveshape */ 130 virtual double renderWaveShape (const double currentPhase) = 0; 131 132 private: 133 SmoothedValue<double> amplitude, phaseIncrement; 134 135 double frequency = 0.0; 136 double phasePos = 0.0; 137 double sampleRate = 44100.0; 138 139 int initialNote = 0; 140 double maxFreq = 0.0, minFreq = 0.0; 141 142 //============================================================================== 143 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OscillatorBase) 144 }; 145 146 //============================================================================== 147 /** 148 Sine sound struct - applies to MIDI channel 1 149 */ 150 struct SineSound : public SynthesiserSound 151 { SineSoundSineSound152 SineSound () {} 153 appliesToNoteSineSound154 bool appliesToNote (int) override { return true; } 155 appliesToChannelSineSound156 bool appliesToChannel (int midiChannel) override { return (midiChannel == 1); } 157 158 //============================================================================== 159 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SineSound) 160 }; 161 162 /** 163 Sine voice struct that renders a sin waveshape 164 */ 165 struct SineVoice : public OscillatorBase 166 { SineVoiceSineVoice167 SineVoice() {} 168 canPlaySoundSineVoice169 bool canPlaySound (SynthesiserSound* sound) override { return dynamic_cast<SineSound*> (sound) != nullptr; } 170 renderWaveShapeSineVoice171 double renderWaveShape (const double currentPhase) override { return sin (currentPhase); } 172 173 //============================================================================== 174 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SineVoice) 175 }; 176 177 //============================================================================== 178 /** 179 Square sound struct - applies to MIDI channel 2 180 */ 181 struct SquareSound : public SynthesiserSound 182 { SquareSoundSquareSound183 SquareSound() {} 184 appliesToNoteSquareSound185 bool appliesToNote (int) override { return true; } 186 appliesToChannelSquareSound187 bool appliesToChannel (int midiChannel) override { return (midiChannel == 2); } 188 189 //============================================================================== 190 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SquareSound) 191 }; 192 193 /** 194 Square voice struct that renders a square waveshape 195 */ 196 struct SquareVoice : public OscillatorBase 197 { SquareVoiceSquareVoice198 SquareVoice() {} 199 canPlaySoundSquareVoice200 bool canPlaySound (SynthesiserSound* sound) override { return dynamic_cast<SquareSound*> (sound) != nullptr; } 201 renderWaveShapeSquareVoice202 double renderWaveShape (const double currentPhase) override { return (currentPhase < MathConstants<double>::pi ? 0.0 : 1.0); } 203 204 //============================================================================== 205 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SquareVoice) 206 }; 207 208 //============================================================================== 209 /** 210 Sawtooth sound - applies to MIDI channel 3 211 */ 212 struct SawSound : public SynthesiserSound 213 { SawSoundSawSound214 SawSound() {} 215 appliesToNoteSawSound216 bool appliesToNote (int) override { return true; } 217 appliesToChannelSawSound218 bool appliesToChannel (int midiChannel) override { return (midiChannel == 3); } 219 220 //============================================================================== 221 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SawSound) 222 }; 223 224 /** 225 Sawtooth voice that renders a sawtooth waveshape 226 */ 227 struct SawVoice : public OscillatorBase 228 { SawVoiceSawVoice229 SawVoice() {} 230 canPlaySoundSawVoice231 bool canPlaySound (SynthesiserSound* sound) override { return dynamic_cast<SawSound*> (sound) != nullptr; } 232 renderWaveShapeSawVoice233 double renderWaveShape (const double currentPhase) override { return (1.0 / MathConstants<double>::pi) * currentPhase - 1.0; } 234 235 //============================================================================== 236 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SawVoice) 237 }; 238 239 //============================================================================== 240 /** 241 Triangle sound - applies to MIDI channel 4 242 */ 243 struct TriangleSound : public SynthesiserSound 244 { TriangleSoundTriangleSound245 TriangleSound() {} 246 appliesToNoteTriangleSound247 bool appliesToNote (int) override { return true; } 248 appliesToChannelTriangleSound249 bool appliesToChannel (int midiChannel) override { return (midiChannel == 4); } 250 251 //============================================================================== 252 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TriangleSound) 253 }; 254 255 /** 256 Triangle voice that renders a triangle waveshape 257 */ 258 struct TriangleVoice : public OscillatorBase 259 { TriangleVoiceTriangleVoice260 TriangleVoice() {} 261 canPlaySoundTriangleVoice262 bool canPlaySound (SynthesiserSound* sound) override { return dynamic_cast<TriangleSound*> (sound) != nullptr; } 263 renderWaveShapeTriangleVoice264 double renderWaveShape (const double currentPhase) override 265 { 266 return currentPhase < MathConstants<double>::pi ? -1.0 + (2.0 / MathConstants<double>::pi) * currentPhase 267 : 3.0 - (2.0 / MathConstants<double>::pi) * currentPhase; 268 } 269 270 //============================================================================== 271 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TriangleVoice) 272 }; 273 274 //============================================================================== 275 /** 276 Class to handle the Audio functionality 277 */ 278 class Audio : public AudioIODeviceCallback 279 { 280 public: Audio()281 Audio() 282 { 283 // Set up the audio device manager 284 #ifndef JUCE_DEMO_RUNNER 285 audioDeviceManager.initialiseWithDefaultDevices (0, 2); 286 #endif 287 288 audioDeviceManager.addAudioCallback (this); 289 290 // Set up the synthesiser and add each of the waveshapes 291 synthesiser.clearVoices(); 292 synthesiser.clearSounds(); 293 294 synthesiser.addVoice (new SineVoice()); 295 synthesiser.addVoice (new SquareVoice()); 296 synthesiser.addVoice (new SawVoice()); 297 synthesiser.addVoice (new TriangleVoice()); 298 299 synthesiser.addSound (new SineSound()); 300 synthesiser.addSound (new SquareSound()); 301 synthesiser.addSound (new SawSound()); 302 synthesiser.addSound (new TriangleSound()); 303 } 304 ~Audio()305 ~Audio() override 306 { 307 audioDeviceManager.removeAudioCallback (this); 308 } 309 310 /** Audio callback */ audioDeviceIOCallback(const float **,int,float ** outputChannelData,int numOutputChannels,int numSamples)311 void audioDeviceIOCallback (const float** /*inputChannelData*/, int /*numInputChannels*/, 312 float** outputChannelData, int numOutputChannels, int numSamples) override 313 { 314 AudioBuffer<float> sampleBuffer (outputChannelData, numOutputChannels, numSamples); 315 sampleBuffer.clear(); 316 317 synthesiser.renderNextBlock (sampleBuffer, MidiBuffer(), 0, numSamples); 318 } 319 audioDeviceAboutToStart(AudioIODevice * device)320 void audioDeviceAboutToStart (AudioIODevice* device) override 321 { 322 synthesiser.setCurrentPlaybackSampleRate (device->getCurrentSampleRate()); 323 } 324 audioDeviceStopped()325 void audioDeviceStopped() override {} 326 327 /** Called to turn a synthesiser note on */ noteOn(int channel,int noteNum,float velocity)328 void noteOn (int channel, int noteNum, float velocity) 329 { 330 synthesiser.noteOn (channel, noteNum, velocity); 331 } 332 333 /** Called to turn a synthesiser note off */ noteOff(int channel,int noteNum,float velocity)334 void noteOff (int channel, int noteNum, float velocity) 335 { 336 synthesiser.noteOff (channel, noteNum, velocity, false); 337 } 338 339 /** Called to turn all synthesiser notes off */ allNotesOff()340 void allNotesOff() 341 { 342 for (auto i = 1; i < 5; ++i) 343 synthesiser.allNotesOff (i, false); 344 } 345 346 /** Send pressure change message to synthesiser */ pressureChange(int channel,float newPressure)347 void pressureChange (int channel, float newPressure) 348 { 349 synthesiser.handleChannelPressure (channel, static_cast<int> (newPressure * 127)); 350 } 351 352 /** Send pitch change message to synthesiser */ pitchChange(int channel,float pitchChange)353 void pitchChange (int channel, float pitchChange) 354 { 355 synthesiser.handlePitchWheel (channel, static_cast<int> (pitchChange * 127)); 356 } 357 358 private: 359 #ifndef JUCE_DEMO_RUNNER 360 AudioDeviceManager audioDeviceManager; 361 #else 362 AudioDeviceManager& audioDeviceManager { getSharedAudioDeviceManager (0, 2) }; 363 #endif 364 Synthesiser synthesiser; 365 366 //============================================================================== 367 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Audio) 368 }; 369 370 //============================================================================== 371 /** 372 A Program to draw moving waveshapes onto the LEDGrid 373 */ 374 class WaveshapeProgram : public Block::Program 375 { 376 public: WaveshapeProgram(Block & b)377 WaveshapeProgram (Block& b) : Program (b) {} 378 379 /** Sets the waveshape type to display on the grid */ setWaveshapeType(uint8 type)380 void setWaveshapeType (uint8 type) 381 { 382 block.setDataByte (0, type); 383 } 384 385 /** Generates the Y coordinates for 1.5 cycles of each of the four waveshapes and stores them 386 at the correct offsets in the shared data heap. */ generateWaveshapes()387 void generateWaveshapes() 388 { 389 uint8 sineWaveY[45]; 390 uint8 squareWaveY[45]; 391 uint8 sawWaveY[45]; 392 uint8 triangleWaveY[45]; 393 394 // Set current phase position to 0 and work out the required phase increment for one cycle 395 auto currentPhase = 0.0; 396 auto phaseInc = (1.0 / 30.0) * MathConstants<double>::twoPi; 397 398 for (auto x = 0; x < 30; ++x) 399 { 400 // Scale and offset the sin output to the Lightpad display 401 auto sineOutput = std::sin (currentPhase); 402 sineWaveY[x] = static_cast<uint8> (roundToInt ((sineOutput * 6.5) + 7.0)); 403 404 // Square wave output, set flags for when vertical line should be drawn 405 if (currentPhase < MathConstants<double>::pi) 406 { 407 if (x == 0) 408 squareWaveY[x] = 255; 409 else 410 squareWaveY[x] = 1; 411 } 412 else 413 { 414 if (x > 0 && squareWaveY[x - 1] == 1) 415 squareWaveY[x - 1] = 255; 416 417 squareWaveY[x] = 13; 418 } 419 420 // Saw wave output, set flags for when vertical line should be drawn 421 sawWaveY[x] = 14 - ((x / 2) % 15); 422 423 if (sawWaveY[x] == 0 && sawWaveY[x - 1] != 255) 424 sawWaveY[x] = 255; 425 426 // Triangle wave output 427 triangleWaveY[x] = x < 15 ? static_cast<uint8> (x) : static_cast<uint8> (14 - (x % 15)); 428 429 // Add half cycle to end of array so it loops correctly 430 if (x < 15) 431 { 432 sineWaveY[x + 30] = sineWaveY[x]; 433 squareWaveY[x + 30] = squareWaveY[x]; 434 sawWaveY[x + 30] = sawWaveY[x]; 435 triangleWaveY[x + 30] = triangleWaveY[x]; 436 } 437 438 // Increment the current phase 439 currentPhase += phaseInc; 440 } 441 442 // Store the values for each of the waveshapes at the correct offsets in the shared data heap 443 for (uint8 i = 0; i < 45; ++i) 444 { 445 block.setDataByte (sineWaveOffset + i, sineWaveY[i]); 446 block.setDataByte (squareWaveOffset + i, squareWaveY[i]); 447 block.setDataByte (sawWaveOffset + i, sawWaveY[i]); 448 block.setDataByte (triangleWaveOffset + i, triangleWaveY[i]); 449 } 450 } 451 getLittleFootProgram()452 String getLittleFootProgram() override 453 { 454 return R"littlefoot( 455 456 #heapsize: 256 457 458 int yOffset; 459 460 void drawLEDCircle (int x0, int y0) 461 { 462 blendPixel (0xffff0000, x0, y0); 463 464 int minLedIndex = 0; 465 int maxLedIndex = 14; 466 467 blendPixel (0xff660000, min (x0 + 1, maxLedIndex), y0); 468 blendPixel (0xff660000, max (x0 - 1, minLedIndex), y0); 469 blendPixel (0xff660000, x0, min (y0 + 1, maxLedIndex)); 470 blendPixel (0xff660000, x0, max (y0 - 1, minLedIndex)); 471 472 blendPixel (0xff1a0000, min (x0 + 1, maxLedIndex), min (y0 + 1, maxLedIndex)); 473 blendPixel (0xff1a0000, min (x0 + 1, maxLedIndex), max (y0 - 1, minLedIndex)); 474 blendPixel (0xff1a0000, max (x0 - 1, minLedIndex), min (y0 + 1, maxLedIndex)); 475 blendPixel (0xff1a0000, max (x0 - 1, minLedIndex), max (y0 - 1, minLedIndex)); 476 } 477 478 void repaint() 479 { 480 // Clear LEDs to black 481 fillRect (0xff000000, 0, 0, 15, 15); 482 483 // Get the waveshape type 484 int type = getHeapByte (0); 485 486 // Calculate the heap offset 487 int offset = 1 + (type * 45) + yOffset; 488 489 for (int x = 0; x < 15; ++x) 490 { 491 // Get the corresponding Y coordinate for each X coordinate 492 int y = getHeapByte (offset + x); 493 494 // Draw a vertical line if flag is set or draw an LED circle 495 if (y == 255) 496 { 497 for (int i = 0; i < 15; ++i) 498 drawLEDCircle (x, i); 499 } 500 else if (x % 2 == 0) 501 { 502 drawLEDCircle (x, y); 503 } 504 } 505 506 // Increment and wrap the Y offset to draw a 'moving' waveshape 507 if (++yOffset == 30) 508 yOffset = 0; 509 } 510 511 )littlefoot"; 512 } 513 514 private: 515 //============================================================================== 516 /** Shared data heap is laid out as below. There is room for the waveshape type and 517 the Y coordinates for 1.5 cycles of each of the four waveshapes. */ 518 519 static constexpr uint32 waveshapeType = 0; // 1 byte 520 static constexpr uint32 sineWaveOffset = 1; // 1 byte * 45 521 static constexpr uint32 squareWaveOffset = 46; // 1 byte * 45 522 static constexpr uint32 sawWaveOffset = 91; // 1 byte * 45 523 static constexpr uint32 triangleWaveOffset = 136; // 1 byte * 45 524 525 //============================================================================== 526 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WaveshapeProgram) 527 }; 528 529 //============================================================================== 530 /** 531 A struct that handles the setup and layout of the DrumPadGridProgram 532 */ 533 struct SynthGrid 534 { SynthGridSynthGrid535 SynthGrid (int cols, int rows) 536 : numColumns (cols), 537 numRows (rows) 538 { 539 constructGridFillArray(); 540 } 541 542 /** Creates a GridFill object for each pad in the grid and sets its colour 543 and fill before adding it to an array of GridFill objects 544 */ constructGridFillArraySynthGrid545 void constructGridFillArray() 546 { 547 gridFillArray.clear(); 548 549 for (auto i = 0; i < numRows; ++i) 550 { 551 for (auto j = 0; j < numColumns; ++j) 552 { 553 DrumPadGridProgram::GridFill fill; 554 555 auto padNum = (i * 5) + j; 556 557 fill.colour = notes.contains (padNum) ? baseGridColour 558 : tonics.contains (padNum) ? Colours::white 559 : Colours::black; 560 fill.fillType = DrumPadGridProgram::GridFill::FillType::gradient; 561 gridFillArray.add (fill); 562 } 563 } 564 } 565 getNoteNumberForPadSynthGrid566 int getNoteNumberForPad (int x, int y) const 567 { 568 auto xIndex = x / 3; 569 auto yIndex = y / 3; 570 571 return 60 + ((4 - yIndex) * 5) + xIndex; 572 } 573 574 //============================================================================== 575 int numColumns, numRows; 576 float width, height; 577 578 Array<DrumPadGridProgram::GridFill> gridFillArray; 579 Colour baseGridColour = Colours::green; 580 Colour touchColour = Colours::red; 581 582 Array<int> tonics = { 4, 12, 20 }; 583 Array<int> notes = { 1, 3, 6, 7, 9, 11, 14, 15, 17, 19, 22, 24 }; 584 585 //============================================================================== 586 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SynthGrid) 587 }; 588 589 //============================================================================== 590 /** 591 The main component 592 */ 593 class BlocksSynthDemo : public Component, 594 public TopologySource::Listener, 595 private TouchSurface::Listener, 596 private ControlButton::Listener, 597 private Timer 598 { 599 public: BlocksSynthDemo()600 BlocksSynthDemo() 601 { 602 // Register BlocksSynthDemo as a listener to the PhysicalTopologySource object 603 topologySource.addListener (this); 604 605 #if JUCE_IOS 606 connectButton.setButtonText ("Connect"); 607 connectButton.onClick = [] { BluetoothMidiDevicePairingDialogue::open(); }; 608 addAndMakeVisible (connectButton); 609 #endif 610 611 setSize (600, 400); 612 613 topologyChanged(); 614 } 615 ~BlocksSynthDemo()616 ~BlocksSynthDemo() override 617 { 618 if (activeBlock != nullptr) 619 detachActiveBlock(); 620 621 topologySource.removeListener (this); 622 } 623 paint(Graphics & g)624 void paint (Graphics& g) override 625 { 626 g.setColour (getLookAndFeel().findColour (Label::textColourId)); 627 g.drawText ("Connect a Lightpad Block to play.", 628 getLocalBounds(), Justification::centred, false); 629 } 630 resized()631 void resized() override 632 { 633 #if JUCE_IOS 634 connectButton.setBounds (getRight() - 100, 20, 80, 30); 635 #endif 636 } 637 638 /** Overridden from TopologySource::Listener, called when the topology changes */ topologyChanged()639 void topologyChanged() override 640 { 641 // Reset the activeBlock object 642 if (activeBlock != nullptr) 643 detachActiveBlock(); 644 645 // Get the array of currently connected Block objects from the PhysicalTopologySource 646 auto blocks = topologySource.getBlocks(); 647 648 // Iterate over the array of Block objects 649 for (auto b : blocks) 650 { 651 // Find the first Lightpad 652 if (b->getType() == Block::Type::lightPadBlock) 653 { 654 activeBlock = b; 655 656 // Register BlocksSynthDemo as a listener to the touch surface 657 if (auto surface = activeBlock->getTouchSurface()) 658 surface->addListener (this); 659 660 // Register BlocksSynthDemo as a listener to any buttons 661 for (auto button : activeBlock->getButtons()) 662 button->addListener (this); 663 664 // Get the LEDGrid object from the Lightpad and set its program to the program for the current mode 665 if (auto grid = activeBlock->getLEDGrid()) 666 { 667 // Work out scale factors to translate X and Y touches to LED indexes 668 scaleX = static_cast<float> (grid->getNumColumns() - 1) / (float) activeBlock->getWidth(); 669 scaleY = static_cast<float> (grid->getNumRows() - 1) / (float) activeBlock->getHeight(); 670 671 setLEDProgram (*activeBlock); 672 } 673 674 break; 675 } 676 } 677 } 678 679 private: 680 /** Overridden from TouchSurface::Listener. Called when a Touch is received on the Lightpad */ touchChanged(TouchSurface &,const TouchSurface::Touch & touch)681 void touchChanged (TouchSurface&, const TouchSurface::Touch& touch) override 682 { 683 if (currentMode == waveformSelectionMode && touch.isTouchStart && allowTouch) 684 { 685 if (auto* waveshapeProgram = getWaveshapeProgram()) 686 { 687 // Change the displayed waveshape to the next one 688 ++waveshapeMode; 689 690 if (waveshapeMode > 3) 691 waveshapeMode = 0; 692 693 waveshapeProgram->setWaveshapeType (static_cast<uint8> (waveshapeMode)); 694 695 allowTouch = false; 696 startTimer (250); 697 } 698 } 699 else if (currentMode == playMode) 700 { 701 if (auto* gridProgram = getGridProgram()) 702 { 703 // Translate X and Y touch events to LED indexes 704 auto xLed = roundToInt (touch.startX * scaleX); 705 auto yLed = roundToInt (touch.startY * scaleY); 706 707 // Limit the number of touches per second 708 constexpr auto maxNumTouchMessagesPerSecond = 100; 709 auto now = Time::getCurrentTime(); 710 clearOldTouchTimes (now); 711 712 auto midiChannel = waveshapeMode + 1; 713 714 // Send the touch event to the DrumPadGridProgram and Audio class 715 if (touch.isTouchStart) 716 { 717 gridProgram->startTouch (touch.startX, touch.startY); 718 audio.noteOn (midiChannel, layout.getNoteNumberForPad (xLed, yLed), touch.z); 719 } 720 else if (touch.isTouchEnd) 721 { 722 gridProgram->endTouch (touch.startX, touch.startY); 723 audio.noteOff (midiChannel, layout.getNoteNumberForPad (xLed, yLed), 1.0); 724 } 725 else 726 { 727 if (touchMessageTimesInLastSecond.size() > maxNumTouchMessagesPerSecond / 3) 728 return; 729 730 gridProgram->sendTouch (touch.x, touch.y, touch.z, 731 layout.touchColour); 732 733 // Send pitch change and pressure values to the Audio class 734 audio.pitchChange (midiChannel, (touch.x - touch.startX) / (float) activeBlock->getWidth()); 735 audio.pressureChange (midiChannel, touch.z); 736 } 737 738 touchMessageTimesInLastSecond.add (now); 739 } 740 } 741 } 742 743 /** Overridden from ControlButton::Listener. Called when a button on the Lightpad is pressed */ buttonPressed(ControlButton &,Block::Timestamp)744 void buttonPressed (ControlButton&, Block::Timestamp) override {} 745 746 /** Overridden from ControlButton::Listener. Called when a button on the Lightpad is released */ buttonReleased(ControlButton &,Block::Timestamp)747 void buttonReleased (ControlButton&, Block::Timestamp) override 748 { 749 // Turn any active synthesiser notes off 750 audio.allNotesOff(); 751 752 // Switch modes 753 if (currentMode == waveformSelectionMode) 754 currentMode = playMode; 755 else if (currentMode == playMode) 756 currentMode = waveformSelectionMode; 757 758 // Set the LEDGrid program to the new mode 759 setLEDProgram (*activeBlock); 760 } 761 762 /** Clears the old touch times */ clearOldTouchTimes(const Time now)763 void clearOldTouchTimes (const Time now) 764 { 765 for (auto i = touchMessageTimesInLastSecond.size(); --i >= 0;) 766 if (touchMessageTimesInLastSecond.getReference(i) < now - RelativeTime::seconds (0.33)) 767 touchMessageTimesInLastSecond.remove (i); 768 } 769 770 /** Removes TouchSurface and ControlButton listeners and sets activeBlock to nullptr */ detachActiveBlock()771 void detachActiveBlock() 772 { 773 if (auto surface = activeBlock->getTouchSurface()) 774 surface->removeListener (this); 775 776 for (auto button : activeBlock->getButtons()) 777 button->removeListener (this); 778 779 activeBlock = nullptr; 780 } 781 782 /** Sets the LEDGrid Program for the selected mode */ setLEDProgram(Block & block)783 void setLEDProgram (Block& block) 784 { 785 if (currentMode == waveformSelectionMode) 786 { 787 // Set the LEDGrid program 788 block.setProgram (std::make_unique<WaveshapeProgram>(block)); 789 790 // Initialise the program 791 if (auto* waveshapeProgram = getWaveshapeProgram()) 792 { 793 waveshapeProgram->setWaveshapeType (static_cast<uint8> (waveshapeMode)); 794 waveshapeProgram->generateWaveshapes(); 795 } 796 } 797 else if (currentMode == playMode) 798 { 799 // Set the LEDGrid program 800 auto error = block.setProgram (std::make_unique<DrumPadGridProgram>(block)); 801 802 if (error.failed()) 803 { 804 DBG (error.getErrorMessage()); 805 jassertfalse; 806 } 807 808 // Setup the grid layout 809 if (auto* gridProgram = getGridProgram()) 810 gridProgram->setGridFills (layout.numColumns, layout.numRows, layout.gridFillArray); 811 } 812 } 813 814 /** Stops touch events from triggering multiple waveshape mode changes */ timerCallback()815 void timerCallback() override { allowTouch = true; } 816 817 //============================================================================== getGridProgram()818 DrumPadGridProgram* getGridProgram() 819 { 820 if (activeBlock != nullptr) 821 return dynamic_cast<DrumPadGridProgram*> (activeBlock->getProgram()); 822 823 return nullptr; 824 } 825 getWaveshapeProgram()826 WaveshapeProgram* getWaveshapeProgram() 827 { 828 if (activeBlock != nullptr) 829 return dynamic_cast<WaveshapeProgram*> (activeBlock->getProgram()); 830 831 return nullptr; 832 } 833 834 //============================================================================== 835 enum BlocksSynthMode 836 { 837 waveformSelectionMode = 0, 838 playMode 839 }; 840 841 BlocksSynthMode currentMode = playMode; 842 843 //============================================================================== 844 Audio audio; 845 846 SynthGrid layout { 5, 5 }; 847 PhysicalTopologySource topologySource; 848 Block::Ptr activeBlock; 849 850 Array<Time> touchMessageTimesInLastSecond; 851 852 int waveshapeMode = 0; 853 854 float scaleX = 0.0f; 855 float scaleY = 0.0f; 856 857 bool allowTouch = true; 858 859 //============================================================================== 860 #if JUCE_IOS 861 TextButton connectButton; 862 #endif 863 864 //============================================================================== 865 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BlocksSynthDemo) 866 }; 867