diff --git a/include/effects.h b/include/effects.h index b9989156d..4a89a2627 100644 --- a/include/effects.h +++ b/include/effects.h @@ -149,7 +149,7 @@ #define EFFECT_MATRIX_SILON 160 #define EFFECT_MATRIX_PDPGRID 161 #define EFFECT_MATRIX_AUDIOSPIKE 162 - +#define EFFECT_MATRIX_PDPCMX 163 // Hexagon Effects #define EFFECT_HEXAGON_OUTER_RING 201 diff --git a/include/effects/matrix/PatternAnimatedGIF.h b/include/effects/matrix/PatternAnimatedGIF.h index 25eee850c..b25e96193 100644 --- a/include/effects/matrix/PatternAnimatedGIF.h +++ b/include/effects/matrix/PatternAnimatedGIF.h @@ -154,7 +154,7 @@ class PatternAnimatedGIF : public LEDStripEffect static void screenClearCallback(void) { auto& g = *(g_ptrSystem->EffectManager().g()); - g.fillScreen(g.to16bit(g_gifDecoderState._bkColor)); + g.Clear(g_gifDecoderState._bkColor); } // We decide when to update the screen, so this is a no-op diff --git a/include/effects/matrix/PatternPulse.h b/include/effects/matrix/PatternPulse.h index e5ff8eb25..d085aa234 100644 --- a/include/effects/matrix/PatternPulse.h +++ b/include/effects/matrix/PatternPulse.h @@ -185,7 +185,7 @@ class PatternPulsar : public BeatEffectBase, public LEDStripEffect const int maxNewStarsPerFrame = 8; for (int i = 0; i < maxNewStarsPerFrame; i++) - if (random(4) < g_Analyzer._VURatio) + if (random(4) < g_Analyzer.VURatio()) g()->drawPixel(random(MATRIX_WIDTH), random(MATRIX_HEIGHT), RandomSaturatedColor()); diff --git a/include/effects/matrix/spectrumeffects.h b/include/effects/matrix/spectrumeffects.h index 2d6166b3f..94a5f6a69 100644 --- a/include/effects/matrix/spectrumeffects.h +++ b/include/effects/matrix/spectrumeffects.h @@ -77,13 +77,14 @@ class InsulatorSpectrumEffect : public LEDStripEffect, public BeatEffectBase, pu virtual void Draw() override { - auto peaks = g_Analyzer.GetPeakData(); + // Use const reference to avoid copying PeakData + const PeakData & peaks = g_Analyzer.Peaks(); for (int band = 0; band < min(NUM_BANDS, NUM_FANS); band++) { CRGB color = ColorFromPalette(_Palette, ::map(band, 0, min(NUM_BANDS, NUM_FANS), 0, 255) + beatsin8(1) ); color = color.fadeToBlackBy(255 - 255 * peaks[band]); - color = color.fadeToBlackBy((2.0 - g_Analyzer._VURatio) * 228); + color = color.fadeToBlackBy((2.0 - g_Analyzer.VURatio()) * 228); DrawRingPixels(0, FAN_SIZE * peaks[band], color, NUM_FANS-1-band, 0); } @@ -104,7 +105,7 @@ class InsulatorSpectrumEffect : public LEDStripEffect, public BeatEffectBase, pu // REVIEW(davepl) This might look interesting if it didn't erase... - bool bFlash = g_Analyzer._VURatio > 1.99 && span > 1.9 && elapsed > 0.25; + bool bFlash = g_Analyzer.VURatio() > 1.99 && span > 1.9 && elapsed > 0.25; _allParticles.push_back(SpinningPaletteRingParticle(iInsulator, 0, _Palette, 256.0/FAN_SIZE, 4, -0.5, RING_SIZE_0, 0, LINEARBLEND, true, 1.0, bFlash ? max(0.12f, elapsed/8) : 0)); } @@ -116,7 +117,7 @@ class VUMeter // DrawVUPixels // - // Draw i-th pixel in row y + // Draw i-th pixel in row yVU virtual void DrawVUPixels(std::vector> & GFX, int i, int yVU, int fadeBy = 0, const CRGBPalette16 * pPalette = nullptr) { @@ -153,7 +154,7 @@ class VUMeter const int MAX_FADE = 256; int xHalf = GFX[0]->width()/2-1; - int bars = g_Analyzer._VURatioFade / 2.0 * xHalf; + int bars = g_Analyzer.VURatioFade() / 2.0 * xHalf; bars = min(bars, xHalf); EraseVUMeter(GFX, bars, yVU); @@ -163,14 +164,14 @@ class VUMeter msPeakVU = millis(); iPeakVUy = bars; } - else if (millis() - msPeakVU > MS_PER_SECOND / 2) + else if (millis() - msPeakVU > MILLIS_PER_SECOND / 2) { iPeakVUy = 0; } if (iPeakVUy > 1) { - int fade = MAX_FADE * (millis() - msPeakVU) / (float) MS_PER_SECOND * 2; + int fade = MAX_FADE * (millis() - msPeakVU) / (float) MILLIS_PER_SECOND * 2; DrawVUPixels(GFX, iPeakVUy, yVU, fade); DrawVUPixels(GFX, iPeakVUy-1, yVU, fade); } @@ -206,7 +207,7 @@ class VUMeterVertical : public VUMeter const int MAX_FADE = 256; int size = GFX[0]->width(); - int bars = g_Analyzer._VURatioFade / 2.0 * size; + int bars = g_Analyzer.VURatioFade() / 2.0 * size; bars = min(bars, size); EraseVUMeter(GFX, bars, yVU); @@ -216,14 +217,14 @@ class VUMeterVertical : public VUMeter msPeakVU = millis(); iPeakVUy = bars; } - else if (millis() - msPeakVU > MS_PER_SECOND / 2) + else if (millis() - msPeakVU > MILLIS_PER_SECOND / 2) { iPeakVUy = 0; } if (iPeakVUy > 1) { - int fade = MAX_FADE * (millis() - msPeakVU) / (float) MS_PER_SECOND * 2; + int fade = MAX_FADE * (millis() - msPeakVU) / (float) MILLIS_PER_SECOND * 2; DrawVUPixels(GFX, iPeakVUy, yVU, fade); DrawVUPixels(GFX, iPeakVUy-1, yVU, fade); } @@ -333,18 +334,18 @@ class SpectrumAnalyzerEffect : public LEDStripEffect, virtual public VUMeter // bar 16, for example, it will take all of bar 4 and none of bar 5. For bar 17, it will take 3/4 of bar 4 and 1/4 of bar 5. int ib = iBar % barsPerBand; - value = (g_Analyzer._peak1Decay[iBand] * (barsPerBand - ib) + g_Analyzer._peak1Decay[iNextBand] * (ib) ) / barsPerBand * (pGFXChannel->height() - 1); - value2 = (g_Analyzer._peak2Decay[iBand] * (barsPerBand - ib) + g_Analyzer._peak2Decay[iNextBand] * (ib) ) / barsPerBand * pGFXChannel->height(); + value = (g_Analyzer.Peak2Decay(iBand) * (barsPerBand - ib) + g_Analyzer.Peak2Decay(iNextBand) * (ib) ) / barsPerBand * (pGFXChannel->height() - 1); + value2 = (g_Analyzer.Peak2Decay(iBand) * (barsPerBand - ib) + g_Analyzer.Peak2Decay(iNextBand) * (ib) ) / barsPerBand * pGFXChannel->height(); } else { // One to one case, just use the actual band value we mapped to - value = g_Analyzer._peak1Decay[iBand] * (pGFXChannel->height() - 1); - value2 = g_Analyzer._peak2Decay[iBand] * pGFXChannel->height(); + value = g_Analyzer.Peak2Decay(iBand) * (pGFXChannel->height() - 1); + value2 = g_Analyzer.Peak2Decay(iBand) * pGFXChannel->height(); } - debugV("Band: %d, Value: %f\n", iBar, g_Analyzer._peak1Decay[iBar] ); + debugV("Band: %d, Value: %f\n", iBar, g_Analyzer.Peak2Decay(iBar) ); if (value > pGFXChannel->height()) value = pGFXChannel->height(); @@ -389,11 +390,11 @@ class SpectrumAnalyzerEffect : public LEDStripEffect, virtual public VUMeter { const int PeakFadeTime_ms = 1000; - unsigned long msPeakAge = millis() - g_Analyzer._lastPeak1Time[iBand]; + unsigned long msPeakAge = millis() - g_Analyzer.LastPeak1Time(iBand); if (msPeakAge > PeakFadeTime_ms) msPeakAge = PeakFadeTime_ms; - float agePercent = (float) msPeakAge / (float) MS_PER_SECOND; + float agePercent = (float) msPeakAge / (float) MILLIS_PER_SECOND; uint8_t fadeAmount = std::min(255.0f, agePercent * 256); colorHighlight.fadeToBlackBy(fadeAmount); pGFXChannel->drawLine(xOffset, max(0, yOffset-1), xOffset + barWidth - 1, max(0, yOffset-1), colorHighlight); @@ -487,8 +488,8 @@ class SpectrumAnalyzerEffect : public LEDStripEffect, virtual public VUMeter { // The peaks and their decay rates are global, so we load up our values every time we display so they're current - g_Analyzer._peak1DecayRate = _peak1DecayRate; - g_Analyzer._peak2DecayRate = _peak2DecayRate; + // Load decay rates into analyzer + g_Analyzer.SetPeakDecayRates(_peak1DecayRate, _peak2DecayRate); } virtual void Draw() override @@ -628,8 +629,8 @@ class WaveformEffect : public LEDStripEffect { int top = g_ptrSystem->EffectManager().IsVUVisible() ? 1 : 0; g()->MoveInwardX(top); // Start on Y=1 so we don't shift the VU meter - DrawSpike(MATRIX_WIDTH-1, g_Analyzer._VURatio/2.0); - DrawSpike(0, g_Analyzer._VURatio/2.0); + DrawSpike(MATRIX_WIDTH-1, g_Analyzer.VURatio()/2.0); + DrawSpike(0, g_Analyzer.VURatio()/2.0); } }; @@ -704,7 +705,7 @@ class GhostWave : public WaveformEffect // VURatio is too fast, VURatioFade looks too slow, but averaged between them is just right - float audioLevel = (g_Analyzer._VURatioFade + g_Analyzer._VURatio) / 2; + float audioLevel = (g_Analyzer.VURatioFade() + g_Analyzer.VURatio()) / 2; // Offsetting by 0.25, which is a very low ratio, helps keep the line thin when sound is low //audioLevel = (audioLevel - 0.25) / 1.75; @@ -800,7 +801,7 @@ class SpectrumBarEffect : public LEDStripEffect, public BeatEffectBase { // Draw the spike - auto value = g_Analyzer.BeatEnhance(SPECTRUMBARBEAT_ENHANCE) * g_Analyzer._peak1Decay[iBand]; + auto value = g_Analyzer.BeatEnhance(SPECTRUMBARBEAT_ENHANCE) * g_Analyzer.Peak2Decay(iBand); auto top = std::max(0.0f, halfHeight - value * halfHeight); auto bottom = std::min(MATRIX_HEIGHT-1.0f, halfHeight + value * halfHeight + 1); auto x1 = halfWidth - ((iBand * 2 + offset) % halfWidth); @@ -826,8 +827,7 @@ class SpectrumBarEffect : public LEDStripEffect, public BeatEffectBase // Set the peak decay rates to something that looks good for this effect - g_Analyzer._peak1DecayRate = kPeakDecaySpectrumBar; - g_Analyzer._peak2DecayRate = kPeakDecaySpectrumBar; + g_Analyzer.SetPeakDecayRates(kPeakDecaySpectrumBar, kPeakDecaySpectrumBar); // This effect doesn't clear during drawing, so we need to clear to start the frame diff --git a/include/effects/strip/faneffects.h b/include/effects/strip/faneffects.h index ccbd29337..79a88c331 100644 --- a/include/effects/strip/faneffects.h +++ b/include/effects/strip/faneffects.h @@ -397,14 +397,14 @@ class FanBeatEffect : public LEDStripEffect void OnBeat() { - int passes = g_Analyzer._VURatio; + int passes = (int)g_Analyzer.VURatio(); for (int iPass = 0; iPass < passes; iPass++) { int iFan = random(0, NUM_FANS); - int passes = random(1, g_Analyzer._VURatio); + int innerPasses = random(1, (int)g_Analyzer.VURatio()); CRGB c = CHSV(random(0, 255), 255, 255); - for (int iPass = 0; iPass < passes; iPass++) + for (int iInnerPass = 0; iInnerPass < innerPasses; iInnerPass++) { DrawFanPixels(0, FAN_SIZE, c, Sequential, iFan++); } @@ -412,9 +412,7 @@ class FanBeatEffect : public LEDStripEffect CRGB c = CHSV(random(0, 255), 255, 255); for (int i = NUM_FANS * FAN_SIZE; i < NUM_LEDS; i++) - { g()->setPixel(i, c); - } } void DrawEffect() @@ -424,21 +422,21 @@ class FanBeatEffect : public LEDStripEffect if (latch) { - if (g_Analyzer._VURatio < minVUSeen) - minVUSeen = g_Analyzer._VURatio; + if (g_Analyzer.VURatio() < minVUSeen) + minVUSeen = g_Analyzer.VURatio(); } - if (g_Analyzer._VURatio < 0.25f) // Crossing center going up + if (g_Analyzer.VURatio() < 0.25f) // Crossing center going up { latch = true; - minVUSeen = g_Analyzer._VURatio; + minVUSeen = g_Analyzer.VURatio(); } if (latch) { - if (g_Analyzer._VURatio > 1.5f) + if (g_Analyzer.VURatio() > 1.5f) { - if (random_range(1.0f, 3.0f) < g_Analyzer._VURatio) + if (random_range(1.0f, 3.0f) < g_Analyzer.VURatio()) { latch = false; OnBeat(); @@ -589,7 +587,7 @@ class PaletteReelEffect : public LEDStripEffect { for (int i = 0; i < NUM_FANS; i++) { - if (random(0, 100) < 50 * g_Analyzer._VURatio) // 40% Chance of attempting to do something + if (random(0, 100) < 50 * g_Analyzer.VURatio()) // 40% Chance of attempting to do something { int action = random(0, 3); // Generate a random outcome if (action == 0 || action == 3) @@ -598,7 +596,7 @@ class PaletteReelEffect : public LEDStripEffect } else if (action == 1) { - if (g_Analyzer._VURatio > 0.5f) + if (g_Analyzer.VURatio() > 0.5f) { if (ReelDir[i] == 0) { @@ -613,7 +611,7 @@ class PaletteReelEffect : public LEDStripEffect } else if (action == 2) { - if (g_Analyzer._VURatio > 0.5f) + if (g_Analyzer.VURatio() > 0.5f) { if (ReelDir[i] == 0) // 2 -> Spin Forwards, or accel if already doing so { @@ -634,7 +632,7 @@ class PaletteReelEffect : public LEDStripEffect { for (int i = 0; i < NUM_FANS; i++) { - ReelPos[i] = (ReelPos[i] + ReelDir[i] * (2 + g_Analyzer._VURatio)); + ReelPos[i] = (ReelPos[i] + ReelDir[i] * (2 + g_Analyzer.VURatio())); if (ReelPos[i] < 0) ReelPos[i] += FAN_SIZE; if (ReelPos[i] >= FAN_SIZE) diff --git a/include/effects/strip/fireeffect.h b/include/effects/strip/fireeffect.h index 0415b7c15..ce8941863 100644 --- a/include/effects/strip/fireeffect.h +++ b/include/effects/strip/fireeffect.h @@ -35,6 +35,7 @@ #include "musiceffect.h" #include "soundanalyzer.h" #include "systemcontainer.h" +#include class FireEffect : public LEDStripEffect { @@ -181,9 +182,9 @@ class FireEffect : public LEDStripEffect for (int i = 0; i < LEDCount; i++) { - auto sum = 0; - for (int j = 0; j < CellsPerLED; j++) - sum += heat[i*CellsPerLED + j]; + auto begin = &heat[i * CellsPerLED]; + auto end = begin + CellsPerLED; + int sum = std::accumulate(begin, end, 0); auto avg = sum / CellsPerLED; #if LANTERN @@ -317,7 +318,7 @@ class MusicalPaletteFire : public PaletteFlameEffect, protected BeatEffectBase } else { - GenerateSparks(g_Analyzer._VURatio * 50); + GenerateSparks(g_Analyzer.VURatio() * 50); } } @@ -570,7 +571,7 @@ class SmoothFireEffect : public LEDStripEffect { for (int k = _cLEDs - 1; k >= 3; k--) { - float amount = 0.2f + g_Analyzer._VURatio; // MIN(0.85f, _Drift * deltaTime); + float amount = 0.2f + g_Analyzer.VURatio(); // MIN(0.85f, _Drift * deltaTime); float c0 = 1.0f - amount; float c1 = amount * 0.33f; float c2 = c1; @@ -760,9 +761,9 @@ class BaseFireEffect : public LEDStripEffect int cellsPerLED = CellCount / LEDCount; for (int i = 0; i < LEDCount; i++) { - int sum = 0; - for (int iCell = 0; iCell < cellsPerLED; iCell++) - sum += heat[i * cellsPerLED + iCell]; + auto begin = &heat[i * cellsPerLED]; + auto end = begin + cellsPerLED; + int sum = std::accumulate(begin, end, 0); int avg = sum / cellsPerLED; CRGB color = MapHeatToColor(heat[avg]); int j = bReversed ? (LEDCount - 1 - i) : i; diff --git a/include/effects/strip/meteoreffect.h b/include/effects/strip/meteoreffect.h index e460ec6a3..05af576ad 100644 --- a/include/effects/strip/meteoreffect.h +++ b/include/effects/strip/meteoreffect.h @@ -31,6 +31,8 @@ #pragma once +#include "ledstripeffect.h" + class MeteorChannel { std::vector hue; @@ -98,19 +100,19 @@ class MeteorChannel bLeft[iMeteor] = !bLeft[iMeteor]; } - virtual void Draw(std::shared_ptr pGFX) + virtual void Draw(LEDStripEffect* owner) { + auto pGFX = owner->g(0); static CHSV hsv; hsv.val = 255; hsv.sat = 255; - for (int j = 0; jGetLEDCount(); j++) // fade brightness all LEDs one step + // Fade brightness on all channels + for (int j = 0; j < pGFX->GetLEDCount(); j++) { - if ((!meteorRandomDecay) || (random_range(0, 10)>2)) // BUGBUG Was 5 for everything before atomlight + if ((!meteorRandomDecay) || (random_range(0, 10) > 2)) { - CRGB c = pGFX->getPixel(j); - c.fadeToBlackBy(meteorTrailDecay); - pGFX->setPixel(j, c); + owner->fadePixelToBlackOnAllChannelsBy(j, meteorTrailDecay); } } @@ -119,12 +121,12 @@ class MeteorChannel float spd = speed[i]; #if ENABLE_AUDIO - if (g_Analyzer._VURatio > 1.0) - spd *= g_Analyzer._VURatio; + if (g_Analyzer.VURatio() > 1.0f) + spd *= g_Analyzer.VURatio(); #endif - iPos[i] = (bLeft[i]) ? iPos[i]-spd : iPos[i]+spd; - if (iPos[i]< meteorSize) + iPos[i] = (bLeft[i]) ? iPos[i] - spd : iPos[i] + spd; + if (iPos[i] < meteorSize) { bLeft[i] = false; iPos[i] = meteorSize; @@ -132,23 +134,26 @@ class MeteorChannel if (iPos[i] >= pGFX->GetLEDCount()) { bLeft[i] = true; - iPos[i] = pGFX->GetLEDCount()-1; + iPos[i] = pGFX->GetLEDCount() - 1; } - for (int j = 0; j < meteorSize; j++) // Draw the meteor head + // Draw the meteor head across all channels + for (int j = 0; j < meteorSize; j++) { - int x = iPos[i] - j; - if ((x <= pGFX->GetLEDCount()) && (x >= 1)) + int x = static_cast(iPos[i]) - j; + if ((x <= static_cast(pGFX->GetLEDCount())) && (x >= 1)) { CRGB rgb; hue[i] = hue[i] + 0.025f; if (hue[i] > 255.0f) hue[i] -= 255.0f; - hsv.hue = hue[i]; + hsv.hue = static_cast(hue[i]); hsv2rgb_rainbow(hsv, rgb); + + // Blend with current pixel from primary device, then set on all channels CRGB c = pGFX->getPixel(x); - nblend(c, rgb , 75); - pGFX->setPixel(x, c); + nblend(c, rgb, 75); + owner->setPixelOnAllChannels(x, c); } } } @@ -223,7 +228,8 @@ class MeteorEffect : public LEDStripEffect void Draw() override { - for (int i = 0; i < _Meteors.size(); i++) - _Meteors[i].Draw(_GFX[i]); + // Draw once using channel 0 state while writing to all channels + if (!_Meteors.empty()) + _Meteors[0].Draw(this); } }; diff --git a/include/effects/strip/misceffects.h b/include/effects/strip/misceffects.h index c5db0f069..40df2552f 100644 --- a/include/effects/strip/misceffects.h +++ b/include/effects/strip/misceffects.h @@ -576,22 +576,125 @@ class PDPGridEffect : public LEDStripEffect virtual size_t DesiredFramesPerSecond() const { - return 20; + return 5; } + bool RequiresDoubleBuffering() const override + { + return false; + } + + virtual void Start() override + { + g()->Clear(); + } + virtual void Draw() override { - fadeAllChannelsToBlackBy(255 * g_Values.AppTime.LastFrameTime()); + fadeAllChannelsToBlackBy(60); + g()->MoveY(1); + for (int x = 0; x < MATRIX_WIDTH; x++) + { + // Pick a color, CRGB::Red 90% of the time, CRGB::Green 10% of the time + CRGB color = random(0, 100) > 20 ? CRGB::Black : CRGB(random(0, 100) < 90) ? CRGB::Red : CRGB::Orange; + setPixelOnAllChannels(x, MATRIX_HEIGHT-1, color); + } + } +}; + +// PDPCMXEffect +// +// Connection Machine 5 LED simulation for the PDP-11/34 CMX display + +class PDPCMXEffect : public LEDStripEffect +{ + private: + static constexpr int GROUP_HEIGHT = 5; // Height of each logical group + static constexpr float LED_PROBABILITY = 0.30f; // 30% chance of LED being on + + void scrollGroup(int groupStartY, bool scrollLeft) + { + // Scroll existing LEDs in the group + for (int y = groupStartY; y < groupStartY + GROUP_HEIGHT && y < MATRIX_HEIGHT; y++) + { + if (scrollLeft) + { + // Scroll left: move all pixels one position left + for (int x = 0; x < MATRIX_WIDTH - 1; x++) + { + CRGB color = _GFX[0]->getPixel(x + 1, y); + setPixelOnAllChannels(x, y, color); + } + // Clear the rightmost pixel (will be populated with new random data) + setPixelOnAllChannels(MATRIX_WIDTH - 1, y, CRGB::Black); + } + else + { + // Scroll right: move all pixels one position right + for (int x = MATRIX_WIDTH - 1; x > 0; x--) + { + CRGB color = _GFX[0]->getPixel(x - 1, y); + setPixelOnAllChannels(x, y, color); + } + // Clear the leftmost pixel (will be populated with new random data) + setPixelOnAllChannels(0, y, CRGB::Black); + } + } + + // Add new random LEDs on the appropriate edge + for (int y = groupStartY; y < groupStartY + GROUP_HEIGHT && y < MATRIX_HEIGHT; y++) + { + if (random(100) < (LED_PROBABILITY * 100)) + { + CRGB color = CRGB::Red; + if (scrollLeft) + setPixelOnAllChannels(MATRIX_WIDTH - 1, y, color); + else + setPixelOnAllChannels(0, y, color); + } + } + } + + public: + + PDPCMXEffect() : LEDStripEffect(EFFECT_MATRIX_PDPCMX, "PDPCMXEffect") + { + } + + PDPCMXEffect(const JsonObjectConst& jsonObject) + : LEDStripEffect(jsonObject) + { + } + virtual size_t DesiredFramesPerSecond() const + { + return 30; // Moderate speed for scrolling effect + } + + virtual bool CanDisplayVUMeter() const override + { + return false; + } + + virtual void Start() override + { + g()->Clear(); + } + + virtual void Draw() override + { + // Process each logical group + int numGroups = (MATRIX_HEIGHT + GROUP_HEIGHT - 1) / GROUP_HEIGHT; // Ceiling division + + fadeAllChannelsToBlackBy(5); EVERY_N_MILLISECONDS(200) { - g()->MoveY(1); - for (int x = 0; x < MATRIX_WIDTH; x++) + for (int group = 0; group < numGroups; group++) { - if (random(0, 100) < 20) - setPixelOnAllChannels(x, MATRIX_HEIGHT-1, CRGB::Red); - else - setPixelOnAllChannels(x, MATRIX_HEIGHT-1, CRGB::Black); + int groupStartY = group * GROUP_HEIGHT; + bool scrollLeft = (group % 2 == 0); // Alternate direction: even groups scroll left, odd scroll right + + scrollGroup(groupStartY, scrollLeft); } } } diff --git a/include/effects/strip/musiceffect.h b/include/effects/strip/musiceffect.h index 35322e728..7b9a60bdd 100644 --- a/include/effects/strip/musiceffect.h +++ b/include/effects/strip/musiceffect.h @@ -87,7 +87,9 @@ class BeatEffectBase debugV("BeatEffectBase2::Draw"); double elapsed = SecondsSinceLastBeat(); - auto basslevel = g_Analyzer.GetPeakData()._Level[0] * 2; // Since VURatio was historically a 0-2 range, we do the same + // Access peaks via const reference to avoid copying + const PeakData & peaks = g_Analyzer.Peaks(); + auto basslevel = peaks._Level[0] * 2; // Scale to historical 0-2 range debugV("basslevel: %0.2f", basslevel); _samples.push_back(basslevel); @@ -134,7 +136,7 @@ class SimpleColorBeat : public BeatEffectBase, public LEDStripEffect { ProcessAudio(); - CRGB c = CRGB::Blue * g_Analyzer._VURatio * g_Values.AppTime.LastFrameTime() * 0.75; + CRGB c = CRGB::Blue * g_Analyzer.VURatio() * g_Values.AppTime.LastFrameTime() * 0.75; setPixelsOnAllChannels(0, NUM_LEDS, c, true); fadeAllChannelsToBlackBy(min(255.0,1000.0 * g_Values.AppTime.LastFrameTime())); diff --git a/include/effects/strip/particles.h b/include/effects/strip/particles.h index 33b9db627..cc11d0ede 100644 --- a/include/effects/strip/particles.h +++ b/include/effects/strip/particles.h @@ -411,7 +411,7 @@ class ColorBeatWithFlash : public BeatEffectBase, public ParticleSystem 3 && iInsulator == _iLastInsulator); _iLastInsulator = iInsulator; - CRGB c = CHSV(beatsin8(4), 255, 127.5*g_Analyzer._VURatio); + CRGB c = CHSV(beatsin8(4), 255, 127.5*g_Analyzer.VURatio()); CRGB r = RandomSaturatedColor(); LightInsulator(bMajor ? - 1: iInsulator, 0, bMajor ? r : c, bMajor); } @@ -423,9 +423,9 @@ class ColorBeatWithFlash : public BeatEffectBase, public ParticleSystem::Render(_GFX); @@ -480,7 +480,7 @@ class ColorBeatOverRed : public LEDStripEffect, public BeatEffectBase, public Pa // also have to update and render the particle system, which does the actual pixel drawing. We clear the scene ever // pass and rely on the fade effects of the particles to blend the - float amount = g_Analyzer._VU / 4096; + float amount = g_Analyzer.VU() / 4096; _baseColor = CRGB(500 * amount, 0, 0); setAllOnAllChannels(_baseColor.r, _baseColor.g, _baseColor.b); @@ -772,7 +772,7 @@ class MoltenGlassOnVioletBkgnd : public LEDStripEffect, public BeatEffectBase, p // also have to update and render the particle system, which does the actual pixel drawing. We clear the scene ever // pass and rely on the fade effects of the particles to blend the - uint8_t v = 16 * g_Analyzer._VURatio; + uint8_t v = 16 * g_Analyzer.VURatio(); _baseColor += CRGB(CHSV(200, 255, v)); _baseColor.fadeToBlackBy((min(255.0, 1000.0 * g_Values.AppTime.LastFrameTime()))); setAllOnAllChannels(_baseColor.r, _baseColor.g, _baseColor.b); @@ -862,7 +862,7 @@ class NewMoltenGlassOnVioletBkgnd : public LEDStripEffect, public BeatEffectBase // also have to update and render the particle system, which does the actual pixel drawing. We clear the scene ever // pass and rely on the fade effects of the particles to blend the - uint8_t v = 16 * g_Analyzer._VURatio; + uint8_t v = 16 * g_Analyzer.VURatio(); _baseColor += CRGB(CHSV(200, 255, v)); _baseColor.fadeToBlackBy((min(255.0, 1000.0 * g_Values.AppTime.LastFrameTime()))); setAllOnAllChannels(_baseColor.r, _baseColor.g, _baseColor.b); @@ -923,7 +923,7 @@ class SparklySpinningMusicEffect : public LEDStripEffect, public BeatEffectBase, // also have to update and render the particle system, which does the actual pixel drawing. We clear the scene ever // pass and rely on the fade effects of the particles to blend the - uint8_t v = 32 * g_Analyzer._VURatio; + uint8_t v = 32 * g_Analyzer.VURatio(); _baseColor += CRGB(CHSV(beatsin8(1), 255, v)); _baseColor.fadeToBlackBy((min(255.0, 2500.0 * g_Values.AppTime.LastFrameTime()))); setAllOnAllChannels(_baseColor.r, _baseColor.g, _baseColor.b); @@ -968,7 +968,7 @@ class MusicalHotWhiteInsulatorEffect : public LEDStripEffect, public BeatEffectB // also have to update and render the particle system, which does the actual pixel drawing. We clear the scene ever // pass and rely on the fade effects of the particles to blend the - uint8_t v = 32 * g_Analyzer._VURatio; + uint8_t v = 32 * g_Analyzer.VURatio(); _baseColor += CRGB(CHSV(beatsin8(1), 255, v)); _baseColor.fadeToBlackBy((min(255.0,1000.0 * g_Values.AppTime.LastFrameTime()))); setAllOnAllChannels(_baseColor.r, _baseColor.g, _baseColor.b); diff --git a/include/effects/strip/stareffect.h b/include/effects/strip/stareffect.h index 9f210fe2a..362128877 100644 --- a/include/effects/strip/stareffect.h +++ b/include/effects/strip/stareffect.h @@ -194,7 +194,7 @@ class MusicPulseStar : public Star virtual float IgnitionTime() const { return 0.00f; } virtual float HoldTime() const { return 1.00f; } virtual float FadeTime() const { return 2.00f; } - virtual float GetStarSize() const { return 1 + _objectSize * g_Analyzer._VURatio; } + virtual float GetStarSize() const { return 1 + _objectSize * g_Analyzer.VURatio(); } }; #endif @@ -506,11 +506,11 @@ template class StarryNightEffect : public LEDStripEffect { double prob = _newStarProbability; - prob = (prob / 100) + (g_Analyzer._VURatio - 1.0) * _musicFactor; + prob = (prob / 100) + (g_Analyzer.VURatio() - 1.0) * _musicFactor; constexpr auto kProbabilitySpan = 1.0; - if (g_Analyzer._VU > 0) + if (g_Analyzer.VU() > 0) { if (random_range(0.0, kProbabilitySpan) < g_Values.AppTime.LastFrameTime() * prob) { @@ -547,7 +547,7 @@ template class StarryNightEffect : public LEDStripEffect else { g()->blurRows(g()->leds, MATRIX_WIDTH, MATRIX_HEIGHT, 0, _blurFactor * 255); - fadeAllChannelsToBlackBy(55 * (2.0 - g_Analyzer._VURatioFade)); + fadeAllChannelsToBlackBy(55 * (2.0 - g_Analyzer.VURatioFade())); } for(auto i = _allParticles.begin(); i != _allParticles.end(); i++) diff --git a/include/effects/strip/tempeffect.h b/include/effects/strip/tempeffect.h index 13ca16a4d..e8d11c763 100644 --- a/include/effects/strip/tempeffect.h +++ b/include/effects/strip/tempeffect.h @@ -146,7 +146,7 @@ class VUInsulatorsEffect : public LEDStripEffect DrawVUPixels(iPeakVUy, fade, vu_gpGreen); } - int bars = ::map(g_Analyzer._VU, g_Analyzer._MinVU, 150.0, 1, _cLEDs - 1); + int bars = ::map(g_Analyzer.VU(), g_Analyzer.MinVU(), 150.0, 1, _cLEDs - 1); if (bars >= iPeakVUy) { msPeakVU = millis(); diff --git a/include/gfxbase.h b/include/gfxbase.h index d0c6998ef..a185c9ea4 100644 --- a/include/gfxbase.h +++ b/include/gfxbase.h @@ -65,6 +65,7 @@ #pragma once #include +#include #include "Adafruit_GFX.h" #include "pixeltypes.h" #include "effects/matrix/Boid.h" @@ -109,6 +110,7 @@ class GFXBase : public Adafruit_GFX protected: size_t _width; size_t _height; + size_t _ledcount; // 32 Entries in the 5-bit gamma table static constexpr auto gamma5 = to_array @@ -176,34 +178,34 @@ class GFXBase : public Adafruit_GFX virtual size_t GetLEDCount() const { - return _width * _height; + return _ledcount; } static uint8_t beatcos8(accum88 beats_per_minute, uint8_t lowest = 0, uint8_t highest = 255, uint32_t timebase = 0, uint8_t phase_offset = 0) { uint8_t beat = beat8(beats_per_minute, timebase); - uint8_t beatcos = cos8(beat + phase_offset); - uint8_t rangewidth = highest - lowest; - uint8_t scaledbeat = scale8(beatcos, rangewidth); - uint8_t result = lowest + scaledbeat; + uint8_t beatCos = cos8(beat + phase_offset); + uint8_t rangeWidth = highest - lowest; + uint8_t scaledBeat = scale8(beatCos, rangeWidth); + uint8_t result = lowest + scaledBeat; return result; } static uint8_t mapsin8(uint8_t theta, uint8_t lowest = 0, uint8_t highest = 255) { - uint8_t beatsin = sin8(theta); - uint8_t rangewidth = highest - lowest; - uint8_t scaledbeat = scale8(beatsin, rangewidth); - uint8_t result = lowest + scaledbeat; + uint8_t beatSin = sin8(theta); + uint8_t rangeWidth = highest - lowest; + uint8_t scaledBeat = scale8(beatSin, rangeWidth); + uint8_t result = lowest + scaledBeat; return result; } static uint8_t mapcos8(uint8_t theta, uint8_t lowest = 0, uint8_t highest = 255) { - uint8_t beatcos = cos8(theta); - uint8_t rangewidth = highest - lowest; - uint8_t scaledbeat = scale8(beatcos, rangewidth); - uint8_t result = lowest + scaledbeat; + uint8_t beatCos = cos8(theta); + uint8_t rangeWidth = highest - lowest; + uint8_t scaledBeat = scale8(beatCos, rangeWidth); + uint8_t result = lowest + scaledBeat; return result; } @@ -238,16 +240,19 @@ class GFXBase : public Adafruit_GFX else fill_solid(leds, _width * _height, color); } - virtual bool isValidPixel(uint x, uint y) const + + __attribute__((always_inline)) + virtual bool isValidPixel(uint x, uint y) const noexcept { // Check that the pixel location is within the matrix's bounds return x < _width && y < _height; } - virtual bool isValidPixel(uint n) const + __attribute__((always_inline)) + virtual bool isValidPixel(uint n) const noexcept { // Check that the pixel location is within the matrix's bounds - return n < _width * _height; + return n < _ledcount; } // Matrices that are built from individually addressable strips like WS2812b generally @@ -266,7 +271,8 @@ class GFXBase : public Adafruit_GFX // If your matrix uses a different approach, you can override this function and implement it // in the XY() function of your class - virtual uint16_t xy(uint16_t x, uint16_t y) const + __attribute__((always_inline)) + inline virtual uint16_t xy(uint16_t x, uint16_t y) const noexcept { if (x & 0x01) { @@ -293,7 +299,7 @@ class GFXBase : public Adafruit_GFX #define XY(x, y) xy(x, y) #endif - virtual CRGB getPixel(int16_t x, int16_t y) const + __attribute__((always_inline)) virtual CRGB getPixel(int16_t x, int16_t y) const { if (isValidPixel(x, y)) return leds[XY(x, y)]; @@ -301,7 +307,7 @@ class GFXBase : public Adafruit_GFX throw std::runtime_error(str_sprintf("Invalid index in getPixel: x=%d, y=%d, NUM_LEDS=%d", x, y, NUM_LEDS).c_str()); } - virtual CRGB getPixel(int16_t i) const + __attribute__((always_inline)) virtual CRGB getPixel(int16_t i) const { if (isValidPixel(i)) return leds[i]; @@ -309,19 +315,19 @@ class GFXBase : public Adafruit_GFX throw std::runtime_error(str_sprintf("Invalid index in getPixel: i=%d, NUM_LEDS=%d", i, NUM_LEDS).c_str()); } - virtual void addColor(int16_t i, CRGB c) + __attribute__((always_inline)) virtual void addColor(int16_t i, CRGB c) { if (isValidPixel(i)) leds[i] += c; } - virtual void drawPixel(int16_t x, int16_t y, CRGB color) + __attribute__((always_inline)) virtual void drawPixel(int16_t x, int16_t y, CRGB color) { if (isValidPixel(x, y)) leds[XY(x, y)] = color; } - void drawPixel(int16_t x, int16_t y, uint16_t color) override + __attribute__((always_inline)) void drawPixel(int16_t x, int16_t y, uint16_t color) override { if (isValidPixel(x, y)) leds[XY(x, y)] = from16Bit(color); @@ -337,7 +343,7 @@ class GFXBase : public Adafruit_GFX setPixel(x, y, pLEDs[y * _width + x]); } - virtual void setPixel(int16_t x, int16_t y, uint16_t color) + __attribute__((always_inline)) virtual void setPixel(int16_t x, int16_t y, uint16_t color) { if (isValidPixel(x, y)) leds[XY(x, y)] = from16Bit(color); @@ -345,7 +351,7 @@ class GFXBase : public Adafruit_GFX debugE("Invalid setPixel request: x=%d, y=%d, NUM_LEDS=%d", x, y, NUM_LEDS); } - virtual void setPixel(int16_t x, int16_t y, CRGB color) + __attribute__((always_inline)) virtual void setPixel(int16_t x, int16_t y, CRGB color) { if (isValidPixel(x, y)) leds[XY(x, y)] = color; @@ -353,7 +359,7 @@ class GFXBase : public Adafruit_GFX debugE("Invalid setPixel request: x=%d, y=%d, NUM_LEDS=%d", x, y, NUM_LEDS); } - virtual void setPixel(int16_t x, int r, int g, int b) + __attribute__((always_inline)) virtual void setPixel(int16_t x, int r, int g, int b) { if (isValidPixel(x)) setPixel(x, CRGB(r, g, b)); @@ -362,7 +368,30 @@ class GFXBase : public Adafruit_GFX } - virtual void setPixel(int x, CRGB color) + // Fast per-pixel fade toward black by 'fadeValue' (0..255). + // Applies scale = 255 - fadeValue to the pixel's RGB in-place. + __attribute__((always_inline)) void fadePixelToBlackBy(int16_t x, int16_t y, uint8_t fadeValue) noexcept + { + CRGB &px = leds[XY(x, y)]; + const uint8_t scale = 255 - fadeValue; + const uint16_t scale_fixed = (uint16_t)scale + 1; + px.r = (uint8_t)((((uint16_t)px.r) * scale_fixed) >> 8); + px.g = (uint8_t)((((uint16_t)px.g) * scale_fixed) >> 8); + px.b = (uint8_t)((((uint16_t)px.b) * scale_fixed) >> 8); + } + + // Linear-index overload + __attribute__((always_inline)) void fadePixelToBlackBy(int16_t i, uint8_t fadeValue) noexcept + { + CRGB &px = leds[i]; + const uint8_t scale = 255 - fadeValue; + const uint16_t scale_fixed = (uint16_t)scale + 1; + px.r = (uint8_t)((((uint16_t)px.r) * scale_fixed) >> 8); + px.g = (uint8_t)((((uint16_t)px.g) * scale_fixed) >> 8); + px.b = (uint8_t)((((uint16_t)px.b) * scale_fixed) >> 8); + } + + __attribute__((always_inline)) virtual void setPixel(int x, CRGB color) noexcept { if (isValidPixel(x)) leds[x] = color; @@ -377,7 +406,7 @@ class GFXBase : public Adafruit_GFX // are partially off the matrix. This is important for the pulsar effect. Note that // the Adafruit versions do no bounds checking - virtual void DrawSafeCircle(int centerX, int centerY, int radius, CRGB color) + virtual void DrawSafeCircle(int centerX, int centerY, int radius, CRGB color) noexcept { int x = radius; int y = 0; @@ -579,13 +608,13 @@ class GFXBase : public Adafruit_GFX } } - // Crossfade current palette slowly toward the target palette + // Cross-fade current palette slowly toward the target palette // // Each time that nblendPaletteTowardPalette is called, small changes // are made to currentPalette to bring it closer to matching targetPalette. // You can control how many changes are made in each call: // - the default of 24 is a good balance - // - meaningful values are 1-48. 1=veeeeeeeery slow, 48=quickest + // - meaningful values are 1-48. 1=very very slow, 48=quickest // - "0" means do not change the currentPalette at all; freeze void PausePalette(bool bPaused) @@ -782,8 +811,8 @@ class GFXBase : public Adafruit_GFX void ResetOscillators() { - memset(osci, 0, sizeof(osci)); - memset(p, 0, sizeof(p)); + std::fill_n(osci, 6, 0); + std::fill_n(p, 6, 0); } // All the Caleidoscope functions work directly within the screenbuffer (leds array). @@ -1115,7 +1144,7 @@ class GFXBase : public Adafruit_GFX // copy the rectangle defined with 2 points x0, y0, x1, y1 // to the rectangle beginning at x2, x3 - void Copy(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2) const + void Copy(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2) { for (int y = y0; y < y1 + 1; y++) { @@ -1126,7 +1155,7 @@ class GFXBase : public Adafruit_GFX } } - void BresenhamLine(int x0, int y0, int x1, int y1, CRGB color, bool bMerge = false) const + void BresenhamLine(int x0, int y0, int x1, int y1, CRGB color, bool bMerge = false) { int dx = abs(x1 - x0); // Delta in x direction int dy = abs(y1 - y0); // Delta in y direction @@ -1162,7 +1191,7 @@ class GFXBase : public Adafruit_GFX } } - void BresenhamLine(int x0, int y0, int x1, int y1, uint8_t colorIndex, bool bMerge = false) const + void BresenhamLine(int x0, int y0, int x1, int y1, uint8_t colorIndex, bool bMerge = false) { BresenhamLine(x0, y0, x1, y1, ColorFromCurrentPalette(colorIndex), bMerge); } @@ -1172,13 +1201,10 @@ class GFXBase : public Adafruit_GFX BresenhamLine(x0, y0, x1, y1, color); } - void DimAll(uint8_t value) const + void DimAll(uint8_t value) { for (int i = 0; i < NUM_LEDS; i++) - { - // if ((leds[i].r != 255) || (leds[i].g != 255) || (leds[i].b != 255)) // Don't dim pure white - leds[i].nscale8(value); - } + fadePixelToBlackBy(i, 255 - value); } CRGB ColorFromCurrentPalette(uint8_t index = 0, uint8_t brightness = 255, TBlendType blendType = LINEARBLEND) const @@ -1301,7 +1327,11 @@ class GFXBase : public Adafruit_GFX } // end column loop } /// MoveY - virtual void PrepareFrame() {} + virtual void PrepareFrame() + { + } - virtual void PostProcessFrame(uint16_t localPixelsDrawn, uint16_t wifiPixelsDrawn) {} + virtual void PostProcessFrame(uint16_t, uint16_t) + { + } }; diff --git a/include/globals.h b/include/globals.h index 381f01a94..20e9e69b9 100644 --- a/include/globals.h +++ b/include/globals.h @@ -150,11 +150,10 @@ #define USE_M5 1 #endif + #if USE_M5 #include "M5Unified.h" -#undef min // They define a min() on us #endif - #define EFFECT_CROSS_FADE_TIME 1200.0 // How long for an effect to ramp brightness fader down and back during effect change // Thread priorities @@ -224,1050 +223,56 @@ extern RemoteDebug Debug; // Let everyone in the project know about it #include "custom_globals.h" -#elif DEMO - - // This is a simple demo configuration. To build, simply connect the data lead from a WS2812B - // strip to pin 5 or other pin marked PIN0 below. This does not use the OLED, LCD, or anything fancy, it simply drives the - // LEDs with a simple rainbow effect as specified in effects.cpp for DEMO. - // - // Please ensure you supply sufficent power to your strip, as even the DEMO of 144 LEDs, if set - // to white, would overload a USB port. - #ifndef PROJECT_NAME - #define PROJECT_NAME "Demo" - #endif - - #define MATRIX_WIDTH 144 - #define MATRIX_HEIGHT 1 - #define NUM_LEDS (MATRIX_WIDTH*MATRIX_HEIGHT) - #define NUM_CHANNELS 1 - #define ENABLE_AUDIO 0 - - // Once you have a working project, selectively enable various additional features by setting - // them to 1 in the list below. This DEMO config assumes no audio (mic), or screen, etc. - - #ifndef ENABLE_WIFI - #define ENABLE_WIFI 0 // Connect to WiFi - #endif - - #define INCOMING_WIFI_ENABLED 0 // Accepting incoming color data and commands - #define TIME_BEFORE_LOCAL 0 // How many seconds before the lamp times out and shows local content - #define ENABLE_NTP 0 // Set the clock from the web - #define ENABLE_OTA 0 // Accept over the air flash updates - - #ifndef LED_PIN0 - #if USE_M5 - #define LED_PIN0 32 - #elif LILYGOTDISPLAYS3 - #define LED_PIN0 21 - #else - #define LED_PIN0 5 - #endif - #endif - - // The webserver serves files that are baked into the device firmware. When running you should be able to - // see/select the list of effects by visiting the chip's IP in a browser. You can get the chip's IP by - // watching the serial output or checking your router for the DHCP given to a new device; often they're - // named "esp32-" followed by a seemingly random 6-digit hexadecimal number. - - #ifndef ENABLE_WEBSERVER - #define ENABLE_WEBSERVER 0 // Turn on the internal webserver - #endif - -#elif M5DEMO - - // This is the DEMO project customized for the M5 that includes screen support and other - // features that make it well suited to the demo strip that runs in Dave's Garage - - #ifndef PROJECT_NAME - #define PROJECT_NAME "M5Demo" - #endif - - #define MATRIX_WIDTH 144*5+38 - #define MATRIX_HEIGHT 1 - #define NUM_LEDS (MATRIX_WIDTH*MATRIX_HEIGHT) - #define NUM_CHANNELS 1 - #define COLOR_ORDER EOrder::RGB - - #define ENABLE_AUDIOSERIAL 0 // Report peaks at 2400baud on serial port for PETRock consumption - #define ENABLE_WIFI 1 // Connect to WiFi - #define INCOMING_WIFI_ENABLED 1 // Accepting incoming color data and commands - #define WAIT_FOR_WIFI 0 // Hold in setup until we have WiFi - for strips without effects - #define TIME_BEFORE_LOCAL 2 // How many seconds before the lamp times out and shows local content - #define ENABLE_WEBSERVER 0 // Turn on the internal webserver - #define ENABLE_NTP 1 // Set the clock from the web - #define ENABLE_OTA 0 // Accept over the air flash updates - #define ENABLE_REMOTE 0 // IR Remote Control - #define ENABLE_AUDIO 1 // Listen for audio from the microphone and process it - #define COLORDATA_SERVER_ENABLED 0 - - #if USE_PSRAM - #define MAX_BUFFERS 500 - #else - #define MAX_BUFFERS 24 - #endif - - #define DEFAULT_EFFECT_INTERVAL (60*60*24*5) - - #ifndef LED_PIN0 - #if USE_M5 - #define LED_PIN0 32 - #elif LILYGOTDISPLAYS3 - #define LED_PIN0 21 - #else - #define LED_PIN0 5 - #endif - #endif - - #define TOGGLE_BUTTON_1 37 - #define TOGGLE_BUTTON_2 39 - -#elif LANTERN - - // A railway-style lantern with concentric rings of light (16+12+8+1) - - #ifndef PROJECT_NAME - #define PROJECT_NAME "Lantern" - #endif - - #define NUM_FANS 1 - #define NUM_RINGS 4 - #define FAN_SIZE (RING_SIZE_0 + RING_SIZE_1 + RING_SIZE_2 + RING_SIZE_3) - #define RING_SIZE_0 16 - #define RING_SIZE_1 12 - #define RING_SIZE_2 8 - #define RING_SIZE_3 1 - #define MATRIX_WIDTH 6 - #define MATRIX_HEIGHT 2 - #define NUM_LEDS (MATRIX_WIDTH*MATRIX_HEIGHT) - #define NUM_CHANNELS 1 - #define ENABLE_AUDIO 1 - - // Once you have a working project, selectively enable various additional features by setting - // them to 1 in the list below. This config assumes no audio (mic), or screen, etc. - - #define ENABLE_WIFI 0 // Connect to WiFi - #define INCOMING_WIFI_ENABLED 0 // Accepting incoming color data and commands - #define TIME_BEFORE_LOCAL 0 // How many seconds before the lamp times out and shows local contexnt - #define ENABLE_NTP 0 // Set the clock from the web - #define ENABLE_OTA 0 // Accept over the air flash updates - - #ifndef LED_PIN0 - #if M5STICKC - #define LED_PIN0 33 - #elif M5STICKCPLUS || M5STACKCORE2 || M5STICKCPLUS2 - #define LED_PIN0 32 - #else - #define LED_PIN0 5 - #endif - #endif - - // The webserver serves files that are baked into the device firmware. When running you should be able to - // see/select the list of effects by visiting the chip's IP in a browser. You can get the chip's IP by - // watching the serial output or checking your router for the DHCP given to a new device; often they're - // named "esp32-" followed by a seemingly random 6-digit hexadecimal number. - - - #define ENABLE_WEBSERVER 0 // Turn on the internal webserver - #define DEFAULT_EFFECT_INTERVAL 1000 * 60 * 60 * 24 // One a day! - - #define TOGGLE_BUTTON_1 37 - #define TOGGLE_BUTTON_2 39 - -#elif PDPGRID - - // A matrix grid display for the front of the PDP-11 - - #ifndef PROJECT_NAME - #define PROJECT_NAME "PDPGrid" - #endif - - #define NUM_FANS 1 - #define NUM_RINGS 4 - #define FAN_SIZE (RING_SIZE_0 + RING_SIZE_1 + RING_SIZE_2 + RING_SIZE_3) - #define RING_SIZE_0 16 - #define RING_SIZE_1 12 - #define RING_SIZE_2 8 - #define RING_SIZE_3 1 - #define MATRIX_WIDTH 14 - #define MATRIX_HEIGHT 16 - #define NUM_LEDS (MATRIX_WIDTH*MATRIX_HEIGHT) - #define NUM_CHANNELS 1 - #define ENABLE_AUDIO 1 - - #define POWER_LIMIT_MW 1000 - - // Once you have a working project, selectively enable various additional features by setting - // them to 1 in the list below. This config assumes no audio (mic), or screen, etc. - - #define ENABLE_WIFI 1 // Connect to WiFi - #define INCOMING_WIFI_ENABLED 1 // Accepting incoming color data and commands - #define TIME_BEFORE_LOCAL 1 // How many seconds before the lamp times out and shows local contexnt - #define ENABLE_NTP 1 // Set the clock from the web - #define ENABLE_OTA 1 // Accept over the air flash updates - #define ENABLE_WEBSERVER 1 // Turn on the internal webserver - - #ifndef LED_PIN0 - #define LED_PIN0 32 - #endif - - // The webserver serves files that are baked into the device firmware. When running you should be able to - // see/select the list of effects by visiting the chip's IP in a browser. You can get the chip's IP by - // watching the serial output or checking your router for the DHCP given to a new device; often they're - // named "esp32-" followed by a seemingly random 6-digit hexadecimal number. - - #define DEFAULT_EFFECT_INTERVAL 0 - - #define TOGGLE_BUTTON_1 37 - #define TOGGLE_BUTTON_2 39 - -#elif TREESET - - #ifndef PROJECT_NAME - #define PROJECT_NAME "Treeset" - #endif - - #define ENABLE_WIFI 1 // Connect to WiFi - #define INCOMING_WIFI_ENABLED 0 // Accepting incoming color data and commands - #define WAIT_FOR_WIFI 0 // Hold in setup until we have WiFi - for strips without effects - #define TIME_BEFORE_LOCAL 0 // How many seconds before the lamp times out and shows local content - #define ENABLE_WEBSERVER 1 // Turn on the internal webserver - #define ENABLE_NTP 0 // Set the clock from the web - #define ENABLE_OTA 1 // Accept over the air flash updates - #define ENABLE_REMOTE 1 // IR Remote Control - #define ENABLE_AUDIO 1 // Listen for audio from the microphone and process it - - #ifndef LED_PIN0 - #define LED_PIN0 26 - #endif - - #define NUM_CHANNELS 1 - #define RING_SIZE_0 24 - #define BONUS_PIXELS 0 - #define MATRIX_WIDTH 5 - #define MATRIX_HEIGHT RING_SIZE_0 - #define NUM_FANS MATRIX_WIDTH - #define FAN_SIZE MATRIX_HEIGHT - #define NUM_LEDS (MATRIX_WIDTH*MATRIX_HEIGHT) - #define ENABLE_REMOTE 1 // IR Remote Control - #define ENABLE_AUDIO 1 // Listen for audio from the microphone and process it - #define IR_REMOTE_PIN 25 - #define LED_FAN_OFFSET_BU 12 - - #define TOGGLE_BUTTON 37 - -#elif WROVERKIT +#endif +// This is a simple demo configuration used when no other project is defined; it's only purpose is +// to serve as a build to be run for [all-deps] +#ifndef MATRIX_WIDTH #define MATRIX_WIDTH 144 - #define MATRIX_HEIGHT 1 +#endif +#ifndef MATRIX_HEIGHT + #define MATRIX_HEIGHT 8 +#endif +#ifndef NUM_LEDS #define NUM_LEDS (MATRIX_WIDTH*MATRIX_HEIGHT) +#endif +#ifndef NUM_CHANNELS #define NUM_CHANNELS 1 - #define NUM_RINGS 5 - #define RING_SIZE_0 24 - - #define USE_LCD 1 - - #define ENABLE_WIFI 1 // Connect to WiFi - #define INCOMING_WIFI_ENABLED 1 // Doesn't work smoothly with the screen on for some reason! - #define TIME_BEFORE_LOCAL 2 // How many seconds before the lamp times out and shows local content - #define ENABLE_NTP 1 // Set the clock from the web - #define ENABLE_OTA 1 // Accept over the air flash updates - #define WAIT_FOR_WIFI 0 // Don't *need* it so don't wait for it - - #ifndef LED_PIN0 - #define LED_PIN0 5 - #endif - - // The webserver serves files that are baked into the device firmware. When running you should be able to - // see/select the list of effects by visiting the chip's IP in a browser. You can get the chip's IP by - // watching the serial output or checking your router for the DHCP given to a new device; often they're - // named "esp32-" followed by a seemingly random 6-digit hexadecimal number. - - #define ENABLE_WEBSERVER 1 // Turn on the internal webserver - - -#elif LASERLINE - - #ifndef PROJECT_NAME - #define PROJECT_NAME "Laser Line" - #endif - - - #define ENABLE_AUDIOSERIAL 0 // Report peaks at 2400baud on serial port for PETRock consumption - #define ENABLE_WIFI 0 // Connect to WiFi - #define INCOMING_WIFI_ENABLED 0 // Accepting incoming color data and commands - #define WAIT_FOR_WIFI 0 // Hold in setup until we have WiFi - for strips without effects - #define TIME_BEFORE_LOCAL 0 // How many seconds before the lamp times out and shows local content - #define ENABLE_WEBSERVER 0 // Turn on the internal webserver - #define ENABLE_NTP 0 // Set the clock from the web - #define ENABLE_OTA 0 // Accept over the air flash updates - #define ENABLE_REMOTE 0 // IR Remote Control - #define ENABLE_AUDIO 1 // Listen for audio from the microphone and process it - - #define DEFAULT_EFFECT_INTERVAL (60*60*24*5) - - #define NUM_CHANNELS 1 - #define RING_SIZE_0 24 - #define BONUS_PIXELS 0 - #define MATRIX_WIDTH 700 - #define MATRIX_HEIGHT 1 - #define NUM_FANS MATRIX_WIDTH - #define FAN_SIZE MATRIX_HEIGHT - #define NUM_LEDS (MATRIX_WIDTH*MATRIX_HEIGHT) - #define LED_FAN_OFFSET_BU 6 - - #define TOGGLE_BUTTON_1 37 - #define TOGGLE_BUTTON_2 39 - - #ifndef LED_PIN0 - #define LED_PIN0 32 - #endif - -#elif MESMERIZER - - // This project uses a HUB75 matrix (standard 64 pixels wide and 32 high), to show a wide range of - // effects. Its primary target device is the Mesmerizer board designed by Dave Plummer, but has been - // known to work with at least one other type of device as well. - - #ifndef PROJECT_NAME - #define PROJECT_NAME "Mesmerizer" - #endif - - #define SHOW_FPS_ON_MATRIX 0 - #define ENABLE_AUDIOSERIAL 0 // Report peaks at 2400baud on serial port for PETRock consumption - #define ENABLE_WIFI 1 // Connect to WiFi - #define INCOMING_WIFI_ENABLED 1 // Accepting incoming color data and commands - #define WAIT_FOR_WIFI 0 // Hold in setup until we have WiFi - for strips without effects - #define TIME_BEFORE_LOCAL 2 // How many seconds before the lamp times out and shows local content - #define ENABLE_WEBSERVER 1 // Turn on the internal webserver - #define ENABLE_NTP 1 // Set the clock from the web - #define ENABLE_OTA 1 // Accept over the air flash updates - #define ENABLE_REMOTE 1 // IR Remote Control - #define ENABLE_AUDIO 1 // Listen for audio from the microphone and process it - #define SCALE_AUDIO_EXPONENTIAL 0 - #define EFFECT_PERSISTENCE_CRITICAL 1 // Require effects serialization to succeed - - #define DEFAULT_EFFECT_INTERVAL (MILLIS_PER_SECOND * 60 * 2) - #define MILLIS_PER_FRAME 0 - - #define NUM_CHANNELS 1 - #define RING_SIZE_0 24 - #define BONUS_PIXELS 0 - #define MATRIX_WIDTH 64 - #define MATRIX_HEIGHT 32 - #define NUM_FANS 128 - #define FAN_SIZE 16 - #define NUM_BANDS 16 - #define NUM_LEDS (MATRIX_WIDTH*MATRIX_HEIGHT) - #define IR_REMOTE_PIN 39 - #define INPUT_PIN 36 - #define LED_FAN_OFFSET_BU 6 - - #define TOGGLE_BUTTON_1 0 - - // The mesmerizer mic isn't quite as sensitive as the M5 mic that the code was originally written for - // so we adjust by a scalar to get the same effect. - - #define AUDIO_MIC_SCALAR 1.5 - - #define COLOR_ORDER EOrder::RGB - -#elif TTGO - - // Variant of Spectrum set up for a TTGO using a MAX4466 microphone on pin27 - - // This project is set up as a 48x16 matrix of 16x16 WS2812B panels such as: https://amzn.to/3ABs5DK - // It displays a spectrum analyzer and music visualizer - - #ifndef PROJECT_NAME - #define PROJECT_NAME "TTGO" - #endif - - #define ENABLE_WIFI 1 // Connect to WiFi - #define INCOMING_WIFI_ENABLED 1 // Accepting incoming color data and commands - #define WAIT_FOR_WIFI 0 // Hold in setup until we have WiFi - for strips without effects - #define TIME_BEFORE_LOCAL 2 // How many seconds before the lamp times out and shows local content - #define ENABLE_WEBSERVER 1 // Turn on the internal webserver - #define ENABLE_NTP 1 // Set the clock from the web - #define ENABLE_OTA 0 // Accept over the air flash updates - #define ENABLE_REMOTE 1 // IR Remote Control - #define ENABLE_AUDIO 1 // Listen for audio from the microphone and process it - - #define DEFAULT_EFFECT_INTERVAL (60*60*24) - - #define MAX_BUFFERS 20 - - #ifndef LED_PIN0 - #define LED_PIN0 21 // Note that TFT board on TFTGO uses pins 19, 18, 5, 16, 23, and 4 - #endif - #define NUM_CHANNELS 1 - #define RING_SIZE_0 24 - #define BONUS_PIXELS 0 - #define MATRIX_WIDTH 48 - #define MATRIX_HEIGHT 16 - #define NUM_FANS MATRIX_WIDTH - #define FAN_SIZE MATRIX_HEIGHT - #define NUM_LEDS (MATRIX_WIDTH*MATRIX_HEIGHT) - #define IR_REMOTE_PIN 22 - #define LED_FAN_OFFSET_BU 6 - - #define TOGGLE_BUTTON_1 35 - -#elif XMASTREES - - // This project is set up as a 48x16 matrix of 16x16 WS2812B panels such as: https://amzn.to/3ABs5DK - // It uses an M5StickCPlus which has a microphone and LCD built in: https://amzn.to/3CrvCFh - // It displays a spectrum analyzer and music visualizer - - #ifndef PROJECT_NAME - #define PROJECT_NAME "X-mas Trees" - #endif - - #define ENABLE_WIFI 1 // Connect to WiFi - #define INCOMING_WIFI_ENABLED 1 // Accepting incoming color data and commands - #define WAIT_FOR_WIFI 0 // Hold in setup until we have WiFi - for strips without effects - #define TIME_BEFORE_LOCAL 2 // How many seconds before the lamp times out and shows local content - #define ENABLE_WEBSERVER 1 // Turn on the internal webserver - #define ENABLE_NTP 1 // Set the clock from the web - #define ENABLE_OTA 0 // Accept over the air flash updates - #define ENABLE_REMOTE 1 // IR Remote Control - #define ENABLE_AUDIO 1 // Listen for audio from the microphone and process it - - #define DEFAULT_EFFECT_INTERVAL (60*60*24) - - #define MAX_BUFFERS 20 - - #ifndef LED_PIN0 - #define LED_PIN0 26 - #endif - #define NUM_CHANNELS 1 - #define RING_SIZE_0 24 - #define BONUS_PIXELS 0 - #define MATRIX_WIDTH 24 - #define MATRIX_HEIGHT 5 - #define FAN_SIZE MATRIX_WIDTH - #define NUM_FANS MATRIX_HEIGHT - #define NUM_LEDS (MATRIX_WIDTH*MATRIX_HEIGHT) - #define IR_REMOTE_PIN 25 - #define LED_FAN_OFFSET_BU 6 - - #define TOGGLE_BUTTON_1 37 - #define TOGGLE_BUTTON_2 39 - -#elif ATOMLIGHT - - // This is the "Tiki Atomic Fire Lamp" project, which is an LED lamp with 4 arms of 53 LEDs each. - // Each arm is wired as a separate channel. - - #ifndef PROJECT_NAME - #define PROJECT_NAME "Atom Light" - #endif - - #define ENABLE_WIFI 1 // Connect to WiFi - #define INCOMING_WIFI_ENABLED 1 // Accepting incoming color data and commands - #define WAIT_FOR_WIFI 0 // Hold in setup until we have WiFi - for strips without effects - #define TIME_BEFORE_LOCAL 3 // How many seconds before the lamp times out and shows local content - - #define MAX_BUFFERS 30 // Times 4 channels, but they're only NUM_LEDS big - #define NUM_CHANNELS 4 // One per spoke - #define MATRIX_WIDTH 53 // Number of pixels wide (how many LEDs per channel) - #define MATRIX_HEIGHT 1 // Number of pixels tall - #define NUM_LEDS (MATRIX_WIDTH * MATRIX_HEIGHT) - #define ENABLE_REMOTE 1 // IR Remote Control - #define IR_REMOTE_PIN 35 // Eric's is PIN 35 - #define ENABLE_AUDIO 1 // Listen for audio from the microphone and process it - #define USE_SCREEN 0 // Normally we use a tiny board inside the lamp with no screen - #define FAN_SIZE NUM_LEDS // Allows us to use fan effects on the spokes - #define NUM_FANS 1 // Our fans are on channels, not in sequential order, so only one "fan" - #define NUM_RINGS 1 - #define LED_FAN_OFFSET_BU 0 - #define BONUS_PIXELS 0 - - // Original Wiring: - // Fine red = 3.3v - // brown = gnd - // orange = IO15 - // yellow = IO14 - // green = IO13 - // blue = IO12 - // purple = IO4 - - // Wiring is: - - #define LED_PIN0 5 - #define LED_PIN1 16 - #define LED_PIN2 17 - #define LED_PIN3 18 - - #define DEFAULT_EFFECT_INTERVAL (1000*60*5) - -#elif SPIRALLAMP - - // This is the "Tiki Atomic Fire Lamp" project, which is an LED lamp with 4 arms of 53 LEDs each. - // Each arm is wired as a separate channel. - - #ifndef PROJECT_NAME - #define PROJECT_NAME "Spiral Light" - #endif - - #define ENABLE_WIFI 1 // Connect to WiFi - #define INCOMING_WIFI_ENABLED 1 // Accepting incoming color data and commands - #define WAIT_FOR_WIFI 0 // Hold in setup until we have WiFi - for strips without effects - #define TIME_BEFORE_LOCAL 3 // How many seconds before the lamp times out and shows local content - #define ENABLE_OTA 0 - - #define MAX_BUFFERS 30 // Times 4 channels, but they're only NUM_LEDS big - #define NUM_CHANNELS 2 // One per spoke - #define MATRIX_WIDTH 172 // Number of pixels wide (how many LEDs per channel) - #define MATRIX_HEIGHT 1 // Number of pixels tall - #define NUM_LEDS (MATRIX_WIDTH * MATRIX_HEIGHT) - #define ENABLE_REMOTE 1 // IR Remote Control - #define IR_REMOTE_PIN 26 - #define ENABLE_AUDIO 1 // Listen for audio from the microphone and process it - #define USE_SCREEN 1 // Normally we use a tiny board inside the lamp with no screen - #define FAN_SIZE NUM_LEDS // Allows us to use fan effects on the spokes - #define NUM_FANS 1 // Our fans are on channels, not in sequential order, so only one "fan" +#endif +#ifndef NUM_RINGS #define NUM_RINGS 1 - #define FULL_COLOR_REMOTE_FILL 1 // Remote control color buttons fill the whole strip - #define BRIGHTNESS_MIN 0 // Allow OFF button to turn lamp entirely off - - // Wiring is: - - #define TOGGLE_BUTTON_1 39 - #define TOGGLE_BUTTON_2 37 - - #define LED_PIN0 32 - #define LED_PIN1 33 - - #define DEFAULT_EFFECT_INTERVAL (1000*60*5) - -#elif PLATECOVER - - // This is the "Tiki Atomic Fire Lamp" project, which is an LED lamp with 4 arms of 53 LEDs each. - // Each arm is wired as a separate channel. - - #ifndef PROJECT_NAME - #define PROJECT_NAME "Plate Cover" - #endif - - #define ENABLE_ESPNOW 1 // Connect to ESPNOW and listen for packets - #define ENABLE_WIFI 0 // Connect to WiFi - #define INCOMING_WIFI_ENABLED 0 // Accepting incoming color data and commands - #define WAIT_FOR_WIFI 0 // Hold in setup until we have WiFi - for strips without effects - #define TIME_BEFORE_LOCAL 3 // How many seconds before the lamp times out and shows local content - #define MAX_BUFFERS 60 // Times 4 channels, but they're only NUM_LEDS big - #define NUM_CHANNELS 1 // One per spoke - #define MATRIX_WIDTH 40 // Number of pixels wide (how many LEDs per channel) - #define MATRIX_HEIGHT 1 // Number of pixels tall - #define NUM_LEDS 40 - #define ENABLE_REMOTE 1 // IR Remote Control - #define IR_REMOTE_PIN 26 - #define ENABLE_AUDIO 1 // Listen for audio from the microphone and process it - #define USE_SCREEN 1 // Normally we use a tiny board inside the lamp with no screen - #define FAN_SIZE NUM_LEDS // Allows us to use fan effects on the spokes - // Wiring is: - - #define TOGGLE_BUTTON_1 39 - #define TOGGLE_BUTTON_2 37 - - #ifndef LED_PIN0 - #define LED_PIN0 32 - #endif - - #define DEFAULT_EFFECT_INTERVAL 0 // No scheduled effect changes - -#elif UMBRELLA - - #ifndef PROJECT_NAME - #define PROJECT_NAME "Umbrella" - #endif - - #define COLOR_ORDER EOrder::RGB - #define ENABLE_WIFI 1 // Connect to WiFi - #define INCOMING_WIFI_ENABLED 1 // Accepting incoming color data and commands - #define WAIT_FOR_WIFI 0 // Hold in setup until we have WiFi - for strips without effects - #define TIME_BEFORE_LOCAL 2 // How many seconds before the lamp times out and shows local content - #define ENABLE_OTA 1 // Enable over the air updates to the flash - #define ENABLE_REMOTE 1 // IR Remote Control - #define IR_REMOTE_PIN 39 - #define ENABLE_AUDIO 1 // Listen for audio from the microphone and process it - #define MAX_BUFFERS 40 - - #define DEFAULT_EFFECT_INTERVAL (1000*30 * 60) - - - // The "Tiki Fire Umbrella" project, with 8 channels - - #ifndef LED_PIN0 - #define LED_PIN0 5 // Only one pin, it's routed to all 8 spokes. Independent turned out not to be that useful. - #endif - - #define NUM_CHANNELS 1 - #define MATRIX_WIDTH 228 // Number of pixels wide (how many LEDs per channel) - #define MATRIX_HEIGHT 1 // Number of pixels tall - - #define ONBOARD_LED_R 16 - #define ONBOARD_LED_G 17 - #define ONBOARD_LED_B 18 - - #define TOGGLE_BUTTON_2 0 - -#elif MAGICMIRROR - - // A magic infinity mirror such as: https://amzn.to/3lEZo2D - // I then replaced the white LEDs with a WS2812B strip and a heltec32 module: - - #ifndef PROJECT_NAME - #define PROJECT_NAME "Magic Mirror" - #endif - - #define ENABLE_WIFI 0 // Connect to WiFi - #define INCOMING_WIFI_ENABLED 0 // Accepting incoming color data and commands - #define WAIT_FOR_WIFI 0 // Hold in setup until we have WiFi - for strips without effects - #define TIME_BEFORE_LOCAL 1 // How many seconds before the lamp times out and shows local content - - #define DEFAULT_EFFECT_INTERVAL (10*60*24) - - #ifndef LED_PIN0 - #define LED_PIN0 26 - #endif - #define NUM_CHANNELS 1 - #define BONUS_PIXELS 0 - #define NUM_FANS 1 - #define FAN_SIZE 100 - #define MATRIX_WIDTH (NUM_FANS * FAN_SIZE + BONUS_PIXELS) - #define NUM_LEDS (MATRIX_WIDTH*MATRIX_HEIGHT) - #define MATRIX_HEIGHT NUM_FANS - #define ENABLE_REMOTE 1 // IR Remote Control - #define ENABLE_AUDIO 1 // Listen for audio from the microphone and process it - #define IR_REMOTE_PIN 15 - - #define LED_FAN_OFFSET_BU 6 - -#elif HEXAGON - - // The LED strips I use for Christmas lights under my eaves - - #ifndef PROJECT_NAME - #define PROJECT_NAME "Hexagon" - #endif - - #define ENABLE_WEBSERVER 1 // Turn on the internal webserver - #define ENABLE_WIFI 1 // Connect to WiFi - #define INCOMING_WIFI_ENABLED 1 // Accepting incoming color data and commands - #define WAIT_FOR_WIFI 0 // Hold in setup until we have WiFi - for strips without effects - #define TIME_BEFORE_LOCAL 1 // How many seconds before the lamp times out and shows local content - - #define NUM_CHANNELS 1 - #define MATRIX_WIDTH (271) // My maximum run, and about all you can do at 30fps - #define MATRIX_HEIGHT 1 - #define NUM_LEDS (MATRIX_WIDTH * MATRIX_HEIGHT) - #define ENABLE_REMOTE 0 // IR Remote Control - #define ENABLE_AUDIO 0 // Listen for audio from the microphone and process it - - #ifndef LED_PIN0 - #define LED_PIN0 5 - #endif - - #define DEFAULT_EFFECT_INTERVAL (1000*20) - - #define HEX_MAX_DIMENSION 19 // How big the hex is - it's biggest row and the number of rows - #define HEX_HALF_DIMENSION 10 // How many rows from top to middle inclusive - -#elif LEDSTRIP - - // The LED strips I use for Christmas lights under my eaves - - #ifndef PROJECT_NAME - #define PROJECT_NAME "Ledstrip" - #endif - - #ifndef ENABLE_WEBSERVER - #define ENABLE_WEBSERVER 0 // Turn on the internal webserver - #endif - - #define ENABLE_WIFI 1 // Connect to WiFi - #define INCOMING_WIFI_ENABLED 1 // Accepting incoming color data and commands - - #define WAIT_FOR_WIFI 1 // Hold in setup until we have WiFi - for strips without effects - #define TIME_BEFORE_LOCAL 5 // How many seconds before the lamp times out and shows local content - #define COLORDATA_SERVER_ENABLED 0 // Also provides a response packet - #define NUM_CHANNELS 1 - #define MATRIX_WIDTH (8*144) // My maximum run, and about all you can do at 30fps - #define MATRIX_HEIGHT 1 - #define NUM_LEDS (MATRIX_WIDTH * MATRIX_HEIGHT) - #define ENABLE_REMOTE 0 // IR Remote Control - #define ENABLE_AUDIO 0 // Listen for audio from the microphone and process it - - #ifndef LED_PIN0 - #define LED_PIN0 5 - #endif - - #define DEFAULT_EFFECT_INTERVAL (1000*20) - - #define RING_SIZE_0 1 - #define RING_SIZE_1 2 - #define RING_SIZE_2 4 - #define RING_SIZE_3 8 - #define RING_SIZE_4 16 - -#elif CHIEFTAIN - - // The LED strips I use for Christmas lights under my eaves - - #ifndef PROJECT_NAME - #define PROJECT_NAME "Chieftain" - #endif - - #define ENABLE_WIFI 1 // Connect to WiFi - #define INCOMING_WIFI_ENABLED 1 // Accepting incoming color data and commands - #define WAIT_FOR_WIFI 0 // Hold in setup until we have WiFi - for strips without effects - #define TIME_BEFORE_LOCAL 5 // How many seconds before the lamp times out and shows local content - - #define NUM_CHANNELS 1 - #define MATRIX_WIDTH (12) - #define MATRIX_HEIGHT 1 - #define NUM_LEDS (MATRIX_WIDTH * MATRIX_HEIGHT) - #define ENABLE_REMOTE 0 // IR Remote Control - #define ENABLE_AUDIO 1 // Listen for audio from the microphone and process it - #ifndef LED_PIN0 - #define LED_PIN0 5 - #endif - - #define DEFAULT_EFFECT_INTERVAL (1000*20) - - #define RING_SIZE_0 1 - #define RING_SIZE_1 2 - #define RING_SIZE_2 4 - #define RING_SIZE_3 8 - #define RING_SIZE_4 16 - -#elif BELT - - // I was asked to wear something sparkly once, so I made an LED belt... - - #ifndef PROJECT_NAME - #define PROJECT_NAME "Belt" - #endif - - #define ENABLE_WIFI 0 // Connect to WiFi - #define INCOMING_WIFI_ENABLED 0 // Accepting incoming color data and commands - #define WAIT_FOR_WIFI 0 // Hold in setup until we have WiFi - for strips without effects - #define TIME_BEFORE_LOCAL 1 // How many seconds before the lamp times out and shows local content - - #define NUM_CHANNELS 1 - #define MATRIX_WIDTH (1*144) - #define MATRIX_HEIGHT 1 - #define NUM_LEDS (MATRIX_WIDTH * MATRIX_HEIGHT) - #define ENABLE_REMOTE 0 // IR Remote Control - #define ENABLE_AUDIO 0 // Listen for audio from the microphone and process it - #ifndef LED_PIN0 - #define LED_PIN0 17 - #endif - #define DEFAULT_EFFECT_INTERVAL (1000*60*60*24) - -#elif SPECTRUM - - // This project is set up as a 48x16 matrix of 16x16 WS2812B panels such as: https://amzn.to/3ABs5DK - // It uses an M5StickCPlus which has a microphone and LCD built in: https://amzn.to/3CrvCFh - // It displays a spectrum analyzer and music visualizer - - #ifndef PROJECT_NAME - #define PROJECT_NAME "Spectrum" - #endif - - #define ENABLE_AUDIOSERIAL 0 // Report peaks at 2400baud on serial port for PETRock consumption - #define ENABLE_WIFI 1 // Connect to WiFi - #define WAIT_FOR_WIFI 0 // Hold in setup until we have WiFi - for strips without effects - #define TIME_BEFORE_LOCAL 2 // How many seconds before the lamp times out and shows local content - #define ENABLE_WEBSERVER 1 // Turn on the internal webserver - #define ENABLE_NTP 1 // Set the clock from the web - #define ENABLE_OTA 0 // Accept over the air flash updates - #define ENABLE_REMOTE 1 // IR Remote Control - #define ENABLE_AUDIO 1 // Listen for audio from the microphone and process it - - #if USE_PSRAM - #define INCOMING_WIFI_ENABLED 1 // Accepting incoming color data and commands - #define COLORDATA_SERVER_ENABLED 1 - #define MAX_BUFFERS 500 - #else - #define INCOMING_WIFI_ENABLED 0 // Do not accept incoming color data and commands - #define COLORDATA_SERVER_ENABLED 0 - #define MIN_BUFFERS 1 - #define MAX_BUFFERS 1 - #endif - - #define DEFAULT_EFFECT_INTERVAL (60*60*24*5) - - #ifndef LED_PIN0 - #if SPECTRUM_WROVER_KIT - #define LED_PIN0 5 - #elif ELECROW - #define LED_PIN0 19 - #else - #define LED_PIN0 26 - #endif - #endif - - #if ELECROW - #define IR_REMOTE_PIN 20 - #endif - - #define NUM_CHANNELS 1 - #define RING_SIZE_0 24 - #define BONUS_PIXELS 0 - #define MATRIX_WIDTH 48 - #define MATRIX_HEIGHT 16 - #define NUM_FANS MATRIX_WIDTH - #define FAN_SIZE MATRIX_HEIGHT - #define NUM_BANDS 16 - #define NUM_LEDS (MATRIX_WIDTH*MATRIX_HEIGHT) - #define LED_FAN_OFFSET_BU 6 - - //#define MIN_VU 400 - //#define NOISE_FLOOR 30 - //#define NOISE_CUTOFF 5 - - #if !(ELECROW) - #define TOGGLE_BUTTON_1 37 - #define TOGGLE_BUTTON_2 39 - #endif - -#elif HELMET - - #ifndef PROJECT_NAME - #define PROJECT_NAME "Helmet" - #endif - - #define POWER_LIMIT_MW 1000 - - #define ENABLE_AUDIOSERIAL 0 // Report peaks at 2400baud on serial port for PETRock consumption - #define ENABLE_WIFI 1 // Connect to WiFi - #define WAIT_FOR_WIFI 0 // Hold in setup until we have WiFi - for strips without effects - #define TIME_BEFORE_LOCAL 2 // How many seconds before the lamp times out and shows local content - #define ENABLE_WEBSERVER 1 // Turn on the internal webserver - #define ENABLE_NTP 1 // Set the clock from the web - #define ENABLE_OTA 0 // Accept over the air flash updates - #define ENABLE_REMOTE 1 // IR Remote Control - #define ENABLE_AUDIO 1 // Listen for audio from the microphone and process it - - #if USE_PSRAM - #define INCOMING_WIFI_ENABLED 1 // Accepting incoming color data and commands - #define COLORDATA_SERVER_ENABLED 1 - #define MAX_BUFFERS 500 - #else - #define INCOMING_WIFI_ENABLED 0 // Do not accept incoming color data and commands - #define COLORDATA_SERVER_ENABLED 0 - #define MIN_BUFFERS 1 - #define MAX_BUFFERS 1 - #endif - - #define DEFAULT_EFFECT_INTERVAL 0 // Do not auto-advance unless the button is presssed - - #ifndef LED_PIN0 - #define LED_PIN0 26 - #endif - - #define NUM_CHANNELS 1 - #define RING_SIZE_0 24 - #define BONUS_PIXELS 0 - #define MATRIX_WIDTH 32 - #define MATRIX_HEIGHT 8 - #define NUM_FANS MATRIX_WIDTH - #define FAN_SIZE MATRIX_HEIGHT - #define NUM_BANDS 16 - #define NUM_LEDS (MATRIX_WIDTH*MATRIX_HEIGHT) - #define LED_FAN_OFFSET_BU 6 - - #define TOGGLE_BUTTON_1 39 - #define TOGGLE_BUTTON_2 37 - -#elif FANSET - - // An M5 stick that controls the 10 RGB fans in my PC - - #ifndef PROJECT_NAME - #define PROJECT_NAME "Fan set" - #endif - - #define ENABLE_AUDIOSERIAL 0 // Report peaks at 2400baud on serial port for PETRock consumption - #define ENABLE_WIFI 1 // Connect to WiFi - #define INCOMING_WIFI_ENABLED 0 // Accepting incoming color data and commands - #define WAIT_FOR_WIFI 0 // Hold in setup until we have WiFi - for strips without effects - #define TIME_BEFORE_LOCAL 2 // How many seconds before the lamp times out and shows local content - #define ENABLE_WEBSERVER 1 // Turn on the internal webserver - #define ENABLE_NTP 0 // Set the clock from the web - #define ENABLE_OTA 0 // Accept over the air flash updates - #define ENABLE_REMOTE 1 // IR Remote Control - #define ENABLE_AUDIO 1 // Listen for audio from the microphone and process it - #define COLORDATA_SERVER_ENABLED 0 - - #define MIN_BUFFERS 1 // Keep buffers low because we have little memory to work with - #define MAX_BUFFERS 1 - - #define DEFAULT_EFFECT_INTERVAL (60*60*24*5) - - #ifndef LED_PIN0 - #define LED_PIN0 26 - #endif - - #define BONUS_PIXELS 32 // Extra pixels - in this case, my case strip - #define NUM_CHANNELS 1 // Everything wired sequentially on a single channel - #define NUM_FANS 10 // My system has 10 fans. Because RGB. - #define NUM_BANDS 8 - #define NUM_RINGS 1 // Fans have a single outer ring of pixels - #define FAN_SIZE 16 // Each fan's pixel ring has 16 LEDs - #define FAN_LEN (NUM_FANS * FAN_SIZE) - #define MATRIX_WIDTH (NUM_FANS * FAN_SIZE + BONUS_PIXELS) - #define NUM_LEDS (MATRIX_WIDTH) - #define LED_FAN_OFFSET_BU 3 - #define MATRIX_HEIGHT 1 - - // Being case-mounted normally, the FANSET needs a more sensitive mic so the NOISE_CUTOFF value is are lower than spectrum - - #define NOISE_CUTOFF 0 - #define NOISE_FLOOR 0.0f - - #define TOGGLE_BUTTON_1 37 - #define TOGGLE_BUTTON_2 39 - -#elif SINGLE_INSULATOR - - #ifndef PROJECT_NAME - #define PROJECT_NAME "Single Insulator" - #endif - - // A single glass insulator with a 12-pixel ring and then a 7=pixel "bonus" ring in the middle - #define ENABLE_WIFI 0 // Connect to WiFi - #define INCOMING_WIFI_ENABLED 0 // Accepting incoming color data and commands - #define WAIT_FOR_WIFI 0 // Hold in setup until we have WiFi - for strips without effects - #define TIME_BEFORE_LOCAL 1 // How many seconds before the lamp times out and shows local content - - #define DEFAULT_EFFECT_INTERVAL (10*60*24) - - #define NUM_CHANNELS 1 - #define BONUS_PIXELS 7 - #define NUM_FANS 1 - #define FAN_SIZE 12 - #define MATRIX_WIDTH (NUM_FANS * FAN_SIZE + BONUS_PIXELS) - #define NUM_LEDS (MATRIX_WIDTH*MATRIX_HEIGHT) - #define MATRIX_HEIGHT NUM_FANS - - #define ENABLE_REMOTE 0 // IR Remote Control - #define ENABLE_AUDIO 1 // Listen for audio from the microphone and process it - - #define LED_FAN_OFFSET_BU 6 - - #ifndef LED_PIN0 - #if M5STICKC - #define LED_PIN0 26 - #else - #define LED_PIN0 5 - #endif - #endif - -#elif INSULATORS - - // A set of 5 Hemmingray glass insulators that each have a ring of 12 LEDs. Music reactive to the beat. - - #ifndef PROJECT_NAME - #define PROJECT_NAME "Insulators" - #endif - - #define ENABLE_WIFI 0 // Connect to WiFi - #define INCOMING_WIFI_ENABLED 0 // Accepting incoming color data and commands - #define WAIT_FOR_WIFI 0 // Hold in setup until we have WiFi - for strips without effects - #define TIME_BEFORE_LOCAL 0 // How many seconds before the lamp times out and shows local content - - #define DEFAULT_EFFECT_INTERVAL (0) - - #ifndef LED_PIN0 - #define LED_PIN0 26 - #endif - - #define NUM_CHANNELS 1 - #define RING_SIZE_0 12 - #define BONUS_PIXELS 0 - #define MATRIX_WIDTH 5 - #define MATRIX_HEIGHT RING_SIZE_0 - #define NUM_FANS MATRIX_WIDTH - #define FAN_SIZE MATRIX_HEIGHT - #define NUM_LEDS (MATRIX_WIDTH*MATRIX_HEIGHT) - #define ENABLE_REMOTE 0 // IR Remote Control - #define ENABLE_AUDIO 1 // Listen for audio from the microphone and process it - #define IR_REMOTE_PIN 26 - #define LED_FAN_OFFSET_BU 6 - - #define TOGGLE_BUTTON_1 37 - #define TOGGLE_BUTTON_2 39 - -#elif CUBE - - // A cube of 5 x 5 x 5 LEDs - - #ifndef PROJECT_NAME - #define PROJECT_NAME "Cube" - #endif - - #define ENABLE_WIFI 1 // Connect to WiFi - #define INCOMING_WIFI_ENABLED 1 // Accepting incoming color data and commands - #define WAIT_FOR_WIFI 0 // Hold in setup until we have WiFi - for strips without effects - #define TIME_BEFORE_LOCAL 5 // How many seconds before the cube times out and shows local content - #define ENABLE_WEBSERVER 1 // Enable the webserver to control the effects - - #define DEFAULT_EFFECT_INTERVAL (1000 * 60 * 10) // 10 min - - #ifndef LED_PIN0 - #define LED_PIN0 26 - #endif - - #define NUM_CHANNELS 1 - #define RING_SIZE_0 25 // Treat each layer as one ring - #define BONUS_PIXELS 0 - #define MATRIX_WIDTH 5 // 5 layers - #define MATRIX_HEIGHT RING_SIZE_0 - #define NUM_FANS MATRIX_WIDTH - #define FAN_SIZE MATRIX_HEIGHT - #define NUM_LEDS (MATRIX_WIDTH*MATRIX_HEIGHT) - #define ENABLE_REMOTE 0 // IR Remote Control - #define ENABLE_AUDIO 1 // Listen for audio from the microphone and process it - #define IR_REMOTE_PIN 26 - #define LED_FAN_OFFSET_BU 6 - #define ENABLE_OTA 0 - - #define TOGGLE_BUTTON 37 - #define COLOR_ORDER EOrder::RGB - -#else - - // This is a simple demo configuration used when no other project is defined; it's only purpose is - // to serve as a build to be run for [all-deps] - - #define MATRIX_WIDTH 144 - #define MATRIX_HEIGHT 8 - #define NUM_LEDS (MATRIX_WIDTH*MATRIX_HEIGHT) - #define NUM_CHANNELS 8 - #define NUM_RINGS 5 - #define RING_SIZE_0 24 +#endif +#ifndef RING_SIZE_0 + #define RING_SIZE_0 MATRIX_WIDTH +#endif - // Once you have a working project, selectively enable various additional features by setting - // them to 1 in the list below. This DEMO config assumes no audio (mic), or screen, etc. +// Once you have a working project, selectively enable various additional features by setting +// them to 1 in the list below. This DEMO config assumes no audio (mic), or screen, etc. - #define ENABLE_AUDIO 1 +#ifndef ENABLE_AUDIO + #define ENABLE_AUDIO 0 +#endif +#ifndef ENABLE_WIFI #define ENABLE_WIFI 1 // Connect to WiFi +#endif +#ifndef INCOMING_WIFI_ENABLED #define INCOMING_WIFI_ENABLED 1 // Accepting incoming color data and commands +#endif +#ifndef TIME_BEFORE_LOCAL #define TIME_BEFORE_LOCAL 1 // How many seconds before the lamp times out and shows local content +#endif +#ifndef ENABLE_NTP #define ENABLE_NTP 1 // Set the clock from the web +#endif +#ifndef ENABLE_OTA #define ENABLE_OTA 1 +#endif +#ifndef ENABLE_WEBSERVER #define ENABLE_WEBSERVER 1 // Turn on the internal webserver +#endif +#ifndef LED_PIN0 #define LED_PIN0 5 - #define LED_PIN1 16 - #define LED_PIN2 17 - #define LED_PIN3 18 - #define LED_PIN4 32 - #define LED_PIN5 33 - #define LED_PIN6 23 - #define LED_PIN7 22 #endif #ifndef PROJECT_NAME @@ -1301,7 +306,11 @@ extern RemoteDebug Debug; // Let everyone in the project know about it #endif #ifndef MAX_BUFFERS -#define MAX_BUFFERS (180) // Six seconds at 30fps + #if USE_PSRAM + #define MAX_BUFFERS 500 + #else + #define MAX_BUFFERS 24 + #endif #endif #ifndef ENABLE_WEBSERVER @@ -1339,15 +348,6 @@ extern RemoteDebug Debug; // Let everyone in the project know about it #ifndef NUM_BANDS // How many bands in the spectrum analyzer #define NUM_BANDS 16 #endif - #ifndef NOISE_FLOOR - #define NOISE_FLOOR 30 - #endif - #ifndef NOISE_CUTOFF - #define NOISE_CUTOFF 10 - #endif - #ifndef AUDIO_MIC_SCALAR - #define AUDIO_MIC_SCALAR 1.0 - #endif #ifndef AUDIO_PEAK_REMOTE_TIMEOUT #define AUDIO_PEAK_REMOTE_TIMEOUT 1000.0f // How long after remote PeakData before local microphone is used again #endif @@ -1365,11 +365,6 @@ extern RemoteDebug Debug; // Let everyone in the project know about it #endif #endif - -#ifndef LED_PIN0 // Which pin the LEDs are connected to -#define LED_PIN0 5 -#endif - #ifndef NUM_RINGS // How many rings in each tree/insulator/etc #define NUM_RINGS 1 #endif @@ -1417,7 +412,7 @@ extern RemoteDebug Debug; // Let everyone in the project know about it #endif #ifndef DEFAULT_EFFECT_INTERVAL -#define DEFAULT_EFFECT_INTERVAL 1000*30 +#define DEFAULT_EFFECT_INTERVAL 1000*60 #endif #ifndef MILLIS_PER_FRAME diff --git a/include/improvserial.h b/include/improvserial.h index ac7a4bb6b..1abfa0d32 100644 --- a/include/improvserial.h +++ b/include/improvserial.h @@ -36,6 +36,7 @@ #include "network.h" #include "hexdump.h" #include "globals.h" +#include #define IMPROV_LOG_FILE "/improv.log" @@ -225,9 +226,7 @@ class ImprovSerial if (at == 8 + data_len + 1) { - uint8_t checksum = 0x00; - for (uint8_t i = 0; i < at; i++) - checksum += raw[i]; + uint8_t checksum = std::accumulate(raw, raw + at, static_cast(0)); if (checksum != byte) { @@ -368,9 +367,7 @@ class ImprovSerial log_write("..Sending current state response for state: 0x%02hhX", state); - uint8_t checksum = 0x00; - for (uint8_t d : data) - checksum += d; + uint8_t checksum = std::accumulate(data.begin(), data.end(), static_cast(0)); data[10] = checksum; this->write_data_(data); @@ -389,9 +386,7 @@ class ImprovSerial log_write("..Sending error response for error: 0x%02hhX", error); - uint8_t checksum = 0x00; - for (uint8_t d : data) - checksum += d; + uint8_t checksum = std::accumulate(data.begin(), data.end(), static_cast(0)); data[10] = checksum; this->write_data_(data); } @@ -407,9 +402,7 @@ class ImprovSerial log_write("..Sending RPC response with %zu bytes of data", response.size()); - uint8_t checksum = 0x00; - for (uint8_t d : data) - checksum += d; + uint8_t checksum = std::accumulate(data.begin(), data.end(), static_cast(0)); data.push_back(checksum); this->write_data_(data); diff --git a/include/ledmatrixgfx.h b/include/ledmatrixgfx.h index ae01c225b..9e6ef3cee 100644 --- a/include/ledmatrixgfx.h +++ b/include/ledmatrixgfx.h @@ -34,7 +34,7 @@ #if USE_HUB75 -#include +#include "globals.h" // // Matrix Panel @@ -119,7 +119,7 @@ class LEDMatrixGFX : public GFXBase return (int) totalPower; } - uint16_t xy(uint16_t x, uint16_t y) const override + __attribute__((always_inline)) uint16_t xy(uint16_t x, uint16_t y) const noexcept override { // Note the x,y are unsigned so can't be less than zero if (x < _width && y < _height) @@ -151,16 +151,17 @@ class LEDMatrixGFX : public GFXBase // before them on the next buffer swap. So we clear the backbuffer and then the leds, which point to // the current front buffer. TLDR: We clear both the front and back buffers to avoid flicker between effects. - if (color == CRGB::Black) + if (color.g == color.r && color.r == color.b) { - memset((void *) backgroundLayer.backBuffer(), 0, sizeof(LEDMatrixGFX::SM_RGB) * _width * _height); - memset((void *) leds, 0, sizeof(CRGB) * _width * _height); + memset((void *) leds, color.r, sizeof(CRGB) * _ledcount); + memset((void *) backgroundLayer.backBuffer(), color.r, sizeof(LEDMatrixGFX::SM_RGB) * _ledcount); } - else + else { - for (int i = 0; i < NUM_LEDS; i++) + SM_RGB* buf = (SM_RGB*)backgroundLayer.backBuffer(); + for (int i = 0; i < _ledcount; ++i) { - backgroundLayer.backBuffer()[i] = rgb24(color.r, color.g, color.b); + buf[i] = rgb24(color.r, color.g, color.b); leds[i] = color; } } diff --git a/include/ledstripeffect.h b/include/ledstripeffect.h index 6d8beff16..9aa3f5a78 100644 --- a/include/ledstripeffect.h +++ b/include/ledstripeffect.h @@ -17,7 +17,7 @@ // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License -// along with Nightdriver. It is normally found in copying.txt +// along with NightDriver. It is normally found in copying.txt // If not, see . // // Description: @@ -476,18 +476,17 @@ class LEDStripEffect : public IJSONSerializable void fadePixelToBlackOnAllChannelsBy(int pixel, uint8_t fadeValue) const { - for (auto& device : _GFX) - { - CRGB crgb = device->getPixel(pixel); - crgb.fadeToBlackBy(fadeValue); - device->setPixel(pixel, crgb); - } + if (pixel >= 0 && pixel < _cLEDs) + for (auto& device : _GFX) + device->fadePixelToBlackBy(pixel, fadeValue); + } void fadeAllChannelsToBlackBy(uint8_t fadeValue) const { - for (int i = 0; i < _cLEDs; i++) - fadePixelToBlackOnAllChannelsBy(i, fadeValue); + for (auto& device : _GFX) + for (int i = 0; i < _cLEDs; i++) + device->fadePixelToBlackBy(i, fadeValue); } void setAllOnAllChannels(uint8_t r, uint8_t g, uint8_t b) const diff --git a/include/ledstripgfx.h b/include/ledstripgfx.h index ecaa03c03..4768103eb 100644 --- a/include/ledstripgfx.h +++ b/include/ledstripgfx.h @@ -219,7 +219,7 @@ class HexagonGFX : public LEDStripGFX // the Xth pixel in row Y. It's up to you not to overrun the width of that row, but // it will just blend into the next row if you do. - virtual uint16_t xy(uint16_t x, uint16_t y) const override + __attribute__((always_inline)) virtual uint16_t xy(uint16_t x, uint16_t y) const noexcept override { auto start = getStartIndexOfRow(y); if (y & 0x01) diff --git a/include/socketserver.h b/include/socketserver.h index 5b5b70108..d7aeaa5f7 100644 --- a/include/socketserver.h +++ b/include/socketserver.h @@ -78,7 +78,7 @@ struct SocketResponse uint32_t watts; // 4 } __attribute__((packed)); -static_assert(sizeof(double) == 8); // SocketResponse on wire uses 8 byte floats +static_assert(sizeof(double) == 8); // SocketResponse on wire uses 8 byte doubles static_assert(sizeof(float) == 4); // PeakData on wire uses 4 byte floats // Two things must be true for this to work and interop with the C# side: floats must be 8 bytes, not the default diff --git a/include/soundanalyzer.h b/include/soundanalyzer.h index 842aba2f0..6572348e2 100644 --- a/include/soundanalyzer.h +++ b/include/soundanalyzer.h @@ -33,76 +33,105 @@ #pragma once +#include "globals.h" +#include +#include #include -#include #include +#include +#include -#define MS_PER_SECOND 1000 +#ifndef SPECTRUM_BAND_SCALE_MEL +#define SPECTRUM_BAND_SCALE_MEL 0 +#endif -// These are the audio variables that are referenced by many audio effects. In order to allow non-audio code to reference them too without -// including all the audio code (such as logging code, etc.), we put the publicly exposed variables into a structure, and then the SoundAnalyzer -// class will inherit them. -// -// In the non-audio case, there's a stub class that includes ONLY the audio variables and none of the code or buffers. -// -// In both cases, the AudioVariables are accessible as g_Analyzer. It'll just be a stub in the non-audio case +// Default FFT size if not provided by build flags or elsewhere +#ifndef MAX_SAMPLES +#define MAX_SAMPLES 256 +#endif -struct AudioVariables -{ - float _VURatio = 1.0; // Current VU as a ratio to its recent min and max - float _VURatioFade = 1.0; // Same as gVURatio but with a slow decay - float _VU = 0.0; // Instantaneous read of VU value - float _PeakVU = 0.0; // How high our peak VU scale is in live mode - float _MinVU = 0.0; // How low our peak VU scale is in live mode - unsigned long _cSamples = 0U; // Total number of samples successfully collected - int _AudioFPS = 0; // Framerate of the audio sampler - int _serialFPS = 0; // How many serial packets are processed per second - uint _msLastRemote = 0; // When the last Peak data came in from external (ie: WiFi) +// Per-microphone analyzers can be tuned independently via this struct. +// Defaults below start with Mesmerizer values; others can diverge over time. + +// You can adjust the amount of compression (which makes the bar) +struct AudioInputParams { + float windowPowerCorrection; // Windowing power correction (Hann ~4.0) + float energyNoiseAdapt; // Noise floor rise rate when signal present + float energyNoiseDecay; // Noise floor decay factor when below floor + float energySmoothAlpha; // One-pole smoother for per-band signal + float energyEnvDecay; // Envelope fall (autoscale) per frame + float energyMinEnv; // Min envelope to avoid div-by-zero + float bandCompLow; // Low-band compensation scalar + float bandCompHigh; // High-band compensation scalar + float frameSilenceGate; // Gate entire frame if below this after norm + float normNoiseGate; // Gate individual bands below this after norm + float envFloorFromNoise; // Multiplier on mean noise floor to cap normalization + float frameSNRGate; // Gate frame if raw SNR (max/noiseMax) is below this + float postScale; // Post-scale for normalized band values (applied before clamp) + float compressGamma; // Exponent for compression on normalized v (e.g., 1/3=cuberoot, 1/6=sqrt(cuberoot)) + float quietEnvFloorGate; // Gate entire frame if envFloor (noiseMean*envFloorFromNoise) < this (0 disables) }; -#if !ENABLE_AUDIO -class SoundAnalyzer : public AudioVariables // Non-audio case. Inherits only the AudioVariables so that any project can -{ // reference them in g_Analyzer +// Mesmerizer (default) tuning +static constexpr AudioInputParams kParamsMesmerizer{ + 4.0f, // windowPowerCorrection + 0.02f, // energyNoiseAdapt + 0.98f, // energyNoiseDecay + 0.25f, // energySmoothAlpha + 0.99f, // energyEnvDecay + 0.000001f, // energyMinEnv + 0.25f, // bandCompLow + 10.0f, // bandCompHigh + 0.00f, // frameSilenceGate + 0.00f, // normNoiseGate + 3.0f, // envFloorFromNoise (cap auto-gain at ~1/4 of pure-noise) + 0.0f, // frameSNRGate (require ~3:1 SNR to show frame) + 1.0f, // postScale (Mesmerizer default) + 0.333333f, // compressGamma (cube root) + 30000000 // quietEnvFloorGate }; -#else // Audio case +// PC Remote uses Mesmerizer defaults +static constexpr AudioInputParams kParamsPCRemote = kParamsMesmerizer; -void IRAM_ATTR AudioSamplerTaskEntry(void *); -void IRAM_ATTR AudioSerialTaskEntry(void *); +// M5 variants use a higher postScale by default +static constexpr AudioInputParams kParamsM5{ + 4.0f, 0.02f, 0.98f, 0.25f, 0.99f, 0.000001f, 0.25f, 10.0f, 0.00f, 0.00f, 3.0f, 0.0f, 1.5f, + 0.16666667f, + 30000000.0f +}; +static constexpr AudioInputParams kParamsM5Plus2{ + 4.0f, 0.02f, 0.98f, 0.25f, 0.99f, 0.000001f, 0.25f, 10.0f, 0.00f, 0.00f, 3.0f, 0.0f, 1.5f, + 0.16666667f, + 30000000.0f +}; -// PeakData class +// PeakData // -// Simple data class that holds the music peaks for up to 32 bands. When the sound analyzer finishes a pass, its -// results are simplified down to this small class of band peaks. +// Simple data class that holds the music peaks for up to NUM_BANDS bands. Moved above +// the ENABLE_AUDIO conditional so both stub and full implementations can expose +// a consistent interface returning PeakData. #ifndef MIN_VU -#define MIN_VU 2 // Minimum VU value to use for the span when computing VURatio. Contributes to -#endif // how dynamic the music is (smaller values == more dynamic) - - -#ifndef GAINDAMPEN - #define GAINDAMPEN 10 // How slowly brackets narrow in for spectrum bands + #define MIN_VU 0.05f #endif #ifndef VUDAMPEN - #define VUDAMPEN 0 // How slowly VU reacts + #define VUDAMPEN 0 #endif -#define VUDAMPENMIN 1 // How slowly VU min creeps up to test noise floor -#define VUDAMPENMAX 1 // How slowly VU max drops down to test noise ceiling +#define VUDAMPENMIN 1 +#define VUDAMPENMAX 1 -// PeakData -// -// Keeps track of a set of peaks for a sample pass +#ifndef NUM_BANDS +#define NUM_BANDS 1 +#endif class PeakData { + public: + float _Level[NUM_BANDS]; -public: - - double _Level[NUM_BANDS]; - -public: typedef enum { MESMERIZERMIC, @@ -113,205 +142,279 @@ class PeakData PeakData() { - // BUGBUG (davepl) consider std::fill - for (auto & i: _Level) - i = 0.0f; - // RAIDRAID(robertl): Isn't that just - // std::fill(_Level.begin(), _Level.end(), 0.0f); + std::fill_n(_Level, NUM_BANDS, 0.0f); } - - explicit PeakData(double *pDoubles) + explicit PeakData(const float *pFloats) { - SetData(pDoubles); + SetData(pFloats); } - PeakData &operator=(const PeakData &other) { if (this == &other) return *this; - for (int i = 0; i < NUM_BANDS; i++) - _Level[i] = other._Level[i]; + std::copy(other._Level, other._Level + NUM_BANDS, _Level); return *this; } - - float operator[](std::size_t n) const + float operator[](std::size_t n) const { return _Level[n]; } + void SetData(const float *pFloats) { - return _Level[n]; + std::copy(pFloats, pFloats + NUM_BANDS, _Level); } +}; - static float GetBandScalar(MicrophoneType mic, int i) +// Return parameter set for a given microphone source. +static inline const AudioInputParams &ParamsFor(PeakData::MicrophoneType t) +{ + switch (t) { - switch (mic) - { - case MESMERIZERMIC: - { - static constexpr std::array Scalars16 = {0.4, .5, 0.75, 1.0, 0.6, 0.6, 0.8, 0.8, 1.2, 1.5, 3.0, 3.0, 3.0, 3.0, 3.5, 2.5}; // {0.08, 0.12, 0.3, 0.35, 0.35, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.4, 1.4, 1.0, 1.0, 1.0}; - float result = (NUM_BANDS == 16) ? Scalars16[i] : 1.0; - return result; - } - case PCREMOTE: - { - - static constexpr std::array Scalars16 = {1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0}; - float result = (NUM_BANDS == 16) ? Scalars16[i] : 1.0; - return result; - } - case M5PLUS2: - { - static constexpr std::array Scalars16 = {0.5, 1.0, 2.5, 2.2, 1.5, 2.0, 2.0, 2.0, 1.5, 1.5, 1.5, 1.5, 1.0, 0.8, 1.0, 1.0}; - float result = (NUM_BANDS == 16) ? Scalars16[i] : 1.0; - return result; - } - default: - { - static constexpr std::array Scalars16 = {0.5, .5, 0.8, 1.0, 1.5, 1.2, 1.5, 1.6, 2.0, 2.0, 2.0, 3.0, 3.0, 3.0, 5.0, 2.5}; - float result = (NUM_BANDS == 16) ? Scalars16[i] : 1.0; - return result; - } - } + case PeakData::PCREMOTE: return kParamsPCRemote; + case PeakData::M5: return kParamsM5; + case PeakData::M5PLUS2: return kParamsM5Plus2; + case PeakData::MESMERIZERMIC: + default: return kParamsMesmerizer; } +} + +// Interface for SoundAnalyzer (audio and non-audio variants) +class ISoundAnalyzer +{ + public: + virtual ~ISoundAnalyzer() = default; + virtual float VURatio() const = 0; + virtual float VURatioFade() const = 0; + virtual float VU() const = 0; + virtual float PeakVU() const = 0; + virtual float MinVU() const = 0; + virtual int AudioFPS() const = 0; + virtual int SerialFPS() const = 0; + virtual PeakData::MicrophoneType MicMode() const = 0; + virtual const PeakData &Peaks() const = 0; + virtual float Peak2Decay(int band) const = 0; + virtual const float *Peak2DecayData() const = 0; + virtual const float *Peak1DecayData() const = 0; + virtual float Peak1Decay(int band) const = 0; + virtual const unsigned long *Peak1Times() const = 0; + virtual unsigned long LastPeak1Time(int band) const = 0; + virtual void SetPeakDecayRates(float r1, float r2) = 0; +}; + +#if !ENABLE_AUDIO + +#ifndef NUM_BANDS +#define NUM_BANDS 1 +#endif - void ApplyScalars(MicrophoneType mic) +class SoundAnalyzer : public ISoundAnalyzer // Non-audio case stub +{ + PeakData _emptyPeaks; // zero-initialized + public: + float VURatio() const override { - for (int i = 0; i < NUM_BANDS; i++) - _Level[i] *= GetBandScalar(mic, i); + return 0.0f; } - - void SetData(const double * pDoubles) + float VURatioFade() const override + { + return 0.0f; + } + float VU() const override + { + return 0.0f; + } + float PeakVU() const override + { + return 0.0f; + } + float MinVU() const override + { + return 0.0f; + } + int AudioFPS() const override + { + return 0; + } + int SerialFPS() const override + { + return 0; + } + PeakData::MicrophoneType MicMode() const override + { + return PeakData::MESMERIZERMIC; + } + const PeakData &Peaks() const override + { + return _emptyPeaks; + } + float Peak2Decay(int) const override + { + return 0.0f; + } + const float *Peak2DecayData() const override + { + static float zeros[NUM_BANDS] = {0}; + return zeros; + } + const float *Peak1DecayData() const override + { + static float zeros[NUM_BANDS] = {0}; + return zeros; + } + float Peak1Decay(int) const override + { + return 0.0f; + } + const unsigned long *Peak1Times() const override + { + static unsigned long zeros[NUM_BANDS] = {0}; + return zeros; + } + unsigned long LastPeak1Time(int) const override + { + return 0; + } + void SetPeakDecayRates(float, float) override { - for (int i = 0; i < NUM_BANDS; i++) - _Level[i] = pDoubles[i]; } }; +#else // Audio case + +void IRAM_ATTR AudioSamplerTaskEntry(void *); +void IRAM_ATTR AudioSerialTaskEntry(void *); + // SoundAnalyzer // // The SoundAnalyzer class uses I2S to read samples from the microphone and then runs an FFT on the // results to generate the peaks in each band, as well as tracking an overall VU and VU ratio, the // latter being the ratio of the current VU to the trailing min and max VU. -class SoundAnalyzer : public AudioVariables +class SoundAnalyzer : public ISoundAnalyzer { - static constexpr size_t MAX_SAMPLES = 256; - std::unique_ptr ptrSampleBuffer; - - // I'm old enough I can only hear up to about 12K, but feel free to adjust. Remember from - // school that you need to sample at double the frequency you want to process, so 24000 is 12K - - static constexpr size_t SAMPLING_FREQUENCY = 20000; + // Give internal audio task functions access to private members + friend void IRAM_ATTR AudioSamplerTaskEntry(void *); + friend void IRAM_ATTR AudioSerialTaskEntry(void *); + + float _VURatio = 1.0f; + float _VURatioFade = 1.0f; + float _VU = 0.0f; + float _PeakVU = 0.0f; + float _MinVU = 0.0f; + unsigned long _cSamples = 0U; + int _AudioFPS = 0; + int _serialFPS = 0; + uint _msLastRemote = 0; + + // I'm old enough I can only hear up to about 12000Hz, but feel free to adjust. Remember from + // school that you need to sample at double the frequency you want to process, so 24000 samples is 12000Hz + + static constexpr size_t SAMPLING_FREQUENCY = 24000; static constexpr size_t LOWEST_FREQ = 100; static constexpr size_t HIGHEST_FREQ = SAMPLING_FREQUENCY / 2; - static constexpr size_t _sampling_period_us = PERIOD_FROM_FREQ(SAMPLING_FREQUENCY); + static inline size_t SamplingPeriodMicros() + { + return (size_t)PERIOD_FROM_FREQ(SAMPLING_FREQUENCY); + } - int _cutOffsBand[NUM_BANDS]; // The upper frequency for each band - float _oldVU; // Old VU value for damping - float _oldPeakVU; // Old peak VU value for damping - float _oldMinVU; // Old min VU value for damping - double * _vPeaks; // The peak value for each band + size_t _clipCount = 0; // number of clipped samples seen + float _dcOffsetEMA = 0.0f; // exponential moving average of DC offset (abs) + float _envEMA = 0.0f; // EMA of _energyMaxEnv + size_t _frameGateHits = 0; // frames gated by params.frameSilenceGate + size_t _bandGateHits = 0; // total band gate hits + size_t _framesProcessed = 0; // total processed frames + + float _oldVU; // Old VU value for damping + float _oldPeakVU; // Old peak VU value for damping + float _oldMinVU; // Old min VU value for damping + float * _vPeaks; // Normalized band energies 0..1 + float * _livePeaks; // Attack-limited display peaks per band + int _bandBinStart[NUM_BANDS]; + int _bandBinEnd[NUM_BANDS]; + float _energyMaxEnv = 1.0f; // adaptive envelope for autoscaling + float _noiseFloor[NUM_BANDS] = {0}; // adaptive per-band noise floor + float _rawPrev[NUM_BANDS] = {0}; // previous raw (noise-subtracted) power for smoothing + + // Peak decay internals (private) + unsigned long _lastPeak1Time[NUM_BANDS] = {0}; + float _peak1Decay[NUM_BANDS] = {0}; + float _peak2Decay[NUM_BANDS] = {0}; + float _peak1DecayRate = 1.25f; + float _peak2DecayRate = 1.25f; PeakData::MicrophoneType _MicMode; + AudioInputParams _params; // Active tuning params for current mic mode - // GetBandIndex + // Energy spectrum processing (implemented inline below) // - // Given a frequency, returns the index of the band that frequency belongs to - - int GetBandIndex(float frequency) - { - for (int i = 0; i < NUM_BANDS; i++) - if (frequency < _cutOffsBand[i]) - return i; - - // If we never found a band that includes the freq under its limit, it's in the top bar - return NUM_BANDS-1; - } - - // GetBucketFrequency + // Calculate a logarithmic scale for the bands like you would find on a graphic equalizer display // - // Given a bucket index, returns the frequency that bucket represents - - float GetBucketFrequency(int bin_index) - { - float bin_width = SAMPLING_FREQUENCY / (MAX_SAMPLES / 2); - float frequency = bin_width * bin_index; - return frequency; - } - double * _vReal; - double * _vImaginary; + private: + static constexpr int kBandOffset = 1; // number of lowest source bands to skip in layout + float *_vReal = nullptr; + float *_vImaginary = nullptr; + std::unique_ptr ptrSampleBuffer; // sample buffer storage + PeakData _Peaks; // cached last normalized peaks (moved earlier for inline method visibility) - // SampleBuffer::Reset - // - // Resets (clears) everything about the buffer except for the time stamp. + // Reset and clear the FFT buffers void Reset() { - for (int i = 0; i < MAX_SAMPLES; i++) - { - _vReal[i] = 0.0; - _vImaginary[i] = 0.0f; - } - for (int i = 0; i < NUM_BANDS; i++) - _vPeaks[i] = 0; + if (!_vReal) + return; + std::fill_n(_vReal, MAX_SAMPLES, 0.0f); + if (_vImaginary) + std::fill_n(_vImaginary, MAX_SAMPLES, 0.0f); + std::fill_n(_vPeaks, NUM_BANDS, 0.0f); } - // SampleBuffer::FFT - // - // Run the FFT on the sample buffer. When done the first two buckets are VU data and only the first MAX_SAMPLES/2 - // are valid. For each bucket afterward you can call BucketFrequency to find out what freq corresponds to what bucket + // Perform the FFT void FFT() { - ArduinoFFT _FFT(_vReal, _vImaginary, MAX_SAMPLES, SAMPLING_FREQUENCY); + ArduinoFFT _FFT(_vReal, _vImaginary, MAX_SAMPLES, SAMPLING_FREQUENCY); _FFT.dcRemoval(); - _FFT.windowing(FFTWindow::Blackman, FFTDirection::Forward); + _FFT.windowing(FFTWindow::Hann, FFTDirection::Forward); _FFT.compute(FFTDirection::Forward); _FFT.complexToMagnitude(); } - void FillBufferI2S() + // Sample the audio + + void SampleAudio() { constexpr auto bytesExpected = MAX_SAMPLES * sizeof(ptrSampleBuffer[0]); - size_t bytesRead = 0; - - #if USE_M5 - if (M5.Mic.record((int16_t *)ptrSampleBuffer.get(), MAX_SAMPLES, SAMPLING_FREQUENCY, false)) - bytesRead = bytesExpected; - #else - ESP_ERROR_CHECK(i2s_start(I2S_NUM_0)); - ESP_ERROR_CHECK(i2s_read(I2S_NUM_0, (void *) ptrSampleBuffer.get(), bytesExpected, &bytesRead, 100 / portTICK_PERIOD_MS)); - ESP_ERROR_CHECK(i2s_stop(I2S_NUM_0)); - #endif - +#if USE_M5 + if (M5.Mic.record((int16_t *)ptrSampleBuffer.get(), MAX_SAMPLES, SAMPLING_FREQUENCY, false)) + bytesRead = bytesExpected; +#else + ESP_ERROR_CHECK(i2s_start(I2S_NUM_0)); + ESP_ERROR_CHECK( + i2s_read(I2S_NUM_0, (void *)ptrSampleBuffer.get(), bytesExpected, &bytesRead, 100 / portTICK_PERIOD_MS)); + ESP_ERROR_CHECK(i2s_stop(I2S_NUM_0)); +#endif if (bytesRead != bytesExpected) { debugW("Could only read %u bytes of %u in FillBufferI2S()\n", bytesRead, bytesExpected); return; } - + int16_t smin = INT16_MAX, smax = INT16_MIN; + long sum = 0; for (int i = 0; i < MAX_SAMPLES; i++) { - _vReal[i] = ptrSampleBuffer[i]; + int16_t s = ptrSampleBuffer[i]; + _vReal[i] = (float)s; + if (s < smin) smin = s; + if (s > smax) smax = s; + sum += s; } + if (smin <= -32768 || smax >= 32767) + _clipCount++; + float dc = fabsf((float)sum / (float)MAX_SAMPLES); + _dcOffsetEMA = _dcOffsetEMA * 0.95f + dc * 0.05f; // slow EMA } - // UpdateVU - // - // This function is responsible for updating the Volume Unit (VU) values: the current VU (_VU), - // the peak VU (_PeakVU), and the minimum VU (_MinVU). - // - // Firstly, it updates the current VU (_VU) based on the new incoming value (newval). - // If the new value is greater than the old VU (_oldVU), it assigns the new value to _VU. - // Otherwise, it applies a damping calculation that drifts _VU towards the new value. - // - // Then, it updates the peak VU (_PeakVU) by checking if the current VU (_VU) has exceeded the previous peak. - // If it has, it updates _PeakVU to the current VU. - // Otherwise, it applies a damping calculation that drifts the peak VU towards the current VU. - // - // Lastly, it updates the minimum VU (_MinVU) by checking if the current VU has dipped below the previous minimum. - // If it has, it updates _MinVU to the current VU. - // Otherwise, it applies another damping calculation that drifts the minimum VU towards the current VU. + // Update the VU and peak values based on the new sample void UpdateVU(float newval) { @@ -319,19 +422,12 @@ class SoundAnalyzer : public AudioVariables _VU = newval; else _VU = (_oldVU * VUDAMPEN + newval) / (VUDAMPEN + 1); - _oldVU = _VU; - - // If we crest above the max VU, update the max VU up to that. Otherwise drift it towards the new value. - if (_VU > _PeakVU) _PeakVU = _VU; else _PeakVU = (_oldPeakVU * VUDAMPENMAX + _VU) / (VUDAMPENMAX + 1); _oldPeakVU = _PeakVU; - - // If we dip below the min VU, update the min VU down to that. Otherwise drift it towards the new value. - if (_VU < _MinVU) _MinVU = _VU; else @@ -339,178 +435,366 @@ class SoundAnalyzer : public AudioVariables _oldMinVU = _MinVU; } - // SampleBuffer::ProcessPeaks - // - // Runs through and figures out what the peak level is in each of the bands. Also calculates - // the overall VU level and adjusts the auto gain. - - PeakData ProcessPeaks() - { - // Find the peak and the average - - double averageSum = 0.0f; + // Compute the band layout based on the sampling frequency and number of bands - for (int i = 0; i < NUM_BANDS; i++) - _vPeaks[i] = 0.0f; + // This computes the start and end bins for each band based on the sampling frequency, + // ensuring that the bands are spaced logarithmically or in Mel scale as configured. + // The results are stored in _bandBinStart and _bandBinEnd arrays. - for (int i = 2; i < MAX_SAMPLES / 2; i++) + void ComputeBandLayout() + { + const float fMin = LOWEST_FREQ; + const float fMax = std::min(HIGHEST_FREQ, SAMPLING_FREQUENCY / 2.0f); + const float binWidth = (float)SAMPLING_FREQUENCY / (MAX_SAMPLES / 2.0f); + int prevBin = 2; +#if SPECTRUM_BAND_SCALE_MEL + auto hzToMel = [](float f) { return 2595.0f * log10f(1.0f + f / 700.0f); }; + auto melToHz = [](float m) { return 700.0f * (powf(10.0f, m / 2595.0f) - 1.0f); }; + float melMin = hzToMel(fMin); + float melMax = hzToMel(fMax); +#endif + for (int b = 0; b < NUM_BANDS; b++) { - _vReal[i] = _vReal[i] / MAX_SAMPLES * AUDIO_MIC_SCALAR; - - int freq = GetBucketFrequency(i-2); - if (freq >= LOWEST_FREQ) + // Shift the effective band index by kBandOffset so logical band 0 starts higher + const int logicalIdx = b + kBandOffset; + const float fracHi = (float)(logicalIdx + 1) / (float)(NUM_BANDS + kBandOffset); +#if SPECTRUM_BAND_SCALE_MEL + float edgeMel = melMin + (melMax - melMin) * fracHi; + float edgeHiFreq = melToHz(edgeMel); +#else + float ratio = fMax / fMin; + float edgeHiFreq = fMin * powf(ratio, fracHi); +#endif + int hiBin = (int)lroundf(edgeHiFreq / binWidth); + hiBin = std::clamp(hiBin, prevBin + 1, (int)(MAX_SAMPLES / 2 - 1)); + _bandBinStart[b] = prevBin; + _bandBinEnd[b] = hiBin; + prevBin = hiBin; + } + _bandBinEnd[NUM_BANDS - 1] = (MAX_SAMPLES / 2 - 1); + } + + PeakData ProcessPeaksEnergy() + { + // Band offset handled in ComputeBandLayout so index 0 is the lowest VISIBLE band + float frameMax = 0.0f; + float frameSumRaw = 0.0f; + float noiseSum = 0.0f; + float noiseMaxAll = 0.0f; // Tracks max noise floor across bands + // Display gain and band floor constants + constexpr float kDisplayGain = 1.5f; + constexpr float kBandFloorMin = 0.01f; + constexpr float kBandFloorScale = 0.05f; + + // Remember that a slower attack will yield a smoother display but a lower one, as it's always + // below the actual for some amount of time + + constexpr float kLiveAttackPerSec = 5.0f; + + // Envelope gating factor and max relative EMA + float _gateEnv = 1.0f; + float _vMaxRelEMA = 1.0f; + // Delta time for attack limiting (replace with actual frame time if available) + float dt = 0.016f; + // Allocate _livePeaks if not already done + if (!_livePeaks) { + _livePeaks = (float *)PreferPSRAMAlloc(NUM_BANDS * sizeof(_livePeaks[0])); + for (int i = 0; i < NUM_BANDS; ++i) _livePeaks[i] = 0.0f; + } + _framesProcessed++; + const float binWidth = (float)SAMPLING_FREQUENCY / (MAX_SAMPLES / 2.0f); + for (int b = 0; b < NUM_BANDS; b++) + { + int start = _bandBinStart[b]; + int end = _bandBinEnd[b]; + if (end <= start) { - // Track the average and the peak value + _vPeaks[b] = 0.0f; + continue; + } + float sumPower = 0.0f; + for (int k = start; k < end; k++) + { + float mag = _vReal[k]; + sumPower += mag * mag; + } + int widthBins = end - start; + float avgPower = (widthBins > 0) ? (sumPower / (float)widthBins) : 0.0f; + avgPower *= _params.windowPowerCorrection; + + // Apply frequency-dependent compensation + float compensation = _params.bandCompLow + (_params.bandCompHigh - _params.bandCompLow) * ((float)b / (NUM_BANDS - 1)); + avgPower *= compensation; + + // Track pre-subtraction max for SNR gating + if (avgPower > frameSumRaw) + frameSumRaw = avgPower; + + if (avgPower > _noiseFloor[b]) + _noiseFloor[b] = _noiseFloor[b] * (1.0f - _params.energyNoiseAdapt) + (float)avgPower * _params.energyNoiseAdapt; + else + _noiseFloor[b] *= _params.energyNoiseDecay; + + // Accumulate noise stats + noiseSum += _noiseFloor[b]; + if (_noiseFloor[b] > noiseMaxAll) + noiseMaxAll = _noiseFloor[b]; + + float signal = (float)avgPower - _noiseFloor[b]; + if (signal < 0.0f) + signal = 0.0f; +#if ENABLE_AUDIO_SMOOTHING + // Weighted average: 0.25 * (2 * current + left + right) + float left = (b > 0) ? _rawPrev[b - 1] : signal; + float right = (b < NUM_BANDS - 1) ? _rawPrev[b + 1] : signal; + float smoothed = 0.125f * (6.0f * signal + left + right); + _rawPrev[b] = smoothed; + _vPeaks[b] = smoothed; + if (smoothed > frameMax) + frameMax = smoothed; +#else + _vPeaks[b] = signal; + if (signal > frameMax) + frameMax = signal; +#endif + } + if (frameMax > _energyMaxEnv) + _energyMaxEnv = frameMax; + else + _energyMaxEnv = std::max(_params.energyMinEnv, _energyMaxEnv * _params.energyEnvDecay); - double vVal = _vReal[i]; - averageSum += vVal; + _envEMA = _envEMA * 0.95f + _energyMaxEnv * 0.05f; - // If it's above the noise floor, figure out which band this belongs to and - // if it's a new peak for that band, record that fact + // Raw SNR-based frame gate (pre-normalization) to suppress steady HVAC and similar backgrounfd noises - int iBand = GetBandIndex(freq); - if (vVal > _vPeaks[iBand]) - _vPeaks[iBand] = _vReal[i]; + float snrRaw = frameSumRaw / (noiseMaxAll + 1e-9f); + if (snrRaw < _params.frameSNRGate) + { + _frameGateHits++; + for (int b = 0; b < NUM_BANDS; ++b) + { + _vPeaks[b] = 0.0f; + _Peaks._Level[b] = 0.0f; } + UpdateVU(0.0f); + return _Peaks; } - averageSum = averageSum / (MAX_SAMPLES / 2 - 2); - // Noise gate - if the signal in this band is below a threshold we define, then we say there's no energy in this band + // Anchor normalization to noise-derived floor to avoid auto-gain blow-up + float noiseMean = noiseSum / (float)NUM_BANDS; + float envFloor = std::max(_params.energyMinEnv, noiseMean * _params.envFloorFromNoise); + float normDen = std::max(_energyMaxEnv, envFloor); + const float invEnv = 1.0f / normDen; + float sumNorm = 0.0f; - for (int i = 0; i < NUM_BANDS; i++) + // Now that layout skips the lowest bins, emit all NUM_BANDS directly with no reindexing + for (int b = 0; b < NUM_BANDS; b++) { - _vPeaks[i] *= PeakData::GetBandScalar(_MicMode, i); - if (_vPeaks[i] < NOISE_CUTOFF) - _vPeaks[i] = 0.0f; + float vTarget = _vPeaks[b] * invEnv; + vTarget = cbrtf(std::max(0.0f, vTarget)); + if (vTarget > 1.0f) vTarget = 1.0f; + vTarget *= _gateEnv; + vTarget *= kDisplayGain; + if (vTarget > 1.0f) vTarget = 1.0f; + const float bandFloor = std::max(kBandFloorMin, kBandFloorScale * _vMaxRelEMA); + if (vTarget < bandFloor) { vTarget = 0.0f; _bandGateHits++; } + float vCurr = _livePeaks[b]; + float vNew = vTarget; + if (vTarget > vCurr) + { + const float maxRise = kLiveAttackPerSec * dt; + vNew = vCurr + std::min(vTarget - vCurr, maxRise); + } + if (vNew > 1.0f) vNew = 1.0f; + if (vNew < 0.0f) vNew = 0.0f; + _livePeaks[b] = vNew; + _vPeaks[b] = vNew; + _Peaks._Level[b] = vNew; + sumNorm += vNew; } + UpdateVU(sumNorm / (float)NUM_BANDS); + return _Peaks; + } - // Print out the low 4 and high 4 bands so we can monitor levels in the debugger if needed - EVERY_N_SECONDS(1) - { - debugV("Raw Peaks: %0.1lf %0.1lf %0.1lf %0.1lf <--> %0.1lf %0.1lf %0.1lf %0.1lf", - _vPeaks[0], _vPeaks[1], _vPeaks[2], _vPeaks[3], _vPeaks[12], _vPeaks[13], _vPeaks[14], _vPeaks[15]); - } - // If you want the peaks to be a lot more prominent, you can exponentially raise the values - // and then they'll be scaled back down linearly, but you'd have to adjust allBandsPeak - // accordingly as well as the value there now is based on no exponential scaling. - // - // - - #if SCALE_AUDIO_EXPONENTIAL - for (int i = 0; i < NUM_BANDS; i++) - _vPeaks[i] = powf(_vPeaks[i], 2.0); - #endif + public: + // Current beat/level ratio value used by visual effects. + // Typically maintained by higher-level audio logic. + // Range ~[0..something], consumer-specific. - double allBandsPeak = 0; - for (int i = 0; i < NUM_BANDS; i++) - allBandsPeak = max(allBandsPeak, _vPeaks[i]); + inline float VURatio() const override + { + return _VURatio; + } - // It's hard to know what to use for a "minimum" volume so I aimed for a light ambient noise background - // just triggering the bottom pixel, and real silence yielding darkness + // Smoothed/decayed version of VURatio for more graceful visuals. + // Use when you want beat emphasis without sharp jumps. - allBandsPeak = std::max((double)NOISE_FLOOR, allBandsPeak); - debugV("All Bands Peak: %lf", allBandsPeak); + inline float VURatioFade() const override + { + return _VURatioFade; + } - // Normalize all the bands relative to allBandsPeak - for (int i = 0; i < NUM_BANDS; i++) - _vPeaks[i] /= allBandsPeak; + // Average normalized energy this frame (0..1 after gating/compression). + // Updated in ProcessPeaksEnergy()/SetPeakData via UpdateVU(). - if (_VURatio < 1.0) + inline float VU() const override + { + return _VU; + } - for (int i = 0; i < NUM_BANDS; i++) - _vPeaks[i] = max(0.0, _vPeaks[i]); + // Highest recent VU observed (peak hold with damping). + // Useful for setting adaptive effect ceilings. - // We'll use the average as the gVU. I assume the average of the samples tracks sound pressure level, but don't really know... + inline float PeakVU() const override + { + return _PeakVU; + } - float newval = averageSum; + // Lowest recent VU observed (floor with damping). + // Useful as denominator clamps for normalized ratios. - debugV("AverageSum : %f", averageSum); - debugV("Newval : %f", newval); + inline float MinVU() const override + { + return _MinVU; + } - UpdateVU(newval); + // Measured audio processing frames-per-second. + // For diagnostics/telemetry; not critical to effects logic. - EVERY_N_MILLISECONDS(100) - { - auto peaks = GetPeakData(); - debugV("Audio Data -- Sum: %0.2f, _MinVU: %0.2f, _PeakVU: %0.2f, _VU: %f, Peak0: %f, Peak1: %f, Peak2: %f, Peak3: %f", averageSum, _MinVU, _PeakVU, _VU, peaks[0], peaks[1], peaks[2], peaks[3]); - } + inline int AudioFPS() const override + { + return _AudioFPS; + } - return PeakData(_vPeaks); + // Measured serial streaming FPS (if enabled). + // For diagnostics; may be zero if not used. + + inline int SerialFPS() const override + { + return _serialFPS; } - // - // Calculate a logarithmic scale for the bands like you would find on a graphic equalizer display - // + // Indicates whether peaks came from local mic or remote source. + // Effects may choose to show status based on this. - void CalculateBandCutoffs(float lowFreq, float highFreq) + inline PeakData::MicrophoneType MicMode() const override { - if (NUM_BANDS == 16) - { - static constexpr std::array cutOffs16Band = - { - 200, 380, 580, 780, 980, 1200, 1600, 1800, 2000, 2412, 3162, 3781, 5312, 6310, 8400, (int)HIGHEST_FREQ - }; + return _MicMode; + } - for (int i = 0; i < NUM_BANDS; i++) - _cutOffsBand[i] = cutOffs16Band[i]; - } - else - { - // Calculate the logarithmic spacing for the frequency bands - float f1 = LOWEST_FREQ; - float f2 = HIGHEST_FREQ; + // Returns the latest per-band normalized peaks (0..1). + // Pointer remains valid until next ProcessPeaksEnergy/SetPeakData. - // Calculate the ratio based on logarithmic scale - float log_f1 = log10(f1); - float log_f2 = log10(f2); - float delta = (log_f2 - log_f1) / (NUM_BANDS - 1); + inline const PeakData &Peaks() const override + { + return _Peaks; + } - for (int i = 0; i < NUM_BANDS; i++) - { - // Calculate the upper frequency for each band - _cutOffsBand[i] = round(pow(10, log_f1 + delta * (i + 1))); - debugV("BAND %d: %d\n", i, _cutOffsBand[i]); - } - } + // Returns the slower-decay overlay level for the given band (0..1). + // Used by some visuals to draw trailing bars/dots. + + inline float Peak2Decay(int band) const override + { + return (band >= 0 && band < NUM_BANDS) ? _peak2Decay[band] : 0.0f; } -public: + // Direct access to the secondary (slower) decay array. + // Array length is NUM_BANDS. + + inline const float *Peak2DecayData() const override + { + return _peak2Decay; + } - PeakData _Peaks; // The peak data for the last sample pass + // Direct access to the primary (faster) decay array. + // Array length is NUM_BANDS. - SoundAnalyzer() + inline const float *Peak1DecayData() const override { - ptrSampleBuffer.reset( (int16_t *)heap_caps_malloc(MAX_SAMPLES * sizeof(int16_t), MALLOC_CAP_8BIT) ); - if (!ptrSampleBuffer) - throw std::runtime_error("Failed to allocate sample buffer"); + return _peak1Decay; + } + + // Returns the faster-decay overlay level for the given band (0..1). + // Band index is clamped by caller; returns 0 on out-of-range. + + inline float Peak1Decay(int band) const override + { + return (band >= 0 && band < NUM_BANDS) ? _peak1Decay[band] : 0.0f; + } + + // Returns timestamps (ms) of last primary-peak rise per band. + // Useful for triggering time-based band effects. + + inline const unsigned long *Peak1Times() const override + { + return _lastPeak1Time; + } + + // Helper to read the last primary-peak time for a specific band. + // Returns 0 if band is out of range. - _vReal = (double *)PreferPSRAMAlloc(MAX_SAMPLES * sizeof(_vReal[0])); - _vImaginary = (double *)PreferPSRAMAlloc(MAX_SAMPLES * sizeof(_vImaginary[0])); - _vPeaks = (double *)PreferPSRAMAlloc(NUM_BANDS * sizeof(_vPeaks[0])); + inline unsigned long LastPeak1Time(int band) const override + { + return (band >= 0 && band < NUM_BANDS) ? _lastPeak1Time[band] : 0; + } + + // Configure how quickly the two decay overlays drop over time. + // r1 = fast track, r2 = slow track; higher = faster decay. + + inline void SetPeakDecayRates(float r1, float r2) override + { + _peak1DecayRate = r1; + _peak2DecayRate = r2; + } - _oldVU = 0.0f; - _oldPeakVU = 0.0f; - _oldMinVU = 0.0f; + // Construct analyzer, allocate buffers (PSRAM-preferred), set initial state. + // Throws std::runtime_error on allocation failure. Computes band layout once. - CalculateBandCutoffs(LOWEST_FREQ, SAMPLING_FREQUENCY / 2.0); + SoundAnalyzer() + { + ptrSampleBuffer.reset((int16_t *)heap_caps_malloc(MAX_SAMPLES * sizeof(int16_t), MALLOC_CAP_8BIT)); + if (!ptrSampleBuffer) + throw std::runtime_error("Failed to allocate sample buffer"); + _vReal = (float *)PreferPSRAMAlloc(MAX_SAMPLES * sizeof(_vReal[0])); + _vImaginary = (float *)PreferPSRAMAlloc(MAX_SAMPLES * sizeof(_vImaginary[0])); + _vPeaks = (float *)PreferPSRAMAlloc(NUM_BANDS * sizeof(_vPeaks[0])); + _livePeaks = (float *)PreferPSRAMAlloc(NUM_BANDS * sizeof(_livePeaks[0])); + if (!_vReal || !_vImaginary || !_vPeaks || !_livePeaks) + throw std::runtime_error("Failed to allocate FFT buffers"); + for (int i = 0; i < NUM_BANDS; ++i) _livePeaks[i] = 0.0f; + _oldVU = _oldPeakVU = _oldMinVU = 0.0f; + _MicMode = PeakData::MESMERIZERMIC; + _params = ParamsFor(_MicMode); + /* ValidateAudioConfig(); */ + ComputeBandLayout(); Reset(); } + // Free any heap/PSRAM buffers allocated by the constructor. + // Safe to call at shutdown/reset. + ~SoundAnalyzer() { free(_vReal); free(_vImaginary); free(_vPeaks); + free(_livePeaks); } // These functions allow access to the last-acquired sample buffer and its size so that // effects can draw the waveform or do other things with the raw audio data - const int16_t * GetSampleBuffer() const + // Return pointer to last captured raw samples (int16). + // Valid until the next FillBufferI2S() call. + + const int16_t *GetSampleBuffer() const { return ptrSampleBuffer.get(); } + // Return count of samples in the sample buffer (MAX_SAMPLES). + // Pairs with GetSampleBuffer() when drawing waveforms. + const size_t GetSampleBufferSize() const { return MAX_SAMPLES; @@ -523,65 +807,77 @@ class SoundAnalyzer : public AudioVariables // made up of the VURatioFade multiplier. So passing a 0.75 is a lot of beat enhancement, whereas // 0.25 is a little bit. + // Compute a blend factor using VURatioFade to "pulse" visuals. + // amt in [0..1] controls how strongly the ratio influences the result. + float BeatEnhance(float amt) { return ((1.0 - amt) + (_VURatioFade / 2.0) * amt); } // flash record size, for recording 5 second + // SampleBufferInitI2S + // + // install and start i2s driver + + // Configure and start the I2S (or M5) input at SAMPLING_FREQUENCY. + // Board-specific branches set pins and ADC/I2S modes as needed. + void SampleBufferInitI2S() { // install and start i2s driver debugV("Begin SamplerBufferInitI2S..."); - #if USE_M5 - +#if USE_M5 // Can't use speaker and mic at the same time, and speaker defaults on, so turn it off M5.Speaker.setVolume(255); M5.Speaker.end(); + auto cfg = M5.Mic.config(); + cfg.sample_rate = SAMPLING_FREQUENCY; + cfg.noise_filter_level = 0; + cfg.magnification = 8; + M5.Mic.config(cfg); M5.Mic.begin(); - #elif ELECROW || USE_I2S_AUDIO - - const i2s_config_t i2s_config = { - .mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX), - .sample_rate = SAMPLING_FREQUENCY, - .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, - .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, - .communication_format = I2S_COMM_FORMAT_STAND_I2S, - .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, - .dma_buf_count = 2, - .dma_buf_len = (int) MAX_SAMPLES, - .use_apll = false - }; - - // i2s pin configuration - const i2s_pin_config_t pin_config = { - .bck_io_num = I2S_BCLK_PIN, - .ws_io_num = I2S_WS_PIN, - .data_out_num = -1, // not used - .data_in_num = INPUT_PIN - }; - - ESP_ERROR_CHECK( i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL) ); - ESP_ERROR_CHECK( i2s_set_pin(I2S_NUM_0, &pin_config) ); - ESP_ERROR_CHECK( i2s_start(I2S_NUM_0) ); - - #else - - // This block is TTGO, MESMERIZER, SPECTRUM_WROVER_KIT and other I2S. - // +#elif ELECROW || USE_I2S_AUDIO + + const i2s_config_t i2s_config = {.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX), + .sample_rate = SAMPLING_FREQUENCY, + .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, + .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, + .communication_format = I2S_COMM_FORMAT_STAND_I2S, + .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, + .dma_buf_count = 2, + .dma_buf_len = (int)MAX_SAMPLES, + .use_apll = false}; + + // i2s pin configuration + const i2s_pin_config_t pin_config = {.bck_io_num = I2S_BCLK_PIN, + .ws_io_num = I2S_WS_PIN, + .data_out_num = -1, // not used + .data_in_num = INPUT_PIN}; + + ESP_ERROR_CHECK(i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL)); + ESP_ERROR_CHECK(i2s_set_pin(I2S_NUM_0, &pin_config)); + ESP_ERROR_CHECK(i2s_start(I2S_NUM_0)); + +#else + + // This block is for TTGO, MESMERIZER, SPECTRUM_WROVER_KIT and other projects that + // use an analog mic connected to the input pin. + + static_assert(SOC_I2S_SUPPORTS_ADC, "This ESP32 model does not support ADC built-in mode"); + i2s_config_t i2s_config; - i2s_config.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX); - // Note: Post IDF4, I2S_MODE_ADC_BUILT_IN is no longer supported - // and it was never available on models after original ESP32-Nothing. - // See: https://github.com/espressif/arduino-esp32/issues/9564 - #if defined(I2S_MODE_ADC_BUILT_IN) - i2s_config.mode |= I2S_MODE_ADC_BUILT_IN; + #if SOC_I2S_SUPPORTS_ADC + i2s_config.mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN); + #else + i2s_config.mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX); #endif + i2s_config.sample_rate = SAMPLING_FREQUENCY; i2s_config.dma_buf_len = MAX_SAMPLES; i2s_config.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT; @@ -596,26 +892,18 @@ class SoundAnalyzer : public AudioVariables ESP_ERROR_CHECK(i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL)); ESP_ERROR_CHECK(i2s_set_adc_mode(ADC_UNIT_1, ADC1_CHANNEL_0)); - #endif +#endif debugV("SamplerBufferInitI2S Complete\n"); } - PeakData::MicrophoneType MicMode() - { - return _MicMode; - } - - unsigned long _lastPeak1Time[NUM_BANDS] = {0}; - float _peak1Decay[NUM_BANDS] = {0}; - float _peak2Decay[NUM_BANDS] = {0}; - float _peak1DecayRate = 1.25f; - float _peak2DecayRate = 1.25f; - // DecayPeaks // // Every so many ms we decay the peaks by a given amount + // Apply time-based decay to the two peak overlay arrays. + // Called once per frame; uses AppTime.LastFrameTime() and configurable rates. + inline void DecayPeaks() { float decayAmount1 = std::max(0.0, g_Values.AppTime.LastFrameTime() * _peak1DecayRate); @@ -627,19 +915,11 @@ class SoundAnalyzer : public AudioVariables _peak2Decay[iBand] -= min(decayAmount2, _peak2Decay[iBand]); } - // Manual smoothing if desired - - #if ENABLE_AUDIO_SMOOTHING - for (int iBand = 1; iBand < NUM_BANDS - 1; iBand += 2) - { - _peak1Decay[iBand] = (_peak1Decay[iBand] * 2 + _peak1Decay[iBand - 1] + _peak1Decay[iBand + 1]) / 4; - _peak2Decay[iBand] = (_peak2Decay[iBand] * 2 + _peak2Decay[iBand - 1] + _peak2Decay[iBand + 1]) / 4; - } - #endif + // Removed old smoothing of decay overlays; smoothing now applies to live peaks in ProcessPeaksEnergy() } - // Update the local band peaks from the global sound data. If we establish a new peak in any band, - // we reset the peak timestamp on that band + // Update the per-band decay overlays from the latest peaks. + // Rises are limited by VU_REACTIVITY_RATIO; records timestamps on new primary peaks. inline void UpdatePeakData() { @@ -647,61 +927,94 @@ class SoundAnalyzer : public AudioVariables { if (_Peaks[i] > _peak1Decay[i]) { - const float maxIncrease = std::max(0.0, g_Values.AppTime.LastFrameTime() * _peak1DecayRate * VU_REACTIVITY_RATIO); + const float maxIncrease = + std::max(0.0, g_Values.AppTime.LastFrameTime() * _peak1DecayRate * VU_REACTIVITY_RATIO); _peak1Decay[i] = std::min(_Peaks[i], _peak1Decay[i] + maxIncrease); _lastPeak1Time[i] = millis(); } if (_Peaks[i] > _peak2Decay[i]) { - const float maxIncrease = std::max(0.0, g_Values.AppTime.LastFrameTime() * _peak2DecayRate * VU_REACTIVITY_RATIO); + const float maxIncrease = + std::max(0.0, g_Values.AppTime.LastFrameTime() * _peak2DecayRate * VU_REACTIVITY_RATIO); _peak2Decay[i] = std::min(_Peaks[i], _peak2Decay[i] + maxIncrease); } } } - inline PeakData GetPeakData() const - { - return _Peaks; - } + // Accept externally provided peaks (e.g., over WiFi) and update internal state. + // Also recomputes VU from the new band values and records source time. inline void SetPeakData(const PeakData &peaks) { - debugV("Manually setting peaks!"); - Serial.print(" #"); _msLastRemote = millis(); _Peaks = peaks; + std::copy(&_Peaks._Level[0], &_Peaks._Level[0] + NUM_BANDS, _vPeaks); + float sum = std::accumulate(_vPeaks, _vPeaks + NUM_BANDS, 0.0f); + UpdateVU(sum / NUM_BANDS); } + // Expose computed band start indices (inclusive) for diagnostics. + // Length is NUM_BANDS; pairs with BandBinEnds(). + + inline const int *BandBinStarts() const + { + return _bandBinStart; + } + + // Expose computed band end indices (exclusive) for diagnostics. + // Length is NUM_BANDS; pairs with BandBinStarts(). + + inline const int *BandBinEnds() const + { + return _bandBinEnd; + } +#if ENABLE_AUDIO_DEBUG + // Print per-band [start-end] bin ranges over Serial for debugging. + // Useful to verify spacing and coverage with current config. + + void DumpBandLayout() const + { + Serial.println("Band layout (start-end):"); + for (int b = 0; b < NUM_BANDS; b++) + { + Serial.print(b); + Serial.print(": "); + Serial.print(_bandBinStart[b]); + Serial.print('-'); + Serial.println(_bandBinEnd[b]); + } + } +#endif + // // RunSamplerPass // + // Perform one audio acquisition/processing step. + // Uses local mic if no recent remote peaks; otherwise trusts remote and only updates VU. + inline void RunSamplerPass() { if (millis() - _msLastRemote > AUDIO_PEAK_REMOTE_TIMEOUT) { - #if M5STICKCPLUS2 - _MicMode = PeakData::M5PLUS2; - #elif USE_M5 - _MicMode = PeakData::M5; - #else - _MicMode = PeakData::MESMERIZERMIC; - #endif - +#if M5STICKCPLUS2 + _MicMode = PeakData::M5PLUS2; +#elif USE_M5 + _MicMode = PeakData::M5; +#else + _MicMode = PeakData::MESMERIZERMIC; +#endif + _params = ParamsFor(_MicMode); Reset(); - FillBufferI2S(); + SampleAudio(); FFT(); - _Peaks = ProcessPeaks(); + ProcessPeaksEnergy(); } else { - // Calculate a total VU from the band data - float sum = 0.0f; - for (int i = 0; i < NUM_BANDS; i++) - sum += _Peaks[i]; - - // Scale it so that it is not always in the top red + float sum = std::accumulate(&_Peaks._Level[0], &_Peaks._Level[0] + NUM_BANDS, 0.0f); _MicMode = PeakData::PCREMOTE; + _params = ParamsFor(_MicMode); UpdateVU(sum / NUM_BANDS); } } diff --git a/install_intelhex.py b/install_intelhex.py new file mode 100644 index 000000000..dcaeb6b74 --- /dev/null +++ b/install_intelhex.py @@ -0,0 +1,6 @@ +Import("env") + +try: + import intelhex +except ImportError: + env.Execute("$PYTHONEXE -m pip install intelhex") \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 0e9671233..429a7a096 100644 --- a/platformio.ini +++ b/platformio.ini @@ -34,6 +34,7 @@ upload_port = monitor_port = build_type = release upload_speed = 2000000 +monitor_speed = 115200 build_flags = -std=gnu++2a -g3 -Ofast @@ -48,23 +49,23 @@ build_src_flags = -Wformat=2 ; Warnings for our code only # format is error number(optional :path_with_wildcards(optional :line_number)) check_flags = cppcheck: --suppress=*:*src/uzlib/src/* -lib_deps = crankyoldgit/IRremoteESP8266 @ ^2.7.20 - fastled/FastLED @ ^3.9.20 - adafruit/Adafruit BusIO @ ^1.9.1 - adafruit/Adafruit GFX Library @ ^1.10.12 - olikraus/U8g2 @ ^2.28.8 - kosme/arduinoFFT @ ^2.0 - esp32async/ESPAsyncWebServer @ ^3.7.0 - bblanchon/ArduinoJson @ ^7.3.0 - thomasfredericks/Bounce2 @ ^2.7.0 +lib_deps = crankyoldgit/IRremoteESP8266 @ ^2.8.6 + fastled/FastLED @ ^3.10.1 + adafruit/Adafruit BusIO @ ^1.17.2 + adafruit/Adafruit GFX Library @ ^1.12.1 + olikraus/U8g2 @ ^2.36.12 + kosme/arduinoFFT @ ^2.0.4 + esp32async/ESPAsyncWebServer @ ^3.7.10 + bblanchon/ArduinoJson @ ^7.4.2 + thomasfredericks/Bounce2 @ ^2.72.0 https://github.com/PlummersSoftwareLLC/RemoteDebug QRCode @ ^0.0.1 - Bodmer/TJpg_Decoder @ ^1.0.8 + Bodmer/TJpg_Decoder @ ^1.1.0 plageoj/UrlEncode @ ^1.0.1 -; This partition table attempts to fit everything in 4M of flash. +; This partition table is the default and fits everything in 4M of flash without OTA. +board_build.partitions = config/partitions_custom_noota.csv -board_build.partitions = config/partitions_custom.csv ; ================================= @@ -85,27 +86,51 @@ build_flags = -D_IR_ENABLE_DEFAULT_=false ; Don't automatically includ ; They extend the "base" config, both in general and in terms of build flags and dependencies, ; where applicable. -[dev_adafruit_feather] -extends = base -board = adafruit_feather_esp32s3_tft -monitor_speed = 1500000 -upload_speed = 1500000 -lib_deps = ${base.lib_deps} - TFT_eSPI - [dev_esp32] extends = base board = esp32dev monitor_speed = 115200 upload_speed = 921600 -[dev_esp32-s3] +[dev_esp32_s3] extends = base board = esp32-s3-devkitc-1 board_build.mcu = esp32s3 board_build.f_cpu = 240000000L monitor_speed = 115200 upload_speed = 1000000 +build_flags = ${base.build_flags} + +[dev_adafruit_feather] +extends = dev_esp32_s3 +board = adafruit_feather_esp32s3_tft +monitor_speed = 115200 +upload_speed = 1500000 +lib_deps = ${dev_esp32_s3.lib_deps} + bodmer/TFT_eSPI @ ^2.5.43 +build_flags = ${dev_esp32_s3.build_flags} + -DUSE_SCREEN=1 + -DUSE_TFTSPI=1 + -DUSER_SETUP_LOADED + -DTOUCH_CS=0 + -DST7789_2_DRIVER + -DTFT_WIDTH=135 + -DTFT_HEIGHT=240 + -DTFT_BL=45 + -DTFT_BACKLIGHT_ON=HIGH + -DTFT_RGB_ORDER=TFT_RGB + -DTFT_CS=7 + -DTFT_DC=39 + -DTFT_RST=40 + -DTFT_MOSI=35 + -DTFT_MISO=37 + -DTFT_SCLK=36 + -DLOAD_GLCD=1 + -DSPI_FREQUENCY=27000000 + -DESP32FEATHERTFT=1 + -DTOGGLE_BUTTON_1=0 + -DUSE_HSPI_PORT=1 + -DNUM_INFO_PAGES=1 [dev_heltec_wifi] extends = base @@ -126,21 +151,25 @@ lib_deps = ${dev_heltec_wifi.lib_deps} heltecautomation/Heltec ESP32 Dev-Boards @ ^1.1.1 [dev_heltec_wifi_v3] -extends = dev_heltec_wifi +extends = dev_esp32_s3 board = heltec_wifi_kit_32_V3 -build_flags = -DARDUINO_HELTEC_WIFI_KIT_32_V3=1 +build_flags = -DUSE_SCREEN=1 + -DARDUINO_HELTEC_WIFI_KIT_32=1 + -DARDUINO_HELTEC_WIFI_KIT_32_V3=1 -DUSE_SSD1306=1 - ${dev_heltec_wifi.build_flags} -lib_deps = ${dev_heltec_wifi.lib_deps} + ${dev_esp32_s3.build_flags} +lib_deps = ${dev_esp32_s3.lib_deps} heltecautomation/Heltec ESP32 Dev-Boards @ ^1.1.1 [dev_heltec_wifi_lora_v3] -extends = dev_heltec_wifi +extends = dev_esp32_s3 board = heltec_wifi_lora_32_V3 -build_flags = -DARDUINO_HELTEC_WIFI_LORA_32_V3=1 +build_flags = -DUSE_SCREEN=1 + -DARDUINO_HELTEC_WIFI_KIT_32=1 + -DARDUINO_HELTEC_WIFI_LORA_32_V3=1 -DUSE_SSD1306=1 - ${dev_heltec_wifi.build_flags} -lib_deps = ${dev_heltec_wifi.lib_deps} + ${dev_esp32_s3.build_flags} +lib_deps = ${dev_esp32_s3.lib_deps} heltecautomation/Heltec ESP32 Dev-Boards @ ^1.1.1 [dev_lolin_d32_pro] @@ -156,21 +185,21 @@ upload_speed = 1500000 build_flags = -DUSE_SCREEN=1 ${base.build_flags} lib_deps = ${base.lib_deps} - m5stack/M5Unified @ ^0.1.17 + m5stack/M5Unified @ ^0.2.7 -[dev_m5stick-c] +[dev_m5stick_c] extends = dev_m5 board = m5stick-c build_flags = -DM5STICKC=1 ${dev_m5.build_flags} -[dev_m5stick-c-plus] +[dev_m5stick_c_plus] extends = dev_m5 board = m5stick-c ; Requires the M5StickC Plus (note the Plus) build_flags = -DM5STICKCPLUS=1 ${dev_m5.build_flags} -[dev_m5stick-c-plus2] +[dev_m5stick_c_plus2] extends = dev_m5 board = m5stick-c ; Requires the M5StickC Plus2 (note the Plus2) upload_speed = 2000000 @@ -191,13 +220,13 @@ board_build.partitions = config/partitions_custom_8M.csv board_upload.flash_size = 8MB [dev_elecrow_mesmerizer] -extends = dev_esp32-s3 +extends = dev_esp32_s3 board = esp32-s3-devkitc-1 monitor_speed = 115200 upload_speed = 1500000 -lib_deps = ${dev_esp32-s3.lib_deps} - lovyan03/LovyanGFX@^1.1.7 -build_flags = ${dev_esp32-s3.build_flags} +lib_deps = ${dev_esp32_s3.lib_deps} + lovyan03/LovyanGFX @ ^1.1.7 +build_flags = ${dev_esp32_s3.build_flags} [dev_tinypico] extends = base @@ -211,31 +240,33 @@ board = esp-wrover-kit monitor_speed = 115200 upload_speed = 1500000 lib_deps = ${base.lib_deps} - adafruit/Adafruit ILI9341 @ ^1.5.10 + adafruit/Adafruit ILI9341 @ ^1.5.10 [dev_mesmerizer] extends = dev_wrover +board = esp-wrover-kit upload_speed = 2000000 +monitor_speed = 115200 board_build.partitions = config/partitions_custom_8M.csv board_upload.flash_size = 8MB -[dev_lilygo-tdisplay-s3] -extends = dev_esp32-s3 +[dev_lilygo_tdisplay_s3] +extends = dev_esp32_s3 +board = esp32-s3-devkitc-1 build_flags = -DLILYGOTDISPLAYS3=1 -DPIN_SDA=21 -DTOGGLE_BUTTON_1=0 -DTOGGLE_BUTTON_2=14 - ${dev_esp32-s3.build_flags} + ${dev_esp32_s3.build_flags} [dev_lilygo_amoled] -extends = base +extends = dev_esp32_s3 board = lilygo-t-amoled monitor_speed = 115200 upload_speed = 1500000 -board_build.partitions = config/partitions_custom_noota.csv -build_flags = ${base.build_flags} +build_flags = ${dev_esp32_s3.build_flags} -DLILYGO_TDISPLAY_AMOLED_SERIES=1 -DARDUINO_USB_CDC_ON_BOOT=1 -DAMOLED_S3=1 @@ -247,11 +278,11 @@ build_flags = ${base.build_flags} -DLV_CONF_PATH="../../../../include/amoled/lv_conf.h" -DLV_CONF_INCLUDE_SIMPLE ${psram_flags.build_flags} - ${unity_double_flags.build_flags} -lib_deps = lewisxhe/SensorLib @ ^0.1.4 - lewisxhe/XPowersLib @ ^0.2.1 - https://github.com/lvgl/lvgl.git#release/v8.3 - ${base.lib_deps} +lib_deps = ${dev_esp32_s3.lib_deps} + lewisxhe/SensorLib@^0.1.4 + lewisxhe/XPowersLib@^0.2.1 + https://github.com/lvgl/lvgl.git#release/v8.3 + ${base.lib_deps} ; =================== ; Capability sections @@ -272,25 +303,21 @@ build_flags = -DUSE_PSRAM=1 ; Libs needed to link with a TTGO Module [ttgo_deps] lib_deps = https://github.com/Xinyuan-LilyGO/TTGO-T-Display - TFT_eSPI - -; Build flags for enabling high precision Unity double -[unity_double_flags] -build_flags = -DUNITY_INCLUDE_DOUBLE - -DUNITY_DOUBLE_PRECISION=1e-12 ; Make doubles real 8 byte doubles + bodmer/TFT_eSPI @ ^2.5.43 ; ================================================================ ; Basic environment section, automatically inherited by all others ; [env] -platform = platformio/espressif32@^6.3.0 +platform = platformio/espressif32 @ ^6.12.0 framework = arduino build_type = release build_unflags = -std=gnu++11 lib_extra_dirs = ${PROJECT_DIR}/lib monitor_filters = esp32_exception_decoder -extra_scripts = pre:tools/bake_site.py +extra_scripts = pre:install_intelhex.py + pre:tools/bake_site.py post:tools/merge_image.py board_build.embed_files = site/dist/index.html.gz site/dist/index.js.gz @@ -302,15 +329,35 @@ board_build.embed_txtfiles = config/timezones.json ; [mesmerizer_config] -build_flags = -DMESMERIZER=1 +build_flags = -DPROJECT_NAME="\"Mesmerizer\"" + -DMESMERIZER=1 -DUSE_HUB75=1 -DSHOW_VU_METER=1 + -DEFFECTS_FULL=1 + -DCOLOR_ORDER=EOrder::RGB + -DENABLE_WIFI=1 + -DINCOMING_WIFI_ENABLED=1 + -DWAIT_FOR_WIFI=0 + -DTIME_BEFORE_LOCAL=2 + -DENABLE_WEBSERVER=1 + -DENABLE_NTP=1 + -DENABLE_OTA=1 + -DENABLE_REMOTE=1 + -DENABLE_AUDIO=1 + -DMILLIS_PER_FRAME=0 + -DMATRIX_WIDTH=64 + -DMATRIX_HEIGHT=32 + -DNUM_BANDS=16 + -DIR_REMOTE_PIN=39 + -DINPUT_PIN=36 + -DTOGGLE_BUTTON_1=0 ${psram_flags.build_flags} ${remote_flags.build_flags} lib_deps = https://github.com/PlummersSoftwareLLC/SmartMatrix.git https://github.com/PlummersSoftwareLLC/GifDecoder.git bitbank2/AnimatedGIF @ ^1.4.7 + ${base.lib_deps} board_build.embed_files = assets/bmp/brokenclouds.jpg assets/bmp/brokencloudsnight.jpg @@ -341,52 +388,127 @@ board_build.embed_files = assets/bmp/brokenclouds.jpg assets/gif/nyancat.gif ${env.board_build.embed_files} -; ==================== -; Project environments +; ===================== +; Base Board Environments - REMOVED ; -; These are the actual environments defined in this file. Each configures one project for one -; specific device. They extend the "dev_" config for the device in question, both in general -; and in terms of build flags and dependencies, where applicable. +; These intermediate sections have been eliminated. Device sections now include +; their board definitions, and project environments extend dev_* sections directly. + +; ==================== +; Project Environments ; -; Note: when adding a new environment to the list, don't forget to explicitly add the device -; section's build_flags and lib_deps options (using ${dev_.