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