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