/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2017 - ROLI Ltd. JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 5 End-User License Agreement and JUCE 5 Privacy Policy (both updated and effective as of the 27th April 2017). End User License Agreement: www.juce.com/juce-5-licence Privacy Policy: www.juce.com/juce-5-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ #pragma once #include "../JuceLibraryCode/JuceHeader.h" #include "Audio.h" #include "WaveshapeProgram.h" //============================================================================== /** 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 (int i = 0; i < numRows; ++i) { for (int j = 0; j < numColumns; ++j) { DrumPadGridProgram::GridFill fill; int 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 { int xIndex = x / 3; int 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 MainComponent : public Component, public TopologySource::Listener, private TouchSurface::Listener, private ControlButton::Listener, #if JUCE_IOS private Button::Listener, #endif private Timer { public: MainComponent() { setSize (600, 400); // Register MainContentComponent as a listener to the PhysicalTopologySource object topologySource.addListener (this); #if JUCE_IOS connectButton.setButtonText ("Connect"); connectButton.addListener (this); addAndMakeVisible (connectButton); #endif }; ~MainComponent() { if (activeBlock != nullptr) detachActiveBlock(); } 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.getCurrentTopology().blocks; // Iterate over the array of Block objects for (auto b : blocks) { // Find the first Lightpad if (b->getType() == Block::Type::lightPadBlock) { activeBlock = b; // Register MainContentComponent as a listener to the touch surface if (auto surface = activeBlock->getTouchSurface()) surface->addListener (this); // Register MainContentComponent 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) / activeBlock->getWidth(); scaleY = static_cast (grid->getNumRows() - 1) / 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) { // 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) { // Translate X and Y touch events to LED indexes int xLed = roundToInt (touch.startX * scaleX); int yLed = roundToInt (touch.startY * scaleY); // Limit the number of touches per second constexpr int maxNumTouchMessagesPerSecond = 100; auto now = Time::getCurrentTime(); clearOldTouchTimes (now); int 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) / 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); } #if JUCE_IOS void buttonClicked (Button* b) override { if (b == &connectButton) BluetoothMidiDevicePairingDialogue::open(); } #endif /** Clears the old touch times */ void clearOldTouchTimes (const Time now) { for (int i = touchMessageTimesInLastSecond.size(); --i >= 0;) if (touchMessageTimesInLastSecond.getReference(i) < now - juce::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) { // Create a new WaveshapeProgram for the LEDGrid waveshapeProgram = new WaveshapeProgram (block); // Set the LEDGrid program block.setProgram (waveshapeProgram); // Initialise the program waveshapeProgram->setWaveshapeType (static_cast (waveshapeMode)); waveshapeProgram->generateWaveshapes(); } else if (currentMode == playMode) { // Create a new DrumPadGridProgram for the LEDGrid gridProgram = new DrumPadGridProgram (block); // Set the LEDGrid program auto error = block.setProgram (gridProgram); if (error.failed()) { DBG (error.getErrorMessage()); jassertfalse; } // Setup the grid layout gridProgram->setGridFills (layout.numColumns, layout.numRows, layout.gridFillArray); } } /** Stops touch events from triggering multiple waveshape mode changes */ void timerCallback() override { allowTouch = true; } enum BlocksSynthMode { waveformSelectionMode = 0, playMode }; BlocksSynthMode currentMode = playMode; //============================================================================== Audio audio; DrumPadGridProgram* gridProgram = nullptr; WaveshapeProgram* waveshapeProgram = nullptr; SynthGrid layout { 5, 5 }; PhysicalTopologySource topologySource; Block::Ptr activeBlock; Array touchMessageTimesInLastSecond; int waveshapeMode = 0; float scaleX = 0.0; float scaleY = 0.0; bool allowTouch = true; //============================================================================== #if JUCE_IOS TextButton connectButton; #endif //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent) };