/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2016 - ROLI Ltd. Permission is granted to use this software under the terms of either: a) the GPL v2 (or any later version) b) the Affero GPL v3 Details of these licenses can be found at: www.gnu.org/licenses JUCE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ------------------------------------------------------------------------------ To release a closed-source product which uses JUCE, commercial licenses are available: visit www.juce.com for more information. ============================================================================== */ DrumPadGridProgram::DrumPadGridProgram (LEDGrid& lg) : Program (lg) {} int DrumPadGridProgram::getPadIndex (float posX, float posY) const { posX = juce::jmin (0.99f, posX / ledGrid.block.getWidth()); posY = juce::jmin (0.99f, posY / ledGrid.block.getHeight()); const uint32 offset = ledGrid.getDataByte (visiblePads_byte) ? numColumns1_byte : numColumns0_byte; const int numColumns = ledGrid.getDataByte (offset + numColumns0_byte); const int numRows = ledGrid.getDataByte (offset + numRows0_byte); return int (posX * numColumns) + int (posY * numRows) * numColumns; } void DrumPadGridProgram::startTouch (float startX, float startY) { const auto padIdx = getPadIndex (startX, startY); for (size_t i = 0; i < 4; ++i) { if (ledGrid.getDataByte (touchedPads_byte + i) == 0) { ledGrid.setDataByte (touchedPads_byte + i, static_cast (padIdx + 1)); break; } } } void DrumPadGridProgram::endTouch (float startX, float startY) { const auto padIdx = getPadIndex (startX, startY); for (size_t i = 0; i < 4; ++i) if (ledGrid.getDataByte (touchedPads_byte + i) == (padIdx + 1)) ledGrid.setDataByte (touchedPads_byte + i, 0); } void DrumPadGridProgram::sendTouch (float x, float y, float z, LEDColour colour) { LEDGrid::ProgramEventMessage e; e.values[0] = 0x20000000 + (juce::jlimit (0, 255, juce::roundToInt (x * (255.0f / ledGrid.block.getWidth()))) << 16) + (juce::jlimit (0, 255, juce::roundToInt (y * (255.0f / ledGrid.block.getHeight()))) << 8) + juce::jlimit (0, 255, juce::roundToInt (z * 255.0f)); e.values[1] = (int32) colour.getARGB(); ledGrid.sendProgramEvent (e); } //============================================================================== void DrumPadGridProgram::setGridFills (int numColumns, int numRows, const juce::Array& fills) { uint8 visiblePads = ledGrid.getDataByte (visiblePads_byte); setGridFills (numColumns, numRows, fills, visiblePads * numColumns1_byte); } void DrumPadGridProgram::setGridFills (int numColumns, int numRows, const juce::Array& fills, uint32 byteOffset) { jassert (numColumns * numRows == fills.size()); ledGrid.setDataByte (byteOffset + numColumns0_byte, (uint8) numColumns); ledGrid.setDataByte (byteOffset + numRows0_byte, (uint8) numRows); uint32 i = 0; for (auto fill : fills) { if (i >= maxNumPads) { jassertfalse; break; } const uint32 colourOffsetBytes = byteOffset + colours0_byte + i * colourSizeBytes; const uint32 colourOffsetBits = colourOffsetBytes * 8; ledGrid.setDataBits (colourOffsetBits, 5, fill.colour.getRed() >> 3); ledGrid.setDataBits (colourOffsetBits + 5, 6, fill.colour.getGreen() >> 2); ledGrid.setDataBits (colourOffsetBits + 11, 5, fill.colour.getBlue() >> 3); ledGrid.setDataByte (byteOffset + fillTypes0_byte + i, static_cast (fill.fillType)); ++i; } } void DrumPadGridProgram::triggerSlideTransition (int newNumColumns, int newNumRows, const juce::Array& newFills, SlideDirection direction) { uint8 newVisible = ledGrid.getDataByte (visiblePads_byte) ? 0 : 1; setGridFills (newNumColumns, newNumRows, newFills, newVisible * numColumns1_byte); ledGrid.setDataByte (visiblePads_byte, newVisible); ledGrid.setDataByte (slideDirection_byte, (uint8) direction); } //============================================================================== void DrumPadGridProgram::setPadAnimationState (uint32 padIdx, double loopTimeSecs, double currentProgress) { // Only 16 animated pads are supported. jassert (padIdx < 16); // Compensate for bluetooth latency & led resolution, tweaked by eye for POS app. currentProgress = std::fmod (currentProgress + 0.1, 1.0); uint16 aniValue = uint16 (juce::roundToInt ((255 << 8) * currentProgress)); uint16 aniIncrement = loopTimeSecs > 0.0 ? uint16 (juce::roundToInt (((255 << 8) / 25.0) / loopTimeSecs)) : 0; uint32 offset = 8 * animationTimers_byte + 32 * padIdx; ledGrid.setDataBits (offset, 16, aniValue); ledGrid.setDataBits (offset + 16, 16, aniIncrement); } void DrumPadGridProgram::suspendAnimations() { for (uint32 i = 0; i < 16; ++i) { uint32 offset = 8 * animationTimers_byte + 32 * i; ledGrid.setDataBits (offset + 16, 16, 0); } // Hijack touch dimming ledGrid.setDataByte (touchedPads_byte, 255); } void DrumPadGridProgram::resumeAnimations() { // Unhijack touch dimming ledGrid.setDataByte (touchedPads_byte, 0); } //============================================================================== uint32 DrumPadGridProgram::getHeapSize() { return totalDataSize; } juce::String DrumPadGridProgram::getLittleFootProgram() { return R"littlefoot( int dimFactor; int dimDelay; int slideAnimationProgress; int lastVisiblePads; int getGridColour (int index, int colourMapOffset) { int bit = (2 + colourMapOffset) * 8 + index * 16; return makeARGB (255, getHeapBits (bit, 5) << 3, getHeapBits (bit + 5, 6) << 2, getHeapBits (bit + 11, 5) << 3); } // Returns the current progress and also increments it for next frame int getAnimationProgress (int index) { // Only 16 animated pads supported if (index > 15) return 0; int offsetBits = 162 * 8 + index * 32; int currentProgress = getHeapBits (offsetBits, 16); int increment = getHeapBits (offsetBits + 16, 16); int nextFrame = currentProgress + increment; // Set incremented 16 bit number. setHeapByte (162 + index * 4, nextFrame & 0xff); setHeapByte (163 + index * 4, nextFrame >> 8); return currentProgress; } void outlineRect (int colour, int x, int y, int w) { fillRect (colour, x, y, w, 1); fillRect (colour, x, y + w - 1, w, 1); fillRect (colour, x, y + 1, 1, w - 1); fillRect (colour, x + w - 1, y + 1, 1, w - 1); } void drawPlus (int colour, int x, int y, int w) { fillRect (colour, x, y + (w / 2), w, 1); fillRect (colour, x + (w / 2), y, 1, w); } void fillGradientRect (int colour, int x, int y, int w) { if (colour != 0xff000000) { int divisor = w + w - 1; for (int yy = 0; yy < w; ++yy) { for (int xx = yy; xx < w; ++xx) { int gradColour = blendARGB (colour, makeARGB (((xx + yy) * 250) / divisor, 0, 0, 0)); setLED (x + xx, y + yy, gradColour); setLED (x + yy, y + xx, gradColour); } } } } // TODO: Tom M: This is massaged to work with 3x3 pads and for dots to sync // with Apple POS loop length. Rework to be more robust & flexible. void drawPizzaLED (int colour, int x, int y, int w, int progress) { --w; x += 1; int numToDo = ((8 * progress) / 255) + 1; int totalLen = w * 4; for (int i = 1; i <= numToDo; ++i) { setLED (x, y, colour); if (i < w) ++x; else if (i < (w * 2)) ++y; else if (i < (w * 3)) --x; else if (i < totalLen) --y; } } void drawPad (int padX, int padY, int padW, int colour, int fill, int animateProgress) { animateProgress >>= 8; // 16 bit to 8 bit int halfW = padW / 2; if (fill == 0) // Gradient fill { fillGradientRect (colour, padX, padY, padW); } else if (fill == 1) // Filled { fillRect (colour, padX, padY, padW, padW); } else if (fill == 2) // Hollow { outlineRect (colour, padX, padY, padW); } else if (fill == 3) // Hollow with plus { outlineRect (colour, padX, padY, padW); drawPlus (0xffffffff, padX, padY, padW); } else if (fill == 4) // Pulsing dot { int pulseCol = blendARGB (colour, makeARGB (animateProgress, 0, 0, 0)); setLED (padX + halfW, padY + halfW, pulseCol); } else if (fill == 5) // Blinking dot { int blinkCol = animateProgress > 64 ? makeARGB (255, 0, 0, 0) : colour; setLED (padX + halfW, padY + halfW, blinkCol); } else if (fill == 6) // Pizza filled { outlineRect (blendARGB (colour, makeARGB (220, 0, 0, 0)), padX, padY, padW); // Dim outline setLED (padX + halfW, padY + halfW, colour); // Bright centre drawPizzaLED (colour, padX, padY, padW, animateProgress); } else if (fill == 7) // Pizza hollow { outlineRect (blendARGB (colour, makeARGB (220, 0, 0, 0)), padX, padY, padW); // Dim outline drawPizzaLED (colour, padX, padY, padW, animateProgress); return; } } void fadeHeatMap() { for (int i = 0; i < 225; ++i) { int colourOffset = 226 + i * 4; int colour = getHeapInt (colourOffset); int alpha = (colour >> 24) & 0xff; if (alpha > 0) { alpha -= getHeapByte (1126 + i); setHeapInt (colourOffset, alpha < 0 ? 0 : ((alpha << 24) | (colour & 0xffffff))); } } } void addToHeatMap (int x, int y, int colour) { if (x >= 0 && y >= 0 && x < 15 && y < 15) { int offset = 226 + 4 * (x + y * 15); colour = blendARGB (getHeapInt (offset), colour); setHeapInt (offset, colour); int decay = ((colour >> 24) & 0xff) / 14; // change divisor to change trail times offset = 1126 + (x + y * 15); setHeapByte (offset, decay > 0 ? decay : 1); } } int getHeatmapColour (int x, int y) { return getHeapInt (226 + 4 * (x + y * 15)); } int isPadActive (int index) { if (getHeapInt (158) == 0) // None active return 0; ++index; return index == getHeapByte (158) || index == getHeapByte (159) || index == getHeapByte (160) || index == getHeapByte (161); } void updateDimFactor() { if (getHeapInt (158) == 0) { if (--dimDelay <= 0) { dimFactor -= 12; if (dimFactor < 0) dimFactor = 0; } } else { dimFactor = 180; dimDelay = 12; } } void drawPads (int offsetX, int offsetY, int colourMapOffset) { int padsPerSide = getHeapByte (0 + colourMapOffset); if (padsPerSide < 2) return; int blockW = 15 / padsPerSide; int blockPlusGapW = blockW + (15 - padsPerSide * blockW) / (padsPerSide - 1); for (int padY = 0; padY < padsPerSide; ++padY) { for (int padX = 0; padX < padsPerSide; ++padX) { int ledX = offsetX + padX * blockPlusGapW; int ledY = offsetY + padY * blockPlusGapW; if (ledX < 15 && ledY < 15 && (ledX + blockW) >= 0 && (ledY + blockW) >= 0) { int padIdx = padX + padY * padsPerSide; bool padActive = isPadActive (padIdx); int blendCol = padActive ? 255 : 0; int blendAmt = padActive ? dimFactor >> 1 : dimFactor; int colour = blendARGB (getGridColour (padIdx, colourMapOffset), makeARGB (blendAmt, blendCol, blendCol, blendCol)); int fillType = getHeapByte (colourMapOffset + 52 + padIdx); int animate = getAnimationProgress (padIdx); drawPad (ledX, ledY, blockW, colour, fillType, animate); } } } } void slideAnimatePads() { int nowVisible = getHeapByte (155); if (lastVisiblePads != nowVisible) { lastVisiblePads = nowVisible; if (slideAnimationProgress <= 0) slideAnimationProgress = 15; } // If animation is complete, draw normally. if (slideAnimationProgress <= 0) { drawPads (0, 0, 78 * nowVisible); slideAnimationProgress = 0; } else { int direction = getHeapByte (156); slideAnimationProgress -= 1; int inPos = nowVisible == 0 ? 0 : 78; int outPos = nowVisible == 0 ? 78 : 0; if (direction == 0) // Up { drawPads (0, slideAnimationProgress - 16, outPos); drawPads (0, slideAnimationProgress, inPos); } else if (direction == 1) // Down { drawPads (0, 16 - slideAnimationProgress, outPos); drawPads (0, 0 - slideAnimationProgress, inPos); } else if (direction == 2) // Left { drawPads (16 - slideAnimationProgress, 0, outPos); drawPads (slideAnimationProgress, 0, inPos); } else if (direction == 3) // Right { drawPads (16 - slideAnimationProgress, 0, outPos); drawPads (0 - slideAnimationProgress, 0, inPos); } else // None { drawPads (0, 0, 78 * nowVisible); slideAnimationProgress = 0; } } } void repaint() { // showErrorOnFail, showRepaintTime, showMovingDot //enableDebug (true, true, false); // Clear LEDs to black, update dim animation fillRect (0xff000000, 0, 0, 15, 15); updateDimFactor(); // Does the main painting of pads slideAnimatePads(); // Overlay heatmap for (int y = 0; y < 15; ++y) for (int x = 0; x < 15; ++x) blendLED (x, y, getHeatmapColour (x, y)); fadeHeatMap(); } // DrumPadGridProgram::sendTouch results in this callback, giving // us more touch updates per frame and therefore smoother trails. void handleMessage (int pos, int colour) { if ((pos >> 24) != 0x20) return; int tx = ((pos >> 16) & 0xff) - 13; int ty = ((pos >> 8) & 0xff) - 13; int tz = pos & 0xff; tz = tz > 30 ? tz : 30; int ledCentreX = tx >> 4; int ledCentreY = ty >> 4; int adjustX = (tx - (ledCentreX << 4)) >> 2; int adjustY = (ty - (ledCentreY << 4)) >> 2; for (int dy = -2; dy <= 2; ++dy) { for (int dx = -2; dx <= 2; ++dx) { int distance = dx * dx + dy * dy; int level = distance == 0 ? 255 : (distance == 1 ? 132 : (distance < 5 ? 9 : (distance == 5 ? 2 : 0))); level += (dx * adjustX); level += (dy * adjustY); level = (tz * level) >> 8; if (level > 0) addToHeatMap (ledCentreX + dx, ledCentreY + dy, makeARGB (level, colour >> 16, colour >> 8, colour)); } } } )littlefoot"; }