diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json
index fdf6db78..2b12517f 100644
--- a/assets/webconfig/i18n/en.json
+++ b/assets/webconfig/i18n/en.json
@@ -282,6 +282,7 @@
"edt_conf_enum_hsv": "HSV",
"edt_conf_enum_left_right": "Left to right",
"edt_conf_enum_linear": "Linear",
+ "edt_conf_enum_decay": "Decay",
"edt_conf_enum_logdebug": "Debug",
"edt_conf_enum_logsilent": "Silent",
"edt_conf_enum_logverbose": "Verbose",
@@ -375,6 +376,14 @@
"edt_conf_smooth_type_title": "Type",
"edt_conf_smooth_updateDelay_expl": "Delay the output in case your ambient light is faster than your TV.",
"edt_conf_smooth_updateDelay_title": "Update delay",
+ "edt_conf_smooth_interpolationRate_expl": "Speed of the calculation of smooth intermediate frames.",
+ "edt_conf_smooth_interpolationRate_title": "Interpolation Rate",
+ "edt_conf_smooth_outputRate_title": "Output Rate",
+ "edt_conf_smooth_outputRate_expl": "The output speed to your led controller.",
+ "edt_conf_smooth_decay_title": "Decay-Power",
+ "edt_conf_smooth_decay_expl": "The speed of decay. 1 is linear, greater values are have stronger effect.",
+ "edt_conf_smooth_dithering_title": "Dithering",
+ "edt_conf_smooth_dithering_expl": "Improve color accuracy at high output speeds by alternating between adjacent colors.",
"edt_conf_smooth_updateFrequency_expl": "The output speed to your led controller.",
"edt_conf_smooth_updateFrequency_title": "Update frequency",
"edt_conf_v4l2_blueSignalThreshold_expl": "Darkens low blue values (recognized as black)",
diff --git a/assets/webconfig/js/ui_utils.js b/assets/webconfig/js/ui_utils.js
index cae6b50c..245f9d89 100644
--- a/assets/webconfig/js/ui_utils.js
+++ b/assets/webconfig/js/ui_utils.js
@@ -184,7 +184,7 @@ function initLanguageSelection()
for (var i = 0; i < availLang.length; i++)
{
$("#language-select").append('');
- }
+ }
var langLocale = storedLang;
@@ -533,7 +533,7 @@ function createJsonEditor(container,schema,setconfig,usePanel,arrayre)
{
for(var key in editor.root.editors)
{
- editor.getEditor("root."+key).setValue( window.serverConfig[key] );
+ editor.getEditor("root."+key).setValue(Object.assign({}, editor.getEditor("root."+key).value, window.serverConfig[key] ));
}
}
diff --git a/config/hyperion.config.json.default b/config/hyperion.config.json.default
index 170d55de..8804b451 100644
--- a/config/hyperion.config.json.default
+++ b/config/hyperion.config.json.default
@@ -48,12 +48,16 @@
"smoothing" :
{
- "enable" : true,
- "type" : "linear",
- "time_ms" : 200,
- "updateFrequency" : 25.0000,
- "updateDelay" : 0,
- "continuousOutput" : true
+ "enable" : true,
+ "type" : "linear",
+ "time_ms" : 200,
+ "updateFrequency" : 25.0000,
+ "interpolationRate" : 25.0000,
+ "outputRate" : 25.0000,
+ "decay" : 1,
+ "dithering" : false,
+ "updateDelay" : 0,
+ "continuousOutput" : true
},
"grabberV4L2" :
diff --git a/libsrc/hyperion/LinearColorSmoothing.cpp b/libsrc/hyperion/LinearColorSmoothing.cpp
index 1583f61e..e81943f3 100644
--- a/libsrc/hyperion/LinearColorSmoothing.cpp
+++ b/libsrc/hyperion/LinearColorSmoothing.cpp
@@ -6,15 +6,48 @@
#include
#include
+#include
+#include
+
+/// The number of microseconds per millisecond = 1000.
+const int64_t MS_PER_MICRO = 1000;
+
+#if defined(COMPILER_GCC)
+#define ALWAYS_INLINE inline __attribute__((__always_inline__))
+#elif defined(COMPILER_MSVC)
+#define ALWAYS_INLINE __forceinline
+#else
+#define ALWAYS_INLINE inline
+#endif
+
+/// Clamps the rounded values to the byte-interval of [0, 255].
+ALWAYS_INLINE long clampRounded(const floatT x) {
+ return std::min(255l, std::max(0l, std::lroundf(x)));
+}
+
+/// The number of bits that are used for shifting the fixed point values
+const int FPShift = (sizeof(uint64_t)*8 - (12 + 9));
+
+/// The number of bits that are reduce the shifting when converting from fixed to floating point. 8 bits = 256 values
+const int SmallShiftBis = sizeof(uint8_t)*8;
+
+/// The number of bits that are used for shifting the fixed point values plus SmallShiftBis
+const int FPShiftSmall = (sizeof(uint64_t)*8 - (12 + 9 + SmallShiftBis));
+
+const char* SETTINGS_KEY_SMOOTHING_TYPE = "type";
+const char* SETTINGS_KEY_INTERPOLATION_RATE = "interpolationRate";
+const char* SETTINGS_KEY_OUTPUT_RATE = "outputRate";
+const char* SETTINGS_KEY_DITHERING = "dithering";
+const char* SETTINGS_KEY_DECAY = "decay";
using namespace hyperion;
-const int64_t DEFAUL_SETTLINGTIME = 200; // settlingtime in ms
-const double DEFAUL_UPDATEFREQUENCY = 25; // updatefrequncy in hz
-const int64_t DEFAUL_UPDATEINTERVALL = static_cast(1000 / DEFAUL_UPDATEFREQUENCY); // updateintervall in ms
-const unsigned DEFAUL_OUTPUTDEPLAY = 0; // outputdelay in ms
+const int64_t DEFAUL_SETTLINGTIME = 200; // settlingtime in ms
+const double DEFAUL_UPDATEFREQUENCY = 25; // updatefrequncy in hz
+const int64_t DEFAUL_UPDATEINTERVALL = static_cast(1000 / DEFAUL_UPDATEFREQUENCY); // updateintervall in ms
+const unsigned DEFAUL_OUTPUTDEPLAY = 0; // outputdelay in ms
-LinearColorSmoothing::LinearColorSmoothing(const QJsonDocument& config, Hyperion* hyperion)
+LinearColorSmoothing::LinearColorSmoothing(const QJsonDocument &config, Hyperion *hyperion)
: QObject(hyperion)
, _log(Logger::getInstance("SMOOTHING"))
, _hyperion(hyperion)
@@ -22,11 +55,13 @@ LinearColorSmoothing::LinearColorSmoothing(const QJsonDocument& config, Hyperion
, _settlingTime(DEFAUL_SETTLINGTIME)
, _timer(new QTimer(this))
, _outputDelay(DEFAUL_OUTPUTDEPLAY)
+ , _smoothingType(SmoothingType::Linear)
, _writeToLedsEnable(false)
, _continuousOutput(false)
, _pause(false)
, _currentConfigId(0)
, _enabled(false)
+ , tempValues(std::vector(0, 0l))
{
// init cfg 0 (default)
addConfig(DEFAUL_SETTLINGTIME, DEFAUL_UPDATEFREQUENCY, DEFAUL_OUTPUTDEPLAY);
@@ -34,38 +69,54 @@ LinearColorSmoothing::LinearColorSmoothing(const QJsonDocument& config, Hyperion
selectConfig(0, true);
// add pause on cfg 1
- SMOOTHING_CFG cfg = {true, 0, 0, 0};
+ SMOOTHING_CFG cfg = {SmoothingType::Linear, 0, 0, 0, 0, 0, false};
_cfgList.append(cfg);
// listen for comp changes
connect(_hyperion, &Hyperion::compStateChangeRequest, this, &LinearColorSmoothing::componentStateChange);
// timer
connect(_timer, &QTimer::timeout, this, &LinearColorSmoothing::updateLeds);
+
+ Info(_log, "LinearColorSmoothing sizeof floatT == %d", (sizeof(floatT)));
}
-void LinearColorSmoothing::handleSettingsUpdate(settings::type type, const QJsonDocument& config)
+void LinearColorSmoothing::handleSettingsUpdate(settings::type type, const QJsonDocument &config)
{
- if(type == settings::SMOOTHING)
+ if (type == settings::SMOOTHING)
{
// std::cout << "LinearColorSmoothing::handleSettingsUpdate" << std::endl;
// std::cout << config.toJson().toStdString() << std::endl;
QJsonObject obj = config.object();
- if(enabled() != obj["enable"].toBool(true))
+ if (enabled() != obj["enable"].toBool(true))
setEnable(obj["enable"].toBool(true));
_continuousOutput = obj["continuousOutput"].toBool(true);
- SMOOTHING_CFG cfg = {false,
- static_cast(obj["time_ms"].toInt(DEFAUL_SETTLINGTIME)),
- static_cast(1000.0/obj["updateFrequency"].toDouble(DEFAUL_UPDATEFREQUENCY)),
- static_cast(obj["updateDelay"].toInt(DEFAUL_OUTPUTDEPLAY))
- };
+ SMOOTHING_CFG cfg = {SmoothingType::Linear,true, 0, 0, 0, 0, 0, false, 1};
+
+ const QString typeString = obj[SETTINGS_KEY_SMOOTHING_TYPE].toString();
+
+ if(typeString == "linear") {
+ cfg.smoothingType = SmoothingType::Linear;
+ } else if(typeString == "decay") {
+ cfg.smoothingType = SmoothingType::Decay;
+ }
+
+ cfg.pause = false;
+ cfg.settlingTime = static_cast(obj["time_ms"].toInt(DEFAUL_SETTLINGTIME));
+ cfg.updateInterval = static_cast(1000.0 / obj["updateFrequency"].toDouble(DEFAUL_UPDATEFREQUENCY));
+ cfg.outputRate = obj[SETTINGS_KEY_OUTPUT_RATE].toDouble(DEFAUL_UPDATEFREQUENCY);
+ cfg.interpolationRate = obj[SETTINGS_KEY_INTERPOLATION_RATE].toDouble(DEFAUL_UPDATEFREQUENCY);
+ cfg.outputDelay = static_cast(obj["updateDelay"].toInt(DEFAUL_OUTPUTDEPLAY));
+ cfg.dithering = obj[SETTINGS_KEY_DITHERING].toBool(false);
+ cfg.decay = obj[SETTINGS_KEY_DECAY].toDouble(1.0);
+
//Debug( _log, "smoothing cfg_id %d: pause: %d bool, settlingTime: %d ms, interval: %d ms (%u Hz), updateDelay: %u frames", _currentConfigId, cfg.pause, cfg.settlingTime, cfg.updateInterval, unsigned(1000.0/cfg.updateInterval), cfg.outputDelay );
_cfgList[0] = cfg;
// if current id is 0, we need to apply the settings (forced)
- if( _currentConfigId == 0)
+ if (_currentConfigId == 0)
{
//Debug( _log, "_currentConfigId == 0");
selectConfig(0, true);
@@ -79,15 +130,18 @@ void LinearColorSmoothing::handleSettingsUpdate(settings::type type, const QJson
int LinearColorSmoothing::write(const std::vector &ledValues)
{
- _targetTime = QDateTime::currentMSecsSinceEpoch() + _settlingTime;
+ _targetTime = micros() + (MS_PER_MICRO * _settlingTime);
_targetValues = ledValues;
+ rememberFrame(ledValues);
+
// received a new target color
if (_previousValues.empty())
{
// not initialized yet
- _previousTime = QDateTime::currentMSecsSinceEpoch();
+ _previousWriteTime = micros();
_previousValues = ledValues;
+ _previousInterpolationTime = micros();
//Debug( _log, "Start Smoothing timer: settlingTime: %d ms, interval: %d ms (%u Hz), updateDelay: %u frames", _settlingTime, _updateInterval, unsigned(1000.0/_updateInterval), _outputDelay );
QMetaObject::invokeMethod(_timer, "start", Qt::QueuedConnection, Q_ARG(int, _updateInterval));
@@ -96,7 +150,7 @@ int LinearColorSmoothing::write(const std::vector &ledValues)
return 0;
}
-int LinearColorSmoothing::updateLedValues(const std::vector& ledValues)
+int LinearColorSmoothing::updateLedValues(const std::vector &ledValues)
{
int retval = 0;
if (!_enabled)
@@ -110,75 +164,366 @@ int LinearColorSmoothing::updateLedValues(const std::vector& ledValues
return retval;
}
-void LinearColorSmoothing::updateLeds()
+void LinearColorSmoothing::intitializeComponentVectors(const size_t ledCount)
{
- int64_t now = QDateTime::currentMSecsSinceEpoch();
- int64_t deltaTime = _targetTime - now;
-
- //Debug(_log, "elapsed Time [%d], _targetTime [%d] - now [%d], deltaTime [%d]", now -_previousTime, _targetTime, now, deltaTime);
- if (deltaTime < 0)
+ // (Re-)Initialize the color-vectors that store the Mean-Value
+ if (_ledCount != ledCount)
{
- _previousValues = _targetValues;
- _previousTime = now;
+ _ledCount = ledCount;
- queueColors(_previousValues);
- _writeToLedsEnable = _continuousOutput;
+ const size_t len = 3 * ledCount;
+
+ meanValues = std::vector(len, 0.0f);
+ residualErrors = std::vector(len, 0.0f);
+ tempValues = std::vector(len, 0l);
}
- else
+
+ // Zero the temp vector
+ std::fill(tempValues.begin(), tempValues.end(), 0l);
+}
+
+void LinearColorSmoothing::writeDirect()
+{
+ const int64_t now = micros();
+ _previousValues = _targetValues;
+ _previousWriteTime = now;
+
+ queueColors(_previousValues);
+ _writeToLedsEnable = _continuousOutput;
+}
+
+
+void LinearColorSmoothing::writeFrame()
+{
+ const int64_t now = micros();
+ _previousWriteTime = now;
+ queueColors(_previousValues);
+ _writeToLedsEnable = _continuousOutput;
+}
+
+
+ALWAYS_INLINE int64_t LinearColorSmoothing::micros() const
+{
+ const auto now = std::chrono::high_resolution_clock::now();
+ return (std::chrono::duration_cast(now.time_since_epoch())).count();
+}
+
+void LinearColorSmoothing::assembleAndDitherFrame()
+{
+ if (meanValues.empty())
{
- _writeToLedsEnable = true;
+ return;
+ }
- //std::cout << "LinearColorSmoothing::updateLeds> _previousValues: "; LedDevice::printLedValues ( _previousValues );
+ // The number of leds present in each frame
+ const size_t N = _targetValues.size();
- float k = 1.0f - 1.0f * deltaTime / (_targetTime - _previousTime);
+ for (size_t i = 0; i < N; ++i)
+ {
+ // Add residuals for error diffusion (temporal dithering)
+ const floatT fr = meanValues[3 * i + 0] + residualErrors[3 * i + 0];
+ const floatT fg = meanValues[3 * i + 1] + residualErrors[3 * i + 1];
+ const floatT fb = meanValues[3 * i + 2] + residualErrors[3 * i + 2];
- int reddif = 0, greendif = 0, bluedif = 0;
+ // Convert to to 8-bit value
+ const long ir = clampRounded(fr);
+ const long ig = clampRounded(fg);
+ const long ib = clampRounded(fb);
- for (size_t i = 0; i < _previousValues.size(); ++i)
- {
- ColorRgb & prev = _previousValues[i];
- ColorRgb & target = _targetValues[i];
+ // Update the colors
+ ColorRgb &prev = _previousValues[i];
+ prev.red = (uint8_t)ir;
+ prev.green = (uint8_t)ig;
+ prev.blue = (uint8_t)ib;
- reddif = target.red - prev.red;
- greendif = target.green - prev.green;
- bluedif = target.blue - prev.blue;
-
- prev.red += (reddif < 0 ? -1:1) * std::ceil(k * std::abs(reddif));
- prev.green += (greendif < 0 ? -1:1) * std::ceil(k * std::abs(greendif));
- prev.blue += (bluedif < 0 ? -1:1) * std::ceil(k * std::abs(bluedif));
- }
- _previousTime = now;
-
- //std::cout << "LinearColorSmoothing::updateLeds> _targetValues: "; LedDevice::printLedValues ( _targetValues );
-
- queueColors(_previousValues);
+ // Determine the component errors
+ residualErrors[3 * i + 0] = fr - ir;
+ residualErrors[3 * i + 1] = fg - ig;
+ residualErrors[3 * i + 2] = fb - ib;
}
}
-void LinearColorSmoothing::queueColors(const std::vector & ledColors)
+void LinearColorSmoothing::assembleFrame()
+{
+ if (meanValues.empty())
+ {
+ return;
+ }
+
+ // The number of leds present in each frame
+ const size_t N = _targetValues.size();
+
+ for (size_t i = 0; i < N; ++i)
+ {
+ // Convert to to 8-bit value
+ const long ir = clampRounded(meanValues[3 * i + 0]);
+ const long ig = clampRounded(meanValues[3 * i + 1]);
+ const long ib = clampRounded(meanValues[3 * i + 2]);
+
+ // Update the colors
+ ColorRgb &prev = _previousValues[i];
+ prev.red = (uint8_t)ir;
+ prev.green = (uint8_t)ig;
+ prev.blue = (uint8_t)ib;
+ }
+}
+
+ALWAYS_INLINE void LinearColorSmoothing::aggregateComponents(const std::vector& colors, std::vector& weighted, const floatT weight) {
+ // Determine the integer-scale by converting the weight to fixed point
+ const uint64_t scale = (1l<(weight);
+
+ const size_t N = colors.size();
+
+ for (size_t i = 0; i < N; ++i)
+ {
+ const ColorRgb &color = colors[i];
+
+ // Scale the colors
+ const uint64_t red = scale * color.red;
+ const uint64_t green = scale * color.green;
+ const uint64_t blue = scale * color.blue;
+
+ // Accumulate in the vector
+ weighted[3 * i + 0] += red;
+ weighted[3 * i + 1] += green;
+ weighted[3 * i + 2] += blue;
+ }
+}
+
+void LinearColorSmoothing::interpolateFrame()
+{
+ const int64_t now = micros();
+
+ // The number of leds present in each frame
+ const size_t N = _targetValues.size();
+
+ intitializeComponentVectors(N);
+
+ /// Time where the frame has been shown
+ int64_t frameStart;
+
+ /// Time where the frame display would have ended
+ int64_t frameEnd = now;
+
+ /// Time where the current window has started
+ const int64_t windowStart = now - (MS_PER_MICRO * _settlingTime);
+
+ /// The total weight of the frames that were included in our window; sum of the individual weights
+ floatT fs = 0.0f;
+
+ // To calculate the mean component we iterate over all relevant frames;
+ // from the most recent to the oldest frame that still clips our moving-average window given by time (now)
+ for (auto it = _frameQueue.rbegin(); it != _frameQueue.rend() && frameEnd > windowStart; ++it)
+ {
+ // Starting time of a frame in the window is clipped to the window start
+ frameStart = std::max(windowStart, it->time);
+
+ // Weight the current frame relative to the overall window based on start and end times
+ const floatT weight = _weightFrame(frameStart, frameEnd, windowStart);
+ fs += weight;
+
+ // Aggregate the RGB components of this frame's LED colors using the individual weighting
+ aggregateComponents(it->colors, tempValues, weight);
+
+ // The previous (earlier) frame display has ended when the current frame stared to show,
+ // so we can use this as the frame-end time for next iteration
+ frameEnd = frameStart;
+ }
+
+ /// The inverse scaling factor for the color components, clamped to (0, 1.0]; 1.0 for fs < 1, 1 : fs otherwise
+ const floatT inv_fs = ((fs < 1.0f) ? 1.0f : 1.0f / fs) / (1 << SmallShiftBis);
+
+ // Normalize the mean component values for the window (fs)
+ for (size_t i = 0; i < 3 * N; ++i)
+ {
+ meanValues[i] = (tempValues[i] >> FPShiftSmall) * inv_fs;
+ }
+
+ _previousInterpolationTime = now;
+}
+
+void LinearColorSmoothing::performDecay(const int64_t now) {
+ /// The target time when next frame interpolation should be performed
+ const int64_t interpolationTarget = _previousInterpolationTime + _interpolationIntervalMicros;
+
+ /// The target time when next write operation should be performed
+ const int64_t writeTarget = _previousWriteTime + _outputIntervalMicros;
+
+ /// Whether a frame interpolation is pending
+ const bool interpolatePending = now > interpolationTarget;
+
+ /// Whether a write is pending
+ const bool writePending = now > writeTarget;
+
+ // Check whether a new interpolation frame is due
+ if (interpolatePending)
+ {
+ interpolateFrame();
+ ++_interpolationCounter;
+
+ // Assemble the frame now when no dithering is applied
+ if(!_dithering) {
+ assembleFrame();
+ }
+ }
+
+ // Check whether to frame output is due
+ if (writePending)
+ {
+ // Dither the frame to diffuse rounding errors
+ if(_dithering) {
+ assembleAndDitherFrame();
+ }
+
+ writeFrame();
+ ++_renderedCounter;
+ }
+
+ // Check for sleep when no operation is pending.
+ // As our QTimer is not capable of sub 1ms timing but instead performs spinning -
+ // we have to do µsec-sleep to free CPU time; otherwise the thread would consume 100% CPU time.
+ if(_updateInterval <= 0 && !(interpolatePending || writePending)) {
+ const int64_t nextActionExpected = std::min(interpolationTarget, writeTarget);
+ const int64_t microsTillNextAction = nextActionExpected - now;
+ const int64_t SLEEP_MAX_MICROS = 1000l; // We want to use usleep for up to 1ms
+ const int64_t SLEEP_RES_MICROS = 100l; // Expected resolution is >= 100µs on stock linux
+
+ if(microsTillNextAction > SLEEP_RES_MICROS) {
+ const int64_t wait = std::min(microsTillNextAction - SLEEP_RES_MICROS, SLEEP_MAX_MICROS);
+ //usleep(wait);
+ std::this_thread::sleep_for(std::chrono::microseconds(wait));
+ }
+ }
+
+ // Write stats every 30 sec
+ if ((now > (_renderedStatTime + 30 * 1000000)) && (_renderedCounter > _renderedStatCounter))
+ {
+ Info(_log, "decay - rendered frames [%d] (%f/s), interpolated frames [%d] (%f/s) in [%f ms]"
+ , _renderedCounter - _renderedStatCounter
+ , (1.0f * (_renderedCounter - _renderedStatCounter) / ((now - _renderedStatTime) / 1000000.0f))
+ , _interpolationCounter - _interpolationStatCounter
+ , (1.0f * (_interpolationCounter - _interpolationStatCounter) / ((now - _renderedStatTime) / 1000000.0f))
+ , (now - _renderedStatTime) / 1000.0f
+ );
+ _renderedStatTime = now;
+ _renderedStatCounter = _renderedCounter;
+ _interpolationStatCounter = _interpolationCounter;
+ }
+}
+
+void LinearColorSmoothing::performLinear(const int64_t now) {
+ const int64_t deltaTime = _targetTime - now;
+ const float k = 1.0f - 1.0f * deltaTime / (_targetTime - _previousWriteTime);
+ const size_t N = _previousValues.size();
+
+ for (size_t i = 0; i < N; ++i)
+ {
+ const ColorRgb &target = _targetValues[i];
+ ColorRgb &prev = _previousValues[i];
+
+ const int reddif = target.red - prev.red;
+ const int greendif = target.green - prev.green;
+ const int bluedif = target.blue - prev.blue;
+
+ prev.red += (reddif < 0 ? -1:1) * std::ceil(k * std::abs(reddif));
+ prev.green += (greendif < 0 ? -1:1) * std::ceil(k * std::abs(greendif));
+ prev.blue += (bluedif < 0 ? -1:1) * std::ceil(k * std::abs(bluedif));
+ }
+
+ writeFrame();
+}
+
+void LinearColorSmoothing::updateLeds()
+{
+ const int64_t now = micros();
+ const int64_t deltaTime = _targetTime - now;
+
+ //Debug(_log, "elapsed Time [%d], _targetTime [%d] - now [%d], deltaTime [%d]", now -_previousWriteTime, _targetTime, now, deltaTime);
+ if (deltaTime < 0)
+ {
+ writeDirect();
+ return;
+ }
+
+ switch (_smoothingType)
+ {
+ case Decay:
+ performDecay(now);
+ break;
+
+ case Linear:
+ // Linear interpolation is default
+ default:
+ performLinear(now);
+ break;
+ }
+}
+
+void LinearColorSmoothing::rememberFrame(const std::vector &ledColors)
+{
+ //Info(_log, "rememberFrame - before _frameQueue.size() [%d]", _frameQueue.size());
+
+ const int64_t now = micros();
+
+ // Maintain the queue by removing outdated frames
+ const int64_t windowStart = now - (MS_PER_MICRO * _settlingTime);
+
+ int p = -1; // Start with -1 instead of 0, so we keep the last frame at least partially clipping the window
+
+ // As the frames are ordered chronologically we scan from the front (oldest) till we find the first fresh frame
+ for (auto it = _frameQueue.begin(); it != _frameQueue.end() && it->time < windowStart; ++it)
+ {
+ ++p;
+ }
+
+ if (p > 0)
+ {
+ //Info(_log, "rememberFrame - erasing %d frames", p);
+ _frameQueue.erase(_frameQueue.begin(), _frameQueue.begin() + p);
+ }
+
+ // Append the latest frame at back of the queue
+ const REMEMBERED_FRAME frame = REMEMBERED_FRAME(now, ledColors);
+ _frameQueue.push_back(frame);
+
+ //Info(_log, "rememberFrame - after _frameQueue.size() [%d]", _frameQueue.size());
+}
+
+
+void LinearColorSmoothing::clearRememberedFrames()
+{
+ _frameQueue.clear();
+
+ _ledCount = 0;
+ meanValues.clear();
+ residualErrors.clear();
+ tempValues.clear();
+}
+
+void LinearColorSmoothing::queueColors(const std::vector &ledColors)
{
//Debug(_log, "queueColors - _outputDelay[%d] _outputQueue.size() [%d], _writeToLedsEnable[%d]", _outputDelay, _outputQueue.size(), _writeToLedsEnable);
if (_outputDelay == 0)
{
// No output delay => immediate write
- if ( _writeToLedsEnable && !_pause)
+ if (_writeToLedsEnable && !_pause)
{
-// if ( ledColors.size() == 0 )
-// qFatal ("No LedValues! - in LinearColorSmoothing::queueColors() - _outputDelay == 0");
-// else
+ // if ( ledColors.size() == 0 )
+ // qFatal ("No LedValues! - in LinearColorSmoothing::queueColors() - _outputDelay == 0");
+ // else
emit _hyperion->ledDeviceData(ledColors);
}
}
else
{
// Push new colors in the delay-buffer
- if ( _writeToLedsEnable )
+ if (_writeToLedsEnable)
_outputQueue.push_back(ledColors);
// If the delay-buffer is filled pop the front and write to device
- if (_outputQueue.size() > 0 )
+ if (_outputQueue.size() > 0)
{
- if ( _outputQueue.size() > _outputDelay || !_writeToLedsEnable )
+ if (_outputQueue.size() > _outputDelay || !_writeToLedsEnable)
{
if (!_pause)
{
@@ -196,17 +541,19 @@ void LinearColorSmoothing::clearQueuedColors()
_previousValues.clear();
_targetValues.clear();
+
+ clearRememberedFrames();
}
void LinearColorSmoothing::componentStateChange(hyperion::Components component, bool state)
{
_writeToLedsEnable = state;
- if(component == hyperion::COMP_LEDDEVICE)
+ if (component == hyperion::COMP_LEDDEVICE)
{
clearQueuedColors();
}
- if(component == hyperion::COMP_SMOOTHING)
+ if (component == hyperion::COMP_SMOOTHING)
{
setEnable(state);
}
@@ -230,7 +577,17 @@ void LinearColorSmoothing::setPause(bool pause)
unsigned LinearColorSmoothing::addConfig(int settlingTime_ms, double ledUpdateFrequency_hz, unsigned updateDelay)
{
- SMOOTHING_CFG cfg = {false, settlingTime_ms, int64_t(1000.0/ledUpdateFrequency_hz), updateDelay};
+ SMOOTHING_CFG cfg = {
+ SmoothingType::Linear,
+ false,
+ settlingTime_ms,
+ int64_t(1000.0 / ledUpdateFrequency_hz),
+ ledUpdateFrequency_hz,
+ ledUpdateFrequency_hz,
+ updateDelay,
+ false,
+ 1
+ };
_cfgList.append(cfg);
//Debug( _log, "smoothing cfg %d: pause: %d bool, settlingTime: %d ms, interval: %d ms (%u Hz), updateDelay: %u frames", _cfgList.count()-1, cfg.pause, cfg.settlingTime, cfg.updateInterval, unsigned(1000.0/cfg.updateInterval), cfg.outputDelay );
@@ -240,17 +597,26 @@ unsigned LinearColorSmoothing::addConfig(int settlingTime_ms, double ledUpdateFr
unsigned LinearColorSmoothing::updateConfig(unsigned cfgID, int settlingTime_ms, double ledUpdateFrequency_hz, unsigned updateDelay)
{
unsigned updatedCfgID = cfgID;
- if ( cfgID < static_cast(_cfgList.count()) )
+ if (cfgID < static_cast(_cfgList.count()))
{
- SMOOTHING_CFG cfg = {false, settlingTime_ms, int64_t(1000.0/ledUpdateFrequency_hz), updateDelay};
+ SMOOTHING_CFG cfg = {
+ SmoothingType::Linear,
+ false,
+ settlingTime_ms,
+ int64_t(1000.0 / ledUpdateFrequency_hz),
+ ledUpdateFrequency_hz,
+ ledUpdateFrequency_hz,
+ updateDelay,
+ false,
+ 1};
_cfgList[updatedCfgID] = cfg;
}
else
{
- updatedCfgID = addConfig ( settlingTime_ms, ledUpdateFrequency_hz, updateDelay);
+ updatedCfgID = addConfig(settlingTime_ms, ledUpdateFrequency_hz, updateDelay);
}
-// Debug( _log, "smoothing updatedCfgID %u: settlingTime: %d ms, "
-// "interval: %d ms (%u Hz), updateDelay: %u frames", cfgID, _settlingTime, int64_t(1000.0/ledUpdateFrequency_hz), unsigned(ledUpdateFrequency_hz), updateDelay );
+ // Debug( _log, "smoothing updatedCfgID %u: settlingTime: %d ms, "
+ // "interval: %d ms (%u Hz), updateDelay: %u frames", cfgID, _settlingTime, int64_t(1000.0/ledUpdateFrequency_hz), unsigned(ledUpdateFrequency_hz), updateDelay );
return updatedCfgID;
}
@@ -264,18 +630,54 @@ bool LinearColorSmoothing::selectConfig(unsigned cfg, bool force)
}
//Debug( _log, "selectConfig FORCED - _currentConfigId [%u], force [%d]", cfg, force);
- if ( cfg < (unsigned)_cfgList.count())
+ if (cfg < (unsigned)_cfgList.count())
{
- _settlingTime = _cfgList[cfg].settlingTime;
- _outputDelay = _cfgList[cfg].outputDelay;
- _pause = _cfgList[cfg].pause;
+ _smoothingType = _cfgList[cfg].smoothingType;
+ _settlingTime = _cfgList[cfg].settlingTime;
+ _outputDelay = _cfgList[cfg].outputDelay;
+ _pause = _cfgList[cfg].pause;
+ _outputRate = _cfgList[cfg].outputRate;
+ _outputIntervalMicros = int64_t(1000000.0 / _outputRate); // 1s = 1e6 µs
+ _interpolationRate = _cfgList[cfg].interpolationRate;
+ _interpolationIntervalMicros = int64_t(1000000.0 / _interpolationRate);
+ _dithering = _cfgList[cfg].dithering;
+ _decay = _cfgList[cfg].decay;
+ _invWindow = 1.0f / (MS_PER_MICRO * _settlingTime);
+
+ // Set _weightFrame based on the given decay
+ const float decay = _decay;
+ const floatT inv_window = _invWindow;
+
+ // For decay != 1 use power-based approach for calculating the moving average values
+ if(std::abs(decay - 1.0f) > std::numeric_limits::epsilon()) {
+ // Exponential Decay
+ _weightFrame = [inv_window,decay](const int64_t fs, const int64_t fe, const int64_t ws) {
+ const floatT s = (fs - ws) * inv_window;
+ const floatT t = (fe - ws) * inv_window;
+
+ return (decay + 1) * (std::pow(t, decay) - std::pow(s, decay));
+ };
+ } else {
+ // For decay == 1 use linear interpolation of the moving average values
+ // Linear Decay
+ _weightFrame = [inv_window](const int64_t fs, const int64_t fe, const int64_t ws) {
+ // Linear weighting = (end - start) * scale
+ return static_cast((fe - fs) * inv_window);
+ };
+ }
+
+ _renderedStatTime = micros();
+ _renderedCounter = 0;
+ _renderedStatCounter = 0;
+ _interpolationCounter = 0;
+ _interpolationStatCounter = 0;
if (_cfgList[cfg].updateInterval != _updateInterval)
{
QMetaObject::invokeMethod(_timer, "stop", Qt::QueuedConnection);
_updateInterval = _cfgList[cfg].updateInterval;
- if ( this->enabled() && this->_writeToLedsEnable )
+ if (this->enabled() && this->_writeToLedsEnable)
{
//Debug( _log, "_cfgList[cfg].updateInterval != _updateInterval - Restart timer - _updateInterval [%d]", _updateInterval);
QMetaObject::invokeMethod(_timer, "start", Qt::QueuedConnection, Q_ARG(int, _updateInterval));
@@ -290,6 +692,9 @@ bool LinearColorSmoothing::selectConfig(unsigned cfg, bool force)
// DebugIf( enabled() && !_pause, _log, "set smoothing cfg: %u settlingTime: %d ms, interval: %d ms, updateDelay: %u frames", _currentConfigId, _settlingTime, _updateInterval, _outputDelay );
// DebugIf( _pause, _log, "set smoothing cfg: %d, pause", _currentConfigId );
+ const float thalf = (1.0-std::pow(1.0/2, 1.0/_decay))*_settlingTime;
+ Info( _log, "%s - Time: %d ms, outputRate %f Hz, interpolationRate: %f Hz, timer: %d ms, Dithering: %d, Decay: %f -> HalfTime: %f ms", _smoothingType == SmoothingType::Decay ? "decay" : "linear", _settlingTime, _outputRate, _interpolationRate, _updateInterval, _dithering ? 1 : 0, _decay, thalf);
+
return true;
}
diff --git a/libsrc/hyperion/LinearColorSmoothing.h b/libsrc/hyperion/LinearColorSmoothing.h
index c3472999..2219a9c3 100644
--- a/libsrc/hyperion/LinearColorSmoothing.h
+++ b/libsrc/hyperion/LinearColorSmoothing.h
@@ -2,6 +2,7 @@
// STL includes
#include
+#include
// Qt includes
#include
@@ -13,14 +14,62 @@
// settings
#include
+// The type of float
+#define floatT float // Select double, float or __fp16
+
class QTimer;
class Logger;
class Hyperion;
+/// The type of smoothing to perform
+enum SmoothingType {
+ /// "Linear" smoothing algorithm
+ Linear,
+
+ /// Decay based smoothing algorithm
+ Decay,
+};
+
/// Linear Smooting class
///
/// This class processes the requested led values and forwards them to the device after applying
-/// a linear smoothing effect. This class can be handled as a generic LedDevice.
+/// a smoothing effect to LED colors. This class can be handled as a generic LedDevice.
+///
+/// Currently, two types of smoothing are supported:
+///
+/// - Linear: A linear smoothing effect that interpolates the previous to the target colors.
+/// - Decay: A temporal smoothing effect that uses a decay based algorithm that interpolates
+/// colors based on the age of previous frames and a given decay-power.
+///
+/// The smoothing is performed on a history of relevant LED-color frames that are
+/// incorporated in the smoothing window (given by the configured settling time).
+///
+/// For each moment, all ingress frames that were received during the smoothing window
+/// are reduced to the concrete color values using a weighted moving average. This is
+/// done by applying a decay-controlled weighting-function to individual the colors of
+/// each frame.
+///
+/// Decay
+/// =====
+/// The decay-power influences the weight of individual frames based on their 'age'.
+///
+/// * A decay value of 1 indicates linear decay. The colors are given by the moving average
+/// with a weight that is strictly proportionate to the fraction of time each frame was
+/// visible during the smoothing window. As a result, equidistant frames will have an
+/// equal share when calculating an intermediate frame.
+///
+/// * A decay value greater than 1 indicates non-linear decay. With higher powers, the
+/// decay is stronger. I.e. newer frames in the smoothing window will have more influence
+/// on colors of intermediate frames than older ones.
+///
+/// Dithering
+/// =========
+/// A temporal dithering algorithm is used to minimize rounding errors, when downsampling
+/// the average color values to the 8-bit RGB resolution of the LED-device. Effectively,
+/// this performs diffusion of the residual errors across multiple egress frames.
+///
+///
+
class LinearColorSmoothing : public QObject
{
Q_OBJECT
@@ -30,14 +79,14 @@ public:
/// @param config The configuration document smoothing
/// @param hyperion The hyperion parent instance
///
- LinearColorSmoothing(const QJsonDocument& config, Hyperion* hyperion);
+ LinearColorSmoothing(const QJsonDocument &config, Hyperion *hyperion);
/// LED values as input for the smoothing filter
///
/// @param ledValues The color-value per led
/// @return Zero on success else negative
///
- virtual int updateLedValues(const std::vector& ledValues);
+ virtual int updateLedValues(const std::vector &ledValues);
void setEnable(bool enable);
void setPause(bool pause);
@@ -52,7 +101,7 @@ public:
///
/// @return The index of the cfg which can be passed to selectConfig()
///
- unsigned addConfig(int settlingTime_ms, double ledUpdateFrequency_hz=25.0, unsigned updateDelay=0);
+ unsigned addConfig(int settlingTime_ms, double ledUpdateFrequency_hz = 25.0, unsigned updateDelay = 0);
///
/// @brief Update a smoothing cfg which can be used with selectConfig()
@@ -65,7 +114,7 @@ public:
///
/// @return The index of the cfg which can be passed to selectConfig()
///
- unsigned updateConfig(unsigned cfgID, int settlingTime_ms, double ledUpdateFrequency_hz=25.0, unsigned updateDelay=0);
+ unsigned updateConfig(unsigned cfgID, int settlingTime_ms, double ledUpdateFrequency_hz = 25.0, unsigned updateDelay = 0);
///
/// @brief select a smoothing cfg given by cfg index from addConfig()
@@ -82,7 +131,7 @@ public slots:
/// @param type settingyType from enum
/// @param config configuration object
///
- void handleSettingsUpdate(settings::type type, const QJsonDocument& config);
+ void handleSettingsUpdate(settings::type type, const QJsonDocument &config);
private slots:
/// Timer callback which writes updated led values to the led device
@@ -96,13 +145,12 @@ private slots:
void componentStateChange(hyperion::Components component, bool state);
private:
-
/**
* Pushes the colors into the output queue and popping the head to the led-device
*
* @param ledColors The colors to queue
*/
- void queueColors(const std::vector & ledColors);
+ void queueColors(const std::vector &ledColors);
void clearQueuedColors();
/// write updated values as input for the smoothing filter
@@ -113,10 +161,10 @@ private:
virtual int write(const std::vector &ledValues);
/// Logger instance
- Logger* _log;
+ Logger *_log;
/// Hyperion instance
- Hyperion* _hyperion;
+ Hyperion *_hyperion;
/// The interval at which to update the leds (msec)
int64_t _updateInterval;
@@ -125,7 +173,7 @@ private:
int64_t _settlingTime;
/// The Qt timer object
- QTimer * _timer;
+ QTimer *_timer;
/// The timestamp at which the target data should be fully applied
int64_t _targetTime;
@@ -134,15 +182,45 @@ private:
std::vector _targetValues;
/// The timestamp of the previously written led data
- int64_t _previousTime;
+ int64_t _previousWriteTime;
+
+ /// The timestamp of the previously data interpolation
+ int64_t _previousInterpolationTime;
/// The previously written led data
std::vector _previousValues;
/// The number of updates to keep in the output queue (delayed) before being output
unsigned _outputDelay;
+
/// The output queue
- std::list > _outputQueue;
+ std::deque> _outputQueue;
+
+ /// A frame of led colors used for temporal smoothing
+ class REMEMBERED_FRAME
+ {
+ public:
+ /// The time this frame was received
+ int64_t time;
+
+ /// The led colors
+ std::vector colors;
+
+ REMEMBERED_FRAME ( REMEMBERED_FRAME && ) = default;
+ REMEMBERED_FRAME ( const REMEMBERED_FRAME & ) = default;
+ REMEMBERED_FRAME & operator= ( const REMEMBERED_FRAME & ) = default;
+
+ REMEMBERED_FRAME(const int64_t time, const std::vector colors)
+ : time(time)
+ , colors(colors)
+ {}
+ };
+
+ /// The type of smoothing to perform
+ SmoothingType _smoothingType;
+
+ /// The queue of temporarily remembered frames
+ std::deque _frameQueue;
/// Prevent sending data to device when no intput data is sent
bool _writeToLedsEnable;
@@ -153,17 +231,146 @@ private:
/// Flag for pausing
bool _pause;
+ /// The rate at which color frames should be written to LED device.
+ double _outputRate;
+
+ /// The interval time in microseconds for writing of LED Frames.
+ int64_t _outputIntervalMicros;
+
+ /// The rate at which interpolation of LED frames should be performed.
+ double _interpolationRate;
+
+ /// The interval time in microseconds for interpolation of LED Frames.
+ int64_t _interpolationIntervalMicros;
+
+ /// Whether to apply temproral dithering to diffuse rounding errors when downsampling to 8-bit RGB colors.
+ bool _dithering;
+
+ /// The decay power > 0. A value of exactly 1 is linear decay, higher numbers indicate a faster decay rate.
+ double _decay;
+
+ /// Value of 1.0 / settlingTime; inverse of the window size used for weighting of frames.
+ floatT _invWindow;
+
struct SMOOTHING_CFG
{
- bool pause;
- int64_t settlingTime;
- int64_t updateInterval;
- unsigned outputDelay;
- };
+ /// The type of smoothing to perform
+ SmoothingType smoothingType;
+ /// Whether to pause output
+ bool pause;
+
+ /// The time of the smoothing window.
+ int64_t settlingTime;
+
+ /// The interval time in millisecons of the timer used for scheduling LED update operations. A value of 0 indicates sub-millisecond timing.
+ int64_t updateInterval;
+
+ // The rate at which color frames should be written to LED device.
+ double outputRate;
+
+ /// The rate at which interpolation of LED frames should be performed.
+ double interpolationRate;
+
+ /// The number of frames the output is delayed
+ unsigned outputDelay;
+
+ /// Whether to apply temproral dithering to diffuse rounding errors when downsampling to 8-bit RGB colors. Improves color accuracy.
+ bool dithering;
+
+ /// The decay power > 0. A value of exactly 1 is linear decay, higher numbers indicate a faster decay rate.
+ double decay;
+ };
/// smooth config list
QVector _cfgList;
unsigned _currentConfigId;
- bool _enabled;
+ bool _enabled;
+
+ /// Pushes the colors into the frame queue and cleans outdated frames from memory.
+ ///
+ /// @param ledColors The next colors to queue
+ void rememberFrame(const std::vector &ledColors);
+
+ /// Frees the LED frames that were queued for calculating the moving average.
+ void clearRememberedFrames();
+
+ /// (Re-)Initializes the color-component vectors with given number of values.
+ ///
+ /// @param ledCount The number of colors.
+ void intitializeComponentVectors(const size_t ledCount);
+
+ /// The number of led component-values that must be held per color; i.e. size of the color vectors reds / greens / blues
+ size_t _ledCount = 0;
+
+ /// The average component colors red, green, blue of the leds
+ std::vector meanValues;
+
+ /// The residual component errors of the leds
+ std::vector residualErrors;
+
+ /// The accumulated led color values in 64-bit fixed point domain
+ std::vector tempValues;
+
+ /// Writes the target frame RGB data to the LED device without any interpolation.
+ void writeDirect();
+
+ /// Writes the assembled RGB data to the LED device.
+ void writeFrame();
+
+ /// Assembles a frame of LED colors in order to write RGB data to the LED device.
+ /// Temporal dithering is applied to diffuse the downsampling error for RGB color components.
+ void assembleAndDitherFrame();
+
+ /// Assembles a frame of LED colors in order to write RGB data to the LED device.
+ /// No dithering is applied, RGB color components are just rounded to nearest integer.
+ void assembleFrame();
+
+ /// Prepares a frame of LED colors by interpolating using the current smoothing window
+ void interpolateFrame();
+
+ /// Performes a decay-based smoothing effect. The frames are interpolated based on their age and a given decay-power.
+ ///
+ /// The ingress frames that were received during the current smoothing window are reduced using a weighted moving average
+ /// by applying the weighting-function to the color components of each frame.
+ ///
+ /// When downsampling the average color values to the 8-bit RGB resolution of the LED device, rounding errors are minimized
+ /// by temporal dithering algorithm (error diffusion of residual errors).
+ void performDecay(const int64_t now);
+
+ /// Performs a linear smoothing effect
+ void performLinear(const int64_t now);
+
+ /// Aggregates the RGB components of the LED colors using the given weight and updates weighted accordingly
+ ///
+ /// @param colors The LED colors to aggregate.
+ /// @param weighted The target vector, that accumulates the terms.
+ /// @param weight The weight to use.
+ static inline void aggregateComponents(const std::vector& colors, std::vector& weighted, const floatT weight);
+
+ /// Gets the current time in microseconds from high precision system clock.
+ inline int64_t micros() const;
+
+ /// The time, when the rendering statistics were logged previously
+ int64_t _renderedStatTime;
+
+ /// The total number of frames that were rendered to the LED device
+ int64_t _renderedCounter;
+
+ /// The count of frames that have been rendered to the LED device when statistics were shown previously
+ int64_t _renderedStatCounter;
+
+ /// The total number of frames that were interpolated using the smoothing algorithm
+ int64_t _interpolationCounter;
+
+ /// The count of frames that have been interpolated when statistics were shown previously
+ int64_t _interpolationStatCounter;
+
+ /// Frame weighting function for finding the frame's integral value
+ ///
+ /// @param frameStart The start of frame time.
+ /// @param frameEnd The end of frame time.
+ /// @param windowStart The window start time.
+ /// @returns The frame weight.
+ std::function _weightFrame;
};
diff --git a/libsrc/hyperion/schema/schema-smoothing.json b/libsrc/hyperion/schema/schema-smoothing.json
index 9e3f64ad..4b7f14dd 100644
--- a/libsrc/hyperion/schema/schema-smoothing.json
+++ b/libsrc/hyperion/schema/schema-smoothing.json
@@ -14,11 +14,10 @@
{
"type" : "string",
"title" : "edt_conf_smooth_type_title",
- "enum" : ["linear"],
+ "enum" : ["linear", "decay"],
"default" : "linear",
"options" : {
- "enum_titles" : ["edt_conf_enum_linear"],
- "hidden":true
+ "enum_titles" : ["edt_conf_enum_linear", "edt_conf_enum_decay"]
},
"propertyOrder" : 2
},
@@ -27,7 +26,7 @@
"type" : "integer",
"title" : "edt_conf_smooth_time_ms_title",
"minimum" : 25,
- "maximum": 600,
+ "maximum": 5000,
"default" : 200,
"append" : "edt_append_ms",
"propertyOrder" : 3
@@ -37,11 +36,47 @@
"type" : "number",
"title" : "edt_conf_smooth_updateFrequency_title",
"minimum" : 1.0,
- "maximum" : 100.0,
+ "maximum" : 2000.0,
"default" : 25.0,
"append" : "edt_append_hz",
"propertyOrder" : 4
},
+ "interpolationRate" :
+ {
+ "type" : "number",
+ "title" : "edt_conf_smooth_interpolationRate_title",
+ "minimum" : 1.0,
+ "maximum": 1000.0,
+ "default" : 0,
+ "append" : "edt_append_hz",
+ "propertyOrder" : 5
+ },
+ "outputRate" :
+ {
+ "type" : "number",
+ "title" : "edt_conf_smooth_outputRate_title",
+ "minimum" : 1.0,
+ "maximum": 1000.0,
+ "default" : 0,
+ "append" : "edt_append_hz",
+ "propertyOrder" : 6
+ },
+ "decay" :
+ {
+ "type" : "number",
+ "title" : "edt_conf_smooth_decay_title",
+ "default" : 1.0,
+ "minimum" : 1.0,
+ "maximum": 20.0,
+ "propertyOrder" : 7
+ },
+ "dithering" :
+ {
+ "type" : "boolean",
+ "title" : "edt_conf_smooth_dithering_title",
+ "default" : true,
+ "propertyOrder" : 8
+ },
"updateDelay" :
{
"type" : "integer",
@@ -50,14 +85,14 @@
"maximum": 2048,
"default" : 0,
"append" : "edt_append_ms",
- "propertyOrder" : 5
+ "propertyOrder" : 9
},
"continuousOutput" :
{
"type" : "boolean",
"title" : "edt_conf_smooth_continuousOutput_title",
"default" : true,
- "propertyOrder" : 6
+ "propertyOrder" : 10
}
},
"additionalProperties" : false