diff --git a/cmake/SfizzConfig.cmake b/cmake/SfizzConfig.cmake index 8b9174ad..7c0b42d8 100644 --- a/cmake/SfizzConfig.cmake +++ b/cmake/SfizzConfig.cmake @@ -48,6 +48,9 @@ else() target_link_libraries(sfizz-sndfile INTERFACE ${SNDFILE_LIBRARIES}) endif() +add_library(sfizz-pugixml STATIC "src/external/pugixml/src/pugixml.cpp") +target_include_directories(sfizz-pugixml PUBLIC "src/external/pugixml/src") + # If we build with Clang use libc++ if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND NOT ANDROID) set(USE_LIBCPP ON CACHE BOOL "Use libc++ with clang") diff --git a/lv2/CMakeLists.txt b/lv2/CMakeLists.txt index fa16000f..84e56897 100644 --- a/lv2/CMakeLists.txt +++ b/lv2/CMakeLists.txt @@ -13,7 +13,7 @@ set (LV2PLUGIN_TTL_SRC_FILES ) add_library (${LV2PLUGIN_PRJ_NAME} SHARED ${PROJECT_NAME}.c ${LV2PLUGIN_TTL_SRC_FILES}) target_link_libraries (${LV2PLUGIN_PRJ_NAME} ${PROJECT_NAME}::${PROJECT_NAME}) -target_include_directories(${LV2PLUGIN_PRJ_NAME} PRIVATE .) +target_include_directories(${LV2PLUGIN_PRJ_NAME} PRIVATE . external/ardour) sfizz_enable_lto_if_needed (${LV2PLUGIN_PRJ_NAME}) # Remove the "lib" prefix, rename the target name and build it in the .lv build dir diff --git a/lv2/sfizz.c b/lv2/sfizz.c index dd274abc..279295a7 100644 --- a/lv2/sfizz.c +++ b/lv2/sfizz.c @@ -47,10 +47,13 @@ #include #include +#include + #include #include #include #include +#include #include #define DEFAULT_SFZ_FILE "/home/paul/Documents/AVL_Percussions/AVL_Drumkits_Percussion-1.0-Alt.sfz" @@ -90,6 +93,7 @@ typedef struct LV2_URID_Unmap *unmap; LV2_Worker_Schedule *worker; LV2_Log_Log *log; + LV2_Midnam *midnam; // Ports const LV2_Atom_Sequence *control_port; @@ -145,6 +149,7 @@ typedef struct int max_block_size; int sample_counter; float sample_rate; + _Atomic(bool) must_update_midnam; } sfizz_plugin_t; enum @@ -310,6 +315,9 @@ instantiate(const LV2_Descriptor *descriptor, if (!strcmp((**f).URI, LV2_LOG__log)) self->log = (**f).data; + + if (!strcmp((**f).URI, LV2_MIDNAM__update)) + self->midnam = (**f).data; } // Setup the loggers @@ -684,6 +692,11 @@ run(LV2_Handle instance, uint32_t sample_count) // Render the block sfizz_render_block(self->synth, self->output_buffers, 2, (int)sample_count); + + if (self->midnam && atomic_exchange(&self->must_update_midnam, false)) + { + self->midnam->update(self->midnam->handle); + } } static uint32_t @@ -770,6 +783,8 @@ sfizz_lv2_update_file_info(sfizz_plugin_t* self, const char* file_path) lv2_log_note(&self->logger, "[sfizz] Number of masters: %d\n", sfizz_get_num_masters(self->synth)); lv2_log_note(&self->logger, "[sfizz] Number of groups: %d\n", sfizz_get_num_groups(self->synth)); lv2_log_note(&self->logger, "[sfizz] Number of regions: %d\n", sfizz_get_num_regions(self->synth)); + + atomic_store(&self->must_update_midnam, true); } static LV2_State_Status @@ -1066,12 +1081,44 @@ work_response(LV2_Handle instance, return LV2_WORKER_SUCCESS; } +static char * +midnam_model(LV2_Handle instance) +{ + char *model = malloc(64); + if (!model) + return NULL; + + sprintf(model, "Sfizz LV2:%p", instance); + return model; +} + +static char * +midnam_export(LV2_Handle instance) +{ + sfizz_plugin_t *self = (sfizz_plugin_t *)instance; + + char *model = midnam_model(instance); + if (!model) + return NULL; + + char *xml = sfizz_export_midnam(self->synth, model); + free(model); + return xml; +} + +static void +midnam_free(char *string) +{ + free(string); +} + static const void * extension_data(const char *uri) { static const LV2_Options_Interface options = {lv2_get_options, lv2_set_options}; static const LV2_State_Interface state = {save, restore}; static const LV2_Worker_Interface worker = {work, work_response, NULL}; + static const LV2_Midnam_Interface midnam = {midnam_export, midnam_model, midnam_free}; // Advertise the extensions we support if (!strcmp(uri, LV2_OPTIONS__interface)) @@ -1080,6 +1127,8 @@ extension_data(const char *uri) return &state; else if (!strcmp(uri, LV2_WORKER__interface)) return &worker; + else if (!strcmp(uri, LV2_MIDNAM__interface)) + return &midnam; return NULL; } diff --git a/lv2/sfizz.ttl.in b/lv2/sfizz.ttl.in index 01cf0b9d..650a4492 100644 --- a/lv2/sfizz.ttl.in +++ b/lv2/sfizz.ttl.in @@ -5,7 +5,7 @@ @prefix lv2: . @prefix midi: . @prefix opts: . -@prefix patch: . +@prefix patch: . @prefix pg: . @prefix pprop: . @prefix rdf: . @@ -14,6 +14,10 @@ @prefix units: . @prefix urid: . @prefix work: . +@prefix midnam: . + +midnam:interface a lv2:ExtensionData . +midnam:update a lv2:Feature . <#config> a pg:Group ; @@ -65,6 +69,9 @@ lv2:optionalFeature lv2:hardRTCapable, opts:options ; lv2:extensionData opts:interface, state:interface, work:interface ; + lv2:optionalFeature midnam:update ; + lv2:extensionData midnam:interface ; + patch:writable <@LV2PLUGIN_URI@:sfzfile> ; lv2:port [ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 117c3a4b..a03a1b66 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -30,7 +30,7 @@ target_sources(sfizz_static PRIVATE ${SFIZZ_SOURCES} sfizz/sfizz_wrapper.cpp) target_include_directories (sfizz_static PUBLIC .) target_include_directories (sfizz_static PUBLIC external) target_link_libraries (sfizz_static PUBLIC absl::strings absl::span) -target_link_libraries (sfizz_static PRIVATE sfizz_parser absl::flat_hash_map Threads::Threads sfizz-sndfile) +target_link_libraries (sfizz_static PRIVATE sfizz_parser absl::flat_hash_map Threads::Threads sfizz-sndfile sfizz-pugixml) add_library (sfizz::parser ALIAS sfizz_parser) add_library (sfizz::sfizz ALIAS sfizz_static) @@ -47,7 +47,7 @@ if (SFIZZ_SHARED) target_sources(sfizz_shared PRIVATE ${SFIZZ_SOURCES} sfizz/sfizz_wrapper.cpp sfizz/sfizz.cpp) target_include_directories (sfizz_shared PRIVATE .) target_include_directories (sfizz_static PRIVATE external) - target_link_libraries (sfizz_shared PRIVATE absl::strings absl::span sfizz_parser absl::flat_hash_map Threads::Threads sfizz-sndfile) + target_link_libraries (sfizz_shared PRIVATE absl::strings absl::span sfizz_parser absl::flat_hash_map Threads::Threads sfizz-sndfile sfizz-pugixml) target_compile_definitions(sfizz_shared PRIVATE SFIZZ_EXPORT_SYMBOLS) set_target_properties (sfizz_shared PROPERTIES OUTPUT_NAME sfizz PUBLIC_HEADER "sfizz.h;sfizz.hpp") set_property (TARGET sfizz_shared PROPERTY SOVERSION ${PROJECT_VERSION_MAJOR}) diff --git a/src/sfizz.h b/src/sfizz.h index cb9141fb..7b2a0c66 100644 --- a/src/sfizz.h +++ b/src/sfizz.h @@ -94,6 +94,16 @@ SFIZZ_EXPORTED_API int sfizz_get_num_masters(sfizz_synth_t* synth); * @return int the number of curves */ SFIZZ_EXPORTED_API int sfizz_get_num_curves(sfizz_synth_t* synth); +/** + * @brief Export a MIDI Name document describing the the currently loaded + * SFZ file. + * + * @param synth The synth + * @param model the model name used if a non-empty string, otherwise generated + * + * @return char* a newly allocated XML string, which must be freed after use + */ +SFIZZ_EXPORTED_API char* sfizz_export_midnam(sfizz_synth_t* synth, const char* model); /** * @brief Returns the number of preloaded samples for the current SFZ file. * diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 464b3f75..ce59e159 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -11,6 +11,7 @@ #include "MidiState.h" #include "ScopedFTZ.h" #include "StringViewHelpers.h" +#include "pugixml.hpp" #include "absl/algorithm/container.h" #include "absl/strings/str_replace.h" #include @@ -577,6 +578,94 @@ int sfz::Synth::getNumCurves() const noexcept return numCurves; } +std::string sfz::Synth::exportMidnam(absl::string_view model) const +{ + pugi::xml_document doc; + + // TODO: perhaps define this somewhere, or use some metadata from the SFZ + const char *manufacturer = "Paul Ferrand"; + const char *defaultModel = "Sfizz"; + + if (model.empty()) + model = defaultModel; + + doc.append_child(pugi::node_doctype).set_value( + "MIDINameDocument PUBLIC" + " \"-//MIDI Manufacturers Association//DTD MIDINameDocument 1.0//EN\"" + " \"http://www.midi.org/dtds/MIDINameDocument10.dtd\""); + + pugi::xml_node root = doc.append_child("MIDINameDocument"); + + root.append_child(pugi::node_comment) + .set_value("Generated by Sfizz for the current instrument"); + + root.append_child("Author"); + + pugi::xml_node device = root.append_child("MasterDeviceNames"); + device.append_child("Manufacturer") + .append_child(pugi::node_pcdata).set_value(manufacturer); + device.append_child("Model") + .append_child(pugi::node_pcdata).set_value(std::string(model).c_str()); + + { + pugi::xml_node devmode = device.append_child("CustomDeviceMode"); + devmode.append_attribute("Name").set_value("Default"); + + pugi::xml_node nsas = devmode.append_child("ChannelNameSetAssignments"); + for (unsigned c = 0; c < 16; ++c) { + pugi::xml_node nsa = nsas.append_child("ChannelNameSetAssign"); + nsa.append_attribute("Channel").set_value(std::to_string(c + 1).c_str()); + nsa.append_attribute("NameSet").set_value("Play"); + } + } + + { + pugi::xml_node chns = device.append_child("ChannelNameSet"); + chns.append_attribute("Name").set_value("Play"); + + pugi::xml_node acs = chns.append_child("AvailableForChannels"); + for (unsigned c = 0; c < 16; ++c) { + pugi::xml_node ac = acs.append_child("AvailableChannel"); + ac.append_attribute("Channel").set_value(std::to_string(c + 1).c_str()); + ac.append_attribute("Available").set_value("true"); + } + + chns.append_child("UsesControlNameList") + .append_attribute("Name").set_value("Controls"); + } + + { + pugi::xml_node cns = device.append_child("ControlNameList"); + cns.append_attribute("Name").set_value("Controls"); + for (const CCNamePair& pair : ccNames) { + pugi::xml_node cn = cns.append_child("Control"); + cn.append_attribute("Type").set_value("7bit"); + cn.append_attribute("Number").set_value(std::to_string(pair.first).c_str()); + cn.append_attribute("Name").set_value(pair.second.c_str()); + } + } + + /// + struct string_writer : pugi::xml_writer { + std::string result; + + string_writer() + { + result.reserve(8192); + } + + void write(const void* data, size_t size) override + { + result.append(static_cast(data), size); + } + }; + + /// + string_writer writer; + doc.save(writer); + return std::move(writer.result); +} + const sfz::Region* sfz::Synth::getRegionView(int idx) const noexcept { return (size_t)idx < regions.size() ? regions[idx].get() : nullptr; diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 6605951c..28d6dde9 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -111,6 +111,10 @@ public: * @return int */ int getNumCurves() const noexcept; + /** + * @brief Export a MIDI Name document describing the loaded instrument + */ + std::string exportMidnam(absl::string_view model = {}) const; /** * @brief Get a raw view into a specific region. This is mostly used * for testing. diff --git a/src/sfizz/sfizz_wrapper.cpp b/src/sfizz/sfizz_wrapper.cpp index 7f237077..8a29c1e8 100644 --- a/src/sfizz/sfizz_wrapper.cpp +++ b/src/sfizz/sfizz_wrapper.cpp @@ -49,6 +49,11 @@ int sfizz_get_num_curves(sfizz_synth_t* synth) auto self = reinterpret_cast(synth); return self->getNumCurves(); } +char* sfizz_export_midnam(sfizz_synth_t* synth, const char* model) +{ + auto self = reinterpret_cast(synth); + return strdup(self->exportMidnam(model ? model : "").c_str()); +} size_t sfizz_get_num_preloaded_samples(sfizz_synth_t* synth) { auto self = reinterpret_cast(synth);