diff --git a/CHANGELOG.md b/CHANGELOG.md index 06669ab..da932ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +* **Added** switch data file exporter for Moho (formerly Anime Studio) and OpenToonz ([issue #69](https://github.com/DanielSWolf/rhubarb-lip-sync/issues/69)) * **Improved** animation rule for OW sound: animating it as E-F rather than F. ## Version 1.9.1 diff --git a/README.adoc b/README.adoc index 5ec1dbf..f166a1c 100644 --- a/README.adoc +++ b/README.adoc @@ -23,6 +23,7 @@ Rhubarb Lip Sync allows you to quickly create 2D mouth animation from voice reco Rhubarb Lip Sync integrates with the following applications: * *Adobe After Effects* (see <>) +* *Moho* and *OpenToonz* (see <>) * *Spine* by Esoteric Software (see <>) * *Vegas Pro* by Magix (see <>) * *Visionaire Studio* (see https://www.visionaire-studio.net/forum/thread/mouth-animation-using-rhubarb-lip-sync[external link]) @@ -44,6 +45,13 @@ You can use Rhubarb Lip Sync to animate dialog right from Adobe After Effects. F image:img/after-effects.png[] +[[moho]] +=== Moho and OpenToonz + +Rhubarb Lip Sync can create .dat switch data files, which are understood by Moho and OpenToonz. You can set the frame rate using the `--datFrameRate` option; to control the shape names, use the `--datUsePrestonBlair` flag. For more details, see <>. + +image:img/moho.png[] + [[spine]] === Spine by Esoteric Software @@ -123,7 +131,7 @@ Rhubarb Lip Sync is a command-line tool that is currently available for Windows The following command-line options are the most common: -[cols="2,5"] +[cols="2,5a"] |=== | Option | Description @@ -136,7 +144,7 @@ The following command-line options are the most common: _Default value: ``pocketSphinx``_ | `-f` __, `--exportFormat` __ -| The export format. Options: `tsv` (tab-separated values, see <>), `xml` (see <>), `json` (see <>). +| The export format. Options: `tsv` (tab-separated values, see <>), `xml` (see <>), `json` (see <>), `dat` (see <>). _Default value: ``tsv``_ @@ -161,6 +169,33 @@ _Default value: ``GHX``_ | `-h`, `--help` | Displays usage information and exits. + +| `--datFrameRate` _number_ +| Only valid when using the `dat` export format. Controls the frame rate for the output file. + +_Default value: 24_ + +| `--datUsePrestonBlair` +| Only valid when using the `dat` export format. Uses Preston Blair mouth shapes names instead of the default alphabetical ones. This applies the following mapping: + +!=== +! Alphabetic name ! Preston Blair name + +! A ! MBP +! B ! etc +! C ! E +! D ! AI +! E ! O +! F ! U +! G ! FV +! H ! L +! X ! rest +!=== + +CAUTION: This mapping is only applied when exporting, _after_ the recording has been animated. To control which mouth shapes to use, use the <> option _with the alphabetic names_. + +TIP: For optimal results, make sure your mouth drawings follow the guidelines in the <> section. This is easier if you stick to the alphabetic names instead of the Preston Blair names. The only situation where you _need_ to use the Preston Blair names is when you're using OpenToonz, because OpenToonz only supports the Preston Blair names. + |=== ==== Advanced command-line options ==== diff --git a/img/moho.png b/img/moho.png new file mode 100644 index 0000000..98d2404 Binary files /dev/null and b/img/moho.png differ diff --git a/rhubarb/CMakeLists.txt b/rhubarb/CMakeLists.txt index 41507c3..17b257f 100644 --- a/rhubarb/CMakeLists.txt +++ b/rhubarb/CMakeLists.txt @@ -360,6 +360,8 @@ target_link_libraries(rhubarb-core # ... rhubarb-exporters add_library(rhubarb-exporters + src/exporters/DatExporter.cpp + src/exporters/DatExporter.h src/exporters/Exporter.h src/exporters/exporterTools.cpp src/exporters/exporterTools.h diff --git a/rhubarb/src/exporters/DatExporter.cpp b/rhubarb/src/exporters/DatExporter.cpp new file mode 100644 index 0000000..cf3bf3f --- /dev/null +++ b/rhubarb/src/exporters/DatExporter.cpp @@ -0,0 +1,72 @@ +#include "DatExporter.h" +#include "animation/targetShapeSet.h" +#include + +using std::chrono::duration; +using std::chrono::duration_cast; +using std::string; + +DatExporter::DatExporter(const ShapeSet& targetShapeSet, double frameRate, bool convertToPrestonBlair) : + frameRate(frameRate), + convertToPrestonBlair(convertToPrestonBlair), + prestonBlairShapeNames { + { Shape::A, "MBP" }, + { Shape::B, "etc" }, + { Shape::C, "E" }, + { Shape::D, "AI" }, + { Shape::E, "O" }, + { Shape::F, "U" }, + { Shape::G, "FV" }, + { Shape::H, "L" }, + { Shape::X, "rest" }, + } +{ + // Animation works with a fixed frame rate of 100. + // Downsampling to much less than 25 fps may result in dropped frames. + // Upsampling to more than 100 fps doesn't make sense. + const double minFrameRate = 24.0; + const double maxFrameRate = 100.0; + + if (frameRate < minFrameRate || frameRate > maxFrameRate) { + throw std::runtime_error(fmt::format("Frame rate must be between {} and {} fps.", minFrameRate, maxFrameRate)); + } + + if (convertToPrestonBlair) { + for (Shape shape : targetShapeSet) { + if (prestonBlairShapeNames.find(shape) == prestonBlairShapeNames.end()) { + throw std::runtime_error(fmt::format("Mouth shape {} cannot be converted to Preston Blair shape names.")); + } + } + } +} + +void DatExporter::exportAnimation(const ExporterInput& input, std::ostream& outputStream) { + outputStream << "MohoSwitch1" << "\n"; + + // Output shapes with start times + int lastFrameNumber = 0; + for (auto& timedShape : input.animation) { + const int frameNumber = toFrameNumber(timedShape.getStart()); + if (frameNumber == lastFrameNumber) continue; + + const string shapeName = toString(timedShape.getValue()); + outputStream << frameNumber << " " << shapeName << "\n"; + lastFrameNumber = frameNumber; + } + + // Output closed mouth with end time + int frameNumber = toFrameNumber(input.animation.getRange().getEnd()); + if (frameNumber == lastFrameNumber) ++frameNumber; + const string shapeName = toString(convertToTargetShapeSet(Shape::X, input.targetShapeSet)); + outputStream << frameNumber << " " << shapeName << "\n"; +} + +string DatExporter::toString(Shape shape) const { + return convertToPrestonBlair + ? prestonBlairShapeNames.at(shape) + : boost::lexical_cast(shape); +} + +int DatExporter::toFrameNumber(centiseconds time) const { + return 1 + static_cast(frameRate * duration_cast>(time).count()); +} diff --git a/rhubarb/src/exporters/DatExporter.h b/rhubarb/src/exporters/DatExporter.h new file mode 100644 index 0000000..af6284e --- /dev/null +++ b/rhubarb/src/exporters/DatExporter.h @@ -0,0 +1,21 @@ +#pragma once + +#include "Exporter.h" +#include "core/Shape.h" +#include +#include + +// Exporter for Moho's switch data file format +class DatExporter : public Exporter { +public: + DatExporter(const ShapeSet& targetShapeSet, double frameRate, bool convertToPrestonBlair); + void exportAnimation(const ExporterInput& input, std::ostream& outputStream) override; + +private: + int toFrameNumber(centiseconds time) const; + std::string toString(Shape shape) const; + + double frameRate; + bool convertToPrestonBlair; + std::map prestonBlairShapeNames; +}; diff --git a/rhubarb/src/rhubarb/ExportFormat.cpp b/rhubarb/src/rhubarb/ExportFormat.cpp index 6619f3f..9e15e39 100644 --- a/rhubarb/src/rhubarb/ExportFormat.cpp +++ b/rhubarb/src/rhubarb/ExportFormat.cpp @@ -13,6 +13,7 @@ string ExportFormatConverter::getTypeName() { EnumConverter::member_data ExportFormatConverter::getMemberData() { return member_data { + { ExportFormat::Dat, "dat" }, { ExportFormat::Tsv, "tsv" }, { ExportFormat::Xml, "xml" }, { ExportFormat::Json, "json" } diff --git a/rhubarb/src/rhubarb/ExportFormat.h b/rhubarb/src/rhubarb/ExportFormat.h index 1ff9392..8591c04 100644 --- a/rhubarb/src/rhubarb/ExportFormat.h +++ b/rhubarb/src/rhubarb/ExportFormat.h @@ -3,6 +3,7 @@ #include "tools/EnumConverter.h" enum class ExportFormat { + Dat, Tsv, Xml, Json diff --git a/rhubarb/src/rhubarb/main.cpp b/rhubarb/src/rhubarb/main.cpp index e873422..4e512a9 100644 --- a/rhubarb/src/rhubarb/main.cpp +++ b/rhubarb/src/rhubarb/main.cpp @@ -18,6 +18,7 @@ #include "tools/textFiles.h" #include "lib/rhubarbLib.h" #include "ExportFormat.h" +#include "exporters/DatExporter.h" #include "exporters/TsvExporter.h" #include "exporters/XmlExporter.h" #include "exporters/JsonExporter.h" @@ -82,8 +83,15 @@ unique_ptr createRecognizer(RecognizerType recognizerType) { } } -unique_ptr createExporter(ExportFormat exportFormat) { +unique_ptr createExporter( + ExportFormat exportFormat, + const ShapeSet& targetShapeSet, + double datFrameRate, + bool datUsePrestonBlair +) { switch (exportFormat) { + case ExportFormat::Dat: + return make_unique(targetShapeSet, datFrameRate, datUsePrestonBlair); case ExportFormat::Tsv: return make_unique(); case ExportFormat::Xml: @@ -172,6 +180,16 @@ int main(int platformArgc, char* platformArgv[]) { false, string(), "string", cmd ); + tclap::SwitchArg datUsePrestonBlair( + "", "datUsePrestonBlair", "Only for dat exporter: uses the Preston Blair mouth shape names.", + cmd, false + ); + + tclap::ValueArg datFrameRate( + "", "datFrameRate", "Only for dat exporter: the desired frame rate.", + false, 24.0, "number", cmd + ); + auto exportFormats = vector(ExportFormatConverter::get().getValues()); tclap::ValuesConstraint exportFormatConstraint(exportFormats); tclap::ValueArg exportFormat( @@ -222,6 +240,13 @@ int main(int platformArgc, char* platformArgv[]) { path inputFilePath(inputFileName.getValue()); ShapeSet targetShapeSet = getTargetShapeSet(extendedShapes.getValue()); + unique_ptr exporter = createExporter( + exportFormat.getValue(), + targetShapeSet, + datFrameRate.getValue(), + datUsePrestonBlair.getValue() + ); + logging::log(StartEntry(inputFilePath)); logging::debugFormat("Command line: {}", join(args | transformed([](string arg) { return fmt::format("\"{}\"", arg); }), " ")); @@ -246,7 +271,6 @@ int main(int platformArgc, char* platformArgv[]) { logging::info("Done animating."); // Export animation - unique_ptr exporter = createExporter(exportFormat.getValue()); optional outputFile; if (outputFileName.isSet()) { outputFile = boost::in_place(outputFileName.getValue());