#include "staticSegments.h" #include #include #include "tools/nextCombination.h" using std::vector; int getSyllableCount(const ContinuousTimeline& shapeRules, TimeRange timeRange) { if (timeRange.empty()) return 0; const auto begin = shapeRules.find(timeRange.getStart()); const auto end = std::next(shapeRules.find(timeRange.getEnd(), FindMode::SampleLeft)); // Treat every vowel as one syllable int syllableCount = 0; for (auto it = begin; it != end; ++it) { const ShapeRule shapeRule = it->getValue(); // Disregard phones that are mostly outside the specified time range. const centiseconds phoneMiddle = shapeRule.phoneTiming.getMiddle(); if (phoneMiddle < timeRange.getStart() || phoneMiddle >= timeRange.getEnd()) continue; auto phone = shapeRule.phone; if (phone && isVowel(*phone)) { ++syllableCount; } } return syllableCount; } // A static segment is a prolonged period during which the mouth shape doesn't change vector getStaticSegments( const ContinuousTimeline& shapeRules, const JoiningContinuousTimeline& animation ) { // A static segment must contain a certain number of syllables to look distractingly static const int minSyllableCount = 3; // It must also have a minimum duration. The same number of syllables in fast speech usually // looks good. const centiseconds minDuration = 75_cs; vector result; for (const auto& timedShape : animation) { const TimeRange timeRange = timedShape.getTimeRange(); const bool isStatic = timeRange.getDuration() >= minDuration && getSyllableCount(shapeRules, timeRange) >= minSyllableCount; if (isStatic) { result.push_back(timeRange); } } return result; } // Indicates whether this shape rule can potentially be replaced by a modified version that breaks // up long static segments bool canChange(const ShapeRule& rule) { return rule.phone && isVowel(*rule.phone) && rule.shapeSet.size() == 1; } // Returns a new shape rule that is identical to the specified one, except that it leads to a // slightly different visualization ShapeRule getChangedShapeRule(const ShapeRule& rule) { assert(canChange(rule)); ShapeRule result(rule); // So far, I've only encountered B as a static shape. // If there is ever a problem with another static shape, this function can easily be extended. if (rule.shapeSet == ShapeSet { Shape::B }) { result.shapeSet = { Shape::C }; } return result; } // Contains the start times of all rules to be changed using RuleChanges = vector; // Replaces the indicated shape rules with slightly different ones, breaking up long static segments ContinuousTimeline applyChanges( const ContinuousTimeline& shapeRules, const RuleChanges& changes ) { ContinuousTimeline result(shapeRules); for (centiseconds changedRuleStart : changes) { const Timed timedOriginalRule = *shapeRules.get(changedRuleStart); const ShapeRule changedRule = getChangedShapeRule(timedOriginalRule.getValue()); result.set(timedOriginalRule.getTimeRange(), changedRule); } return result; } class RuleChangeScenario { public: RuleChangeScenario( const ContinuousTimeline& originalRules, const RuleChanges& changes, const AnimationFunction& animate ) : changedRules(applyChanges(originalRules, changes)), animation(animate(changedRules)), staticSegments(getStaticSegments(changedRules, animation)) {} bool isBetterThan(const RuleChangeScenario& rhs) const { // We want zero static segments if (staticSegments.empty() && !rhs.staticSegments.empty()) return true; // Short shapes are better than long ones. Minimize sum-of-squares. if (getSumOfShapeDurationSquares() < rhs.getSumOfShapeDurationSquares()) return true; return false; } int getStaticSegmentCount() const { return static_cast(staticSegments.size()); } ContinuousTimeline getChangedRules() const { return changedRules; } private: ContinuousTimeline changedRules; JoiningContinuousTimeline animation; vector staticSegments; double getSumOfShapeDurationSquares() const { return std::accumulate( animation.begin(), animation.end(), 0.0, [](const double sum, const Timed& timedShape) { const double duration = std::chrono::duration_cast>( timedShape.getDuration() ).count(); return sum + duration * duration; } ); } }; RuleChanges getPossibleRuleChanges(const ContinuousTimeline& shapeRules) { RuleChanges result; for (auto it = shapeRules.begin(); it != shapeRules.end(); ++it) { const ShapeRule rule = it->getValue(); if (canChange(rule)) { result.push_back(it->getStart()); } } return result; } ContinuousTimeline fixStaticSegmentRules( const ContinuousTimeline& shapeRules, const AnimationFunction& animate ) { // The complexity of this function is exponential with the number of replacements. // So let's cap that value. const int maxReplacementCount = 3; // All potential changes const RuleChanges possibleRuleChanges = getPossibleRuleChanges(shapeRules); // Find best solution. Start with a single replacement, then increase as necessary. RuleChangeScenario bestScenario(shapeRules, {}, animate); for ( int replacementCount = 1; bestScenario.getStaticSegmentCount() > 0 && replacementCount <= std::min(static_cast(possibleRuleChanges.size()), maxReplacementCount); ++replacementCount ) { // Only the first elements of `currentRuleChanges` count auto currentRuleChanges(possibleRuleChanges); do { RuleChangeScenario currentScenario( shapeRules, { currentRuleChanges.begin(), currentRuleChanges.begin() + replacementCount }, animate ); if (currentScenario.isBetterThan(bestScenario)) { bestScenario = currentScenario; } } while (next_combination(currentRuleChanges.begin(), currentRuleChanges.begin() + replacementCount, currentRuleChanges.end())); } return bestScenario.getChangedRules(); } // Indicates whether the specified shape rule may result in different shapes depending on context bool isFlexible(const ShapeRule& rule) { return rule.shapeSet.size() > 1; } // Extends the specified time range until it starts and ends with a non-flexible shape rule, if // possible TimeRange extendToFixedRules( const TimeRange& timeRange, const ContinuousTimeline& shapeRules ) { auto first = shapeRules.find(timeRange.getStart()); while (first != shapeRules.begin() && isFlexible(first->getValue())) { --first; } auto last = shapeRules.find(timeRange.getEnd(), FindMode::SampleLeft); while (std::next(last) != shapeRules.end() && isFlexible(last->getValue())) { ++last; } return { first->getStart(), last->getEnd() }; } JoiningContinuousTimeline avoidStaticSegments( const ContinuousTimeline& shapeRules, const AnimationFunction& animate ) { const auto animation = animate(shapeRules); const vector staticSegments = getStaticSegments(shapeRules, animation); if (staticSegments.empty()) { return animation; } // Modify shape rules to eliminate static segments ContinuousTimeline fixedShapeRules(shapeRules); for (const TimeRange& staticSegment : staticSegments) { // Extend time range to the left and right so we don't lose adjacent rules that might // influence the animation const TimeRange extendedStaticSegment = extendToFixedRules(staticSegment, shapeRules); // Fix shape rules within the static segment const auto fixedSegmentShapeRules = fixStaticSegmentRules( { extendedStaticSegment, ShapeRule::getInvalid(), fixedShapeRules }, animate ); for (const auto& timedShapeRule : fixedSegmentShapeRules) { fixedShapeRules.set(timedShapeRule); } } return animate(fixedShapeRules); }