diff --git a/CMakeLists.txt b/CMakeLists.txt index 5073cc5..a4d2137 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,6 +121,7 @@ set(SOURCE_FILES src/Timed.cpp src/TimeRange.cpp src/Timeline.cpp + src/Exporter.cpp ) add_executable(rhubarb ${SOURCE_FILES}) target_link_libraries(rhubarb ${Boost_LIBRARIES} cppFormat sphinxbase pocketSphinx) diff --git a/extras/SonyVegas/Import Rhubarb.cs b/extras/SonyVegas/Import Rhubarb.cs index f5a69f0..b6f2ed7 100644 --- a/extras/SonyVegas/Import Rhubarb.cs +++ b/extras/SonyVegas/Import Rhubarb.cs @@ -63,7 +63,7 @@ public class EntryPoint { VideoTrack videoTrack = vegas.Project.AddVideoTrack(); foreach (XmlElement mouthCueElement in mouthCueElements) { Timecode start = GetTimecode(mouthCueElement.Attributes["start"]); - Timecode length = GetTimecode(mouthCueElement.Attributes["duration"]); + Timecode length = GetTimecode(mouthCueElement.Attributes["end"]) - start; VideoEvent videoEvent = videoTrack.AddVideoEvent(start, length); Media imageMedia = new Media(imageFileNames[mouthCueElement.InnerText]); videoEvent.AddTake(imageMedia.GetVideoStreamByIndex(0)); diff --git a/src/Exporter.cpp b/src/Exporter.cpp new file mode 100644 index 0000000..e3db9ef --- /dev/null +++ b/src/Exporter.cpp @@ -0,0 +1,122 @@ +#include "Exporter.h" +#include +#include +#include +#include + +using std::string; +using boost::property_tree::ptree; +using std::vector; +using std::tuple; +using std::make_tuple; + +template <> +const string& getEnumTypeName() { + static const string name = "ExportFormat"; + return name; +} + +template <> +const vector>& getEnumMembers() { + static const vector> values = { + make_tuple(ExportFormat::TSV, "TSV"), + make_tuple(ExportFormat::XML, "XML"), + make_tuple(ExportFormat::JSON, "JSON") + }; + return values; +} + +std::ostream& operator<<(std::ostream& stream, ExportFormat value) { + return stream << enumToString(value); +} + +std::istream& operator>>(std::istream& stream, ExportFormat& value) { + string name; + stream >> name; + value = parseEnum(name); + return stream; +} + +// Makes sure there is at least one mouth shape +std::vector> dummyShapeIfEmpty(const Timeline& shapes) { + std::vector> result; + std::copy(shapes.begin(), shapes.end(), std::back_inserter(result)); + if (result.empty()) { + // Add zero-length empty mouth + result.push_back(Timed(centiseconds(0), centiseconds(0), Shape::A)); + } + return result; +} + +void TSVExporter::exportShapes(const boost::filesystem::path& inputFilePath, const Timeline& shapes, std::ostream& outputStream) { + UNUSED(inputFilePath); + + // Output shapes with start times + for (auto& timedShape : shapes) { + outputStream << formatDuration(timedShape.getStart()) << "\t" << timedShape.getValue() << "\n"; + } + + // Output closed mouth with end time + outputStream << formatDuration(shapes.getRange().getEnd()) << "\t" << Shape::A << "\n"; +} + +void XMLExporter::exportShapes(const boost::filesystem::path& inputFilePath, const Timeline& shapes, std::ostream& outputStream) { + ptree tree; + + // Add metadata + tree.put("rhubarbResult.metadata.soundFile", inputFilePath.string()); + tree.put("rhubarbResult.metadata.duration", formatDuration(shapes.getRange().getLength())); + + // Add mouth cues + for (auto& timedShape : dummyShapeIfEmpty(shapes)) { + ptree& mouthCueElement = tree.add("rhubarbResult.mouthCues.mouthCue", timedShape.getValue()); + mouthCueElement.put(".start", formatDuration(timedShape.getStart())); + mouthCueElement.put(".end", formatDuration(timedShape.getEnd())); + } + + write_xml(outputStream, tree, boost::property_tree::xml_writer_settings(' ', 2)); +} + +string escapeJSONString(const string& s) { + string result; + for (char c : s) { + switch (c) { + case '"': result += "\\\""; break; + case '\\': result += "\\\\"; break; + case '\b': result += "\\b"; break; + case '\f': result += "\\f"; break; + case '\n': result += "\\n"; break; + case '\r': result += "\\r"; break; + case '\t': result += "\\t"; break; + default: + if (c <= '\x1f') { + result += fmt::format("\\u{0:04x}", c); + } else { + result += c; + } + } + } + return result; +} + +void JSONExporter::exportShapes(const boost::filesystem::path& inputFilePath, const Timeline& 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"; + outputStream << " \"metadata\": {\n"; + outputStream << " \"soundFile\": \"" << escapeJSONString(inputFilePath.string()) << "\",\n"; + outputStream << " \"duration\": " << formatDuration(shapes.getRange().getLength()) << "\n"; + outputStream << " },\n"; + outputStream << " \"mouthCues\": [\n"; + bool isFirst = true; + for (auto& timedShape : dummyShapeIfEmpty(shapes)) { + if (!isFirst) outputStream << ",\n"; + isFirst = false; + outputStream << " { \"start\": " << formatDuration(timedShape.getStart()) + << ", \"end\": " << formatDuration(timedShape.getEnd()) + << ", \"value\": \"" << timedShape.getValue() << "\" }"; + } + outputStream << "\n"; + outputStream << " ]\n"; + outputStream << "}\n"; +} diff --git a/src/Exporter.h b/src/Exporter.h new file mode 100644 index 0000000..f2061a3 --- /dev/null +++ b/src/Exporter.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include + +enum class ExportFormat { + TSV, + XML, + JSON +}; + +template<> +const std::string& getEnumTypeName(); + +template<> +const std::vector>& getEnumMembers(); + +std::ostream& operator<<(std::ostream& stream, ExportFormat value); + +std::istream& operator>>(std::istream& stream, ExportFormat& value); + +class Exporter { +public: + virtual ~Exporter() {} + virtual void exportShapes(const boost::filesystem::path& inputFilePath, const Timeline& shapes, std::ostream& outputStream) = 0; +}; + +class TSVExporter : public Exporter { +public: + void exportShapes(const boost::filesystem::path& inputFilePath, const Timeline& shapes, std::ostream& outputStream) override; +}; + +class XMLExporter : public Exporter { +public: + void exportShapes(const boost::filesystem::path& inputFilePath, const Timeline& shapes, std::ostream& outputStream) override; +}; + +class JSONExporter : public Exporter { +public: + void exportShapes(const boost::filesystem::path& inputFilePath, const Timeline& shapes, std::ostream& outputStream) override; +}; diff --git a/src/main.cpp b/src/main.cpp index 4416ca7..9fb2106 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,4 @@ #include -#include -#include #include #include #include @@ -12,18 +10,18 @@ #include "ProgressBar.h" #include "logging.h" #include -#include -#include +#include "Timeline.h" +#include "Exporter.h" using std::exception; using std::string; using std::vector; using std::unique_ptr; +using std::make_unique; using std::map; using std::chrono::duration; using std::chrono::duration_cast; using boost::filesystem::path; -using boost::property_tree::ptree; namespace tclap = TCLAP; @@ -42,41 +40,20 @@ unique_ptr createAudioStream(path filePath) { try { return std::make_unique(filePath); } catch (...) { - std::throw_with_nested(std::runtime_error(fmt::format("Could not open sound file {0}.", filePath))); + std::throw_with_nested(std::runtime_error(fmt::format("Could not open sound file '{0}'.", filePath.string()))); } } -ptree createXmlTree(const path& filePath, const Timeline& phones, const Timeline& shapes) { - ptree tree; - - // Add sound file path - tree.put("rhubarbResult.info.soundFile", filePath.string()); - - // Add phones - tree.put("rhubarbResult.phones", ""); - for (auto& timedPhone : phones) { - ptree& phoneElement = tree.add("rhubarbResult.phones.phone", timedPhone.getValue()); - phoneElement.put(".start", formatDuration(timedPhone.getStart())); - phoneElement.put(".duration", formatDuration(timedPhone.getLength())); - } - - // Add mouth cues - tree.put("rhubarbResult.mouthCues", ""); - for (auto& timedShape : shapes) { - ptree& mouthCueElement = tree.add("rhubarbResult.mouthCues.mouthCue", timedShape.getValue()); - mouthCueElement.put(".start", formatDuration(timedShape.getStart())); - mouthCueElement.put(".duration", formatDuration(timedShape.getLength())); - } - - return tree; -} - // Tell TCLAP how to handle our types namespace TCLAP { template<> struct ArgTraits { typedef ValueLike ValueCategory; }; + template<> + struct ArgTraits { + typedef ValueLike ValueCategory; + }; } int main(int argc, char *argv[]) { @@ -93,6 +70,9 @@ int main(int argc, char *argv[]) { tclap::ValueArg logLevel("", "logLevel", "The minimum log level to log", false, LogLevel::Debug, &logLevelConstraint, cmd); tclap::ValueArg logFileName("", "logFile", "The log file path.", false, string(), "string", cmd); tclap::ValueArg dialog("d", "dialog", "The text of the dialog.", false, string(), "string", cmd); + auto exportFormats = vector(getEnumValues()); + tclap::ValuesConstraint exportFormatConstraint(exportFormats); + tclap::ValueArg exportFormat("f", "exportFormat", "The export format.", false, ExportFormat::TSV, &exportFormatConstraint, cmd); tclap::UnlabeledValueArg inputFileName("inputFile", "The input file. Must be a sound file in WAVE format.", true, "", "string", cmd); try { @@ -131,9 +111,22 @@ int main(int argc, char *argv[]) { std::cerr << std::endl; - // Print XML - ptree xmlTree = createXmlTree(inputFileName.getValue(), phones, shapes); - boost::property_tree::write_xml(std::cout, xmlTree, boost::property_tree::xml_writer_settings(' ', 2)); + // Export + unique_ptr exporter; + switch (exportFormat.getValue()) { + case ExportFormat::TSV: + exporter = make_unique(); + break; + case ExportFormat::XML: + exporter = make_unique(); + break; + case ExportFormat::JSON: + exporter = make_unique(); + break; + default: + throw std::runtime_error("Unknown export format."); + } + exporter->exportShapes(path(inputFileName.getValue()), shapes, std::cout); return 0; } catch (tclap::ArgException& e) {