diff --git a/src/animation/animationRules.h b/src/animation/animationRules.h index 8e77a94..04707f4 100644 --- a/src/animation/animationRules.h +++ b/src/animation/animationRules.h @@ -61,4 +61,3 @@ struct ShapeRule { // The resulting timeline will always cover the entire duration of the phone (starting at 0 cs). // It may extend into the negative time range if animation is required prior to the sound being heard. Timeline getShapeRules(Phone phone, centiseconds duration, centiseconds previousDuration); - diff --git a/src/animation/mouthAnimation.cpp b/src/animation/mouthAnimation.cpp index 529e094..ec6297c 100644 --- a/src/animation/mouthAnimation.cpp +++ b/src/animation/mouthAnimation.cpp @@ -20,11 +20,11 @@ using boost::adaptors::transformed; using std::pair; using std::tuple; -Timeline createTweens(ContinuousTimeline shapes) { +JoiningTimeline createTweens(JoiningContinuousTimeline shapes) { centiseconds minTweenDuration = 4_cs; centiseconds maxTweenDuration = 10_cs; - Timeline tweens; + JoiningTimeline tweens; for (auto first = shapes.begin(), second = std::next(shapes.begin()); first != shapes.end() && second != shapes.end(); @@ -66,8 +66,8 @@ Timeline createTweens(ContinuousTimeline shapes) { return tweens; } -Timeline animatePauses(const ContinuousTimeline& shapes) { - Timeline result; +JoiningTimeline animatePauses(const JoiningContinuousTimeline& shapes) { + JoiningTimeline result; // Don't close mouth for short pauses for_each_adjacent(shapes.begin(), shapes.end(), [&](const Timed& lhs, const Timed& pause, const Timed& rhs) { @@ -96,8 +96,8 @@ Timeline animatePauses(const ContinuousTimeline& shapes) { return result; } -template -ContinuousTimeline> boundedTimelinetoContinuousOptional(const BoundedTimeline& timeline) { +template +ContinuousTimeline, AutoJoin> boundedTimelinetoContinuousOptional(const BoundedTimeline& timeline) { return { timeline.getRange(), boost::none, timeline | transformed([](const Timed& timedValue) { return Timed>(timedValue.getTimeRange(), timedValue.getValue()); }) @@ -143,8 +143,8 @@ ContinuousTimeline getShapeRules(const BoundedTimeline& phones // always choosing a shape from the current set that resembles the last shape and is somewhat relaxed. // * When speaking, we anticipate vowels, trying to form their shape before the actual vowel. // So whenever we come across a one-shape set, we backtrack a little, spreating that shape to the left. -ContinuousTimeline animate(const ContinuousTimeline& shapeSets) { - ContinuousTimeline shapes(shapeSets.getRange(), X); +JoiningContinuousTimeline animate(const ContinuousTimeline& shapeSets) { + JoiningContinuousTimeline shapes(shapeSets.getRange(), X); Shape referenceShape = X; // Animate forwards @@ -186,7 +186,7 @@ ContinuousTimeline animate(const ContinuousTimeline& shapeSets) return shapes; } -ContinuousTimeline animate(const BoundedTimeline &phones) { +JoiningContinuousTimeline animate(const BoundedTimeline &phones) { // Create timeline of shape rules ContinuousTimeline shapeRules = getShapeRules(phones); @@ -196,16 +196,16 @@ ContinuousTimeline animate(const BoundedTimeline &phones) { shapeRules | transformed([](const Timed& timedRule) { return Timed(timedRule.getTimeRange(), timedRule.getValue().regularShapes); })); // Animate - ContinuousTimeline shapes = animate(shapeSets); + JoiningContinuousTimeline shapes = animate(shapeSets); // Animate pauses - Timeline pauses = animatePauses(shapes); + JoiningTimeline pauses = animatePauses(shapes); for (const auto& pause : pauses) { shapes.set(pause); } // Create inbetweens for smoother animation - Timeline tweens = createTweens(shapes); + JoiningTimeline tweens = createTweens(shapes); for (const auto& tween : tweens) { shapes.set(tween); } diff --git a/src/animation/mouthAnimation.h b/src/animation/mouthAnimation.h index 3844d23..aca06e7 100644 --- a/src/animation/mouthAnimation.h +++ b/src/animation/mouthAnimation.h @@ -4,4 +4,4 @@ #include "Shape.h" #include "ContinuousTimeline.h" -ContinuousTimeline animate(const BoundedTimeline& phones); +JoiningContinuousTimeline animate(const BoundedTimeline& phones); diff --git a/src/audio/voiceActivityDetection.cpp b/src/audio/voiceActivityDetection.cpp index 354df32..c4dc62a 100644 --- a/src/audio/voiceActivityDetection.cpp +++ b/src/audio/voiceActivityDetection.cpp @@ -9,6 +9,7 @@ #include #include "parallel.h" #include "AudioSegment.h" +#include "stringTools.h" using std::vector; using boost::adaptors::transformed; @@ -16,7 +17,7 @@ using fmt::format; using std::runtime_error; using std::unique_ptr; -BoundedTimeline webRtcDetectVoiceActivity(const AudioClip& audioClip, ProgressSink& progressSink) { +JoiningBoundedTimeline webRtcDetectVoiceActivity(const AudioClip& audioClip, ProgressSink& progressSink) { VadInst* vadHandle = WebRtcVad_Create(); if (!vadHandle) throw runtime_error("Error creating WebRTC VAD handle."); @@ -34,7 +35,7 @@ BoundedTimeline webRtcDetectVoiceActivity(const AudioClip& audioClip, Prog ProgressSink& pass2ProgressSink = progressMerger.addSink(0.3); // Detect activity - BoundedTimeline activity(audioClip.getTruncatedRange()); + JoiningBoundedTimeline activity(audioClip.getTruncatedRange()); centiseconds time = 0_cs; const size_t bufferCapacity = audioClip.getSampleRate() / 100; auto processBuffer = [&](const vector& buffer) { @@ -66,11 +67,11 @@ BoundedTimeline webRtcDetectVoiceActivity(const AudioClip& audioClip, Prog return activity; } -BoundedTimeline detectVoiceActivity(const AudioClip& inputAudioClip, int maxThreadCount, ProgressSink& progressSink) { +JoiningBoundedTimeline detectVoiceActivity(const AudioClip& inputAudioClip, int maxThreadCount, ProgressSink& progressSink) { // Prepare audio for VAD const unique_ptr audioClip = inputAudioClip.clone() | resample(16000) | removeDcOffset(); - BoundedTimeline activity(audioClip->getTruncatedRange()); + JoiningBoundedTimeline activity(audioClip->getTruncatedRange()); std::mutex activityMutex; // Split audio into segments and perform parallel VAD @@ -83,7 +84,7 @@ BoundedTimeline detectVoiceActivity(const AudioClip& inputAudioClip, int m } runParallel([&](const TimeRange& segmentRange, ProgressSink& segmentProgressSink) { unique_ptr audioSegment = audioClip->clone() | segment(segmentRange); - BoundedTimeline activitySegment = webRtcDetectVoiceActivity(*audioSegment, segmentProgressSink); + JoiningBoundedTimeline activitySegment = webRtcDetectVoiceActivity(*audioSegment, segmentProgressSink); std::lock_guard lock(activityMutex); for (auto activityRange : activitySegment) { @@ -102,7 +103,7 @@ BoundedTimeline detectVoiceActivity(const AudioClip& inputAudioClip, int m // Shorten activities. WebRTC adds a bit of buffer at the end. const centiseconds tail(5); - for (const auto& utterance : Timeline(activity)) { + for (const auto& utterance : JoiningBoundedTimeline(activity)) { if (utterance.getDuration() > tail && utterance.getEnd() < audioDuration) { activity.clear(utterance.getEnd() - tail, utterance.getEnd()); } diff --git a/src/audio/voiceActivityDetection.h b/src/audio/voiceActivityDetection.h index df0ffd9..e1aa395 100644 --- a/src/audio/voiceActivityDetection.h +++ b/src/audio/voiceActivityDetection.h @@ -3,4 +3,4 @@ #include #include -BoundedTimeline detectVoiceActivity(const AudioClip& audioClip, int maxThreadCount, ProgressSink& progressSink); +JoiningBoundedTimeline detectVoiceActivity(const AudioClip& audioClip, int maxThreadCount, ProgressSink& progressSink); diff --git a/src/exporters/Exporter.h b/src/exporters/Exporter.h index a88502c..ef7e0ce 100644 --- a/src/exporters/Exporter.h +++ b/src/exporters/Exporter.h @@ -7,5 +7,5 @@ class Exporter { public: virtual ~Exporter() {} - virtual void exportShapes(const boost::filesystem::path& inputFilePath, const ContinuousTimeline& shapes, std::ostream& outputStream) = 0; + virtual void exportShapes(const boost::filesystem::path& inputFilePath, const JoiningContinuousTimeline& shapes, std::ostream& outputStream) = 0; }; diff --git a/src/exporters/JsonExporter.cpp b/src/exporters/JsonExporter.cpp index 093bdec..b0bde51 100644 --- a/src/exporters/JsonExporter.cpp +++ b/src/exporters/JsonExporter.cpp @@ -25,7 +25,7 @@ string escapeJsonString(const string& s) { return result; } -void JsonExporter::exportShapes(const boost::filesystem::path& inputFilePath, const ContinuousTimeline& shapes, std::ostream& outputStream) { +void JsonExporter::exportShapes(const boost::filesystem::path& inputFilePath, const JoiningContinuousTimeline& shapes, std::ostream& outputStream) { // Export as JSON. // I'm not using a library because the code is short enough without one and it lets me control the formatting. outputStream << "{\n"; diff --git a/src/exporters/JsonExporter.h b/src/exporters/JsonExporter.h index 13a43ab..112cb8b 100644 --- a/src/exporters/JsonExporter.h +++ b/src/exporters/JsonExporter.h @@ -4,5 +4,5 @@ class JsonExporter : public Exporter { public: - void exportShapes(const boost::filesystem::path& inputFilePath, const ContinuousTimeline& shapes, std::ostream& outputStream) override; + void exportShapes(const boost::filesystem::path& inputFilePath, const JoiningContinuousTimeline& shapes, std::ostream& outputStream) override; }; diff --git a/src/exporters/TsvExporter.cpp b/src/exporters/TsvExporter.cpp index d594868..f4717b1 100644 --- a/src/exporters/TsvExporter.cpp +++ b/src/exporters/TsvExporter.cpp @@ -1,6 +1,6 @@ #include "TsvExporter.h" -void TsvExporter::exportShapes(const boost::filesystem::path& inputFilePath, const ContinuousTimeline& shapes, std::ostream& outputStream) { +void TsvExporter::exportShapes(const boost::filesystem::path& inputFilePath, const JoiningContinuousTimeline& shapes, std::ostream& outputStream) { UNUSED(inputFilePath); // Output shapes with start times diff --git a/src/exporters/TsvExporter.h b/src/exporters/TsvExporter.h index 0539fff..272d562 100644 --- a/src/exporters/TsvExporter.h +++ b/src/exporters/TsvExporter.h @@ -4,6 +4,6 @@ class TsvExporter : public Exporter { public: - void exportShapes(const boost::filesystem::path& inputFilePath, const ContinuousTimeline& shapes, std::ostream& outputStream) override; + void exportShapes(const boost::filesystem::path& inputFilePath, const JoiningContinuousTimeline& shapes, std::ostream& outputStream) override; }; diff --git a/src/exporters/XmlExporter.cpp b/src/exporters/XmlExporter.cpp index a8717b2..c8cb8db 100644 --- a/src/exporters/XmlExporter.cpp +++ b/src/exporters/XmlExporter.cpp @@ -6,7 +6,7 @@ using std::string; using boost::property_tree::ptree; -void XmlExporter::exportShapes(const boost::filesystem::path& inputFilePath, const ContinuousTimeline& shapes, std::ostream& outputStream) { +void XmlExporter::exportShapes(const boost::filesystem::path& inputFilePath, const JoiningContinuousTimeline& shapes, std::ostream& outputStream) { ptree tree; // Add metadata diff --git a/src/exporters/XmlExporter.h b/src/exporters/XmlExporter.h index d9114de..8257fb3 100644 --- a/src/exporters/XmlExporter.h +++ b/src/exporters/XmlExporter.h @@ -4,5 +4,5 @@ class XmlExporter : public Exporter { public: - void exportShapes(const boost::filesystem::path& inputFilePath, const ContinuousTimeline& shapes, std::ostream& outputStream) override; + void exportShapes(const boost::filesystem::path& inputFilePath, const JoiningContinuousTimeline& shapes, std::ostream& outputStream) override; }; diff --git a/src/exporters/exporterTools.cpp b/src/exporters/exporterTools.cpp index ab20fc7..5c2fcaf 100644 --- a/src/exporters/exporterTools.cpp +++ b/src/exporters/exporterTools.cpp @@ -1,7 +1,7 @@ #include "exporterTools.h" // Makes sure there is at least one mouth shape -std::vector> dummyShapeIfEmpty(const Timeline& shapes) { +std::vector> dummyShapeIfEmpty(const JoiningTimeline& shapes) { std::vector> result; std::copy(shapes.begin(), shapes.end(), std::back_inserter(result)); if (result.empty()) { diff --git a/src/exporters/exporterTools.h b/src/exporters/exporterTools.h index e88a8f1..ef04203 100644 --- a/src/exporters/exporterTools.h +++ b/src/exporters/exporterTools.h @@ -4,4 +4,4 @@ #include "Timeline.h" // Makes sure there is at least one mouth shape -std::vector> dummyShapeIfEmpty(const Timeline& shapes); +std::vector> dummyShapeIfEmpty(const JoiningTimeline& shapes); diff --git a/src/lib/rhubarbLib.cpp b/src/lib/rhubarbLib.cpp index 9cf743e..b01b620 100644 --- a/src/lib/rhubarbLib.cpp +++ b/src/lib/rhubarbLib.cpp @@ -10,14 +10,14 @@ using std::u32string; using boost::filesystem::path; using std::unique_ptr; -ContinuousTimeline animateAudioClip( +JoiningContinuousTimeline animateAudioClip( const AudioClip& audioClip, optional dialog, int maxThreadCount, ProgressSink& progressSink) { BoundedTimeline phones = recognizePhones(audioClip, dialog, maxThreadCount, progressSink); - ContinuousTimeline result = animate(phones); + JoiningContinuousTimeline result = animate(phones); return result; } @@ -29,7 +29,7 @@ unique_ptr createWaveAudioClip(path filePath) { } } -ContinuousTimeline animateWaveFile( +JoiningContinuousTimeline animateWaveFile( path filePath, optional dialog, int maxThreadCount, diff --git a/src/lib/rhubarbLib.h b/src/lib/rhubarbLib.h index e3ecaab..97fcb94 100644 --- a/src/lib/rhubarbLib.h +++ b/src/lib/rhubarbLib.h @@ -6,13 +6,13 @@ #include "ProgressBar.h" #include -ContinuousTimeline animateAudioClip( +JoiningContinuousTimeline animateAudioClip( const AudioClip& audioClip, boost::optional dialog, int maxThreadCount, ProgressSink& progressSink); -ContinuousTimeline animateWaveFile( +JoiningContinuousTimeline animateWaveFile( boost::filesystem::path filePath, boost::optional dialog, int maxThreadCount, diff --git a/src/main.cpp b/src/main.cpp index cc3cc4e..4bdb4bb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -125,7 +125,7 @@ int main(int argc, char *argv[]) { vector(argv, argv + argc) | transformed([](char* arg) { return fmt::format("\"{}\"", arg); }), " ")); std::cerr << "Processing input file. "; - ContinuousTimeline animation(TimeRange::zero(), Shape::X); + JoiningContinuousTimeline animation(TimeRange::zero(), Shape::X); { ProgressBar progressBar; diff --git a/src/recognition/phoneRecognition.cpp b/src/recognition/phoneRecognition.cpp index 5e6ab92..30188b0 100644 --- a/src/recognition/phoneRecognition.cpp +++ b/src/recognition/phoneRecognition.cpp @@ -314,8 +314,8 @@ lambda_unique_ptr createDecoder(optional dialog) { return decoder; } -Timeline getNoiseSounds(TimeRange utteranceTimeRange, const Timeline& phones) { - Timeline noiseSounds; +JoiningTimeline getNoiseSounds(TimeRange utteranceTimeRange, const Timeline& phones) { + JoiningTimeline noiseSounds; // Find utterance parts without recogniced phones noiseSounds.set(utteranceTimeRange); @@ -325,7 +325,7 @@ Timeline getNoiseSounds(TimeRange utteranceTimeRange, const Timeline(noiseSounds)) { + for (const auto& unknownSound : JoiningTimeline(noiseSounds)) { bool startsAtZero = unknownSound.getStart() == 0_cs; bool tooShort = unknownSound.getDuration() < minSoundDuration; if (startsAtZero || tooShort) { @@ -386,7 +386,7 @@ Timeline utteranceToPhones( for (const auto& timedWord : words) { wordIds.push_back(getWordId(timedWord.getValue(), *decoder.dict)); } - if (wordIds.empty()) return Timeline(); + if (wordIds.empty()) return {}; // Align the words' phones with speech #if BOOST_VERSION < 105600 // Support legacy syntax @@ -403,7 +403,7 @@ Timeline utteranceToPhones( } // Guess positions of noise sounds - Timeline noiseSounds = getNoiseSounds(utteranceTimeRange, utterancePhones); + JoiningTimeline noiseSounds = getNoiseSounds(utteranceTimeRange, utterancePhones); for (const auto& noiseSound : noiseSounds) { utterancePhones.set(noiseSound.getTimeRange(), Phone::Noise); } @@ -430,7 +430,7 @@ BoundedTimeline recognizePhones( const unique_ptr audioClip = inputAudioClip.clone() | removeDcOffset(); // Split audio into utterances - BoundedTimeline utterances; + JoiningBoundedTimeline utterances; try { utterances = detectVoiceActivity(*audioClip, maxThreadCount, voiceActivationProgressSink); } diff --git a/src/time/BoundedTimeline.h b/src/time/BoundedTimeline.h index f70baf8..b84bd0d 100644 --- a/src/time/BoundedTimeline.h +++ b/src/time/BoundedTimeline.h @@ -2,14 +2,14 @@ #include "Timeline.h" -template -class BoundedTimeline : public Timeline { - using typename Timeline::time_type; - using Timeline::equals; +template +class BoundedTimeline : public Timeline { + using typename Timeline::time_type; + using Timeline::equals; public: - using typename Timeline::iterator; - using Timeline::end; + using typename Timeline::iterator; + using Timeline::end; BoundedTimeline() : range(TimeRange::zero()) @@ -25,7 +25,7 @@ public: { for (auto it = first; it != last; ++it) { // Virtual function call in constructor. Derived constructors shouldn't call this one! - BoundedTimeline::set(*it); + BoundedTimeline::set(*it); } } @@ -42,7 +42,7 @@ public: return range; } - using Timeline::set; + using Timeline::set; iterator set(Timed timedValue) override { // Exit if the value's range is completely out of bounds @@ -54,16 +54,16 @@ public: TimeRange& valueRange = timedValue.getTimeRange(); valueRange.resize(max(range.getStart(), valueRange.getStart()), min(range.getEnd(), valueRange.getEnd())); - return Timeline::set(timedValue); + return Timeline::set(timedValue); } void shift(time_type offset) override { - Timeline::shift(offset); + Timeline::shift(offset); range.shift(offset); } bool operator==(const BoundedTimeline& rhs) const { - return Timeline::equals(rhs) && range == rhs.range; + return Timeline::equals(rhs) && range == rhs.range; } bool operator!=(const BoundedTimeline& rhs) const { @@ -72,4 +72,7 @@ public: private: TimeRange range; -}; \ No newline at end of file +}; + +template +using JoiningBoundedTimeline = BoundedTimeline; diff --git a/src/time/ContinuousTimeline.h b/src/time/ContinuousTimeline.h index 7384db7..0935166 100644 --- a/src/time/ContinuousTimeline.h +++ b/src/time/ContinuousTimeline.h @@ -2,16 +2,16 @@ #include "BoundedTimeline.h" -template -class ContinuousTimeline : public BoundedTimeline { +template +class ContinuousTimeline : public BoundedTimeline { public: ContinuousTimeline(TimeRange range, T defaultValue) : - BoundedTimeline(range), + BoundedTimeline(range), defaultValue(defaultValue) { // Virtual function call in constructor. Derived constructors shouldn't call this one! - ContinuousTimeline::clear(range); + ContinuousTimeline::clear(range); } template @@ -20,7 +20,7 @@ public: { // Virtual function calls in constructor. Derived constructors shouldn't call this one! for (auto it = first; it != last; ++it) { - ContinuousTimeline::set(*it); + ContinuousTimeline::set(*it); } } @@ -33,12 +33,15 @@ public: ContinuousTimeline(range, defaultValue, initializerList.begin(), initializerList.end()) {} - using BoundedTimeline::clear; + using BoundedTimeline::clear; void clear(const TimeRange& range) override { - BoundedTimeline::set(Timed(range, defaultValue)); + BoundedTimeline::set(Timed(range, defaultValue)); } private: T defaultValue; }; + +template +using JoiningContinuousTimeline = ContinuousTimeline; diff --git a/src/time/Timeline.h b/src/time/Timeline.h index b36f69b..6473c5d 100644 --- a/src/time/Timeline.h +++ b/src/time/Timeline.h @@ -26,7 +26,7 @@ namespace internal { } } -template +template class Timeline { public: using time_type = TimeRange::time_type; @@ -94,7 +94,7 @@ public: Timeline(InputIterator first, InputIterator last) { for (auto it = first; it != last; ++it) { // Virtual function call in constructor. Derived constructors don't call this one. - Timeline::set(*it); + Timeline::set(*it); } } @@ -200,14 +200,16 @@ public: return end(); } - // Extend the timed value if it touches elements with equal value - iterator elementBefore = find(timedValue.getStart(), FindMode::SampleLeft); - if (elementBefore != end() && ::internal::valueEquals(*elementBefore, timedValue)) { - timedValue.getTimeRange().resize(elementBefore->getStart(), timedValue.getEnd()); - } - iterator elementAfter = find(timedValue.getEnd(), FindMode::SampleRight); - if (elementAfter != end() && ::internal::valueEquals(*elementAfter, timedValue)) { - timedValue.getTimeRange().resize(timedValue.getStart(), elementAfter->getEnd()); + if (AutoJoin) { + // Extend the timed value if it touches elements with equal value + iterator elementBefore = find(timedValue.getStart(), FindMode::SampleLeft); + if (elementBefore != end() && ::internal::valueEquals(*elementBefore, timedValue)) { + timedValue.getTimeRange().resize(elementBefore->getStart(), timedValue.getEnd()); + } + iterator elementAfter = find(timedValue.getEnd(), FindMode::SampleRight); + if (elementAfter != end() && ::internal::valueEquals(*elementAfter, timedValue)) { + timedValue.getTimeRange().resize(timedValue.getStart(), elementAfter->getEnd()); + } } // Erase overlapping elements @@ -242,6 +244,26 @@ public: return ReferenceWrapper(*this, time); } + // Combines adjacent equal elements into one + template> + void joinAdjacent() { + Timeline copy(*this); + for (auto it = copy.begin(); it != copy.end(); ++it) { + const auto rangeBegin = it; + auto rangeEnd = std::next(rangeBegin); + while (rangeEnd != copy.end() && rangeEnd->getStart() == rangeBegin->getEnd() && ::internal::valueEquals(*rangeEnd, *rangeBegin)) { + ++rangeEnd; + } + + if (rangeEnd != std::next(rangeBegin)) { + Timed combined = *rangeBegin; + combined.setTimeRange({rangeBegin->getStart(), rangeEnd->getEnd()}); + set(combined); + it = rangeEnd; + } + } + } + virtual void shift(time_type offset) { if (offset == time_type::zero()) return; @@ -290,7 +312,10 @@ private: }; template -std::ostream& operator<<(std::ostream& stream, const Timeline& timeline) { +using JoiningTimeline = Timeline; + +template +std::ostream& operator<<(std::ostream& stream, const Timeline& timeline) { stream << "Timeline{"; bool isFirst = true; for (auto element : timeline) { diff --git a/tests/TimelineTests.cpp b/tests/TimelineTests.cpp index c82b989..39724f9 100644 --- a/tests/TimelineTests.cpp +++ b/tests/TimelineTests.cpp @@ -231,17 +231,10 @@ void testSetter(std::function&, Timeline&)> set) { } // Check timeline via iterators - Timed lastElement(centiseconds::min(), centiseconds::min(), std::numeric_limits::min()); for (const auto& element : timeline) { // No element shound have zero-length EXPECT_LT(0_cs, element.getDuration()); - // No two adjacent elements should have the same value; they should have been merged - if (element.getStart() == lastElement.getEnd()) { - EXPECT_NE(lastElement.getValue(), element.getValue()); - } - lastElement = element; - // Element should match expected values for (centiseconds t = std::max(centiseconds::zero(), element.getStart()); t < element.getEnd(); ++t) { optional expectedValue = expectedValues[t.count()]; @@ -300,6 +293,51 @@ TEST(Timeline, indexer_set) { }); } +TEST(Timeline, joinAdjacent) { + Timeline timeline{ + {1_cs, 2_cs, 1}, + {2_cs, 4_cs, 2}, + {3_cs, 6_cs, 2}, + {6_cs, 7_cs, 2}, + // Gap + {8_cs, 10_cs, 2}, + {11_cs, 12_cs, 3} + }; + EXPECT_EQ(6, timeline.size()); + timeline.joinAdjacent(); + EXPECT_EQ(4, timeline.size()); + + Timed expectedJoined[] = { + {1_cs, 2_cs, 1}, + {2_cs, 7_cs, 2}, + // Gap + {8_cs, 10_cs, 2}, + {11_cs, 12_cs, 3} + }; + EXPECT_THAT(timeline, ElementsAreArray(expectedJoined)); +} + +TEST(Timeline, autoJoin) { + JoiningTimeline timeline{ + {1_cs, 2_cs, 1}, + {2_cs, 4_cs, 2}, + {3_cs, 6_cs, 2}, + {6_cs, 7_cs, 2}, + // Gap + {8_cs, 10_cs, 2}, + {11_cs, 12_cs, 3} + }; + Timed expectedJoined[] = { + {1_cs, 2_cs, 1}, + {2_cs, 7_cs, 2}, + // Gap + {8_cs, 10_cs, 2}, + {11_cs, 12_cs, 3} + }; + EXPECT_EQ(4, timeline.size()); + EXPECT_THAT(timeline, ElementsAreArray(expectedJoined)); +} + TEST(Timeline, shift) { Timeline timeline{ { 1_cs, 2_cs, 1 },{ 2_cs, 5_cs, 2 },{ 7_cs, 9_cs, 3 } }; Timeline expected{ { 3_cs, 4_cs, 1 },{ 4_cs, 7_cs, 2 },{ 9_cs, 11_cs, 3 } };