The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

240 lines
12KB

  1. /**
  2. @page example_blocks_synth BlocksSynth
  3. In order to compile and run this application you need to first download the @jucelink{JUCE framework}, which can be obtained from GitHub @jucegithub{here}.
  4. @section blocks_synth_introduction Introduction
  5. BlocksSynth is a JUCE application that turns your Lightpad into a simple monophonic synthesiser capable of playing 4 different waveshapes - sine, square, sawtooth and triangle.
  6. Generate a Projucer project from the PIP file located in the @s_file{JUCE/examples/BLOCKS/} folder, then navigate to the @s_file{BlocksSynthDemo/Builds/} directory and open the code project in your IDE of choice. Run the application and connect your Lightpad (if you do not know how to do this, see @ref connecting_blocks) - it should now display a simple 5x5 grid where each pad plays a note in the chromatic scale using a sine wave starting from the bottom-left (C3). It is possible to play any of the 25 notes but for ease of use tonics (the root note of the scale) are highlighted in white and notes in the C-major scale are highlighted in green. When a note has been played it is possible to change the amplitude using touch pressure and to pitch bend between adjacent notes by sliding left and right. Pressing the mode button on the Lightpad will change to the waveshape selection screen where the currently selected waveshape is rendered on the LEDs and you can switch between the 4 different waveshapes by touching anywhere on the %Block surface.
  7. The concept of a BLOCKS topology and the methods for receiving callbacks from the Block object are covered in the @ref example_blocks_monitor example and the basic methods for displaying grids and setting LEDs on the %Block are covered in the @ref example_blocks_drawing example. This example will cover how to render custom programs on the LEDGrid using the Littlefoot language and how to do some simple audio synthesis using data from the Lightpad.
  8. @section blocks_synth_note_grid Note Grid
  9. In the synthesiser mode the Lightpad displays a 5x5 grid constructed using the DrumPadGridProgram class. The @s_projcode{SynthGrid} struct handles the setup and layout of this grid and sets the colours of the pads to white for tonics, green for notes in the C major scale and black for notes that are not in the C major scale.
  10. @code{.cpp}
  11. void constructGridFillArray()
  12. {
  13. gridFillArray.clear();
  14. for (auto i = 0; i < numRows; ++i)
  15. {
  16. for (auto j = 0; j < numColumns; ++j)
  17. {
  18. DrumPadGridProgram::GridFill fill;
  19. auto padNum = (i * 5) + j;
  20. fill.colour = notes.contains (padNum) ? baseGridColour
  21. : tonics.contains (padNum) ? Colours::white
  22. : Colours::black;
  23. fill.fillType = DrumPadGridProgram::GridFill::FillType::gradient;
  24. gridFillArray.add (fill);
  25. }
  26. }
  27. }
  28. @endcode
  29. The @s_projcode{SynthGrid::getNoteNumberForPad()} method is called in the @s_projcode{BlocksSynthDemo::touchChanged()} callback and returns the corresponding MIDI note number for a Touch coordinate on the Lightpad. This note number is then passed to the @s_projcode{Audio} class to be played on the synthesiser.
  30. @code{.cpp}
  31. int getNoteNumberForPad (int x, int y) const
  32. {
  33. auto xIndex = x / 3;
  34. auto yIndex = y / 3;
  35. return 60 + ((4 - yIndex) * 5) + xIndex;
  36. }
  37. @endcode
  38. When the application is run, the synthesiser note grid would look like this:
  39. @image html BlocksSynth_grid.JPG "Synthesiser note grid"
  40. @section blocks_synth_waveshape_display Waveshape Display
  41. In the waveshape selection mode the Block::Program is set to an instance of the WaveshapeProgram class @s_item{[1]}. This class inherits from %Block::Program so that it can be loaded onto the %LEDGrid and its LittleFoot program can be executed on the Lightpad.
  42. @code{.cpp}
  43. void setLEDProgram (Block& block)
  44. {
  45. if (currentMode == waveformSelectionMode)
  46. {
  47. block.setProgram (new WaveshapeProgram (block)); // [1]
  48. if (auto* waveshapeProgram = getWaveshapeProgram())
  49. {
  50. //...
  51. }
  52. }
  53. //...
  54. }
  55. @endcode
  56. The class itself is relatively simple and contains a method to set which waveshape should be displayed @s_item{[2]}, a method to load the coordinates for each of the four waveshapes into the heap @s_item{[3]} and one pure virtual method overridden from %Block::Program, the Block::Program::getLittleFootProgram() method @s_item{[4]}. The heap is the area of shared memory that is used by the program to communicate with the host computer and the size of this memory is set using the @s_projcode{\#heapsize: XXX} directive where XXX is the number of bytes required @s_item{[5]}.
  57. @code{.cpp}
  58. class WaveshapeProgram : public Block::Program
  59. {
  60. public:
  61. WaveshapeProgram (Block& b) : Program (b) {}
  62. void setWaveshapeType (uint8 type) {...} // [2]
  63. void generateWaveshapes() {...} // [3]
  64. String getLittleFootProgram() override // [4]
  65. {
  66. return R"littlefoot(
  67. #heapsize: 256 // [5]
  68. //...
  69. )littlefoot";
  70. }
  71. //...
  72. @endcode
  73. The string literal returned by the @s_projcode{getLittleFootProgram()} function needs to be preceeded by the "R" prefix and enclosed between "littlefoot" delimiters in order to prevent the characters to be escaped in the program.
  74. In the private section of @s_projcode{WaveshapeProgram} the structure of the shared data heap is laid out with variables containing the offsets for each section and the total size (in bytes) that is required can be determined by adding the last set of bytes required to the last offset, which in this case is 136 + 45 = 181. The heap contains space for a variable that determines which waveshape type to display and the Y coordinates for 1.5 cycles of each of the four waveshapes.
  75. @code{.cpp}
  76. static constexpr uint32 waveshapeType = 0; // 1 byte
  77. static constexpr uint32 sineWaveOffset = 1; // 1 byte * 45
  78. static constexpr uint32 squareWaveOffset = 46; // 1 byte * 45
  79. static constexpr uint32 sawWaveOffset = 91; // 1 byte * 45
  80. static constexpr uint32 triangleWaveOffset = 136; // 1 byte * 45
  81. @endcode
  82. The @s_projcode{WaveshapeProgram::getLittleFootProgram()} method returns the LittleFoot program that will be executed on the BLOCKS device. The @s_projcode{repaint()} method of this program is called at approximately 25Hz and is used to draw the moving waveshape on the LEDs of the Lightpad.
  83. @code{.cpp}
  84. void repaint()
  85. {
  86. fillRect (0xff000000, 0, 0, 15, 15); // [6]
  87. int type = getHeapByte (0);
  88. int offset = 1 + (type * 45) + yOffset; // [7]
  89. for (int x = 0; x < 15; ++x)
  90. {
  91. int y = getHeapByte (offset + x);
  92. if (y == 255)
  93. {
  94. for (int i = 0; i < 15; ++i)
  95. drawLEDCircle (x, i);
  96. }
  97. else if (x % 2 == 0)
  98. {
  99. drawLEDCircle (x, y); // [8]
  100. }
  101. }
  102. if (++yOffset == 30) // [9]
  103. yOffset = 0;
  104. }
  105. @endcode
  106. Each time this method is called, it clears the LEDs by setting them all to black @s_item{[6]} then calculates the heap offset based on the waveshape type that has been set @s_item{[7]} and uses a @s_code{for()} loop to iterate over the 15 LEDs on the X-axis and draw an LED 'circle' using the @s_projcode{drawLEDCircle()} method at the corresponding Y coordinate for the selected waveshape @s_item{[8]}. The read position of the heap is offset using the @s_projcode{yOffset} variable which is incremented each @s_projcode{repaint()} call and wraps back around when the end of the heap section for the selected waveshape is reached to draw a 'moving' waveshape @s_item{[9]}.
  107. @image html BlocksSynth_waveshape.gif "A sine wave dispayed in the waveshape selection mode"
  108. @section blocks_synth_audio Audio
  109. The @s_projcode{Audio} class handles the audio synthesis for this application and overrides the AudioIODeviceCallback::audioDeviceIOCallback() method to call the Synthesiser::renderNextBlock() method of a Synthesiser object.
  110. @code{.cpp}
  111. void audioDeviceIOCallback (const float** /*inputChannelData*/, int /*numInputChannels*/,
  112. float** outputChannelData, int numOutputChannels, int numSamples) override
  113. {
  114. AudioBuffer<float> sampleBuffer (outputChannelData, numOutputChannels, numSamples);
  115. sampleBuffer.clear();
  116. synthesiser.renderNextBlock (sampleBuffer, MidiBuffer(), 0, numSamples);
  117. }
  118. @endcode
  119. This object is initialised to be capable of rendering sine, square, sawtooth and triangle waves on separate MIDI channels in the constructor of @s_projcode{Audio}, and @s_projcode{Audio} contains methods for sending note on, note off, channel pressure and pitch wheel messages to the Synthesiser.
  120. @code{.cpp}
  121. Audio()
  122. {
  123. //...
  124. synthesiser.clearVoices();
  125. synthesiser.clearSounds();
  126. synthesiser.addVoice (new SineVoice());
  127. synthesiser.addVoice (new SquareVoice());
  128. synthesiser.addVoice (new SawVoice());
  129. synthesiser.addVoice (new TriangleVoice());
  130. synthesiser.addSound (new SineSound());
  131. synthesiser.addSound (new SquareSound());
  132. synthesiser.addSound (new SawSound());
  133. synthesiser.addSound (new TriangleSound());
  134. }
  135. @endcode
  136. When a note is triggered on the Lightpad, the @s_projcode{Audio::noteOn()} method is called with 3 arguments: a MIDI channel corresponding to the waveshape that should be generated, a MIDI note number and an initial velocity.
  137. @code{.cpp}
  138. void noteOn (int channel, int noteNum, float velocity)
  139. {
  140. synthesiser.noteOn (channel, noteNum, velocity);
  141. }
  142. void noteOff (int channel, int noteNum, float velocity)
  143. {
  144. synthesiser.noteOff (channel, noteNum, velocity, false);
  145. }
  146. void allNotesOff()
  147. {
  148. for (auto i = 1; i < 5; ++i)
  149. synthesiser.allNotesOff (i, false);
  150. }
  151. @endcode
  152. Whilst the note is playing, the amplitude and pitch are modulated by calling the @s_projcode{Audio::pressureChange()} and @s_projcode{Audio::pitchChange()} methods from the @s_projcode{BlocksSynthDemo::touchChanged()} callback. The pressure value of the Touch instance is used to directly control the Synthesiser amplitude and the distance from the initial note trigger on the X-axis of the Lightpad is scaled to +/-1.0 and used to modulate the frequency of the currently playing note.
  153. @code{.cpp}
  154. void pressureChange (int channel, float newPressure)
  155. {
  156. synthesiser.handleChannelPressure (channel, static_cast<int> (newPressure * 127));
  157. }
  158. void pitchChange (int channel, float pitchChange)
  159. {
  160. synthesiser.handlePitchWheel (channel, static_cast<int> (pitchChange * 127));
  161. }
  162. @endcode
  163. The @s_projcode{Oscillator} base class contains the waveshape rendering code which inherits from SynthesiserVoice and has a pure virtual @s_projcode{Oscillator::renderWaveShape()} method that is overridden by subclasses to render the 4 different waveshapes.
  164. @code{.cpp}
  165. class OscillatorBase : public SynthesiserVoice
  166. {
  167. public:
  168. //...
  169. virtual bool canPlaySound (SynthesiserSound*) override = 0;
  170. virtual double renderWaveShape (const double currentPhase) = 0;
  171. //...
  172. @endcode
  173. @section blocks_synth_summary Summary
  174. This tutorial and the accompanying code project have expanded on the topics covered by previous tutorials, showing you how to display more complex, custom programs on the %LEDGrid using the LittleFoot language and how to control simple audio synthesis parameters using the Lightpad.
  175. @section blocks_synth_see_also See also
  176. - @ref example_blocks_monitor
  177. - @ref example_blocks_drawing
  178. */