diff --git a/VERSION.md b/VERSION.md index cd08859..7ae859d 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1,5 +1,9 @@ # Version history +## Unreleased + +* Support for Ogg Vorbis (.ogg) files + ## Version 1.7.2 * Fixed bug in Rhubarb for Spine where processing failed depending on the number of existing animations. See [issue #34](https://github.com/DanielSWolf/rhubarb-lip-sync/issues/34#issuecomment-378198776). diff --git a/rhubarb/CMakeLists.txt b/rhubarb/CMakeLists.txt index 412e07d..9353edf 100644 --- a/rhubarb/CMakeLists.txt +++ b/rhubarb/CMakeLists.txt @@ -309,11 +309,15 @@ target_link_libraries(rhubarb-animation add_library(rhubarb-audio src/audio/AudioClip.cpp src/audio/AudioClip.h + src/audio/audioFileReading.cpp + src/audio/audioFileReading.h src/audio/AudioSegment.cpp src/audio/AudioSegment.h src/audio/DcOffset.cpp src/audio/DcOffset.h src/audio/ioTools.h + src/audio/OggVorbisFileReader.cpp + src/audio/OggVorbisFileReader.h src/audio/processing.cpp src/audio/processing.h src/audio/SampleRateConverter.cpp @@ -328,6 +332,7 @@ add_library(rhubarb-audio target_include_directories(rhubarb-audio PRIVATE "src/audio") target_link_libraries(rhubarb-audio webRtc + vorbis rhubarb-logging rhubarb-time rhubarb-tools diff --git a/rhubarb/src/audio/OggVorbisFileReader.cpp b/rhubarb/src/audio/OggVorbisFileReader.cpp new file mode 100644 index 0000000..6415b2d --- /dev/null +++ b/rhubarb/src/audio/OggVorbisFileReader.cpp @@ -0,0 +1,119 @@ +#include "OggVorbisFileReader.h" + +#include +#include "vorbis/codec.h" +#include "vorbis/vorbisfile.h" +#include "tools/tools.h" +#include +#include +#include "tools/fileTools.h" + +using boost::filesystem::path; +using std::vector; +using std::make_shared; + +std::string vorbisErrorToString(int64_t errorCode) { + switch (errorCode) { + case OV_EREAD: + return "Read error while fetching compressed data for decode."; + case OV_EFAULT: + return "Internal logic fault; indicates a bug or heap/stack corruption."; + case OV_EIMPL: + return "Feature not implemented"; + case OV_EINVAL: + return "Either an invalid argument, or incompletely initialized argument passed to a call."; + case OV_ENOTVORBIS: + return "The given file/data was not recognized as Ogg Vorbis data."; + case OV_EBADHEADER: + return "The file/data is apparently an Ogg Vorbis stream, but contains a corrupted or undecipherable header."; + case OV_EVERSION: + return "The bitstream format revision of the given Vorbis stream is not supported."; + case OV_ENOTAUDIO: + return "Packet is not an audio packet."; + case OV_EBADPACKET: + return "Error in packet."; + case OV_EBADLINK: + return "The given link exists in the Vorbis data stream, but is not decipherable due to garbacge or corruption."; + case OV_ENOSEEK: + return "The given stream is not seekable."; + default: + return "An unexpected Vorbis error occurred."; + } +} + +template +T throwOnError(T code) { + // OV_HOLE, though technically an error code, is only informational + const bool error = code < 0 && code != OV_HOLE; + if (error) { + const std::string message = + fmt::format("{} (Vorbis error {})", vorbisErrorToString(code), code); + throw std::runtime_error(message); + } + return code; +} + +// RAII wrapper around OggVorbis_File +class OggVorbisFile { +public: + OggVorbisFile(const OggVorbisFile&) = delete; + OggVorbisFile& operator=(const OggVorbisFile&) = delete; + + OggVorbisFile(const path& filePath) { + throwOnError(ov_fopen(filePath.string().c_str(), &file)); + } + + OggVorbis_File* get() { + return &file; + } + + ~OggVorbisFile() { + ov_clear(&file); + } + +private: + OggVorbis_File file; +}; + +OggVorbisFileReader::OggVorbisFileReader(const path& filePath) : + filePath(filePath) +{ + // Make sure that common error cases result in readable exception messages + throwIfNotReadable(filePath); + + OggVorbisFile file(filePath); + + vorbis_info* vorbisInfo = ov_info(file.get(), -1); + sampleRate = vorbisInfo->rate; + channelCount = vorbisInfo->channels; + + sampleCount = throwOnError(ov_pcm_total(file.get(), -1)); +} + +std::unique_ptr OggVorbisFileReader::clone() const { + return std::make_unique(*this); +} + +SampleReader OggVorbisFileReader::createUnsafeSampleReader() const { + return [ + channelCount = channelCount, + file = make_shared(filePath), + currentIndex = size_type(0) + ](size_type index) mutable { + // Seek + if (index != currentIndex) { + throwOnError(ov_pcm_seek(file->get(), index)); + } + + // Read a single sample + value_type** p = nullptr; + long readCount = throwOnError(ov_read_float(file->get(), &p, 1, nullptr)); + if (readCount == 0) { + throw std::runtime_error("Unexpected end of file."); + } + ++currentIndex; + + // Downmix channels + return std::accumulate(*p, *p + channelCount, 0.0f) / channelCount; + }; +} diff --git a/rhubarb/src/audio/OggVorbisFileReader.h b/rhubarb/src/audio/OggVorbisFileReader.h new file mode 100644 index 0000000..76b0324 --- /dev/null +++ b/rhubarb/src/audio/OggVorbisFileReader.h @@ -0,0 +1,20 @@ +#pragma once + +#include "AudioClip.h" +#include + +class OggVorbisFileReader : public AudioClip { +public: + OggVorbisFileReader(const boost::filesystem::path& filePath); + std::unique_ptr clone() const override; + int getSampleRate() const override { return sampleRate; } + size_type size() const override { return sampleCount; } + +private: + SampleReader createUnsafeSampleReader() const override; + + boost::filesystem::path filePath; + int sampleRate; + int channelCount; + size_type sampleCount; +}; diff --git a/rhubarb/src/audio/audioFileReading.cpp b/rhubarb/src/audio/audioFileReading.cpp new file mode 100644 index 0000000..8c83aa5 --- /dev/null +++ b/rhubarb/src/audio/audioFileReading.cpp @@ -0,0 +1,27 @@ +#include "audioFileReading.h" +#include +#include "WaveFileReader.h" +#include +#include "OggVorbisFileReader.h" + +using boost::filesystem::path; +using std::string; +using std::runtime_error; +using fmt::format; + +std::unique_ptr createAudioFileClip(path filePath) { + try { + const string extension = + boost::algorithm::to_lower_copy(boost::filesystem::extension(filePath)); + if (extension == ".wav") { + return std::make_unique(filePath); + } + if (extension == ".ogg") { + return std::make_unique(filePath); + } + throw runtime_error(format( + "Unsupported file extension '{}'. Supported extensions are '.wav' and '.ogg'.", extension)); + } catch (...) { + std::throw_with_nested(runtime_error(format("Could not open sound file {}.", filePath))); + } +} diff --git a/rhubarb/src/audio/audioFileReading.h b/rhubarb/src/audio/audioFileReading.h new file mode 100644 index 0000000..daa8412 --- /dev/null +++ b/rhubarb/src/audio/audioFileReading.h @@ -0,0 +1,7 @@ +#pragma once + +#include +#include "AudioClip.h" +#include + +std::unique_ptr createAudioFileClip(boost::filesystem::path filePath); diff --git a/rhubarb/src/lib/rhubarbLib.cpp b/rhubarb/src/lib/rhubarbLib.cpp index c85283f..ffadf68 100644 --- a/rhubarb/src/lib/rhubarbLib.cpp +++ b/rhubarb/src/lib/rhubarbLib.cpp @@ -3,7 +3,7 @@ #include "recognition/phoneRecognition.h" #include "tools/textFiles.h" #include "animation/mouthAnimation.h" -#include "audio/WaveFileReader.h" +#include "audio/audioFileReading.h" using boost::optional; using std::string; @@ -22,14 +22,6 @@ JoiningContinuousTimeline animateAudioClip( return result; } -unique_ptr createWaveAudioClip(path filePath) { - try { - return std::make_unique(filePath); - } catch (...) { - std::throw_with_nested(std::runtime_error(fmt::format("Could not open sound file {}.", filePath))); - } -} - JoiningContinuousTimeline animateWaveFile( path filePath, optional dialog, @@ -37,5 +29,6 @@ JoiningContinuousTimeline animateWaveFile( int maxThreadCount, ProgressSink& progressSink) { - return animateAudioClip(*createWaveAudioClip(filePath), dialog, targetShapeSet, maxThreadCount, progressSink); + const auto audioClip = createAudioFileClip(filePath); + return animateAudioClip(*audioClip, dialog, targetShapeSet, maxThreadCount, progressSink); }