Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions include/deviceconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

#include "globals.h"

#include <array>
#include <tuple>
#include <vector>

Expand Down Expand Up @@ -124,6 +125,39 @@

class DeviceConfig : public IJSONSerializable
{
public:
enum class OutputDriver : uint8_t
{
WS281x,
HUB75
};

struct RuntimeTopology
{
uint16_t width = MATRIX_WIDTH;
Comment thread
davepl marked this conversation as resolved.
uint16_t height = MATRIX_HEIGHT;
bool serpentine = true;
Comment thread
davepl marked this conversation as resolved.
};

struct RuntimeOutputs
{
OutputDriver driver =
#if USE_HUB75
OutputDriver::HUB75;
#else
OutputDriver::WS281x;
#endif
size_t channelCount = NUM_CHANNELS;
std::array<int8_t, NUM_CHANNELS> outputPins{};
};

struct RuntimeConfig
{
RuntimeTopology topology;
RuntimeOutputs outputs;
};

private:
// Add variables for additional settings to this list
String hostname = cszHostname;
String location = cszLocation;
Expand All @@ -141,12 +175,20 @@ class DeviceConfig : public IJSONSerializable
CRGB globalColor = CRGB::Red;
bool applyGlobalColors = false;
CRGB secondColor = CRGB::Red;
int8_t audioInputPin = AUDIO_INPUT_PIN;
RuntimeTopology runtimeTopology = {};
RuntimeOutputs runtimeOutputs = {};

std::vector<SettingSpec, psram_allocator<SettingSpec>> settingSpecs;
std::vector<std::reference_wrapper<SettingSpec>> settingSpecReferences;
size_t writerIndex;

void SaveToJSON() const;
bool SetTimeZoneInternal(const String& newTimeZone, bool skipWrite);
static std::array<int8_t, NUM_CHANNELS> GetCompiledWS281xPins();
static const char* DriverName(OutputDriver driver);
static bool IsHub75Build();
void LogRuntimeConfig(const char* reason) const;

template <typename T>
void SetAndSave(T& target, const T& source)
Expand Down Expand Up @@ -191,6 +233,13 @@ class DeviceConfig : public IJSONSerializable
static constexpr const char * GlobalColorTag = NAME_OF(globalColor);
static constexpr const char * ApplyGlobalColorsTag = NAME_OF(applyGlobalColors);
static constexpr const char * SecondColorTag = NAME_OF(secondColor);
static constexpr const char * MatrixWidthTag = "matrixWidth";
Comment thread
davepl marked this conversation as resolved.
static constexpr const char * MatrixHeightTag = "matrixHeight";
static constexpr const char * MatrixSerpentineTag = "matrixSerpentine";
static constexpr const char * OutputDriverTag = "outputDriver";
static constexpr const char * WS281xChannelCountTag = "ws281xChannelCount";
static constexpr const char * WS281xPinsTag = "ws281xPins";
static constexpr const char * AudioInputPinTag = "audioInputPin";

DeviceConfig();

Expand Down Expand Up @@ -236,13 +285,15 @@ class DeviceConfig : public IJSONSerializable
void SetRememberCurrentEffect(bool newRememberCurrentEffect);

uint8_t GetBrightness() const { return brightness; }
static ValidateResponse ValidateBrightness(int newBrightness);
static ValidateResponse ValidateBrightness(const String& newBrightness);
void SetBrightness(int newBrightness);

bool ShowVUMeter() const { return showVUMeter; }
void SetShowVUMeter(bool newShowVUMeter);

int GetPowerLimit() const { return powerLimit; }
Comment thread
davepl marked this conversation as resolved.
static ValidateResponse ValidatePowerLimit(int newPowerLimit);
static ValidateResponse ValidatePowerLimit(const String& newPowerLimit);
void SetPowerLimit(int newPowerLimit);

Expand All @@ -257,4 +308,73 @@ class DeviceConfig : public IJSONSerializable

void SetColorSettings(const CRGB& globalColor, const CRGB& secondColor);
void ApplyColorSettings(std::optional<CRGB> globalColor, std::optional<CRGB> secondColor, bool clearGlobalColor, bool applyGlobalColor);

static constexpr uint16_t GetCompiledMatrixWidth() { return MATRIX_WIDTH; }
Comment thread
davepl marked this conversation as resolved.
static constexpr uint16_t GetCompiledMatrixHeight() { return MATRIX_HEIGHT; }
static constexpr size_t GetCompiledLEDCount() { return NUM_LEDS; }
static constexpr size_t GetCompiledChannelCount() { return NUM_CHANNELS; }
static constexpr int GetCompiledAudioInputPin() { return AUDIO_INPUT_PIN; }
static std::array<int8_t, NUM_CHANNELS> GetCompiledPins() { return GetCompiledWS281xPins(); }
static OutputDriver GetCompiledOutputDriver()
{
#if USE_HUB75
return OutputDriver::HUB75;
#else
return OutputDriver::WS281x;
#endif
}

RuntimeConfig GetRuntimeConfig() const { return RuntimeConfig{runtimeTopology, runtimeOutputs}; }
const RuntimeTopology& GetTopology() const { return runtimeTopology; }
const RuntimeOutputs& GetOutputs() const { return runtimeOutputs; }
uint16_t GetMatrixWidth() const { return runtimeTopology.width; }
uint16_t GetMatrixHeight() const { return runtimeTopology.height; }
bool IsMatrixSerpentine() const { return runtimeTopology.serpentine; }
size_t GetActiveLEDCount() const { return static_cast<size_t>(runtimeTopology.width) * runtimeTopology.height; }
int GetAudioInputPin() const { return audioInputPin; }
OutputDriver GetOutputDriver() const { return runtimeOutputs.driver; }
size_t GetChannelCount() const { return runtimeOutputs.channelCount; }
const std::array<int8_t, NUM_CHANNELS>& GetWS281xPins() const { return runtimeOutputs.outputPins; }
// When runtime output settings still match the compiled WS281x transport, we can keep using the
// long-proven FastLED controller path and reserve the new runtime manager for true pin/count changes.
bool UsesCompiledWS281xTransport() const
{
return runtimeOutputs.driver == GetCompiledOutputDriver()
&& runtimeOutputs.channelCount == GetCompiledChannelCount()
&& runtimeOutputs.outputPins == GetCompiledPins();
}
bool SupportsLiveTopology() const { return !IsHub75Build() && runtimeOutputs.driver == OutputDriver::WS281x; }
bool SupportsLiveOutputReconfigure() const { return !IsHub75Build() && runtimeOutputs.driver == OutputDriver::WS281x; }
bool SupportsConfigurableAudioInputPin() const
{
#if ENABLE_AUDIO && !USE_M5 && (USE_I2S_AUDIO || ELECROW)
return true;
#else
return false;
#endif
}
bool SupportsLiveAudioInputReconfigure() const { return false; }
String GetAudioInputModeName() const
{
#if !ENABLE_AUDIO
return "disabled";
#elif USE_M5
return "m5_internal";
#elif USE_I2S_AUDIO || ELECROW
return "i2s";
#else
return "adc_fixed";
#endif
}
bool RequiresRecompileForCurrentRuntimeConfig() const { return runtimeOutputs.driver != GetCompiledOutputDriver(); }
String GetCompiledDriverName() const { return DriverName(GetCompiledOutputDriver()); }
String GetRuntimeDriverName() const { return DriverName(runtimeOutputs.driver); }

ValidateResponse ValidateAudioInputPin(int pin) const;
ValidateResponse ValidateTopology(uint16_t width, uint16_t height, bool serpentine) const;
ValidateResponse ValidateOutputDriver(OutputDriver driver) const;
ValidateResponse ValidateWS281xSettings(size_t channelCount, const std::array<int8_t, NUM_CHANNELS>& pins) const;
ValidateResponse ValidateRuntimeConfig(const RuntimeConfig& config) const;
bool SetRuntimeConfig(const RuntimeConfig& config, bool skipWrite = false, String* errorMessage = nullptr);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does SetRuntimeConfig write to errorMessage? Then that String* should be a const String&

void SetAudioInputPin(int newAudioInputPin);
};
2 changes: 2 additions & 0 deletions include/effectmanager.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,15 @@ class EffectManager : public IJSONSerializable
bool _clearTempEffectWhenExpired = false;
std::atomic_bool _newFrameAvailable = false;
String _effectSetHashString = "";
uint32_t _lastBeatSequence = 0;

