Added --extendedShapes command-line parameter
This commit is contained in:
parent
9712483a75
commit
a8df4ac4f5
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
18
src/main.cpp
18
src/main.cpp
|
@ -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 (...) {
|
||||
|
|
Loading…
Reference in New Issue