From 33453117a4e5726dcef2224acabd383144993b1c Mon Sep 17 00:00:00 2001 From: Maxim Prokhorov Date: Fri, 25 Nov 2022 05:06:06 +0300 Subject: [PATCH] light: fix temperature scale and white channel mode make use of our apply() objects for value replacement in-place also revert the white channel mode change that ignores rgb output factor --- code/espurna/build.cpp | 4 +- code/espurna/light.cpp | 202 ++++++++++++++++++++++++++--------------- 2 files changed, 130 insertions(+), 76 deletions(-) diff --git a/code/espurna/build.cpp b/code/espurna/build.cpp index fd69771c..573ab39d 100644 --- a/code/espurna/build.cpp +++ b/code/espurna/build.cpp @@ -263,9 +263,9 @@ PROGMEM_STRING(BuildTime, __TIME__); // > This macro expands to a string constant that describes the date on which the preprocessor is being run. // > The string constant contains eleven characters and looks like "Feb 12 1996". If the day of the month is less than 10, // > it is padded with a space on the left. -// > +// > // > If GCC cannot determine the current date, it will emit a warning message (once per compilation) and __DATE__ will expand to "??? ?? ????". -// +// // __TIME__ // > This macro expands to a string constant that describes the time at which the preprocessor is being run. // > The string constant contains eight characters and looks like "23:59:01". diff --git a/code/espurna/light.cpp b/code/espurna/light.cpp index 60082528..77f85375 100644 --- a/code/espurna/light.cpp +++ b/code/espurna/light.cpp @@ -504,6 +504,15 @@ struct Pointers { return _data[4]; } + template + void maybeApply(Args&&... args) const { + for (auto ptr : _data) { + if (ptr) { + (*ptr).apply(std::forward(args)...); + } + } + } + private: void reset(LightChannels& channels); @@ -876,6 +885,8 @@ long _lightBrightnessPercent() { // After the channel value was updated through the API (i.e. through changing the `inputValue`), // these functions are expected to be called. Which one is chosen is based on the current settings values. +// Basic brightness application; default when all other processing options are disabled + void _lightValuesWithBrightness(LightChannels& channels) { const auto Brightness = _light_brightness; for (auto& channel : channels) { @@ -883,12 +894,7 @@ void _lightValuesWithBrightness(LightChannels& channels) { } } -template -void _lightChannelMaybeApply(LightChannel* ptr, Args&&... args) { - if (ptr) { - (*ptr).apply(std::forward(args)...); - } -} +// Maintain compatibility with older versions, limit brightness application to the RGB when using 'color mode'. void _lightValuesWithBrightnessExceptWhite(LightChannels& channels) { auto ptr = espurna::light::Pointers(channels); @@ -898,47 +904,76 @@ void _lightValuesWithBrightnessExceptWhite(LightChannels& channels) { (*ptr.green()).apply(Brightness); (*ptr.blue()).apply(Brightness); - _lightChannelMaybeApply(ptr.warm()); - _lightChannelMaybeApply(ptr.cold()); + if (ptr.warm()) { + (*ptr.warm()).apply(); + } + + if (ptr.cold()) { + (*ptr.cold()).apply(); + } } +// Reset inputValue directly in the expression +// Ignores all previous values, should only be used at the beginning + +struct LightResetInput { + LightResetInput() = delete; + explicit LightResetInput(long value) : + _value(value) + {} + + long operator()(long) const { + return _value; + } + + // 0.0 is the 'coldest', 1.0 is the 'warmest' + static LightResetInput forWarm(float factor) { + return LightResetInput(factor * espurna::light::ValueMax); + } + + // opposite value of `forWarm` for the given factor + static LightResetInput forCold(float factor) { + return LightResetInput((1.0f - factor) * espurna::light::ValueMax); + } + +private: + long _value; +}; + // With `useCCT`, balance the value between Warm and Cold channels based on the current `mireds`. struct LightScaledWhite { + static auto constexpr Default = espurna::light::build::WhiteFactor; + LightScaledWhite() = default; explicit LightScaledWhite(float factor) : _factor(factor) {} + static LightScaledWhite with(float factor) { + return LightScaledWhite{factor * Default}; + } + long operator()(long input) const { - return std::lround( - static_cast(input) - * _factor - * espurna::light::build::WhiteFactor); + return std::lround(static_cast(input) * _factor); } private: - float _factor { 1.0f }; + float _factor { Default }; }; void _lightValuesWithCct(LightChannels& channels) { - const auto Brightness = _light_brightness; - - auto ptr = espurna::light::Pointers(channels); - _lightChannelMaybeApply(ptr.red(), Brightness); - _lightChannelMaybeApply(ptr.green(), Brightness); - _lightChannelMaybeApply(ptr.blue(), Brightness); - - const auto Factor = _light_temperature.factor(); + const auto CctFactor = _light_temperature.factor(); const auto White = LightScaledWhite(); - auto& warm = *ptr.warm(); - warm = std::lround(Factor * espurna::light::ValueMax); - warm.apply(White, Brightness); + auto ptr = espurna::light::Pointers(channels); + (*ptr.warm()).apply( + LightResetInput::forWarm(CctFactor), White); + (*ptr.cold()).apply( + LightResetInput::forCold(CctFactor), White); - auto& cold = *ptr.cold(); - cold = std::lround((1.0f - Factor) * espurna::light::ValueMax); - cold.apply(White, Brightness); + const auto Brightness = _light_brightness; + ptr.maybeApply(Brightness); } // To handle both 4 and 5 channels, allow to 'adjust' internal factor calculation after construction @@ -952,9 +987,14 @@ void _lightValuesWithCct(LightChannels& channels) { struct LightRgbWithoutWhite { LightRgbWithoutWhite() = delete; + explicit LightRgbWithoutWhite(espurna::light::Rgb rgb) : + _common(makeCommon(rgb)), + _factor(makeFactor(_common)), + _luminance(makeLuminance(_common)) + {} + explicit LightRgbWithoutWhite(const LightChannels& channels) : - _common(makeCommon(makeRgb(channels))), - _factor(makeFactor(_common)) + LightRgbWithoutWhite{makeRgb(channels)} {} long operator()(long input) const { @@ -968,6 +1008,7 @@ struct LightRgbWithoutWhite { std::forward(args)... }); _factor = makeFactor(_common); + _luminance = makeLuminance(_common); } long inputMin() const { @@ -978,6 +1019,10 @@ struct LightRgbWithoutWhite { return _factor; } + float luminance() const { + return _luminance; + } + private: struct Common { long inputMin; @@ -985,7 +1030,12 @@ private: long outputMax; }; - static float makeFactor(const Common& common) { + static float makeLuminance(Common common) { + const auto raw = (common.inputMin + common.inputMax) / 2; + return static_cast(raw) / espurna::light::ValueMax; + } + + static float makeFactor(Common common) { const auto inputMax = static_cast(common.inputMax); const auto outputMax = static_cast(common.outputMax); return (outputMax > 0.0f) @@ -1016,6 +1066,7 @@ private: Common _common; float _factor; + float _luminance; }; // When `useWhite` is enabled, white channels are 'detached' from the processing and their value depends on the RGB ones. @@ -1035,16 +1086,25 @@ void _lightValuesWithRgbWhite(LightChannels& channels) { (*ptr.green()).apply(rgb, Brightness); (*ptr.blue()).apply(rgb, Brightness); - auto& warm = *ptr.warm(); - warm = rgb.inputMin(); - warm.apply(LightScaledWhite{rgb.factor()}, Brightness); + (*ptr.warm()).apply( + LightResetInput{rgb.inputMin()}, + LightScaledWhite::with(rgb.factor()), + Brightness); - _lightChannelMaybeApply(ptr.cold()); + if (ptr.cold()) { + (*ptr.cold()).apply(); + } } +// Kelvin to RGB approximation algorithm by Tanner Helland +// * https://tannerhelland.com/2012/09/18/convert-temperature-rgb-algorithm-code.html +// Original code for RGB lights from AiLight library by Sacha Telgenhof (@ +// * https://github.com/stelgenhof/AiLight/blob/develop/lib/AiLight/AiLight.cpp + // Instead of the above, use `mireds` value as a range for warm and cold channels, based on the calculated rgb common values // Every value is also scaled by `brightness` after applying all of the previous steps -// Notice that we completely ignore inputs and reset them to either kelvin'ized or MAX value as the first step +// Notice that we completely ignore inputs and reset them to either kelvin'ized or hardcoded ValueMin or ValueMax +// (also, RED **always** stays at ValueMax b/c we never go above 6.6k kelvin) espurna::light::Rgb _lightKelvinRgb(espurna::light::Kelvin kelvin) { kelvin.value /= 100; @@ -1065,37 +1125,31 @@ espurna::light::Rgb _lightKelvinRgb(espurna::light::Kelvin kelvin) { void _lightValuesWithRgbCct(LightChannels& channels) { const auto Temperature = _light_temperature; - - const auto Factor = Temperature.factor(); - const auto Warm = std::lround(Factor * espurna::light::ValueMax); - const auto Cold = std::lround((1.0f - Factor) * espurna::light::ValueMax); - const auto RgbFromKelvin = _lightKelvinRgb(Temperature.kelvin()); - auto ptr = espurna::light::Pointers(channels); - - const auto Brightness = _light_brightness; - - auto& red = *ptr.red(); - red = RgbFromKelvin.red(); - red.apply(Brightness); - - auto& green = *ptr.green(); - green = RgbFromKelvin.green(); - green.apply(Brightness); - - auto& blue = *ptr.blue(); - blue = RgbFromKelvin.blue(); - blue.apply(Brightness); - const auto White = LightScaledWhite(); + auto rgb = LightRgbWithoutWhite{RgbFromKelvin}; + rgb.adjustOutput(rgb.inputMin()); - auto& warm = *ptr.warm(); - warm = Warm; - warm.apply(White, Brightness); + const auto Brightness = _light_brightness; + auto ptr = espurna::light::Pointers(channels); - auto& cold = *ptr.cold(); - cold = Cold; - cold.apply(White, Brightness); + (*ptr.red()).apply( + LightResetInput{RgbFromKelvin.red()}, + rgb, Brightness); + (*ptr.green()).apply( + LightResetInput{RgbFromKelvin.green()}, + rgb, Brightness); + (*ptr.blue()).apply( + LightResetInput{RgbFromKelvin.blue()}, + rgb, Brightness); + + const auto White = LightScaledWhite(rgb.factor()); + (*ptr.warm()).apply( + LightResetInput::forWarm(Temperature.factor()), + White, Brightness); + (*ptr.cold()).apply( + LightResetInput::forCold(Temperature.factor()), + White, Brightness); } // UI hints about channel distribution @@ -3393,6 +3447,8 @@ void _lightConfigure() { espurna::light::settings::color(false); } + _light_use_rgb = espurna::light::settings::rgb(); + const auto has_warm_white = (Channels >= 4) || (Channels >= 1); _light_has_warm_white = has_warm_white; @@ -3411,18 +3467,6 @@ void _lightConfigure() { espurna::light::settings::cct(false); } - const auto last_process_input_values = _light_process_input_values; - _light_process_input_values = - (_light_use_color) ? ( - (_light_use_cct) ? _lightValuesWithRgbCct : - (_light_use_white) ? _lightValuesWithRgbWhite : - _lightValuesWithBrightnessExceptWhite) : - (_light_use_cct) ? - _lightValuesWithCct : - _lightValuesWithBrightness; - - _light_use_rgb = espurna::light::settings::rgb(); - _light_temperature.range( espurna::light::TemperatureRange{ espurna::light::settings::miredsCold(), @@ -3445,6 +3489,16 @@ void _lightConfigure() { _light_channels[index].gamma = (_light_has_color && _light_use_gamma) && _lightUseGamma(Channels, index); } + const auto last_process_input_values = _light_process_input_values; + _light_process_input_values = + (_light_use_color) ? ( + (_light_use_cct) ? _lightValuesWithRgbCct : + (_light_use_white) ? _lightValuesWithRgbWhite : + _lightValuesWithBrightnessExceptWhite) : + (_light_use_cct) ? + _lightValuesWithCct : + _lightValuesWithBrightness; + if (!_light_update && (last_process_input_values != _light_process_input_values)) { lightUpdate(false); }