#include "RuntimeLiveState.h" #include "RuntimeParameterUtils.h" #include #include #include #include namespace { constexpr double kOscSmoothingReferenceFps = 60.0; constexpr double kOscSmoothingMaxStepSeconds = 0.25; std::string SimplifyOscControlKey(const std::string& text) { std::string simplified; for (unsigned char ch : text) { if (std::isalnum(ch)) simplified.push_back(static_cast(std::tolower(ch))); } return simplified; } bool MatchesOscControlKey(const std::string& candidate, const std::string& key) { return candidate == key || SimplifyOscControlKey(candidate) == SimplifyOscControlKey(key); } double ClampOscAlpha(double value) { return (std::max)(0.0, (std::min)(1.0, value)); } double ComputeTimeBasedOscAlpha(double smoothing, double deltaSeconds) { const double clampedSmoothing = ClampOscAlpha(smoothing); if (clampedSmoothing <= 0.0) return 0.0; if (clampedSmoothing >= 1.0) return 1.0; const double clampedDeltaSeconds = (std::max)(0.0, (std::min)(kOscSmoothingMaxStepSeconds, deltaSeconds)); if (clampedDeltaSeconds <= 0.0) return 0.0; const double frameScale = clampedDeltaSeconds * kOscSmoothingReferenceFps; return ClampOscAlpha(1.0 - std::pow(1.0 - clampedSmoothing, frameScale)); } JsonValue BuildOscCommitValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value) { switch (definition.type) { case ShaderParameterType::Boolean: return JsonValue(value.booleanValue); case ShaderParameterType::Enum: return JsonValue(value.enumValue); case ShaderParameterType::Text: return JsonValue(value.textValue); case ShaderParameterType::Trigger: case ShaderParameterType::Float: return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front()); case ShaderParameterType::Vec2: case ShaderParameterType::Color: { JsonValue array = JsonValue::MakeArray(); for (double number : value.numberValues) array.pushBack(JsonValue(number)); return array; } } return JsonValue(); } } void RuntimeLiveState::Clear() { mOscOverlayStates.clear(); } std::size_t RuntimeLiveState::OverlayCount() const { return mOscOverlayStates.size(); } void RuntimeLiveState::ApplyOscUpdates(const std::vector& updates) { const auto now = std::chrono::steady_clock::now(); for (const RuntimeLiveOscUpdate& update : updates) { auto overlayIt = mOscOverlayStates.find(update.routeKey); if (overlayIt == mOscOverlayStates.end()) { OscOverlayState overlay; overlay.layerKey = update.layerKey; overlay.parameterKey = update.parameterKey; overlay.targetValue = update.targetValue; overlay.lastUpdatedTime = now; overlay.lastAppliedTime = now; overlay.generation = 1; mOscOverlayStates[update.routeKey] = std::move(overlay); } else { overlayIt->second.targetValue = update.targetValue; overlayIt->second.lastUpdatedTime = now; overlayIt->second.generation += 1; overlayIt->second.commitQueued = false; } } } void RuntimeLiveState::ApplyOscCommitCompletions(const std::vector& completedCommits) { for (const RuntimeLiveOscCommitCompletion& completedCommit : completedCommits) { auto overlayIt = mOscOverlayStates.find(completedCommit.routeKey); if (overlayIt == mOscOverlayStates.end()) continue; OscOverlayState& overlay = overlayIt->second; if (overlay.commitQueued && overlay.pendingCommitGeneration == completedCommit.generation && overlay.generation == completedCommit.generation) { mOscOverlayStates.erase(overlayIt); } } } void RuntimeLiveState::ApplyToLayerStates( std::vector& states, const RuntimeLiveStateApplyOptions& options, std::vector* commitRequests) { if (states.empty() || mOscOverlayStates.empty()) return; const auto now = options.now; const double clampedSmoothing = ClampOscAlpha(options.smoothing); std::vector overlayKeysToRemove; for (auto& item : mOscOverlayStates) { const std::string& routeKey = item.first; OscOverlayState& overlay = item.second; auto stateIt = std::find_if(states.begin(), states.end(), [&overlay](const RuntimeRenderState& state) { return MatchesOscControlKey(state.layerId, overlay.layerKey) || MatchesOscControlKey(state.shaderId, overlay.layerKey) || MatchesOscControlKey(state.shaderName, overlay.layerKey); }); if (stateIt == states.end()) continue; auto definitionIt = std::find_if(stateIt->parameterDefinitions.begin(), stateIt->parameterDefinitions.end(), [&overlay](const ShaderParameterDefinition& definition) { return MatchesOscControlKey(definition.id, overlay.parameterKey) || MatchesOscControlKey(definition.label, overlay.parameterKey); }); if (definitionIt == stateIt->parameterDefinitions.end()) continue; ShaderParameterValue targetValue; std::string normalizeError; if (!NormalizeAndValidateParameterValue(*definitionIt, overlay.targetValue, targetValue, normalizeError)) continue; if (definitionIt->type == ShaderParameterType::Trigger) { ShaderParameterValue& value = stateIt->parameterValues[definitionIt->id]; const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0]; const double triggerTime = stateIt->timeSeconds; value.numberValues = { previousCount + 1.0, triggerTime }; overlayKeysToRemove.push_back(routeKey); continue; } const bool smoothable = clampedSmoothing > 0.0 && (definitionIt->type == ShaderParameterType::Float || definitionIt->type == ShaderParameterType::Vec2 || definitionIt->type == ShaderParameterType::Color); if (!smoothable) { overlay.currentValue = targetValue; overlay.hasCurrentValue = true; stateIt->parameterValues[definitionIt->id] = overlay.currentValue; if (options.allowCommit && !overlay.commitQueued && now - overlay.lastUpdatedTime >= options.commitDelay && commitRequests) { commitRequests->push_back({ routeKey, overlay.layerKey, overlay.parameterKey, overlay.targetValue, overlay.generation }); overlay.pendingCommitGeneration = overlay.generation; overlay.commitQueued = true; } continue; } if (!overlay.hasCurrentValue) { overlay.currentValue = DefaultValueForDefinition(*definitionIt); auto currentIt = stateIt->parameterValues.find(definitionIt->id); if (currentIt != stateIt->parameterValues.end()) overlay.currentValue = currentIt->second; overlay.hasCurrentValue = true; } if (overlay.currentValue.numberValues.size() != targetValue.numberValues.size()) overlay.currentValue.numberValues = targetValue.numberValues; double smoothingAlpha = clampedSmoothing; if (overlay.lastAppliedTime != std::chrono::steady_clock::time_point()) { const double deltaSeconds = std::chrono::duration_cast>(now - overlay.lastAppliedTime).count(); smoothingAlpha = ComputeTimeBasedOscAlpha(clampedSmoothing, deltaSeconds); } overlay.lastAppliedTime = now; ShaderParameterValue nextValue = targetValue; bool converged = true; for (std::size_t index = 0; index < targetValue.numberValues.size(); ++index) { const double currentNumber = overlay.currentValue.numberValues[index]; const double targetNumber = targetValue.numberValues[index]; const double delta = targetNumber - currentNumber; double nextNumber = currentNumber + delta * smoothingAlpha; if (std::fabs(delta) <= 0.0005) nextNumber = targetNumber; else converged = false; nextValue.numberValues[index] = nextNumber; } if (converged) nextValue.numberValues = targetValue.numberValues; overlay.currentValue = nextValue; overlay.hasCurrentValue = true; stateIt->parameterValues[definitionIt->id] = overlay.currentValue; if (options.allowCommit && converged && !overlay.commitQueued && now - overlay.lastUpdatedTime >= options.commitDelay && commitRequests) { commitRequests->push_back({ routeKey, overlay.layerKey, overlay.parameterKey, BuildOscCommitValue(*definitionIt, overlay.currentValue), overlay.generation }); overlay.pendingCommitGeneration = overlay.generation; overlay.commitQueued = true; } } for (const std::string& overlayKey : overlayKeysToRemove) mOscOverlayStates.erase(overlayKey); }