/* ============================================================================== This file is part of the JUCE examples. Copyright (c) 2020 - Raw Material Software Limited The code included in this file is provided under the terms of the ISC license http://www.isc.org/downloads/software-support-policy/isc-license. Permission To use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ /******************************************************************************* The block below describes the properties of this PIP. A PIP is a short snippet of code that can be read by the Projucer and used to generate a JUCE project. BEGIN_JUCE_PIP_METADATA name: BlocksSynthDemo version: 1.0.0 vendor: JUCE website: http://juce.com description: Blocks synthesiser application. dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats, juce_audio_processors, juce_audio_utils, juce_blocks_basics, juce_core, juce_data_structures, juce_events, juce_graphics, juce_gui_basics, juce_gui_extra exporters: xcode_mac, vs2019, linux_make, xcode_iphone moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1 type: Component mainClass: BlocksSynthDemo useLocalCopy: 1 END_JUCE_PIP_METADATA *******************************************************************************/ #pragma once //============================================================================== /** Base class for oscillators */ class OscillatorBase : public SynthesiserVoice { public: OscillatorBase() { amplitude.reset (getSampleRate(), 0.1); phaseIncrement.reset (getSampleRate(), 0.1); } void startNote (int midiNoteNumber, float velocity, SynthesiserSound*, int) override { frequency = MidiMessage::getMidiNoteInHertz (midiNoteNumber); phaseIncrement.setTargetValue (((MathConstants::twoPi) * frequency) / sampleRate); amplitude.setTargetValue (velocity); // Store the initial note and work out the maximum frequency deviations for pitch bend initialNote = midiNoteNumber; maxFreq = MidiMessage::getMidiNoteInHertz (initialNote + 4) - frequency; minFreq = frequency - MidiMessage::getMidiNoteInHertz (initialNote - 4); } void stopNote (float, bool) override { clearCurrentNote(); amplitude.setTargetValue (0.0); } void pitchWheelMoved (int newValue) override { // Change the phase increment based on pitch bend amount auto frequencyOffset = ((newValue > 0 ? maxFreq : minFreq) * (newValue / 127.0)); phaseIncrement.setTargetValue (((MathConstants::twoPi) * (frequency + frequencyOffset)) / sampleRate); } void controllerMoved (int, int) override {} void channelPressureChanged (int newChannelPressureValue) override { // Set the amplitude based on pressure value amplitude.setTargetValue (newChannelPressureValue / 127.0); } void renderNextBlock (AudioBuffer& outputBuffer, int startSample, int numSamples) override { while (--numSamples >= 0) { auto output = getSample() * amplitude.getNextValue(); for (auto i = outputBuffer.getNumChannels(); --i >= 0;) outputBuffer.addSample (i, startSample, static_cast (output)); ++startSample; } } using SynthesiserVoice::renderNextBlock; /** Returns the next sample */ double getSample() { auto output = renderWaveShape (phasePos); phasePos += phaseIncrement.getNextValue(); if (phasePos > MathConstants::twoPi) phasePos -= MathConstants::twoPi; return output; } /** Subclasses should override this to say whether they can play the given sound */ bool canPlaySound (SynthesiserSound*) override = 0; /** Subclasses should override this to render a waveshape */ virtual double renderWaveShape (const double currentPhase) = 0; private: SmoothedValue amplitude, phaseIncrement; double frequency = 0.0; double phasePos = 0.0; double sampleRate = 44100.0; int initialNote = 0; double maxFreq = 0.0, minFreq = 0.0; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OscillatorBase) }; //============================================================================== /** Sine sound struct - applies to MIDI channel 1 */ struct SineSound : public SynthesiserSound { SineSound () {} bool appliesToNote (int) override { return true; } bool appliesToChannel (int midiChannel) override { return (midiChannel == 1); } //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SineSound) }; /** Sine voice struct that renders a sin waveshape */ struct SineVoice : public OscillatorBase { SineVoice() {} bool canPlaySound (SynthesiserSound* sound) override { return dynamic_cast (sound) != nullptr; } double renderWaveShape (const double currentPhase) override { return sin (currentPhase); } //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SineVoice) }; //============================================================================== /** Square sound struct - applies to MIDI channel 2 */ struct SquareSound : public SynthesiserSound { SquareSound() {} bool appliesToNote (int) override { return true; } bool appliesToChannel (int midiChannel) override { return (midiChannel == 2); } //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SquareSound) }; /** Square voice struct that renders a square waveshape */ struct SquareVoice : public OscillatorBase { SquareVoice() {} bool canPlaySound (SynthesiserSound* sound) override { return dynamic_cast (sound) != nullptr; } double renderWaveShape (const double currentPhase) override { return (currentPhase < MathConstants::pi ? 0.0 : 1.0); } //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SquareVoice) }; //============================================================================== /** Sawtooth sound - applies to MIDI channel 3 */ struct SawSound : public SynthesiserSound { SawSound() {} bool appliesToNote (int) override { return true; } bool appliesToChannel (int midiChannel) override { return (midiChannel == 3); } //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SawSound) }; /** Sawtooth voice that renders a sawtooth waveshape */ struct SawVoice : public OscillatorBase { SawVoice() {} bool canPlaySound (SynthesiserSound* sound) override { return dynamic_cast (sound) != nullptr; } double renderWaveShape (const double currentPhase) override { return (1.0 / MathConstants::pi) * currentPhase - 1.0; } //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SawVoice) }; //============================================================================== /** Triangle sound - applies to MIDI channel 4 */ struct TriangleSound : public SynthesiserSound { TriangleSound() {} bool appliesToNote (int) override { return true; } bool appliesToChannel (int midiChannel) override { return (midiChannel == 4); } //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TriangleSound) }; /** Triangle voice that renders a triangle waveshape */ struct TriangleVoice : public OscillatorBase { TriangleVoice() {} bool canPlaySound (SynthesiserSound* sound) override { return dynamic_cast (sound) != nullptr; } double renderWaveShape (const double currentPhase) override { return currentPhase < MathConstants::pi ? -1.0 + (2.0 / MathConstants::pi) * currentPhase : 3.0 - (2.0 / MathConstants::pi) * currentPhase; } //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TriangleVoice) }; //============================================================================== /** Class to handle the Audio functionality */ class Audio : public AudioIODeviceCallback { public: Audio() { // Set up the audio device manager #ifndef JUCE_DEMO_RUNNER audioDeviceManager.initialiseWithDefaultDevices (0, 2); #endif audioDeviceManager.addAudioCallback (this); // Set up the synthesiser and add each of the waveshapes synthesiser.clearVoices(); synthesiser.clearSounds(); synthesiser.addVoice (new SineVoice()); synthesiser.addVoice (new SquareVoice()); synthesiser.addVoice (new SawVoice()); synthesiser.addVoice (new TriangleVoice()); synthesiser.addSound (new SineSound()); synthesiser.addSound (new SquareSound()); synthesiser.addSound (new SawSound()); synthesiser.addSound (new TriangleSound()); } ~Audio() override { audioDeviceManager.removeAudioCallback (this); } /** Audio callback */ void audioDeviceIOCallback (const float** /*inputChannelData*/, int /*numInputChannels*/, float** outputChannelData, int numOutputChannels, int numSamples) override { AudioBuffer sampleBuffer (outputChannelData, numOutputChannels, numSamples); sampleBuffer.clear(); synthesiser.renderNextBlock (sampleBuffer, MidiBuffer(), 0, numSamples); } void audioDeviceAboutToStart (AudioIODevice* device) override { synthesiser.setCurrentPlaybackSampleRate (device->getCurrentSampleRate()); } void audioDeviceStopped() override {} /** Called to turn a synthesiser note on */ void noteOn (int channel, int noteNum, float velocity) { synthesiser.noteOn (channel, noteNum, velocity); } /** Called to turn a synthesiser note off */ void noteOff (int channel, int noteNum, float velocity) { synthesiser.noteOff (channel, noteNum, velocity, false); } /** Called to turn all synthesiser notes off */ void allNotesOff() { for (auto i = 1; i < 5; ++i) synthesiser.allNotesOff (i, false); } /** Send pressure change message to synthesiser */ void pressureChange (int channel, float newPressure) { synthesiser.handleChannelPressure (channel, static_cast (newPressure * 127)); } /** Send pitch change message to synthesiser */ void pitchChange (int channel, float pitchChange) { synthesiser.handlePitchWheel (channel, static_cast (pitchChange * 127)); } private: #ifndef JUCE_DEMO_RUNNER AudioDeviceManager audioDeviceManager; #else AudioDeviceManager& audioDeviceManager { getSharedAudioDeviceManager (0, 2) }; #endif Synthesiser synthesiser; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Audio) }; //============================================================================== /** A Program to draw moving waveshapes onto the LEDGrid */ class WaveshapeProgram : public Block::Program { public: WaveshapeProgram (Block& b) : Program (b) {} /** Sets the waveshape type to display on the grid */ void setWaveshapeType (uint8 type) { block.setDataByte (0, type); } /** Generates the Y coordinates for 1.5 cycles of each of the four waveshapes and stores them at the correct offsets in the shared data heap. */ void generateWaveshapes() { uint8 sineWaveY[45]; uint8 squareWaveY[45]; uint8 sawWaveY[45]; uint8 triangleWaveY[45]; // Set current phase position to 0 and work out the required phase increment for one cycle auto currentPhase = 0.0; auto phaseInc = (1.0 / 30.0) * MathConstants::twoPi; for (auto x = 0; x < 30; ++x) { // Scale and offset the sin output to the Lightpad display auto sineOutput = std::sin (currentPhase); sineWaveY[x] = static_cast (roundToInt ((sineOutput * 6.5) + 7.0)); // Square wave output, set flags for when vertical line should be drawn if (currentPhase < MathConstants::pi) { if (x == 0) squareWaveY[x] = 255; else squareWaveY[x] = 1; } else { if (x > 0 && squareWaveY[x - 1] == 1) squareWaveY[x - 1] = 255; squareWaveY[x] = 13; } // Saw wave output, set flags for when vertical line should be drawn sawWaveY[x] = 14 - ((x / 2) % 15); if (sawWaveY[x] == 0 && sawWaveY[x - 1] != 255) sawWaveY[x] = 255; // Triangle wave output triangleWaveY[x] = x < 15 ? static_cast (x) : static_cast (14 - (x % 15)); // Add half cycle to end of array so it loops correctly if (x < 15) { sineWaveY[x + 30] = sineWaveY[x]; squareWaveY[x + 30] = squareWaveY[x]; sawWaveY[x + 30] = sawWaveY[x]; triangleWaveY[x + 30] = triangleWaveY[x]; } // Increment the current phase currentPhase += phaseInc; } // Store the values for each of the waveshapes at the correct offsets in the shared data heap for (uint8 i = 0; i < 45; ++i) { block.setDataByte (sineWaveOffset + i, sineWaveY[i]); block.setDataByte (squareWaveOffset + i, squareWaveY[i]); block.setDataByte (sawWaveOffset + i, sawWaveY[i]); block.setDataByte (triangleWaveOffset + i, triangleWaveY[i]); } } String getLittleFootProgram() override { return R"littlefoot( #heapsize: 256 int yOffset; void drawLEDCircle (int x0, int y0) { blendPixel (0xffff0000, x0, y0); int minLedIndex = 0; int maxLedIndex = 14; blendPixel (0xff660000, min (x0 + 1, maxLedIndex), y0); blendPixel (0xff660000, max (x0 - 1, minLedIndex), y0); blendPixel (0xff660000, x0, min (y0 + 1, maxLedIndex)); blendPixel (0xff660000, x0, max (y0 - 1, minLedIndex)); blendPixel (0xff1a0000, min (x0 + 1, maxLedIndex), min (y0 + 1, maxLedIndex)); blendPixel (0xff1a0000, min (x0 + 1, maxLedIndex), max (y0 - 1, minLedIndex)); blendPixel (0xff1a0000, max (x0 - 1, minLedIndex), min (y0 + 1, maxLedIndex)); blendPixel (0xff1a0000, max (x0 - 1, minLedIndex), max (y0 - 1, minLedIndex)); } void repaint() { // Clear LEDs to black fillRect (0xff000000, 0, 0, 15, 15); // Get the waveshape type int type = getHeapByte (0); // Calculate the heap offset int offset = 1 + (type * 45) + yOffset; for (int x = 0; x < 15; ++x) { // Get the corresponding Y coordinate for each X coordinate int y = getHeapByte (offset + x); // Draw a vertical line if flag is set or draw an LED circle if (y == 255) { for (int i = 0; i < 15; ++i) drawLEDCircle (x, i); } else if (x % 2 == 0) { drawLEDCircle (x, y); } } // Increment and wrap the Y offset to draw a 'moving' waveshape if (++yOffset == 30) yOffset = 0; } )littlefoot"; } private: //============================================================================== /** Shared data heap is laid out as below. There is room for the waveshape type and the Y coordinates for 1.5 cycles of each of the four waveshapes. */ static constexpr uint32 waveshapeType = 0; // 1 byte static constexpr uint32 sineWaveOffset = 1; // 1 byte * 45 static constexpr uint32 squareWaveOffset = 46; // 1 byte * 45 static constexpr uint32 sawWaveOffset = 91; // 1 byte * 45 static constexpr uint32 triangleWaveOffset = 136; // 1 byte * 45 //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WaveshapeProgram) }; //============================================================================== /** A struct that handles the setup and layout of the DrumPadGridProgram */ struct SynthGrid { SynthGrid (int cols, int rows) : numColumns (cols), numRows (rows) { constructGridFillArray(); } /** Creates a GridFill object for each pad in the grid and sets its colour and fill before adding it to an array of GridFill objects */ void constructGridFillArray() { gridFillArray.clear(); for (auto i = 0; i < numRows; ++i) { for (auto j = 0; j < numColumns; ++j) { DrumPadGridProgram::GridFill fill; auto padNum = (i * 5) + j; fill.colour = notes.contains (padNum) ? baseGridColour : tonics.contains (padNum) ? Colours::white : Colours::black; fill.fillType = DrumPadGridProgram::GridFill::FillType::gradient; gridFillArray.add (fill); } } } int getNoteNumberForPad (int x, int y) const { auto xIndex = x / 3; auto yIndex = y / 3; return 60 + ((4 - yIndex) * 5) + xIndex; } //============================================================================== int numColumns, numRows; float width, height; Array gridFillArray; Colour baseGridColour = Colours::green; Colour touchColour = Colours::red; Array tonics = { 4, 12, 20 }; Array notes = { 1, 3, 6, 7, 9, 11, 14, 15, 17, 19, 22, 24 }; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SynthGrid) }; //============================================================================== /** The main component */ class BlocksSynthDemo : public Component, public TopologySource::Listener, private TouchSurface::Listener, private ControlButton::Listener, private Timer { public: BlocksSynthDemo() { // Register BlocksSynthDemo as a listener to the PhysicalTopologySource object topologySource.addListener (this); #if JUCE_IOS connectButton.setButtonText ("Connect"); connectButton.onClick = [] { BluetoothMidiDevicePairingDialogue::open(); }; addAndMakeVisible (connectButton); #endif setSize (600, 400); topologyChanged(); } ~BlocksSynthDemo() override { if (activeBlock != nullptr) detachActiveBlock(); topologySource.removeListener (this); } void paint (Graphics& g) override { g.setColour (getLookAndFeel().findColour (Label::textColourId)); g.drawText ("Connect a Lightpad Block to play.", getLocalBounds(), Justification::centred, false); } void resized() override { #if JUCE_IOS connectButton.setBounds (getRight() - 100, 20, 80, 30); #endif } /** Overridden from TopologySource::Listener, called when the topology changes */ void topologyChanged() override { // Reset the activeBlock object if (activeBlock != nullptr) detachActiveBlock(); // Get the array of currently connected Block objects from the PhysicalTopologySource auto blocks = topologySource.getBlocks(); // Iterate over the array of Block objects for (auto b : blocks) { // Find the first Lightpad if (b->getType() == Block::Type::lightPadBlock) { activeBlock = b; // Register BlocksSynthDemo as a listener to the touch surface if (auto surface = activeBlock->getTouchSurface()) surface->addListener (this); // Register BlocksSynthDemo as a listener to any buttons for (auto button : activeBlock->getButtons()) button->addListener (this); // Get the LEDGrid object from the Lightpad and set its program to the program for the current mode if (auto grid = activeBlock->getLEDGrid()) { // Work out scale factors to translate X and Y touches to LED indexes scaleX = static_cast (grid->getNumColumns() - 1) / (float) activeBlock->getWidth(); scaleY = static_cast (grid->getNumRows() - 1) / (float) activeBlock->getHeight(); setLEDProgram (*activeBlock); } break; } } } private: /** Overridden from TouchSurface::Listener. Called when a Touch is received on the Lightpad */ void touchChanged (TouchSurface&, const TouchSurface::Touch& touch) override { if (currentMode == waveformSelectionMode && touch.isTouchStart && allowTouch) { if (auto* waveshapeProgram = getWaveshapeProgram()) { // Change the displayed waveshape to the next one ++waveshapeMode; if (waveshapeMode > 3) waveshapeMode = 0; waveshapeProgram->setWaveshapeType (static_cast (waveshapeMode)); allowTouch = false; startTimer (250); } } else if (currentMode == playMode) { if (auto* gridProgram = getGridProgram()) { // Translate X and Y touch events to LED indexes auto xLed = roundToInt (touch.startX * scaleX); auto yLed = roundToInt (touch.startY * scaleY); // Limit the number of touches per second constexpr auto maxNumTouchMessagesPerSecond = 100; auto now = Time::getCurrentTime(); clearOldTouchTimes (now); auto midiChannel = waveshapeMode + 1; // Send the touch event to the DrumPadGridProgram and Audio class if (touch.isTouchStart) { gridProgram->startTouch (touch.startX, touch.startY); audio.noteOn (midiChannel, layout.getNoteNumberForPad (xLed, yLed), touch.z); } else if (touch.isTouchEnd) { gridProgram->endTouch (touch.startX, touch.startY); audio.noteOff (midiChannel, layout.getNoteNumberForPad (xLed, yLed), 1.0); } else { if (touchMessageTimesInLastSecond.size() > maxNumTouchMessagesPerSecond / 3) return; gridProgram->sendTouch (touch.x, touch.y, touch.z, layout.touchColour); // Send pitch change and pressure values to the Audio class audio.pitchChange (midiChannel, (touch.x - touch.startX) / (float) activeBlock->getWidth()); audio.pressureChange (midiChannel, touch.z); } touchMessageTimesInLastSecond.add (now); } } } /** Overridden from ControlButton::Listener. Called when a button on the Lightpad is pressed */ void buttonPressed (ControlButton&, Block::Timestamp) override {} /** Overridden from ControlButton::Listener. Called when a button on the Lightpad is released */ void buttonReleased (ControlButton&, Block::Timestamp) override { // Turn any active synthesiser notes off audio.allNotesOff(); // Switch modes if (currentMode == waveformSelectionMode) currentMode = playMode; else if (currentMode == playMode) currentMode = waveformSelectionMode; // Set the LEDGrid program to the new mode setLEDProgram (*activeBlock); } /** Clears the old touch times */ void clearOldTouchTimes (const Time now) { for (auto i = touchMessageTimesInLastSecond.size(); --i >= 0;) if (touchMessageTimesInLastSecond.getReference(i) < now - RelativeTime::seconds (0.33)) touchMessageTimesInLastSecond.remove (i); } /** Removes TouchSurface and ControlButton listeners and sets activeBlock to nullptr */ void detachActiveBlock() { if (auto surface = activeBlock->getTouchSurface()) surface->removeListener (this); for (auto button : activeBlock->getButtons()) button->removeListener (this); activeBlock = nullptr; } /** Sets the LEDGrid Program for the selected mode */ void setLEDProgram (Block& block) { if (currentMode == waveformSelectionMode) { // Set the LEDGrid program block.setProgram (std::make_unique(block)); // Initialise the program if (auto* waveshapeProgram = getWaveshapeProgram()) { waveshapeProgram->setWaveshapeType (static_cast (waveshapeMode)); waveshapeProgram->generateWaveshapes(); } } else if (currentMode == playMode) { // Set the LEDGrid program auto error = block.setProgram (std::make_unique(block)); if (error.failed()) { DBG (error.getErrorMessage()); jassertfalse; } // Setup the grid layout if (auto* gridProgram = getGridProgram()) gridProgram->setGridFills (layout.numColumns, layout.numRows, layout.gridFillArray); } } /** Stops touch events from triggering multiple waveshape mode changes */ void timerCallback() override { allowTouch = true; } //============================================================================== DrumPadGridProgram* getGridProgram() { if (activeBlock != nullptr) return dynamic_cast (activeBlock->getProgram()); return nullptr; } WaveshapeProgram* getWaveshapeProgram() { if (activeBlock != nullptr) return dynamic_cast (activeBlock->getProgram()); return nullptr; } //============================================================================== enum BlocksSynthMode { waveformSelectionMode = 0, playMode }; BlocksSynthMode currentMode = playMode; //============================================================================== Audio audio; SynthGrid layout { 5, 5 }; PhysicalTopologySource topologySource; Block::Ptr activeBlock; Array