diff --git a/src/Magnum/Audio/AbstractImporter.cpp b/src/Magnum/Audio/AbstractImporter.cpp index 23a177d7d2..743f5ca428 100644 --- a/src/Magnum/Audio/AbstractImporter.cpp +++ b/src/Magnum/Audio/AbstractImporter.cpp @@ -26,9 +26,12 @@ #include "AbstractImporter.h" #include +#include #include #include +#include "Magnum/FileCallback.h" + #ifndef CORRADE_PLUGINMANAGER_NO_DYNAMIC_PLUGIN_SUPPORT #include "Magnum/Audio/configure.h" #endif @@ -36,7 +39,7 @@ namespace Magnum { namespace Audio { std::string AbstractImporter::pluginInterface() { - return "cz.mosra.magnum.Audio.AbstractImporter/0.1"; + return "cz.mosra.magnum.Audio.AbstractImporter/0.2"; } #ifndef CORRADE_PLUGINMANAGER_NO_DYNAMIC_PLUGIN_SUPPORT @@ -59,10 +62,24 @@ AbstractImporter::AbstractImporter(PluginManager::Manager& man AbstractImporter::AbstractImporter(PluginManager::AbstractManager& manager, const std::string& plugin): PluginManager::AbstractManagingPlugin{manager, plugin} {} +void AbstractImporter::setFileCallback(Containers::Optional>(*callback)(const std::string&, InputFileCallbackPolicy, void*), void* const userData) { + CORRADE_ASSERT(!isOpened(), "Audio::AbstractImporter::setFileCallback(): can't be set while a file is opened", ); + CORRADE_ASSERT(features() & Feature::OpenData, "Audio::AbstractImporter::setFileCallback(): importer doesn't support loading from data, callbacks can't be used", ); + + _fileCallback = callback; + _fileCallbackUserData = userData; + doSetFileCallback(callback, userData); +} + +void AbstractImporter::doSetFileCallback(Containers::Optional>(*)(const std::string&, InputFileCallbackPolicy, void*), void*) {} + bool AbstractImporter::openData(Containers::ArrayView data) { CORRADE_ASSERT(features() & Feature::OpenData, "Audio::AbstractImporter::openData(): feature not supported", {}); + /* We accept empty data here (instead of checking for them and failing so + the check doesn't be done on the plugin side) because for some file + formats it could be valid. */ close(); doOpenData(data); return isOpened(); @@ -74,20 +91,63 @@ void AbstractImporter::doOpenData(Containers::ArrayView) { bool AbstractImporter::openFile(const std::string& filename) { close(); - doOpenFile(filename); + + /* If file loading callbacks are not set or the importer supports handling + them directly, call into the implementation */ + if(!_fileCallback || (doFeatures() & Feature::FileCallback)) { + doOpenFile(filename); + + /* Otherwise, if loading from data is supported, use the callback and pass + the data through to openData(). Mark the file as ready to be closed once + opening is finished. */ + } else if(doFeatures() & Feature::OpenData) { + /* This needs to be duplicated here and in the doOpenFile() + implementation in order to support both following cases: + - plugins that don't support FileCallback but have their own + doOpenFile() implementation (callback needs to be used here, + because the base doOpenFile() implementation might never get + called) + - plugins that support FileCallback but want to delegate the actual + file loading to the default implementation (callback used in the + base doOpenFile() implementation, because this branch is never + taken in that case) */ + const Containers::Optional> data = _fileCallback(filename, InputFileCallbackPolicy::LoadTemporary, _fileCallbackUserData); + if(!data) { + Error() << "Audio::AbstractImporter::openFile(): cannot open file" << filename; + return isOpened(); + } + doOpenData(*data); + _fileCallback(filename, InputFileCallbackPolicy::Close, _fileCallbackUserData); + + /* Shouldn't get here, the assert is fired already in setFileCallback() */ + } else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + return isOpened(); } void AbstractImporter::doOpenFile(const std::string& filename) { CORRADE_ASSERT(features() & Feature::OpenData, "Audio::AbstractImporter::openFile(): not implemented", ); - /* Open file */ - if(!Utility::Directory::exists(filename)) { - Error() << "Trade::AbstractImporter::openFile(): cannot open file" << filename; - return; + /* If callbacks are set, use them. This is the same implementation as in + openFile(), see the comment there for details. */ + if(_fileCallback) { + const Containers::Optional> data = _fileCallback(filename, InputFileCallbackPolicy::LoadTemporary, _fileCallbackUserData); + if(!data) { + Error() << "Audio::AbstractImporter::openFile(): cannot open file" << filename; + return; + } + doOpenData(*data); + _fileCallback(filename, InputFileCallbackPolicy::Close, _fileCallbackUserData); + + /* Otherwise open the file directly */ + } else { + if(!Utility::Directory::exists(filename)) { + Error() << "Audio::AbstractImporter::openFile(): cannot open file" << filename; + return; + } + + doOpenData(Utility::Directory::read(filename)); } - - doOpenData(Utility::Directory::read(filename)); } void AbstractImporter::close() { diff --git a/src/Magnum/Audio/AbstractImporter.h b/src/Magnum/Audio/AbstractImporter.h index 770e03c5ac..d46fec4b04 100644 --- a/src/Magnum/Audio/AbstractImporter.h +++ b/src/Magnum/Audio/AbstractImporter.h @@ -39,9 +39,51 @@ namespace Magnum { namespace Audio { /** @brief Base for audio importer plugins -Provides interface for importing various audio formats. See @ref plugins for -more information and `*Importer` classes in @ref Audio namespace for available -importer plugins. +Provides interface for importing various audio formats. + +@section Audio-AbstractImporter-usage Usage + +Importers are most commonly implemented as plugins. For example, loading an +audio track from the filesystem using the @ref AnyAudioImporter plugin can be +done like this, completely with all error handling: + +@snippet MagnumAudio.cpp AbstractImporter-usage + +See @ref plugins for more information about general plugin usage and +`*Importer` classes in the @ref Audio namespace for available importer plugins. + +@subsection Audio-AbstractImporter-usage-callbacks Loading data from memory, using file callbacks + +Besides loading data directly from the filesystem using @ref openFile() like +shown above, it's possible to use @ref openData() to import data from memory. +Note that the particular importer implementation must support +@ref Feature::OpenData for this method to work. + +Some audio formats sometimes reference other files (such as songs of a +playlist) and in that case you may want to intercept those references and load +them in a custom way as well. For importers that advertise support for this +with @ref Feature::FileCallback this is done by specifying a file loading +callback using @ref setFileCallback(). The callback gets a filename, +@ref InputFileCallbackPolicy and an user pointer as parameters; returns a +non-owning view on the loaded data or a +@ref Corrade::Containers::NullOpt "Containers::NullOpt" to indicate the file +loading failed. For example, loading a scene from memory-mapped files could +look like below. Note that the file loading callback affects @ref openFile() as +well --- you don't have to load the top-level file manually and pass it to +@ref openData(), any importer supporting the callback feature handles that +correctly. + +@snippet MagnumAudio.cpp AbstractImporter-usage-callbacks + +For importers that don't support @ref Feature::FileCallback directly, the base +@ref openFile() implementation will use the file callback to pass the loaded +data through to @ref openData(), in case the importer supports at least +@ref Feature::OpenData. If the importer supports neither @ref Feature::FileCallback +nor @ref Feature::OpenData, @ref setFileCallback() doesn't allow the callbacks +to be set. + +The input file callback signature is the same for @ref Audio::AbstractImporter, +@ref Trade::AbstractImporter and @ref Text::AbstractFont to allow code reuse. @section Audio-AbstractImporter-subclassing Subclassing @@ -74,7 +116,18 @@ class MAGNUM_AUDIO_EXPORT AbstractImporter: public PluginManager::AbstractManagi */ enum class Feature: UnsignedByte { /** Opening files from raw data using @ref openData() */ - OpenData = 1 << 0 + OpenData = 1 << 0, + + /** + * Specifying callbacks for loading additional files referenced + * from the main file using @ref setFileCallback(). If the importer + * doesn't expose this feature, the format is either single-file or + * loading via callbacks is not supported. + * + * See @ref Audio-AbstractImporter-usage-callbacks and particular + * importer documentation for more information. + */ + FileCallback = 1 << 1 }; /** @@ -123,6 +176,90 @@ class MAGNUM_AUDIO_EXPORT AbstractImporter: public PluginManager::AbstractManagi /** @brief Features supported by this importer */ Features features() const { return doFeatures(); } + /** + * @brief File opening callback function + * + * @see @ref Audio-AbstractImporter-usage-callbacks + */ + auto fileCallback() const -> Containers::Optional>(*)(const std::string&, InputFileCallbackPolicy, void*) { return _fileCallback; } + + /** + * @brief File opening callback user data + * + * @see @ref Audio-AbstractImporter-usage-callbacks + */ + void* fileCallbackUserData() const { return _fileCallbackUserData; } + + /** + * @brief Set file opening callback + * + * In case the importer supports @ref Feature::FileCallback, files + * opened through @ref openFile() will be loaded through the provided + * callback. Besides that, all external files referenced by the + * top-level file will be loaded through the callback function as well, + * usually on demand. The callback function gets a filename, + * @ref InputFileCallbackPolicy and the @p userData pointer as input + * and returns a non-owning view on the loaded data as output or a + * @ref Corrade::Containers::NullOpt if loading failed --- because + * empty files might also be valid in some circumstances, @cpp nullptr @ce + * can't be used to indicate a failure. + * + * In case the importer doesn't support @ref Feature::FileCallback but + * supports at least @ref Feature::OpenData, a file opened through + * @ref openFile() will be internally loaded through the provided + * callback and then passed to @ref openData(). First the file is + * loaded with @ref InputFileCallbackPolicy::LoadTemporary passed to + * the callback, then the returned memory view is passed to + * @ref openData() (sidestepping the potential @ref openFile() + * implementation of that particular importer) and after that the + * callback is called again with @ref InputFileCallbackPolicy::Close + * because the semantics of @ref openData() don't require the data to + * be alive after. In case you need a different behavior, use + * @ref openData() directly. + * + * In case @p callback is @cpp nullptr @ce, the current callback (if + * any) is reset. This function expects that the importer supports + * either @ref Feature::FileCallback or @ref Feature::OpenData. If an + * importer supports neither, callbacks can't be used. + * + * It's expected that this function is called *before* a file is + * opened. It's also expected that the loaded data are kept in scope + * for as long as the importer needs them, based on the value of + * @ref InputFileCallbackPolicy. Documentation of particular importers + * provides more information about the expected callback behavior. + * + * Following is an example of setting up a file loading callback for + * fetching compiled-in resources from @ref Corrade::Utility::Resource. + * See the overload below for a more convenient type-safe way to pass + * the user data pointer. + * + * @snippet MagnumAudio.cpp AbstractImporter-setFileCallback + * + * @see @ref Audio-AbstractImporter-usage-callbacks + */ + void setFileCallback(Containers::Optional>(*callback)(const std::string&, InputFileCallbackPolicy, void*), void* userData = nullptr); + + /** + * @brief Set file opening callback + * + * Equivalent to calling the above with a lambda wrapper that casts + * @cpp void* @ce back to @cpp T* @ce and dereferences it in order to + * pass it to @p callback. Example usage: + * + * @snippet MagnumAudio.cpp AbstractImporter-setFileCallback-template + * + * @see @ref Audio-AbstractImporter-usage-callbacks + */ + #ifdef DOXYGEN_GENERATING_OUTPUT + template void setFileCallback(Containers::Optional>(*callback)(const std::string&, InputFileCallbackPolicy, T&), T& userData); + #else + /* Otherwise the user would be forced to use the + operator to convert + a lambda to a function pointer and (besides being weird and + annoying) it's also not portable because it doesn't work on MSVC + 2015 and older versions of MSVC 2017. */ + template void setFileCallback(Callback callback, T& userData); + #endif + /** @brief Whether any file is opened */ bool isOpened() const { return doIsOpened(); } @@ -161,24 +298,44 @@ class MAGNUM_AUDIO_EXPORT AbstractImporter: public PluginManager::AbstractManagi /*@}*/ + protected: + /** + * @brief Implementation for @ref openFile() + * + * If @ref Feature::OpenData is supported, default implementation opens + * the file and calls @ref doOpenData() with its contents. It is + * allowed to call this function from your @ref doOpenFile() + * implementation --- in particular, this implementation will also + * correctly handle callbacks set through @ref setFileCallback(). + * + * This function is not called when file callbacks are set through + * @ref setFileCallback() and @ref Feature::FileCallback is not + * supported --- instead, file is loaded though the callback and data + * passed through to @ref doOpenData(). + */ + virtual void doOpenFile(const std::string& filename); + private: /** @brief Implementation for @ref features() */ virtual Features doFeatures() const = 0; + /** + * @brief Implementation for @ref setFileCallback() + * + * Useful when the importer needs to modify some internal state on + * callback setup. Default implementation does nothing and this + * function doesn't need to be implemented --- the callback function + * and user data pointer are available through @ref fileCallback() and + * @ref fileCallbackUserData(). + */ + virtual void doSetFileCallback(Containers::Optional>(*callback)(const std::string&, InputFileCallbackPolicy, void*), void* userData); + /** @brief Implementation for @ref isOpened() */ virtual bool doIsOpened() const = 0; /** @brief Implementation for @ref openData() */ virtual void doOpenData(Containers::ArrayView data); - /** - * @brief Implementation for @ref openFile() - * - * If @ref Feature::OpenData is supported, default implementation opens - * the file and calls @ref doOpenData() with its contents. - */ - virtual void doOpenFile(const std::string& filename); - /** @brief Implementation for @ref close() */ virtual void doClose() = 0; @@ -190,6 +347,16 @@ class MAGNUM_AUDIO_EXPORT AbstractImporter: public PluginManager::AbstractManagi /** @brief Implementation for @ref data() */ virtual Containers::Array doData() = 0; + + Containers::Optional>(*_fileCallback)(const std::string&, InputFileCallbackPolicy, void*){}; + void* _fileCallbackUserData{}; + + /* Used by the templated version only */ + struct FileCallbackTemplate { + void(*callback)(); + const void* userData; + /* GCC 4.8 complains loudly about missing initializers otherwise */ + } _fileCallbackTemplate{nullptr, nullptr}; }; }} diff --git a/src/Magnum/Text/AbstractFont.h b/src/Magnum/Text/AbstractFont.h index ae6377cb60..68d5b709e1 100644 --- a/src/Magnum/Text/AbstractFont.h +++ b/src/Magnum/Text/AbstractFont.h @@ -92,8 +92,9 @@ data through to @ref openData(), in case the importer supports at least nor @ref Feature::OpenData, @ref setFileCallback() doesn't allow the callbacks to be set. -The input file callback signature is the same for @ref Text::AbstractFont and -@ref Trade::AbstractImporter to allow code reuse. +The input file callback signature is the same for @ref Text::AbstractFont, +@ref Trade::AbstractImporter and @ref Audio::AbstractImporter to allow code +reuse. @section Text-AbstractFont-subclassing Subclassing diff --git a/src/Magnum/Trade/AbstractImporter.h b/src/Magnum/Trade/AbstractImporter.h index b9050368fb..26c8c4c93a 100644 --- a/src/Magnum/Trade/AbstractImporter.h +++ b/src/Magnum/Trade/AbstractImporter.h @@ -97,8 +97,8 @@ data through to @ref openData(), in case the importer supports at least nor @ref Feature::OpenData, @ref setFileCallback() doesn't allow the callbacks to be set. -The input file callback signature is the same for @ref Trade::AbstractImporter -and @ref Text::AbstractFont to allow code reuse. +The input file callback signature is the same for @ref Trade::AbstractImporter, +@ref Audio::AbstractImporter and @ref Text::AbstractFont to allow code reuse. @subsection Trade-AbstractImporter-usage-state Internal importer state