diff --git a/CMakeLists.txt b/CMakeLists.txt index aba26ee..696eed5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -275,8 +275,15 @@ target_link_libraries(rhubarb-core # ... rhubarb-exporters add_library(rhubarb-exporters - src/exporters/Exporter.cpp src/exporters/Exporter.h + src/exporters/exporterTools.cpp + src/exporters/exporterTools.h + src/exporters/JsonExporter.cpp + src/exporters/JsonExporter.h + src/exporters/TsvExporter.cpp + src/exporters/TsvExporter.h + src/exporters/XmlExporter.cpp + src/exporters/XmlExporter.h ) target_include_directories(rhubarb-exporters PUBLIC "src/exporters") target_link_libraries(rhubarb-exporters @@ -379,7 +386,11 @@ target_link_libraries(rhubarb-tools ) # Define Rhubarb executable -add_executable(rhubarb src/main.cpp) +add_executable(rhubarb + src/main.cpp + src/ExportFormat.cpp + src/ExportFormat.h +) target_include_directories(rhubarb PUBLIC "src") target_link_libraries(rhubarb rhubarb-exporters diff --git a/src/ExportFormat.cpp b/src/ExportFormat.cpp new file mode 100644 index 0000000..fe02199 --- /dev/null +++ b/src/ExportFormat.cpp @@ -0,0 +1,28 @@ +#include "ExportFormat.h" + +using std::string; + +ExportFormatConverter& ExportFormatConverter::get() { + static ExportFormatConverter converter; + return converter; +} + +string ExportFormatConverter::getTypeName() { + return "ExportFormat"; +} + +EnumConverter::member_data ExportFormatConverter::getMemberData() { + return member_data{ + { ExportFormat::TSV, "TSV" }, + { ExportFormat::XML, "XML" }, + { ExportFormat::JSON, "JSON" } + }; +} + +std::ostream& operator<<(std::ostream& stream, ExportFormat value) { + return ExportFormatConverter::get().write(stream, value); +} + +std::istream& operator>>(std::istream& stream, ExportFormat& value) { + return ExportFormatConverter::get().read(stream, value); +} diff --git a/src/ExportFormat.h b/src/ExportFormat.h new file mode 100644 index 0000000..648d2d8 --- /dev/null +++ b/src/ExportFormat.h @@ -0,0 +1,21 @@ +#pragma once + +#include "EnumConverter.h" + +enum class ExportFormat { + TSV, + XML, + JSON +}; + +class ExportFormatConverter : public EnumConverter { +public: + static ExportFormatConverter& get(); +protected: + std::string getTypeName() override; + member_data getMemberData() override; +}; + +std::ostream& operator<<(std::ostream& stream, ExportFormat value); + +std::istream& operator>>(std::istream& stream, ExportFormat& value); diff --git a/src/exporters/Exporter.cpp b/src/exporters/Exporter.cpp deleted file mode 100644 index fa6fb04..0000000 --- a/src/exporters/Exporter.cpp +++ /dev/null @@ -1,122 +0,0 @@ -#include "Exporter.h" -#include -#include -#include -#include - -using std::string; -using boost::property_tree::ptree; - -ExportFormatConverter& ExportFormatConverter::get() { - static ExportFormatConverter converter; - return converter; -} - -string ExportFormatConverter::getTypeName() { - return "ExportFormat"; -} - -EnumConverter::member_data ExportFormatConverter::getMemberData() { - return member_data{ - { ExportFormat::TSV, "TSV" }, - { ExportFormat::XML, "XML" }, - { ExportFormat::JSON, "JSON" } - }; -} - -std::ostream& operator<<(std::ostream& stream, ExportFormat value) { - return ExportFormatConverter::get().write(stream, value); -} - -std::istream& operator>>(std::istream& stream, ExportFormat& value) { - return ExportFormatConverter::get().read(stream, value); -} - -// 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(0_cs, 0_cs, Shape::A)); - } - return result; -} - -void TSVExporter::exportShapes(const boost::filesystem::path& inputFilePath, const ContinuousTimeline& 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::X << "\n"; -} - -void XMLExporter::exportShapes(const boost::filesystem::path& inputFilePath, const ContinuousTimeline& shapes, std::ostream& outputStream) { - ptree tree; - - // Add metadata - tree.put("rhubarbResult.metadata.soundFile", inputFilePath.string()); - tree.put("rhubarbResult.metadata.duration", formatDuration(shapes.getRange().getDuration())); - - // 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())); - } - -#if BOOST_VERSION < 105600 // Support legacy syntax - using writer_setting = boost::property_tree::xml_writer_settings; -#else - using writer_setting = boost::property_tree::xml_writer_settings; -#endif - write_xml(outputStream, tree, writer_setting(' ', 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 ContinuousTimeline& 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().getDuration()) << "\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/exporters/Exporter.h b/src/exporters/Exporter.h index 3389b5b..a88502c 100644 --- a/src/exporters/Exporter.h +++ b/src/exporters/Exporter.h @@ -3,43 +3,9 @@ #include #include "ContinuousTimeline.h" #include -#include - -enum class ExportFormat { - TSV, - XML, - JSON -}; - -class ExportFormatConverter : public EnumConverter { -public: - static ExportFormatConverter& get(); -protected: - std::string getTypeName() override; - member_data getMemberData() override; -}; - -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 ContinuousTimeline& shapes, std::ostream& outputStream) = 0; }; - -class TSVExporter : public Exporter { -public: - void exportShapes(const boost::filesystem::path& inputFilePath, const ContinuousTimeline& shapes, std::ostream& outputStream) override; -}; - -class XMLExporter : public Exporter { -public: - void exportShapes(const boost::filesystem::path& inputFilePath, const ContinuousTimeline& shapes, std::ostream& outputStream) override; -}; - -class JSONExporter : public Exporter { -public: - void exportShapes(const boost::filesystem::path& inputFilePath, const ContinuousTimeline& shapes, std::ostream& outputStream) override; -}; diff --git a/src/exporters/JsonExporter.cpp b/src/exporters/JsonExporter.cpp new file mode 100644 index 0000000..35287c9 --- /dev/null +++ b/src/exporters/JsonExporter.cpp @@ -0,0 +1,48 @@ +#include "JsonExporter.h" +#include "exporterTools.h" + +using std::string; + +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 ContinuousTimeline& 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().getDuration()) << "\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/exporters/JsonExporter.h b/src/exporters/JsonExporter.h new file mode 100644 index 0000000..7f151ed --- /dev/null +++ b/src/exporters/JsonExporter.h @@ -0,0 +1,8 @@ +#pragma once + +#include "Exporter.h" + +class JSONExporter : public Exporter { +public: + void exportShapes(const boost::filesystem::path& inputFilePath, const ContinuousTimeline& shapes, std::ostream& outputStream) override; +}; diff --git a/src/exporters/TsvExporter.cpp b/src/exporters/TsvExporter.cpp new file mode 100644 index 0000000..f81d700 --- /dev/null +++ b/src/exporters/TsvExporter.cpp @@ -0,0 +1,13 @@ +#include "TsvExporter.h" + +void TSVExporter::exportShapes(const boost::filesystem::path& inputFilePath, const ContinuousTimeline& 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::X << "\n"; +} diff --git a/src/exporters/TsvExporter.h b/src/exporters/TsvExporter.h new file mode 100644 index 0000000..ad42388 --- /dev/null +++ b/src/exporters/TsvExporter.h @@ -0,0 +1,9 @@ +#pragma once + +#include "Exporter.h" + +class TSVExporter : public Exporter { +public: + void exportShapes(const boost::filesystem::path& inputFilePath, const ContinuousTimeline& shapes, std::ostream& outputStream) override; +}; + diff --git a/src/exporters/XmlExporter.cpp b/src/exporters/XmlExporter.cpp new file mode 100644 index 0000000..0b65d79 --- /dev/null +++ b/src/exporters/XmlExporter.cpp @@ -0,0 +1,29 @@ +#include "XmlExporter.h" +#include +#include +#include "exporterTools.h" + +using std::string; +using boost::property_tree::ptree; + +void XMLExporter::exportShapes(const boost::filesystem::path& inputFilePath, const ContinuousTimeline& shapes, std::ostream& outputStream) { + ptree tree; + + // Add metadata + tree.put("rhubarbResult.metadata.soundFile", inputFilePath.string()); + tree.put("rhubarbResult.metadata.duration", formatDuration(shapes.getRange().getDuration())); + + // 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())); + } + +#if BOOST_VERSION < 105600 // Support legacy syntax + using writer_setting = boost::property_tree::xml_writer_settings; +#else + using writer_setting = boost::property_tree::xml_writer_settings; +#endif + write_xml(outputStream, tree, writer_setting(' ', 2)); +} diff --git a/src/exporters/XmlExporter.h b/src/exporters/XmlExporter.h new file mode 100644 index 0000000..f7b2c8a --- /dev/null +++ b/src/exporters/XmlExporter.h @@ -0,0 +1,8 @@ +#pragma once + +#include "Exporter.h" + +class XMLExporter : public Exporter { +public: + void exportShapes(const boost::filesystem::path& inputFilePath, const ContinuousTimeline& shapes, std::ostream& outputStream) override; +}; diff --git a/src/exporters/exporterTools.cpp b/src/exporters/exporterTools.cpp new file mode 100644 index 0000000..25c1cc5 --- /dev/null +++ b/src/exporters/exporterTools.cpp @@ -0,0 +1,12 @@ +#include "exporterTools.h" + +// 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(0_cs, 0_cs, Shape::A)); + } + return result; +} diff --git a/src/exporters/exporterTools.h b/src/exporters/exporterTools.h new file mode 100644 index 0000000..e88a8f1 --- /dev/null +++ b/src/exporters/exporterTools.h @@ -0,0 +1,7 @@ +#pragma once + +#include "Shape.h" +#include "Timeline.h" + +// Makes sure there is at least one mouth shape +std::vector> dummyShapeIfEmpty(const Timeline& shapes); diff --git a/src/main.cpp b/src/main.cpp index cc24d95..464e134 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,6 +16,10 @@ #include "exceptions.h" #include "textFiles.h" #include "rhubarbLib.h" +#include "ExportFormat.h" +#include "TsvExporter.h" +#include "XmlExporter.h" +#include "JsonExporter.h" using std::exception; using std::string;