std::vector<std::shared_ptr<GFXBase>> _gfx;
std::shared_ptr<LEDStripEffect> _tempEffect;
std::vector<std::reference_wrapper<IFrameEventListener>> _frameEventListeners;
std::vector<std::reference_wrapper<IEffectEventListener>> _effectEventListeners;

void construct(bool clearTempEffect);
void DispatchBeatIfNeeded();

// Implementation is in effects.cpp
void LoadJSONEffects(const JsonArrayConst& effectsArray);
Expand Down
5 changes: 4 additions & 1 deletion include/effects/matrix/PatternWeather.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,10 @@ extern const uint8_t thunderstorm_night_end[] asm("_binary_assets_bmp_thun

static constexpr auto pszDaysOfWeek = to_array( { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" } );

static std::map<const String, EmbeddedFile, std::less<const String>, psram_allocator<std::pair<const String, EmbeddedFile>>> weatherIcons =
// This lookup table is tiny and constructed during global initialization, before the system has
// completed its normal startup path. Keep it on the regular heap so boot does not depend on PSRAM.

static std::map<const String, EmbeddedFile> weatherIcons =
{
{ "01d", EmbeddedFile(clearsky_start, clearsky_end) },
{ "02d", EmbeddedFile(fewclouds_start, fewclouds_end) },
Expand Down
50 changes: 50 additions & 0 deletions include/effects/matrix/spectrumeffects.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,54 @@ class InsulatorSpectrumEffect : public EffectWithId<InsulatorSpectrumEffect>, pu
class VUMeter
{
protected:
mutable uint32_t _lastIndicatorBeatSequence = 0;
mutable uint32_t _lastIndicatorNearBeatSequence = 0;
mutable uint32_t _indicatorUntilMs = 0;
mutable CRGB _indicatorColor = CRGB::Black;

bool UpdateBeatIndicatorState() const
{
const auto & beat = g_Analyzer.LastBeat();
const auto & nearBeat = g_Analyzer.LastNearBeat();
const uint32_t now = millis();

if (beat.sequence != 0 && beat.sequence != _lastIndicatorBeatSequence)
{
// Every accepted beat should restart the lamp pulse. Do not apply
// additional gating here; the analyzer has already classified it.
_lastIndicatorBeatSequence = beat.sequence;
_indicatorColor = CRGB::Red;
_indicatorUntilMs = now + static_cast<uint32_t>(std::clamp(beat.msPerBeat * 0.03f, 20.0f, 36.0f));
return true;
}

if (nearBeat.sequence != 0 && nearBeat.sequence != _lastIndicatorNearBeatSequence)
{
_lastIndicatorNearBeatSequence = nearBeat.sequence;

// Near-miss pulses are shorter and only take effect when a real beat
// did not just restart the indicator above.
_indicatorColor = CRGB::Blue;
_indicatorUntilMs = now + static_cast<uint32_t>(std::clamp(nearBeat.msPerBeat * 0.02f, 14.0f, 24.0f));
return true;
}

return now <= _indicatorUntilMs;
}

void DrawBeatIndicator(std::vector<std::shared_ptr<GFXBase>> & GFX, int yVU) const
{
if (!UpdateBeatIndicatorState())
return;

const int centerLeft = (GFX[0]->width() / 2) - 1;
const int centerRight = centerLeft + 1;

// Overlay the center pair with a saturated pulse so beat classifier
// state is visible even when the rest of the meter uses the palette.
GFX[0]->setPixel(centerLeft, yVU, _indicatorColor);
GFX[0]->setPixel(centerRight, yVU, _indicatorColor);
}

// DrawVUPixels
//
Expand Down Expand Up @@ -186,6 +234,8 @@ class VUMeter

for (int i = 0; i < bars; i++)
DrawVUPixels(GFX, i, yVU, i > bars ? 255 : 0, pPalette);

DrawBeatIndicator(GFX, yVU);
}
};

Expand Down
55 changes: 10 additions & 45 deletions include/effects/strip/musiceffect.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@
//---------------------------------------------------------------------------


#include <deque>

#include "effects.h"
#include "faneffects.h"
#include "values.h"
Expand All @@ -41,8 +39,8 @@
// BeatEffectBase
//
// A specialization of LEDStripEffect, adds a HandleBeat function that allows apps to
// draw based on the music beat. The Draw() function does the audio processing and calls
// HandleBeat() whenever. Apps are free to draw in both Draw() and HandleBeat().
// react to the shared analyzer beat event. Draw() remains free to do its own rendering;
// BeatEffectBase only adapts the central beat callback onto the older HandleBeat API.
//
// The constructor allows you to specify the sensitivity by where the latch points are/
// For a highly sensitive (defaults), keep them both close to 1.0. For a wider beat
Expand All @@ -52,9 +50,6 @@
class BeatEffectBase
{
protected:

const int _maxSamples = 60;
std::deque<float> _samples;
double _lastBeat = 0;
float _minRange = 0;
float _minElapsed = 0;
Expand All @@ -79,47 +74,17 @@ class BeatEffectBase
}


// BeatEffectBase::Draw
//
// Doesn't actually "draw" anything, but rather it scans the audio VU to detect beats, and when it finds one,
// it calls the virtual "HandleBeat" function.
// Beat handling moved into SoundAnalyzer so all effects see the same beat
// timing. ProcessAudio stays as a compatibility no-op for older effects.
virtual void ProcessAudio() {}

virtual void ProcessAudio()
virtual void OnBeat(const BeatInfo& beat)
{
debugV("BeatEffectBase2::Draw");
double elapsed = SecondsSinceLastBeat();

// Access peaks via const reference to avoid copying
const PeakData & peaks = g_Analyzer.Peaks();
auto basslevel = peaks[0] * 2; // Scale to historical 0-2 range

debugV("basslevel: %0.2f", basslevel);
_samples.push_back(basslevel);
float minimum = *min_element(_samples.begin(), _samples.end());
float maximum = *max_element(_samples.begin(), _samples.end());
const float elapsed = std::max(static_cast<float>(_minElapsed), beat.intervalMs / 1000.0f);
const float span = std::max(beat.strength, _minRange);

//Serial.printf("Samples: %d, max: %0.2f, min: %0.2f, span: %0.2f\n", _samples.size(), maximum, minimum, maximum-minimum);

if (_samples.size() >= _maxSamples)
_samples.pop_front();

if (maximum - minimum > _minRange)
{
if (elapsed < _minElapsed)
{
//Serial.printf("False Beat: elapsed: %0.2f, range: %0.2f, time: %0.2lf\n", elapsed, maximum - minimum, g_AppTime.CurrentTime());
// False beat too early, clear data but don't reset lastBeat
_samples.clear();
}
else
{
debugV("Beat: elapsed: %0.2lf, range: %0.2lf\n", elapsed, maximum - minimum);

HandleBeat(false, elapsed, maximum - minimum);
_lastBeat = g_Values.AppTime.CurrentTime();
_samples.clear();
}
}
HandleBeat(beat.major, elapsed, span);
_lastBeat = g_Values.AppTime.CurrentTime();
}
};

Expand Down
Loading