Added --extendedShapes command-line parameter

This commit is contained in:
Daniel Wolf 2016-12-21 22:30:38 +01:00 committed by Daniel Wolf
parent 9712483a75
commit a8df4ac4f5
20 changed files with 140 additions and 24 deletions

View File

@ -229,6 +229,8 @@ add_library(rhubarb-animation
src/animation/shapeRule.cpp
src/animation/shapeRule.h
src/animation/shapeShorthands.h
src/animation/targetShapeSet.cpp
src/animation/targetShapeSet.h
src/animation/timingOptimization.cpp
src/animation/timingOptimization.h
src/animation/tweening.cpp
@ -298,6 +300,7 @@ add_library(rhubarb-exporters
)
target_include_directories(rhubarb-exporters PUBLIC "src/exporters")
target_link_libraries(rhubarb-exporters
rhubarb-animation
rhubarb-core
rhubarb-time
)

View File

@ -11,10 +11,6 @@ Shape getBasicShape(Shape shape);
// Returns the mouth shape that results from relaxing the specified shape.
Shape relax(Shape shape);
// A set of mouth shapes that can be used to represent a certain sound.
// The actual selection will be performed based on similarity with the previous or next shape.
using ShapeSet = std::set<Shape>;
// Gets the shape from a non-empty set of shapes that most closely resembles a reference shape.
Shape getClosestShape(Shape reference, ShapeSet shapes);

View File

@ -5,16 +5,23 @@
#include "pauseAnimation.h"
#include "tweening.h"
#include "timingOptimization.h"
#include "targetShapeSet.h"
JoiningContinuousTimeline<Shape> animate(const BoundedTimeline<Phone> &phones) {
JoiningContinuousTimeline<Shape> animate(const BoundedTimeline<Phone> &phones, const ShapeSet& targetShapeSet) {
// Create timeline of shape rules
const ContinuousTimeline<ShapeRule> shapeRules = getShapeRules(phones);
ContinuousTimeline<ShapeRule> shapeRules = getShapeRules(phones);
// Modify shape rules to only contain allowed shapes -- plus X, which is needed for pauses and will be replaced later
ShapeSet targetShapeSetPlusX = targetShapeSet;
targetShapeSetPlusX.insert(Shape::X);
shapeRules = convertToTargetShapeSet(shapeRules, targetShapeSetPlusX);
// Animate in multiple steps
JoiningContinuousTimeline<Shape> animation = animateRough(shapeRules);
animation = optimizeTiming(animation);
animation = animatePauses(animation);
animation = insertTweens(animation);
animation = convertToTargetShapeSet(animation, targetShapeSet);
for (const auto& timedShape : animation) {
logTimedEvent("shape", timedShape);

View File

@ -3,5 +3,6 @@
#include "Phone.h"
#include "Shape.h"
#include "ContinuousTimeline.h"
#include "targetShapeSet.h"
JoiningContinuousTimeline<Shape> animate(const BoundedTimeline<Phone>& phones);
JoiningContinuousTimeline<Shape> animate(const BoundedTimeline<Phone>& phones, const ShapeSet& targetShapeSet);

View File

@ -0,0 +1,38 @@
#include "targetShapeSet.h"
Shape convertToTargetShapeSet(Shape shape, const ShapeSet& targetShapeSet) {
if (targetShapeSet.find(shape) != targetShapeSet.end()) {
return shape;
}
Shape basicShape = getBasicShape(shape);
if (targetShapeSet.find(basicShape) == targetShapeSet.end()) {
throw std::invalid_argument(fmt::format("Target shape set must contain basic shape {}.", basicShape));
}
return basicShape;
}
ShapeSet convertToTargetShapeSet(const ShapeSet& shapes, const ShapeSet& targetShapeSet) {
ShapeSet result;
for (Shape shape : shapes) {
result.insert(convertToTargetShapeSet(shape, targetShapeSet));
}
return result;
}
ContinuousTimeline<ShapeRule> convertToTargetShapeSet(const ContinuousTimeline<ShapeRule>& shapeRules, const ShapeSet& targetShapeSet) {
ContinuousTimeline<ShapeRule> result(shapeRules);
for (const auto& timedShapeRule : shapeRules) {
ShapeRule rule = timedShapeRule.getValue();
std::get<ShapeSet>(rule) = convertToTargetShapeSet(std::get<ShapeSet>(rule), targetShapeSet);
result.set(timedShapeRule.getTimeRange(), rule);
}
return result;
}
JoiningContinuousTimeline<Shape> convertToTargetShapeSet(const JoiningContinuousTimeline<Shape>& shapes, const ShapeSet& targetShapeSet) {
JoiningContinuousTimeline<Shape> result(shapes);
for (const auto& timedShape : shapes) {
result.set(timedShape.getTimeRange(), convertToTargetShapeSet(timedShape.getValue(), targetShapeSet));
}
return result;
}

View File

@ -0,0 +1,16 @@
#pragma once
#include "Shape.h"
#include "shapeRule.h"
// Returns the closest shape to the specified one that occurs in the target shape set.
Shape convertToTargetShapeSet(Shape shape, const ShapeSet& targetShapeSet);
// Replaces each shape in the specified set with the closest shape that occurs in the target shape set.
ShapeSet convertToTargetShapeSet(const ShapeSet& shapes, const ShapeSet& targetShapeSet);
// Replaces each shape in each rule with the closest shape that occurs in the target shape set.
ContinuousTimeline<ShapeRule> convertToTargetShapeSet(const ContinuousTimeline<ShapeRule>& shapeRules, const ShapeSet& targetShapeSet);
// Replaces each shape in the specified timeline with the closest shape that occurs in the target shape set.
JoiningContinuousTimeline<Shape> convertToTargetShapeSet(const JoiningContinuousTimeline<Shape>& shapes, const ShapeSet& targetShapeSet);

View File

@ -1,12 +1,35 @@
#include "Shape.h"
using std::string;
using std::set;
ShapeConverter& ShapeConverter::get() {
static ShapeConverter converter;
return converter;
}
set<Shape> ShapeConverter::getBasicShapes() {
static const set<Shape> result = [] {
set<Shape> result;
for (int i = 0; i <= static_cast<int>(Shape::LastBasicShape); ++i) {
result.insert(static_cast<Shape>(i));
}
return result;
}();
return result;
}
set<Shape> ShapeConverter::getExtendedShapes() {
static const set<Shape> result = [] {
set<Shape> result;
for (int i = static_cast<int>(Shape::LastBasicShape) + 1; i < static_cast<int>(Shape::EndSentinel); ++i) {
result.insert(static_cast<Shape>(i));
}
return result;
}();
return result;
}
string ShapeConverter::getTypeName() {
return "Shape";
}

View File

@ -1,6 +1,7 @@
#pragma once
#include "EnumConverter.h"
#include <set>
// The classic Hanna-Barbera mouth shapes A-F phus the common supplements G-H
// For reference, see http://sunewatts.dk/lipsync/lipsync/article_02.php
@ -14,6 +15,7 @@ enum class Shape {
D, // Mouth wide open (vowels like f[a]ther, b[a]t, wh[y])
E, // Rounded mouth (vowels like [o]ff)
F, // Puckered lips (y[ou], b[o]y, [w]ay)
LastBasicShape = F,
// Extended shapes
@ -27,6 +29,8 @@ enum class Shape {
class ShapeConverter : public EnumConverter<Shape> {
public:
static ShapeConverter& get();
std::set<Shape> getBasicShapes();
std::set<Shape> getExtendedShapes();
protected:
std::string getTypeName() override;
member_data getMemberData() override;
@ -38,4 +42,9 @@ std::istream& operator>>(std::istream& stream, Shape& value);
inline bool isClosed(Shape shape) {
return shape == Shape::A || shape == Shape::X;
}
}
// A set of mouth shapes.
// This may be used to represent all shapes that can be used to represent a certain sound.
// Alternatively, it can represent all shapes the user wants to allow as program output.
using ShapeSet = std::set<Shape>;

View File

@ -7,5 +7,5 @@
class Exporter {
public:
virtual ~Exporter() {}
virtual void exportShapes(const boost::filesystem::path& inputFilePath, const JoiningContinuousTimeline<Shape>& shapes, std::ostream& outputStream) = 0;
virtual void exportShapes(const boost::filesystem::path& inputFilePath, const JoiningContinuousTimeline<Shape>& shapes, const ShapeSet& targetShapeSet, std::ostream& outputStream) = 0;
};

View File

@ -25,7 +25,7 @@ string escapeJsonString(const string& s) {
return result;
}
void JsonExporter::exportShapes(const boost::filesystem::path& inputFilePath, const JoiningContinuousTimeline<Shape>& shapes, std::ostream& outputStream) {
void JsonExporter::exportShapes(const boost::filesystem::path& inputFilePath, const JoiningContinuousTimeline<Shape>& shapes, const ShapeSet& targetShapeSet, 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";
@ -35,7 +35,7 @@ void JsonExporter::exportShapes(const boost::filesystem::path& inputFilePath, co
outputStream << " },\n";
outputStream << " \"mouthCues\": [\n";
bool isFirst = true;
for (auto& timedShape : dummyShapeIfEmpty(shapes)) {
for (auto& timedShape : dummyShapeIfEmpty(shapes, targetShapeSet)) {
if (!isFirst) outputStream << ",\n";
isFirst = false;
outputStream << " { \"start\": " << formatDuration(timedShape.getStart())

View File

@ -4,5 +4,5 @@
class JsonExporter : public Exporter {
public:
void exportShapes(const boost::filesystem::path& inputFilePath, const JoiningContinuousTimeline<Shape>& shapes, std::ostream& outputStream) override;
void exportShapes(const boost::filesystem::path& inputFilePath, const JoiningContinuousTimeline<Shape>& shapes, const ShapeSet& targetShapeSet, std::ostream& outputStream) override;
};

View File

@ -1,6 +1,7 @@
#include "TsvExporter.h"
#include "targetShapeSet.h"
void TsvExporter::exportShapes(const boost::filesystem::path& inputFilePath, const JoiningContinuousTimeline<Shape>& shapes, std::ostream& outputStream) {
void TsvExporter::exportShapes(const boost::filesystem::path& inputFilePath, const JoiningContinuousTimeline<Shape>& shapes, const ShapeSet& targetShapeSet, std::ostream& outputStream) {
UNUSED(inputFilePath);
// Output shapes with start times
@ -9,5 +10,5 @@ void TsvExporter::exportShapes(const boost::filesystem::path& inputFilePath, con
}
// Output closed mouth with end time
outputStream << formatDuration(shapes.getRange().getEnd()) << "\t" << Shape::X << "\n";
outputStream << formatDuration(shapes.getRange().getEnd()) << "\t" << convertToTargetShapeSet(Shape::X, targetShapeSet) << "\n";
}

View File

@ -4,6 +4,6 @@
class TsvExporter : public Exporter {
public:
void exportShapes(const boost::filesystem::path& inputFilePath, const JoiningContinuousTimeline<Shape>& shapes, std::ostream& outputStream) override;
void exportShapes(const boost::filesystem::path& inputFilePath, const JoiningContinuousTimeline<Shape>& shapes, const ShapeSet& targetShapeSet, std::ostream& outputStream) override;
};

View File

@ -6,7 +6,7 @@
using std::string;
using boost::property_tree::ptree;
void XmlExporter::exportShapes(const boost::filesystem::path& inputFilePath, const JoiningContinuousTimeline<Shape>& shapes, std::ostream& outputStream) {
void XmlExporter::exportShapes(const boost::filesystem::path& inputFilePath, const JoiningContinuousTimeline<Shape>& shapes, const ShapeSet& targetShapeSet, std::ostream& outputStream) {
ptree tree;
// Add metadata
@ -14,7 +14,7 @@ void XmlExporter::exportShapes(const boost::filesystem::path& inputFilePath, con
tree.put("rhubarbResult.metadata.duration", formatDuration(shapes.getRange().getDuration()));
// Add mouth cues
for (auto& timedShape : dummyShapeIfEmpty(shapes)) {
for (auto& timedShape : dummyShapeIfEmpty(shapes, targetShapeSet)) {
ptree& mouthCueElement = tree.add("rhubarbResult.mouthCues.mouthCue", timedShape.getValue());
mouthCueElement.put("<xmlattr>.start", formatDuration(timedShape.getStart()));
mouthCueElement.put("<xmlattr>.end", formatDuration(timedShape.getEnd()));

View File

@ -4,5 +4,5 @@
class XmlExporter : public Exporter {
public:
void exportShapes(const boost::filesystem::path& inputFilePath, const JoiningContinuousTimeline<Shape>& shapes, std::ostream& outputStream) override;
void exportShapes(const boost::filesystem::path& inputFilePath, const JoiningContinuousTimeline<Shape>& shapes, const ShapeSet& targetShapeSet, std::ostream& outputStream) override;
};

View File

@ -1,12 +1,13 @@
#include "exporterTools.h"
#include "targetShapeSet.h"
// Makes sure there is at least one mouth shape
std::vector<Timed<Shape>> dummyShapeIfEmpty(const JoiningTimeline<Shape>& shapes) {
std::vector<Timed<Shape>> dummyShapeIfEmpty(const JoiningTimeline<Shape>& shapes, const ShapeSet& targetShapeSet) {
std::vector<Timed<Shape>> result;
std::copy(shapes.begin(), shapes.end(), std::back_inserter(result));
if (result.empty()) {
// Add zero-length empty mouth
result.push_back(Timed<Shape>(0_cs, 0_cs, Shape::X));
result.push_back(Timed<Shape>(0_cs, 0_cs, convertToTargetShapeSet(Shape::X, targetShapeSet)));
}
return result;
}

View File

@ -4,4 +4,4 @@
#include "Timeline.h"
// Makes sure there is at least one mouth shape
std::vector<Timed<Shape>> dummyShapeIfEmpty(const JoiningTimeline<Shape>& shapes);
std::vector<Timed<Shape>> dummyShapeIfEmpty(const JoiningTimeline<Shape>& shapes, const ShapeSet& targetShapeSet);

View File

@ -13,11 +13,12 @@ using std::unique_ptr;
JoiningContinuousTimeline<Shape> animateAudioClip(
const AudioClip& audioClip,
optional<u32string> dialog,
const ShapeSet& targetShapeSet,
int maxThreadCount,
ProgressSink& progressSink)
{
BoundedTimeline<Phone> phones = recognizePhones(audioClip, dialog, maxThreadCount, progressSink);
JoiningContinuousTimeline<Shape> result = animate(phones);
JoiningContinuousTimeline<Shape> result = animate(phones, targetShapeSet);
return result;
}
@ -32,8 +33,9 @@ unique_ptr<AudioClip> createWaveAudioClip(path filePath) {
JoiningContinuousTimeline<Shape> animateWaveFile(
path filePath,
optional<u32string> dialog,
const ShapeSet& targetShapeSet,
int maxThreadCount,
ProgressSink& progressSink)
{
return animateAudioClip(*createWaveAudioClip(filePath), dialog, maxThreadCount, progressSink);
return animateAudioClip(*createWaveAudioClip(filePath), dialog, targetShapeSet, maxThreadCount, progressSink);
}

View File

@ -5,15 +5,18 @@
#include "AudioClip.h"
#include "ProgressBar.h"
#include <boost/filesystem.hpp>
#include "targetShapeSet.h"
JoiningContinuousTimeline<Shape> animateAudioClip(
const AudioClip& audioClip,
boost::optional<std::u32string> dialog,
const ShapeSet& targetShapeSet,
int maxThreadCount,
ProgressSink& progressSink);
JoiningContinuousTimeline<Shape> animateWaveFile(
boost::filesystem::path filePath,
boost::optional<std::u32string> dialog,
const ShapeSet& targetShapeSet,
int maxThreadCount,
ProgressSink& progressSink);

View File

@ -22,6 +22,7 @@
#include "JsonExporter.h"
#include <boost/iostreams/stream.hpp>
#include <boost/iostreams/device/null.hpp>
#include "targetShapeSet.h"
using std::exception;
using std::string;
@ -81,6 +82,18 @@ unique_ptr<Exporter> createExporter(ExportFormat exportFormat) {
}
}
ShapeSet getTargetShapeSet(const string& extendedShapesString) {
// All basic shapes are mandatory
ShapeSet result(ShapeConverter::get().getBasicShapes());
// Add any extended shapes
for (char ch : extendedShapesString) {
Shape shape = ShapeConverter::get().parse(string(1, ch));
result.insert(shape);
}
return result;
}
int main(int argc, char *argv[]) {
auto pausableStderrSink = addPausableStdErrSink(logging::Level::Warn);
pausableStderrSink->pause();
@ -96,6 +109,7 @@ int main(int argc, char *argv[]) {
tclap::ValueArg<string> logFileName("", "logFile", "The log file path.", false, string(), "string", cmd);
tclap::SwitchArg quietMode("q", "quiet", "Suppresses all output to stderr except for error messages.", cmd, false);
tclap::ValueArg<int> maxThreadCount("", "threads", "The maximum number of worker threads to use.", false, getProcessorCoreCount(), "number", cmd);
tclap::ValueArg<string> extendedShapes("", "extendedShapes", "All extended, optional shapes to use.", false, "GHX", "string", cmd);
tclap::ValueArg<string> dialogFile("d", "dialogFile", "A file containing the text of the dialog.", false, string(), "string", cmd);
auto exportFormats = vector<ExportFormat>(ExportFormatConverter::get().getValues());
tclap::ValuesConstraint<ExportFormat> exportFormatConstraint(exportFormats);
@ -120,6 +134,7 @@ int main(int argc, char *argv[]) {
throw std::runtime_error("Thread count must be 1 or higher.");
}
path inputFilePath(inputFileName.getValue());
ShapeSet targetShapeSet = getTargetShapeSet(extendedShapes.getValue());
// Set up log file
if (logFileName.isSet()) {
@ -140,6 +155,7 @@ int main(int argc, char *argv[]) {
animation = animateWaveFile(
inputFilePath,
dialogFile.isSet() ? readUtf8File(path(dialogFile.getValue())) : boost::optional<u32string>(),
targetShapeSet,
maxThreadCount.getValue(),
progressBar);
}
@ -147,7 +163,7 @@ int main(int argc, char *argv[]) {
// Export animation
unique_ptr<Exporter> exporter = createExporter(exportFormat.getValue());
exporter->exportShapes(inputFilePath, animation, std::cout);
exporter->exportShapes(inputFilePath, animation, targetShapeSet, std::cout);
logging::info("Exiting application normally.");
} catch (...) {