From 722225f9f276059c71b534e071b056ce4370a3d9 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 19 Oct 2020 02:58:01 +0200 Subject: [PATCH] Add OSC and plugin-side communication --- common.mk | 2 + editor/CMakeLists.txt | 1 + editor/src/editor/Editor.cpp | 6 + editor/src/editor/EditorController.h | 10 + lv2/sfizz.c | 44 +++- lv2/sfizz.ttl.in | 7 +- lv2/sfizz_lv2.h | 4 + lv2/sfizz_ui.cpp | 43 +++- src/CMakeLists.txt | 24 +- src/sfizz.h | 74 ++++++ src/sfizz.hpp | 70 +++++ src/sfizz/Messaging.cpp | 367 +++++++++++++++++++++++++++ src/sfizz/Messaging.h | 31 +++ src/sfizz/Synth.cpp | 11 + src/sfizz/Synth.h | 22 ++ src/sfizz/SynthMessaging.cpp | 81 ++++++ src/sfizz/sfizz.cpp | 31 +++ src/sfizz/sfizz_wrapper.cpp | 33 +++ src/sfizz_message.h | 91 +++++++ tests/CMakeLists.txt | 1 + tests/MessagingT.cpp | 95 +++++++ vst/SfizzVstController.cpp | 30 +++ vst/SfizzVstController.h | 8 + vst/SfizzVstEditor.cpp | 34 ++- vst/SfizzVstEditor.h | 12 +- vst/SfizzVstProcessor.cpp | 147 ++++++++--- vst/SfizzVstProcessor.h | 12 +- 27 files changed, 1232 insertions(+), 59 deletions(-) create mode 100644 src/sfizz/Messaging.cpp create mode 100644 src/sfizz/Messaging.h create mode 100644 src/sfizz/SynthMessaging.cpp create mode 100644 src/sfizz_message.h create mode 100644 tests/MessagingT.cpp diff --git a/common.mk b/common.mk index 23d2c1c9..99c0881c 100644 --- a/common.mk +++ b/common.mk @@ -89,6 +89,7 @@ SFIZZ_SOURCES = \ src/sfizz/Logger.cpp \ src/sfizz/LFO.cpp \ src/sfizz/LFODescription.cpp \ + src/sfizz/Messaging.cpp \ src/sfizz/MidiState.cpp \ src/sfizz/OpcodeCleanup.cpp \ src/sfizz/Opcode.cpp \ @@ -112,6 +113,7 @@ SFIZZ_SOURCES = \ src/sfizz/simd/HelpersAVX.cpp \ src/sfizz/Smoothers.cpp \ src/sfizz/Synth.cpp \ + src/sfizz/SynthMessaging.cpp \ src/sfizz/Tuning.cpp \ src/sfizz/utility/SpinMutex.cpp \ src/sfizz/Voice.cpp \ diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 8f676d7e..0244666b 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -42,6 +42,7 @@ add_library(sfizz_editor STATIC EXCLUDE_FROM_ALL src/editor/utility/vstgui_after.h src/editor/utility/vstgui_before.h) target_include_directories(sfizz_editor PUBLIC "src") +target_link_libraries(sfizz_editor PUBLIC sfizz_messaging) target_link_libraries(sfizz_editor PRIVATE sfizz-vstgui) target_link_libraries(sfizz_editor PUBLIC absl::strings) if(APPLE) diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index b4a4608e..5a4c5a05 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -100,6 +100,7 @@ struct Editor::Impl : EditorController::Receiver, IControlListener { SPiano* piano_ = nullptr; void uiReceiveValue(EditId id, const EditValue& v) override; + void uiReceiveMessage(const char* path, const char* sig, const sfizz_arg_t* args) override; void createFrameContents(); @@ -318,6 +319,11 @@ void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) } } +void Editor::Impl::uiReceiveMessage(const char* path, const char* sig, const sfizz_arg_t* args) +{ + // TODO handle the message... +} + void Editor::Impl::createFrameContents() { CViewContainer* mainView; diff --git a/editor/src/editor/EditorController.h b/editor/src/editor/EditorController.h index 4d3db4c9..bbe80115 100644 --- a/editor/src/editor/EditorController.h +++ b/editor/src/editor/EditorController.h @@ -6,6 +6,7 @@ #pragma once #include "EditValue.h" +#include #include #include #include @@ -20,6 +21,7 @@ public: virtual void uiBeginSend(EditId id) = 0; virtual void uiEndSend(EditId id) = 0; virtual void uiSendMIDI(const uint8_t* msg, uint32_t len) = 0; + virtual void uiSendMessage(const char* path, const char* sig, const sfizz_arg_t* args) = 0; class Receiver; void decorate(Receiver* r) { r_ = r; } @@ -27,10 +29,12 @@ public: public: virtual ~Receiver() {} virtual void uiReceiveValue(EditId id, const EditValue& v) = 0; + virtual void uiReceiveMessage(const char* path, const char* sig, const sfizz_arg_t* args) = 0; }; // called by DSP void uiReceiveValue(EditId id, const EditValue& v); + void uiReceiveMessage(const char* path, const char* sig, const sfizz_arg_t* args); private: Receiver* r_ = nullptr; @@ -41,3 +45,9 @@ inline void EditorController::uiReceiveValue(EditId id, const EditValue& v) if (r_) r_->uiReceiveValue(id, v); } + +inline void EditorController::uiReceiveMessage(const char* path, const char* sig, const sfizz_arg_t* args) +{ + if (r_) + r_->uiReceiveMessage(path, sig, args); +} diff --git a/lv2/sfizz.c b/lv2/sfizz.c index b68c527f..44d9077a 100644 --- a/lv2/sfizz.c +++ b/lv2/sfizz.c @@ -148,6 +148,7 @@ typedef struct LV2_URID sfizz_log_status_uri; LV2_URID sfizz_check_modification_uri; LV2_URID sfizz_active_voices_uri; + LV2_URID sfizz_osc_blob_uri; LV2_URID time_position_uri; LV2_URID time_bar_uri; LV2_URID time_bar_beat_uri; @@ -158,6 +159,7 @@ typedef struct // Sfizz related data sfizz_synth_t *synth; + sfizz_client_t *client; bool expect_nominal_block_length; char sfz_file_path[MAX_PATH_SIZE]; char scala_file_path[MAX_PATH_SIZE]; @@ -181,6 +183,9 @@ typedef struct // Paths char bundle_path[MAX_BUNDLE_PATH_SIZE]; + + // OSC + uint8_t osc_temp[OSC_TEMP_SIZE]; } sfizz_plugin_t; enum @@ -236,6 +241,7 @@ sfizz_lv2_map_required_uris(sfizz_plugin_t *self) self->sfizz_log_status_uri = map->map(map->handle, SFIZZ__logStatus); self->sfizz_log_status_uri = map->map(map->handle, SFIZZ__logStatus); self->sfizz_check_modification_uri = map->map(map->handle, SFIZZ__checkModification); + self->sfizz_osc_blob_uri = map->map(map->handle, SFIZZ__OSCBlob); self->time_position_uri = map->map(map->handle, LV2_TIME__Position); self->time_bar_uri = map->map(map->handle, LV2_TIME__bar); self->time_bar_beat_uri = map->map(map->handle, LV2_TIME__barBeat); @@ -423,6 +429,28 @@ sfizz_lv2_update_timeinfo(sfizz_plugin_t *self, int delay, int updates) sfizz_send_playback_state(self->synth, delay, self->speed > 0); } +static void +sfizz_lv2_receive_message(void* data, int delay, const char* path, const char* sig, const sfizz_arg_t* args) +{ + (void)delay; + + sfizz_plugin_t *self = (sfizz_plugin_t *)data; + + // transmit to UI as OSC blob + uint8_t *osc_temp = self->osc_temp; + uint32_t osc_size = sfizz_prepare_message(osc_temp, OSC_TEMP_SIZE, path, sig, args); + if (osc_size > OSC_TEMP_SIZE) + return; + + bool write_ok = + lv2_atom_forge_frame_time(&self->forge, 0) && + lv2_atom_forge_atom(&self->forge, osc_size, self->sfizz_osc_blob_uri) && + lv2_atom_forge_raw(&self->forge, osc_temp, osc_size); + lv2_atom_forge_pad(&self->forge, osc_size); + + (void)write_ok; +} + static LV2_Handle instantiate(const LV2_Descriptor *descriptor, double rate, @@ -570,6 +598,9 @@ instantiate(const LV2_Descriptor *descriptor, } self->synth = sfizz_create_synth(); + self->client = sfizz_create_client(self); + sfizz_set_broadcast_callback(self->synth, &sfizz_lv2_receive_message, self); + sfizz_set_receive_callback(self->client, &sfizz_lv2_receive_message); sfizz_lv2_get_default_sfz_path(instance, self->sfz_file_path, MAX_PATH_SIZE); sfizz_lv2_get_default_scala_path(instance, self->scala_file_path, MAX_PATH_SIZE); @@ -586,6 +617,7 @@ static void cleanup(LV2_Handle instance) { sfizz_plugin_t *self = (sfizz_plugin_t *)instance; + sfizz_delete_client(self->client); sfizz_free(self->synth); free(self); } @@ -952,12 +984,22 @@ run(LV2_Handle instance, uint32_t sample_count) self->unmap->unmap(self->unmap->handle, obj->body.otype)); continue; } - // Got an atom that is a MIDI event } else if (ev->body.type == self->midi_event_uri) { + // Got an atom that is a MIDI event sfizz_lv2_process_midi_event(self, ev); } + else if (ev->body.type == self->sfizz_osc_blob_uri) + { + // Got an atom that is a OSC event + const char *path; + const char *sig; + const sfizz_arg_t *args; + uint8_t buffer[1024]; + if (sfizz_extract_message(LV2_ATOM_BODY_CONST(&ev->body), ev->body.size, buffer, sizeof(buffer), &path, &sig, &args) > 0) + sfizz_send_message(self->synth, self->client, (int)ev->time.frames, path, sig, args); + } } diff --git a/lv2/sfizz.ttl.in b/lv2/sfizz.ttl.in index 5ddd0317..45494016 100644 --- a/lv2/sfizz.ttl.in +++ b/lv2/sfizz.ttl.in @@ -11,6 +11,7 @@ @prefix pprop: . @prefix rdf: . @prefix rdfs: . +@prefix rsz: . @prefix state: . @prefix time: . @prefix ui: . @@ -92,21 +93,23 @@ midnam:update a lv2:Feature . lv2:port [ a lv2:InputPort, atom:AtomPort ; atom:bufferType atom:Sequence ; - atom:supports patch:Message, midi:MidiEvent, time:Position ; + atom:supports patch:Message, midi:MidiEvent, time:Position, <@LV2PLUGIN_URI@:OSCBlob> ; lv2:designation lv2:control ; lv2:index 0 ; lv2:symbol "control" ; lv2:name "Control", "ContrĂ´le"@fr ; + rsz:minimumSize 65536 ; ] , [ a lv2:OutputPort, atom:AtomPort ; atom:bufferType atom:Sequence ; - atom:supports patch:Message ; + atom:supports patch:Message, <@LV2PLUGIN_URI@:OSCBlob> ; lv2:designation lv2:control ; lv2:index 1 ; lv2:symbol "notify" ; lv2:name "Notify", "Notification"@fr ; + rsz:minimumSize 65536 ; ] , [ a lv2:AudioPort, lv2:OutputPort ; lv2:index 2 ; diff --git a/lv2/sfizz_lv2.h b/lv2/sfizz_lv2.h index b0eafe00..e8662cec 100644 --- a/lv2/sfizz_lv2.h +++ b/lv2/sfizz_lv2.h @@ -7,6 +7,8 @@ #pragma once #define MAX_PATH_SIZE 1024 +#define ATOM_TEMP_SIZE 8192 +#define OSC_TEMP_SIZE 8192 #define SFIZZ_URI "http://sfztools.github.io/sfizz" #define SFIZZ_UI_URI "http://sfztools.github.io/sfizz#ui" @@ -19,6 +21,8 @@ // These ones are just for the worker #define SFIZZ__logStatus SFIZZ_URI ":" "log_status" #define SFIZZ__checkModification SFIZZ_URI ":" "check_modification" +// OSC atoms +#define SFIZZ__OSCBlob SFIZZ_URI ":" "OSCBlob" enum { diff --git a/lv2/sfizz_ui.cpp b/lv2/sfizz_ui.cpp index 42866eb7..79e3f9fa 100644 --- a/lv2/sfizz_ui.cpp +++ b/lv2/sfizz_ui.cpp @@ -107,12 +107,17 @@ struct sfizz_ui_t : EditorController, VSTGUIEditorInterface { LV2_URID patch_value_uri; LV2_URID sfizz_sfz_file_uri; LV2_URID sfizz_scala_file_uri; + LV2_URID sfizz_osc_blob_uri; + + uint8_t osc_temp[OSC_TEMP_SIZE]; + alignas(LV2_Atom) uint8_t atom_temp[ATOM_TEMP_SIZE]; protected: void uiSendValue(EditId id, const EditValue& v) override; void uiBeginSend(EditId id) override; void uiEndSend(EditId id) override; void uiSendMIDI(const uint8_t* msg, uint32_t len) override; + void uiSendMessage(const char* path, const char* sig, const sfizz_arg_t* args) override; private: void uiTouch(EditId id, bool t); @@ -192,6 +197,7 @@ instantiate(const LV2UI_Descriptor *descriptor, self->patch_value_uri = map->map(map->handle, LV2_PATCH__value); self->sfizz_sfz_file_uri = map->map(map->handle, SFIZZ__sfzFile); self->sfizz_scala_file_uri = map->map(map->handle, SFIZZ__tuningfile); + self->sfizz_osc_blob_uri = map->map(map->handle, SFIZZ__OSCBlob); // set up the resource path // * on Linux, this is determined by going 2 folders back from the SO path @@ -335,6 +341,14 @@ port_event(LV2UI_Handle ui, } } } + else if (atom->type == self->sfizz_osc_blob_uri) { + const char *path; + const char *sig; + const sfizz_arg_t *args; + uint8_t buffer[1024]; + if (sfizz_extract_message(LV2_ATOM_BODY_CONST(atom), atom->size, buffer, sizeof(buffer), &path, &sig, &args) > 0) + self->uiReceiveMessage(path, sig, args); + } } (void)buffer_size; @@ -422,9 +436,8 @@ void sfizz_ui_t::uiSendValue(EditId id, const EditValue& v) auto sendPath = [this](LV2_URID property, const std::string& value) { LV2_Atom_Forge *forge = &atom_forge; LV2_Atom_Forge_Frame frame; - alignas(LV2_Atom) uint8_t buffer[MAX_PATH_SIZE + 512]; - auto *atom = reinterpret_cast(buffer); - lv2_atom_forge_set_buffer(forge, (uint8_t *)&buffer, sizeof(buffer)); + auto *atom = reinterpret_cast(atom_temp); + lv2_atom_forge_set_buffer(forge, atom_temp, sizeof(atom_temp)); if (lv2_atom_forge_object(forge, &frame, 0, patch_set_uri) && lv2_atom_forge_key(forge, patch_property_uri) && lv2_atom_forge_urid(forge, property) && @@ -514,12 +527,30 @@ void sfizz_ui_t::uiTouch(EditId id, bool t) void sfizz_ui_t::uiSendMIDI(const uint8_t* msg, uint32_t len) { LV2_Atom_Forge *forge = &atom_forge; - alignas(LV2_Atom) uint8_t buffer[512]; - auto *atom = reinterpret_cast(buffer); - lv2_atom_forge_set_buffer(forge, (uint8_t *)&buffer, sizeof(buffer)); + auto *atom = reinterpret_cast(atom_temp); + lv2_atom_forge_set_buffer(forge, atom_temp, sizeof(atom_temp)); if (lv2_atom_forge_atom(forge, len, midi_event_uri) && lv2_atom_forge_write(forge, msg, len)) { write(con, SFIZZ_CONTROL, lv2_atom_total_size(atom), atom_event_transfer_uri, atom); } } + +void sfizz_ui_t::uiSendMessage(const char* path, const char* sig, const sfizz_arg_t* args) +{ + uint8_t *osc_temp = this->osc_temp; + uint32_t osc_size = sfizz_prepare_message(osc_temp, OSC_TEMP_SIZE, path, sig, args); + + if (osc_size > OSC_TEMP_SIZE) + return; + + LV2_Atom_Forge *forge = &atom_forge; + auto *atom = reinterpret_cast(atom_temp); + lv2_atom_forge_set_buffer(forge, atom_temp, sizeof(atom_temp)); + + if (lv2_atom_forge_atom(forge, osc_size, sfizz_osc_blob_uri) && + lv2_atom_forge_raw(forge, osc_temp, osc_size)) + { + write(con, SFIZZ_CONTROL, lv2_atom_total_size(atom), atom_event_transfer_uri, atom); + } +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e8cb136c..f5ad3e7e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -148,6 +148,7 @@ set (SFIZZ_SOURCES sfizz/PowerFollower.cpp sfizz/FlexEGDescription.cpp sfizz/FlexEnvelope.cpp + sfizz/SynthMessaging.cpp sfizz/modulations/ModId.cpp sfizz/modulations/ModKey.cpp sfizz/modulations/ModKeyHash.cpp @@ -206,6 +207,7 @@ set (SFIZZ_PARSER_SOURCES set (SFIZZ_PARSER_OTHER sfizz/OpcodeCleanup.re) source_group ("Other Files" FILES ${SFIZZ_PARSER_OTHER}) +# Sfizz parser library add_library (sfizz_parser STATIC) target_sources (sfizz_parser PRIVATE ${SFIZZ_PARSER_HEADERS} ${SFIZZ_PARSER_SOURCES} ${SFIZZ_PARSER_OTHER}) @@ -213,6 +215,20 @@ target_include_directories (sfizz_parser PUBLIC sfizz) target_include_directories (sfizz_parser PUBLIC external) target_link_libraries (sfizz_parser PUBLIC absl::strings PRIVATE absl::flat_hash_map) +# OSC messaging library +set (SFIZZ_MESSAGING_HEADERS + sfizz/Messaging.h + sfizz_message.h) + +set (SFIZZ_MESSAGING_SOURCES + sfizz/Messaging.cpp) + +add_library (sfizz_messaging STATIC) +target_sources (sfizz_messaging PRIVATE + ${SFIZZ_MESSAGING_HEADERS} ${SFIZZ_MESSAGING_SOURCES}) +target_include_directories (sfizz_messaging PUBLIC ".") +target_link_libraries (sfizz_messaging PUBLIC absl::strings) + # Sfizz static library add_library(sfizz_static STATIC) target_sources(sfizz_static PRIVATE @@ -220,8 +236,8 @@ target_sources(sfizz_static PRIVATE 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 st_audiofile sfizz-pugixml sfizz-spline sfizz-tunings sfizz-kissfft sfizz-cpuid sfizz-jsl sfizz-atomic) -set_target_properties (sfizz_static PROPERTIES OUTPUT_NAME sfizz PUBLIC_HEADER "sfizz.h;sfizz.hpp") +target_link_libraries (sfizz_static PRIVATE sfizz_parser sfizz_messaging absl::flat_hash_map Threads::Threads st_audiofile sfizz-pugixml sfizz-spline sfizz-tunings sfizz-kissfft sfizz-cpuid sfizz-jsl sfizz-atomic) +set_target_properties (sfizz_static PROPERTIES OUTPUT_NAME sfizz PUBLIC_HEADER "sfizz.h;sfizz.hpp;sfizz_message.h") if(SFIZZ_USE_SNDFILE) target_compile_definitions (sfizz_static PUBLIC SFIZZ_USE_SNDFILE=1) target_link_libraries (sfizz_static PUBLIC st_audiofile) @@ -251,7 +267,7 @@ if (SFIZZ_SHARED) ${SFIZZ_HEADERS} ${SFIZZ_SOURCES} ${FAUST_FILES} sfizz/sfizz_wrapper.cpp sfizz/sfizz.cpp) target_include_directories (sfizz_shared PRIVATE .) target_include_directories (sfizz_shared PRIVATE external) - target_link_libraries (sfizz_shared PRIVATE absl::strings absl::span sfizz_parser absl::flat_hash_map Threads::Threads st_audiofile sfizz-pugixml sfizz-spline sfizz-tunings sfizz-kissfft sfizz-cpuid sfizz-jsl sfizz-atomic) + target_link_libraries (sfizz_shared PRIVATE absl::strings absl::span sfizz_parser sfizz_messaging absl::flat_hash_map Threads::Threads st_audiofile sfizz-pugixml sfizz-spline sfizz-tunings sfizz-kissfft sfizz-cpuid sfizz-jsl sfizz-atomic) if(SFIZZ_USE_SNDFILE) target_compile_definitions (sfizz_shared PUBLIC SFIZZ_USE_SNDFILE=1) target_link_libraries (sfizz_shared PUBLIC st_audiofile) @@ -263,7 +279,7 @@ if (SFIZZ_SHARED) target_compile_definitions (sfizz_shared PRIVATE "SFIZZ_ENABLE_RELEASE_ASSERT=1") endif() target_compile_definitions(sfizz_shared PRIVATE SFIZZ_EXPORT_SYMBOLS) - set_target_properties (sfizz_shared PROPERTIES SOVERSION ${PROJECT_VERSION_MAJOR} OUTPUT_NAME sfizz PUBLIC_HEADER "sfizz.h;sfizz.hpp") + set_target_properties (sfizz_shared PROPERTIES SOVERSION ${PROJECT_VERSION_MAJOR} OUTPUT_NAME sfizz PUBLIC_HEADER "sfizz.h;sfizz.hpp;sfizz_message.h") sfizz_enable_lto_if_needed(sfizz_shared) sfizz_enable_fast_math(sfizz_shared) diff --git a/src/sfizz.h b/src/sfizz.h index ec47ad04..b20e7eae 100644 --- a/src/sfizz.h +++ b/src/sfizz.h @@ -10,6 +10,7 @@ */ #pragma once +#include "sfizz_message.h" #include #include @@ -731,6 +732,79 @@ SFIZZ_EXPORTED_API int sfizz_get_cc_label_number(sfizz_synth_t* synth, int label */ SFIZZ_EXPORTED_API const char * sfizz_get_cc_label_text(sfizz_synth_t* synth, int label_index); +/** + * @addtogroup Messaging + * @{ + */ + +/** + * @brief Client for communicating with the synth engine in either direction + * @since 0.6.0 + */ +typedef struct sfizz_client_t sfizz_client_t; + +/** + * @brief Create a new messaging client + * @since 0.6.0 + * + * @param data The opaque data pointer which is passed to the receiver. + * @return The new client. + */ +SFIZZ_EXPORTED_API sfizz_client_t* sfizz_create_client(void* data); + +/** + * @brief Destroy a messaging client + * @since 0.6.0 + * + * @param client The client. + */ +SFIZZ_EXPORTED_API void sfizz_delete_client(sfizz_client_t* client); + +/** + * @brief Get the client data + * @since 0.6.0 + * + * @param client The client. + * @return The client data. + */ +SFIZZ_EXPORTED_API void* sfizz_get_client_data(sfizz_client_t* client); + +/** + * @brief Set the function which receives reply messages from the synth engine. + * @since 0.6.0 + * + * @param client The client. + * @param receive The pointer to the receiving function. + */ +SFIZZ_EXPORTED_API void sfizz_set_receive_callback(sfizz_client_t* client, sfizz_receive_t* receive); + +/** + * @brief Send a message to the synth engine + * @since 0.6.0 + * + * @param synth The synth. + * @param client The client sending the message. + * @param delay The delay of the message in the block, in samples. + * @param path The OSC address pattern. + * @param sig The OSC type tag string. + * @param args The OSC arguments, whose number and format is determined the type tag string. + */ +SFIZZ_EXPORTED_API void sfizz_send_message(sfizz_synth_t* synth, sfizz_client_t* client, int delay, const char* path, const char* sig, const sfizz_arg_t* args); + +/** + * @brief Set the function which receives broadcast messages from the synth engine. + * @since 0.6.0 + * + * @param synth The synth. + * @param broadcast The pointer to the receiving function. + * @param data The opaque data pointer which is passed to the receiver. + */ +SFIZZ_EXPORTED_API void sfizz_set_broadcast_callback(sfizz_synth_t* synth, sfizz_receive_t* broadcast, void* data); + +/** + * @} + */ + #ifdef __cplusplus } #endif diff --git a/src/sfizz.hpp b/src/sfizz.hpp index 81ba6cae..f86eea8a 100644 --- a/src/sfizz.hpp +++ b/src/sfizz.hpp @@ -10,6 +10,7 @@ */ #pragma once +#include "sfizz_message.h" #include #include #include @@ -28,6 +29,7 @@ namespace sfz { class Synth; +class Client; /** * @brief Main class. */ @@ -565,7 +567,75 @@ public: */ const std::vector>& getCCLabels() const noexcept; + /** + * @addtogroup Messaging + * @{ + */ + +private: + struct ClientDeleter { + void operator()(Client *client) const noexcept; + }; + +public: + using ClientPtr = std::unique_ptr; + + /** + * @brief Create a new messaging client + * @since 0.6.0 + * + * @param data The opaque data pointer which is passed to the receiver. + * @return The new client. + */ + static ClientPtr createClient(void* data); + + /** + * @brief Get the client data + * @since 0.6.0 + * + * @param client The client. + * @return The client data. + */ + static void* getClientData(Client& client); + + /** + * @brief Set the function which receives reply messages from the synth engine. + * @since 0.6.0 + * + * @param client The client. + * @param receive The pointer to the receiving function. + */ + static void setReceiveCallback(Client& client, sfizz_receive_t* receive); + + /** + * @brief Send a message to the synth engine + * @since 0.6.0 + * + * @param client The client sending the message. + * @param delay The delay of the message in the block, in samples. + * @param path The OSC address pattern. + * @param sig The OSC type tag string. + * @param args The OSC arguments, whose number and format is determined the type tag string. + */ + void sendMessage(Client& client, int delay, const char* path, const char* sig, const sfizz_arg_t* args); + + /** + * @brief Set the function which receives broadcast messages from the synth engine. + * @since 0.6.0 + * + * @param broadcast The pointer to the receiving function. + * @param data The opaque data pointer which is passed to the receiver. + */ + void setBroadcastCallback(sfizz_receive_t* broadcast, void* data); + + /** + * @} + */ + private: std::unique_ptr synth; }; + +using ClientPtr = Sfizz::ClientPtr; + } diff --git a/src/sfizz/Messaging.cpp b/src/sfizz/Messaging.cpp new file mode 100644 index 00000000..f8d1d731 --- /dev/null +++ b/src/sfizz/Messaging.cpp @@ -0,0 +1,367 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "Messaging.h" +#include +#include +#include +#include + +#ifdef __cplusplus +static_assert( + sizeof(sfizz_arg_t) == sizeof(int64_t) && alignof(sfizz_arg_t) == 8, + "The ABI stability check has failed."); +#endif + +template +static T paddingSize(T count, unsigned align) { + unsigned mask = align - 1; + return (align - (count & mask)) & mask; +}; + +/// +class OSCWriter { +public: + void setOutputBuffer(void* buffer, uint32_t capacity); + uint32_t writeMessage(const char* path, const char* sig, const sfizz_arg_t* args); + +private: + uint32_t appendBytes(const void* src, uint32_t count); + uint32_t appendZeros(uint32_t count); + template uint32_t appendInteger(T integer); + uint32_t appendFloat(float f); + uint32_t appendDouble(float d); + +private: + uint8_t* dstBuffer_ = nullptr; + uint32_t dstCapacity_ = 0; +}; + +class OSCReader { +public: + void setInputBuffer(const void* buffer, uint32_t capacity); + void setAllocationBuffer(void* buffer, uint32_t capacity); + int32_t extractMessage(const char** outPath, const char** outSig, const sfizz_arg_t** outArgs); + +private: + template T* allocate(uint32_t count); + bool extractString(const char*& outStr, uint32_t& outLen); + template bool extractInteger(T& outValue); + bool extractFloat(float& f); + bool extractDouble(double& d); + +private: + const uint8_t* srcBuffer_ = nullptr; + uint32_t srcCapacity_ = 0; + uint8_t* allocBuffer_ = nullptr; + uint32_t allocCapacity_ = 0; +}; + +/// +extern "C" uint32_t sfizz_prepare_message( + void* buffer, uint32_t capacity, + const char* path, const char* sig, const sfizz_arg_t* args) +{ + OSCWriter writer; + writer.setOutputBuffer(buffer, capacity); + return writer.writeMessage(path, sig, args); +} + +extern "C" int32_t sfizz_extract_message( + const void* srcBuffer, uint32_t srcCapacity, + void* argsBuffer, uint32_t argsCapacity, + const char** outPath, const char** outSig, const sfizz_arg_t** outArgs) +{ + OSCReader reader; + reader.setInputBuffer(srcBuffer, srcCapacity); + reader.setAllocationBuffer(argsBuffer, argsCapacity); + return reader.extractMessage(outPath, outSig, outArgs); +} + +/// +void OSCWriter::setOutputBuffer(void* buffer, uint32_t capacity) +{ + dstBuffer_ = reinterpret_cast(buffer); + dstCapacity_ = capacity; +} + +uint32_t OSCWriter::writeMessage(const char* path, const char* sig, const sfizz_arg_t* args) +{ + uint32_t msglen = 0; + + // write path, null byte, and 4byte padding + uint32_t pathlen = static_cast(strlen(path)); + msglen += appendBytes(path, pathlen + 1); + msglen += appendZeros(paddingSize(pathlen + 1, 4)); + + // write comma, signature, null byte, and 4byte padding + uint32_t siglen = static_cast(strlen(sig)); + msglen += appendBytes(",", 1); + msglen += appendBytes(sig, siglen + 1); + msglen += appendZeros(paddingSize(siglen + 2, 4)); + + // write arguments + for (uint32_t i = 0; i < siglen; ++i) { + switch (sig[i]) { + default: + return 0; + case 'i': + case 'c': + case 'r': + msglen += appendInteger(args[i].i); + break; + case 'm': + msglen += appendBytes(args[i].m, 4); + break; + case 'h': + msglen += appendInteger(args[i].h); + break; + case 'f': + msglen += appendFloat(args[i].f); + break; + case 'd': + msglen += appendDouble(args[i].d); + break; + case 's': + case 'S': + { + size_t len = strlen(args[i].s); + msglen += appendBytes(args[i].s, len + 1); + msglen += appendZeros(paddingSize(len + 1, 4)); + } + break; + case 'b': + { + msglen += appendInteger(args[i].b->size); + msglen += appendBytes(args[i].b->data, args[i].b->size); + msglen += appendZeros(paddingSize(args[i].b->size, 4)); + } + break; + case 'T': + case 'F': + case 'N': + case 'I': + break; + } + } + + return msglen; +} + +uint32_t OSCWriter::appendBytes(const void* src, uint32_t count) +{ + uint32_t written = std::min(dstCapacity_, count); + memcpy(dstBuffer_, src, written); + dstBuffer_ += written; + dstCapacity_ -= written; + return count; +} + +uint32_t OSCWriter::appendZeros(uint32_t count) +{ + uint32_t written = std::min(dstCapacity_, count); + memset(dstBuffer_, '\0', written); + dstBuffer_ += written; + dstCapacity_ -= written; + return count; +} + +template uint32_t OSCWriter::appendInteger(T integer) +{ + using U = typename std::make_unsigned::type; + const U uinteger = static_cast(integer); + uint8_t data[sizeof(U)]; + for (unsigned i = 0; i < sizeof(U); ++i) { + unsigned sh = 8 * (sizeof(U) - 1 - i); + data[i] = (uint8_t)((uinteger >> sh) & 0xff); + } + return appendBytes(data, sizeof(U)); +} + +uint32_t OSCWriter::appendFloat(float f) +{ + union { float f; uint32_t i; } u; + u.f = f; + return appendInteger(u.i); +} + +uint32_t OSCWriter::appendDouble(float d) +{ + union { double d; uint64_t i; } u; + u.d = d; + return appendInteger(u.i); +} + +/// +void OSCReader::setInputBuffer(const void* buffer, uint32_t capacity) +{ + srcBuffer_ = reinterpret_cast(buffer); + srcCapacity_ = capacity; +} + +void OSCReader::setAllocationBuffer(void* buffer, uint32_t capacity) +{ + allocBuffer_ = reinterpret_cast(buffer); + allocCapacity_ = capacity; +} + +int32_t OSCReader::extractMessage(const char** outPath, const char** outSig, const sfizz_arg_t** outArgs) +{ + const uint8_t* const srcStart = srcBuffer_; + + // read path, null byte + const char* path; + uint32_t pathlen; + if (!extractString(path, pathlen)) + return 0; + if (outPath) + *outPath = path; + + // read signature, null byte + const char* sig; + uint32_t siglen; + if (!extractString(sig, siglen) || sig[0] != ',') + return 0; + ++sig; + --siglen; + if (outSig) + *outSig = sig; + + // read arguments + sfizz_arg_t* args = allocate(siglen); + if (!args) + return -1; + if (outArgs) + *outArgs = args; + + for (uint32_t i = 0, n = siglen; i < n; ++i) { + switch (sig[i]) { + default: + return 0; + case 'i': + case 'c': + case 'r': + if (!extractInteger(args[i].i)) + return 0; + break; + case 'm': + if (srcCapacity_ < 4) + return 0; + memcpy(args[i].m, srcBuffer_, 4); + srcBuffer_ += 4; + srcCapacity_ -= 4; + break; + case 'h': + if (!extractInteger(args[i].h)) + return 0; + break; + case 'f': + if (!extractFloat(args[i].f)) + return 0; + break; + case 'd': + if (!extractDouble(args[i].d)) + return 0; + break; + case 's': + case 'S': + { + const char* str; + uint32_t len; + if (!extractString(str, len)) + return 0; + args[i].s = str; + } + break; + case 'b': + { + sfizz_blob_t* blob = allocate(1); + if (!blob) + return -1; + args[i].b = blob; + uint32_t len = blob->size; + if (!extractInteger(len)) + return 0; + uint32_t padlen = len + paddingSize(len, 4); + if (srcCapacity_ < padlen) + return 0; + blob->data = srcBuffer_; + blob->size = len; + srcBuffer_ += padlen; + srcCapacity_ -= padlen; + } + break; + case 'T': + case 'F': + case 'N': + case 'I': + break; + } + } + + return srcBuffer_ - srcStart; +} + +template T* OSCReader::allocate(uint32_t count) +{ + uintptr_t pad = paddingSize(reinterpret_cast(allocBuffer_), alignof(T)); + uint32_t size = count * sizeof(T); + if (allocCapacity_ < pad + size) + return nullptr; + void* ptr = allocBuffer_ + pad; + allocBuffer_ += pad + size; + allocCapacity_ -= pad + size; + return reinterpret_cast(ptr); +} + +bool OSCReader::extractString(const char*& outStr, uint32_t& outLen) +{ + const char* str = reinterpret_cast(srcBuffer_); + uint32_t len = static_cast(strnlen(str, srcCapacity_)); + if (len == srcCapacity_) + return false; + uint32_t padlen = len + 1 + paddingSize(len + 1, 4); + if (padlen > srcCapacity_) + return false; + srcBuffer_ += padlen; + srcCapacity_ -= padlen; + outStr = str; + outLen = len; + return true; +} + +template bool OSCReader::extractInteger(T& outValue) +{ + if (srcCapacity_ < sizeof(T)) + return false; + using U = typename std::make_unsigned::type; + U value = 0; + const uint8_t* src = reinterpret_cast(srcBuffer_); + for (unsigned i = 0; i < sizeof(T); ++i) + value = (value << 8) | src[i]; + srcBuffer_ += sizeof(T); + srcCapacity_ -= sizeof(T); + outValue = static_cast(value); + return true; +}; + +bool OSCReader::extractFloat(float& f) +{ + union { float f; uint32_t i; } u; + if (!extractInteger(u.i)) + return false; + f = u.f; + return true; +} + +bool OSCReader::extractDouble(double& d) +{ + union { double d; uint64_t i; } u; + if (!extractInteger(u.i)) + return false; + d = u.d; + return true; +} diff --git a/src/sfizz/Messaging.h b/src/sfizz/Messaging.h new file mode 100644 index 00000000..737fa1d4 --- /dev/null +++ b/src/sfizz/Messaging.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "sfizz_message.h" + +namespace sfz { + +class Client { +public: + explicit Client(void* data) : data_(data) {} + void* getClientData() const { return data_; } + void setReceiveCallback(sfizz_receive_t* receive) { receive_ = receive; } + bool canReceive() const { return receive_ != nullptr; } + void receive(int delay, const char* path, const char* sig, const sfizz_arg_t* args); + +private: + void* data_ = nullptr; + sfizz_receive_t* receive_ = nullptr; +}; + +inline void Client::receive(int delay, const char* path, const char* sig, const sfizz_arg_t* args) +{ + if (receive_) + receive_(data_, delay, path, sig, args); +} + +} // namespace sfz diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 9f2bb846..66f19755 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -298,6 +298,10 @@ struct Synth::Impl final: public Parser::Listener { fs::file_time_type modificationTime_ { }; std::array defaultCCValues_; + + // Messaging + sfizz_receive_t* broadcastReceiver = nullptr; + void* broadcastData = nullptr; }; Synth::Synth() @@ -1953,6 +1957,13 @@ std::bitset Synth::getUsedCCs() const noexcept return used; } +void sfz::Synth::setBroadcastCallback(sfizz_receive_t* broadcast, void* data) +{ + Impl& impl = *impl_; + impl.broadcastReceiver = broadcast; + impl.broadcastData = data; +} + void Synth::Impl::updateUsedCCsFromRegion(std::bitset& usedCCs, const Region& region) { updateUsedCCsFromCCMap(usedCCs, region.offsetCC); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index dbfa21c5..1d971d80 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -8,6 +8,7 @@ #include "AudioSpan.h" #include "LeakDetector.h" #include "Resources.h" +#include "Messaging.h" #include "utility/NumericId.h" #include "parser/Parser.h" #include @@ -585,6 +586,27 @@ public: */ std::bitset getUsedCCs() const noexcept; + /** + * @brief Dispatch the incoming message to the synth engine + * @since 0.6.0 + * + * @param client The client sending the message. + * @param delay The delay of the message in the block, in samples. + * @param path The OSC address pattern. + * @param sig The OSC type tag string. + * @param args The OSC arguments, whose number and format is determined the type tag string. + */ + void dispatchMessage(Client& client, int delay, const char* path, const char* sig, const sfizz_arg_t* args); + + /** + * @brief Set the function which receives broadcast messages from the synth engine. + * @since 0.6.0 + * + * @param broadcast The pointer to the receiving function. + * @param data The opaque data pointer which is passed to the receiver. + */ + void setBroadcastCallback(sfizz_receive_t* broadcast, void* data); + private: struct Impl; std::unique_ptr impl_; diff --git a/src/sfizz/SynthMessaging.cpp b/src/sfizz/SynthMessaging.cpp new file mode 100644 index 00000000..ed5c2bae --- /dev/null +++ b/src/sfizz/SynthMessaging.cpp @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "Synth.h" +#include "StringViewHelpers.h" +#include +#include + +namespace sfz { +static constexpr unsigned maxIndices = 8; + +static bool extractMessage(const char* pattern, const char* path, unsigned* indices); +static uint64_t hashMessagePath(const char* path, const char* sig); + +void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, const char* sig, const sfizz_arg_t* args) +{ + unsigned indices[maxIndices]; + + switch (hashMessagePath(path, sig)) { + #define MATCH(p, s) case hash(p "," s): \ + if (extractMessage(p, path, indices) && !strcmp(sig, s)) + + MATCH("/hello", "") { + client.receive(delay, "/hello", "", nullptr); + break; + } + + // TODO... + } +} + +static bool extractMessage(const char* pattern, const char* path, unsigned* indices) +{ + unsigned nthIndex = 0; + + while (const char *endp = strchr(pattern, '&')) { + if (nthIndex == maxIndices) + return false; + + size_t length = endp - pattern; + if (strncmp(pattern, path, length)) + return false; + pattern += length; + path += length; + + length = 0; + while (absl::ascii_isdigit(path[length])) + ++length; + + if (!absl::SimpleAtoi(absl::string_view(path, length), &indices[nthIndex++])) + return false; + + pattern += 1; + path += length; + } + + return !strcmp(path, pattern); +} + +static uint64_t hashMessagePath(const char* path, const char* sig) +{ + uint64_t h = Fnv1aBasis; + while (unsigned char c = *path++) { + if (!absl::ascii_isdigit(c)) + h = hashByte(c, h); + else { + h = hashByte('&', h); + while (absl::ascii_isdigit(*path)) + ++path; + } + } + h = hashByte(','); + while (unsigned char c = *sig++) + h = hashByte(c, h); + return h; +} + +} // namespace sfz diff --git a/src/sfizz/sfizz.cpp b/src/sfizz/sfizz.cpp index 13cfd8a9..18b2a10c 100644 --- a/src/sfizz/sfizz.cpp +++ b/src/sfizz/sfizz.cpp @@ -5,6 +5,7 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #include "Synth.h" +#include "Messaging.h" #include "sfizz.hpp" #include "absl/memory/memory.h" @@ -305,3 +306,33 @@ const std::vector>& sfz::Sfizz::getCCLabels() c { return synth->getCCLabels(); } + +void sfz::Sfizz::ClientDeleter::operator()(Client *client) const noexcept +{ + delete client; +} + +auto sfz::Sfizz::createClient(void* data) -> ClientPtr +{ + return ClientPtr(new Client(data)); +} + +void* sfz::Sfizz::getClientData(Client& client) +{ + return client.getClientData(); +} + +void sfz::Sfizz::setReceiveCallback(Client& client, sfizz_receive_t* receive) +{ + client.setReceiveCallback(receive); +} + +void sfz::Sfizz::sendMessage(Client& client, int delay, const char* path, const char* sig, const sfizz_arg_t* args) +{ + synth->dispatchMessage(client, delay, path, sig, args); +} + +void sfz::Sfizz::setBroadcastCallback(sfizz_receive_t* broadcast, void* data) +{ + synth->setBroadcastCallback(broadcast, data); +} diff --git a/src/sfizz/sfizz_wrapper.cpp b/src/sfizz/sfizz_wrapper.cpp index 8dffaf6a..0ca6de29 100644 --- a/src/sfizz/sfizz_wrapper.cpp +++ b/src/sfizz/sfizz_wrapper.cpp @@ -7,6 +7,7 @@ #include "Config.h" #include "Macros.h" #include "Synth.h" +#include "Messaging.h" #include "sfizz.h" #include @@ -429,6 +430,38 @@ const char * sfizz_get_cc_label_text(sfizz_synth_t* synth, int label_index) return ccLabels[label_index].second.c_str(); } +sfizz_client_t* sfizz_create_client(void* data) +{ + return reinterpret_cast(new sfz::Client(data)); +} + +void sfizz_delete_client(sfizz_client_t* client) +{ + delete reinterpret_cast(client); +} + +void* sfizz_get_client_data(sfizz_client_t* client) +{ + return reinterpret_cast(client)->getClientData(); +} + +void sfizz_set_receive_callback(sfizz_client_t* client, sfizz_receive_t* receive) +{ + reinterpret_cast(client)->setReceiveCallback(receive); +} + +void sfizz_send_message(sfizz_synth_t* synth, sfizz_client_t* client, int delay, const char* path, const char* sig, const sfizz_arg_t* args) +{ + auto* self = reinterpret_cast(synth); + self->dispatchMessage(*reinterpret_cast(client), delay, path, sig, args); +} + +void sfizz_set_broadcast_callback(sfizz_synth_t* synth, sfizz_receive_t* broadcast, void* data) +{ + auto* self = reinterpret_cast(synth); + self->setBroadcastCallback(broadcast, data); +} + #ifdef __cplusplus } #endif diff --git a/src/sfizz_message.h b/src/sfizz_message.h new file mode 100644 index 00000000..405fbb0f --- /dev/null +++ b/src/sfizz_message.h @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @addtogroup Messaging + * @{ + */ + +/** + * @brief Representation of a binary blob in OSC format + * @since 0.6.0 + */ +typedef struct { + const uint8_t* data; + uint32_t size; +} sfizz_blob_t; + +/** + * @brief Representation of an argument of variant type in OSC format + * @since 0.6.0 + */ +typedef union { + int32_t i; + int64_t h; + float f; + double d; + const char* s; + const sfizz_blob_t* b; + uint8_t m[4]; +} sfizz_arg_t; + +/** + * @brief Generic message receiving function + * @since 0.6.0 + */ +typedef void (sfizz_receive_t)(void* data, int delay, const char* path, const char* sig, const sfizz_arg_t* args); + +/** + * @brief Convert the message to OSC using the provided output buffer + * @since 0.6.0 + * + * @param buffer The output buffer + * @param capacity The capacity of the buffer + * @param path The path + * @param sig The signature + * @param args The arguments + * @return The size necessary to store the converted message in + * entirety, <= capacity if the written message is valid. + */ +uint32_t sfizz_prepare_message( + void* buffer, uint32_t capacity, + const char* path, const char* sig, const sfizz_arg_t* args); + +/** + * @brief Extract the contents of an OSC message + * @since 0.6.0 + * + * @param srcBuffer The data of the OSC message + * @param srcCapacity The size of the OSC message + * @param argsBuffer A buffer where the function can allocate the arguments + * @param argsCapacity The capacity of the argument buffer + * @param outPath A pointer to the variable which receives the path + * @param outSig A pointer to the variable which receives the signature + * @param outArgs A pointer to the variable which receives the arguments + * @return On success, this is the number of bytes read. + * On failure, it is 0 if the OSC message is invalid, + * -1 if there was not enough buffer for the arguments. + */ +int32_t sfizz_extract_message( + const void* srcBuffer, uint32_t srcCapacity, + void* argsBuffer, uint32_t argsCapacity, + const char** outPath, const char** outSig, const sfizz_arg_t** outArgs); + +/** + * @} + */ + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f6f25d9c..8e562df8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -41,6 +41,7 @@ set(SFIZZ_TEST_SOURCES ConcurrencyT.cpp ModulationsT.cpp LFOT.cpp + MessagingT.cpp DataHelpers.h DataHelpers.cpp ) diff --git a/tests/MessagingT.cpp b/tests/MessagingT.cpp new file mode 100644 index 00000000..e8a4938d --- /dev/null +++ b/tests/MessagingT.cpp @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "sfizz/Messaging.h" +#include "catch2/catch.hpp" +#include +#include + +TEST_CASE("[Messaging] OSC message creation") +{ + // http://opensoundcontrol.org/spec-1_0-examples + + { + const char* path = "/oscillator/4/frequency"; + const char* sig = "f"; + sfizz_arg_t args[1]; + args[0].f = 440.0f; + + const uint8_t expected[] = { + 0x2f, /* / */ 0x6f, /* o */ 0x73, /* s */ 0x63, /* c */ + 0x69, /* i */ 0x6c, /* l */ 0x6c, /* l */ 0x61, /* a */ + 0x74, /* t */ 0x6f, /* o */ 0x72, /* r */ 0x2f, /* / */ + 0x34, /* 4 */ 0x2f, /* / */ 0x66, /* f */ 0x72, /* r */ + 0x65, /* e */ 0x71, /* q */ 0x75, /* u */ 0x65, /* e */ + 0x6e, /* n */ 0x63, /* c */ 0x79, /* y */ 0x00, + 0x2c, /* , */ 0x66, /* f */ 0x00, 0x00, + 0x43, 0xdc, 0x00, 0x00, + }; + + uint32_t size = sfizz_prepare_message(nullptr, 0, path, sig, args); + REQUIRE(size == sizeof(expected)); + + uint8_t actual[sizeof(expected)]; + size = sfizz_prepare_message(actual, sizeof(actual), path, sig, args); + REQUIRE(size == sizeof(expected)); + REQUIRE(absl::MakeSpan(actual) == absl::MakeSpan(expected)); + + const char* path2; + const char* sig2; + const sfizz_arg_t* args2; + uint8_t buffer[256]; + REQUIRE(sfizz_extract_message(actual, sizeof(actual), buffer, sizeof(buffer), &path2, &sig2, &args2) > 0); + REQUIRE(!strcmp(path, path2)); + REQUIRE(!strcmp(sig, sig2)); + REQUIRE(args[0].f == 440.0f); + } + + { + const char* path = "/foo"; + const char* sig = "iisff"; + sfizz_arg_t args[5]; + args[0].i = 1000; + args[1].i = -1; + args[2].s = "hello"; + args[3].f = 1.234f; + args[4].f = 5.678f; + + const uint8_t expected[] = { + 0x2f, /* / */ 0x66, /* f */ 0x6f, /* o */ 0x6f, /* o */ + 0x00, 0x00, 0x00, 0x00, + 0x2c, /* , */ 0x69, /* i */ 0x69, /* i */ 0x73, /* s */ + 0x66, /* f */ 0x66, /* f */ 0x00, 0x00, + 0x00, 0x00, 0x03, 0xe8, + 0xff, 0xff, 0xff, 0xff, + 0x68, 0x65, 0x6c, 0x6c, + 0x6f, 0x00, 0x00, 0x00, + 0x3f, 0x9d, 0xf3, 0xb6, + 0x40, 0xb5, 0xb2, 0x2d, + }; + + uint32_t size = sfizz_prepare_message(nullptr, 0, path, sig, args); + REQUIRE(size == sizeof(expected)); + + uint8_t actual[sizeof(expected)]; + size = sfizz_prepare_message(actual, sizeof(actual), path, sig, args); + REQUIRE(size == sizeof(expected)); + REQUIRE(absl::MakeSpan(actual) == absl::MakeSpan(expected)); + + const char* path2; + const char* sig2; + const sfizz_arg_t* args2; + uint8_t buffer[256]; + REQUIRE(sfizz_extract_message(actual, sizeof(actual), buffer, sizeof(buffer), &path2, &sig2, &args2) > 0); + REQUIRE(!strcmp(path, path2)); + REQUIRE(!strcmp(sig, sig2)); + REQUIRE(args[0].i == 1000); + REQUIRE(args[1].i == -1); + REQUIRE(!strcmp(args[2].s, "hello")); + REQUIRE(args[3].f == 1.234f); + REQUIRE(args[4].f == 5.678f); + } +} diff --git a/vst/SfizzVstController.cpp b/vst/SfizzVstController.cpp index 35eba7a0..970f3f1a 100644 --- a/vst/SfizzVstController.cpp +++ b/vst/SfizzVstController.cpp @@ -293,6 +293,24 @@ tresult SfizzVstController::notify(Vst::IMessage* message) _playState = *static_cast(data); } + else if (!strcmp(id, "ReceivedMessage")) { + const void* data = nullptr; + uint32 size = 0; + result = attr->getBinary("Message", data, size); + + if (result != kResultTrue) + return result; + + const char* path; + const char* sig; + const sfizz_arg_t* args; + uint8_t buffer[1024]; + + if (sfizz_extract_message(data, size, buffer, sizeof(buffer), &path, &sig, &args) > 0) { + for (MessageListener* listener : _messageListeners) + listener->onMessageReceived(path, sig, args); + } + } for (StateListener* listener : _stateListeners) listener->onStateChanged(); @@ -312,6 +330,18 @@ void SfizzVstController::removeSfizzStateListener(StateListener* listener) _stateListeners.erase(it); } +void SfizzVstController::addSfizzMessageListener(MessageListener* listener) +{ + _messageListeners.push_back(listener); +} + +void SfizzVstController::removeSfizzMessageListener(MessageListener* listener) +{ + auto it = std::find(_messageListeners.begin(), _messageListeners.end(), listener); + if (it != _messageListeners.end()) + _messageListeners.erase(it); +} + FUnknown* SfizzVstController::createInstance(void*) { return static_cast(new SfizzVstController); diff --git a/vst/SfizzVstController.h b/vst/SfizzVstController.h index bd363a2c..796d6628 100644 --- a/vst/SfizzVstController.h +++ b/vst/SfizzVstController.h @@ -9,6 +9,7 @@ #include "public.sdk/source/vst/vsteditcontroller.h" #include "public.sdk/source/vst/vstparameters.h" #include "vstgui/plugin-bindings/vst3editor.h" +#include class SfizzVstState; using namespace Steinberg; @@ -49,6 +50,9 @@ public: struct StateListener { virtual void onStateChanged() = 0; }; + struct MessageListener { + virtual void onMessageReceived(const char* path, const char* sig, const sfizz_arg_t* args) = 0; + }; const SfizzVstState& getSfizzState() const { return _state; } SfizzVstState& getSfizzState() { return _state; } @@ -62,6 +66,9 @@ public: void addSfizzStateListener(StateListener* listener); void removeSfizzStateListener(StateListener* listener); + void addSfizzMessageListener(MessageListener* listener); + void removeSfizzMessageListener(MessageListener* listener); + /// static FUnknown* createInstance(void*); @@ -72,4 +79,5 @@ private: SfizzUiState _uiState; SfizzPlayState _playState {}; std::vector _stateListeners; + std::vector _messageListeners; }; diff --git a/vst/SfizzVstEditor.cpp b/vst/SfizzVstEditor.cpp index 9746cdf9..0f9bd231 100644 --- a/vst/SfizzVstEditor.cpp +++ b/vst/SfizzVstEditor.cpp @@ -16,15 +16,22 @@ using namespace VSTGUI; static ViewRect sfizzUiViewRect { 0, 0, Editor::viewWidth, Editor::viewHeight }; +enum { + kOscTempSize = 8192, +}; + SfizzVstEditor::SfizzVstEditor(void *controller) - : VSTGUIEditor(controller, &sfizzUiViewRect) + : VSTGUIEditor(controller, &sfizzUiViewRect), + oscTemp_(new uint8_t[kOscTempSize]) { getController()->addSfizzStateListener(this); + getController()->addSfizzMessageListener(this); } SfizzVstEditor::~SfizzVstEditor() { getController()->removeSfizzStateListener(this); + getController()->removeSfizzMessageListener(this); } bool PLUGIN_API SfizzVstEditor::open(void* parent, const VSTGUI::PlatformType& platformType) @@ -105,6 +112,11 @@ void SfizzVstEditor::onStateChanged() updateStateDisplay(); } +void SfizzVstEditor::onMessageReceived(const char* path, const char* sig, const sfizz_arg_t* args) +{ + uiReceiveMessage(path, sig, args); +} + /// void SfizzVstEditor::uiSendValue(EditId id, const EditValue& v) { @@ -192,6 +204,26 @@ void SfizzVstEditor::uiSendMIDI(const uint8_t* data, uint32_t len) ctl->sendMessage(msg); } +void SfizzVstEditor::uiSendMessage(const char* path, const char* sig, const sfizz_arg_t* args) +{ + SfizzVstController* ctl = getController(); + + Steinberg::OPtr msg { ctl->allocateMessage() }; + if (!msg) { + fprintf(stderr, "[Sfizz] UI could not allocate message\n"); + return; + } + + uint8_t* oscTemp = oscTemp_.get(); + uint32_t oscSize = sfizz_prepare_message(oscTemp, kOscTempSize, path, sig, args); + if (oscSize <= kOscTempSize) { + msg->setMessageID("OscMessage"); + Vst::IAttributeList* attr = msg->getAttributes(); + attr->setBinary("Data", oscTemp, oscSize); + ctl->sendMessage(msg); + } +} + /// void SfizzVstEditor::loadSfzFile(const std::string& filePath) { diff --git a/vst/SfizzVstEditor.h b/vst/SfizzVstEditor.h index a0730220..ad45f352 100644 --- a/vst/SfizzVstEditor.h +++ b/vst/SfizzVstEditor.h @@ -16,7 +16,10 @@ namespace VSTGUI { class RunLoop; } using namespace Steinberg; using namespace VSTGUI; -class SfizzVstEditor : public Vst::VSTGUIEditor, public SfizzVstController::StateListener, public EditorController { +class SfizzVstEditor : public Vst::VSTGUIEditor, + public SfizzVstController::StateListener, + public SfizzVstController::MessageListener, + public EditorController { public: explicit SfizzVstEditor(void *controller); ~SfizzVstEditor(); @@ -35,12 +38,16 @@ public: // SfizzVstController::StateListener void onStateChanged() override; + // SfizzVstController::MessageListener + void onMessageReceived(const char* path, const char* sig, const sfizz_arg_t* args) override; + protected: // EditorController void uiSendValue(EditId id, const EditValue& v) override; void uiBeginSend(EditId id) override; void uiEndSend(EditId id) override; void uiSendMIDI(const uint8_t* data, uint32_t len) override; + void uiSendMessage(const char* path, const char* sig, const sfizz_arg_t* args) override; private: void loadSfzFile(const std::string& filePath); @@ -55,4 +62,7 @@ private: #if !defined(__APPLE__) && !defined(_WIN32) SharedPointer _runLoop; #endif + + // messaging + std::unique_ptr oscTemp_; }; diff --git a/vst/SfizzVstProcessor.cpp b/vst/SfizzVstProcessor.cpp index 3825db65..93969467 100644 --- a/vst/SfizzVstProcessor.cpp +++ b/vst/SfizzVstProcessor.cpp @@ -24,10 +24,17 @@ static const char defaultSfzText[] = "sample=*sine" "\n" "ampeg_attack=0.02 ampeg_release=0.1" "\n"; -enum { kMidiEventMaximumSize = 4 }; +enum { + kMidiEventMaximumSize = 4, + kOscTempSize = 8192, +}; + +static const char* kRingIdMidi = "Mid"; +static const char* kRingIdOsc = "Osc"; SfizzVstProcessor::SfizzVstProcessor() - : _fifoToWorker(64 * 1024), _fifoMidiFromUi(64 * 1024) + : _fifoToWorker(64 * 1024), _fifoMessageFromUi(64 * 1024), + _oscTemp(new uint8_t[kOscTempSize]) { setControllerClass(SfizzVstController::cid); @@ -56,6 +63,16 @@ tresult PLUGIN_API SfizzVstProcessor::initialize(FUnknown* context) fprintf(stderr, "[sfizz] new synth\n"); _synth.reset(new sfz::Sfizz); + + auto onMessage = +[](void* data, int delay, const char* path, const char* sig, const sfizz_arg_t* args) + { + auto *self = reinterpret_cast(data); + self->receiveMessage(delay, path, sig, args); + }; + _client = _synth->createClient(this); + _synth->setReceiveCallback(*_client, onMessage); + _synth->setBroadcastCallback(onMessage, this); + _currentStretchedTuning = 0.0; loadSfzFileOrDefault(*_synth, {}); @@ -210,7 +227,7 @@ tresult PLUGIN_API SfizzVstProcessor::process(Vst::ProcessData& data) else synth.disableFreeWheeling(); - processMidiFromUi(); + processMessagesFromUi(); if (Vst::IParameterChanges* pc = data.inputParameterChanges) processControllerChanges(*pc); @@ -404,36 +421,61 @@ void SfizzVstProcessor::processEvents(Vst::IEventList& events) } } -void SfizzVstProcessor::processMidiFromUi() +void SfizzVstProcessor::processMessagesFromUi() { sfz::Sfizz& synth = *_synth; + sfz::Client& client = *_client; + Ring_Buffer& fifo = _fifoMessageFromUi; + RTMessage header; - for (uint32 size = 0; _fifoMidiFromUi.peek(size) && - _fifoMidiFromUi.size_used() >= sizeof(size) + size; ) { - _fifoMidiFromUi.discard(sizeof(size)); + while (fifo.peek(header) && fifo.size_used() >= sizeof(header) + header.size) { + fifo.discard(sizeof(header)); - if (size > kMidiEventMaximumSize) { - _fifoMidiFromUi.discard(size); - continue; + if (header.type == kRingIdMidi) { + if (header.size > kMidiEventMaximumSize) { + fifo.discard(header.size); + continue; + } + + uint8_t data[kMidiEventMaximumSize] = {}; + fifo.get(data, header.size); + + // interpret the MIDI message + switch (data[0] & 0xf0) { + case 0x80: + synth.noteOff(0, data[1] & 0x7f, data[2] & 0x7f); + break; + case 0x90: + synth.noteOn(0, data[1] & 0x7f, data[2] & 0x7f); + break; + case 0xb0: + synth.cc(0, data[1] & 0x7f, data[2] & 0x7f); + break; + case 0xe0: + synth.pitchWheel(0, (data[2] << 7) + data[1] - 8192); + break; + } } + else if (header.type == kRingIdOsc) { + uint8_t* oscTemp = _oscTemp.get(); - uint8_t data[kMidiEventMaximumSize] = {}; - _fifoMidiFromUi.get(data, size); + if (header.size > kOscTempSize) { + fifo.discard(header.size); + continue; + } - // interpret the MIDI message - switch (data[0] & 0xf0) { - case 0x80: - synth.noteOff(0, data[1] & 0x7f, data[2] & 0x7f); - break; - case 0x90: - synth.noteOn(0, data[1] & 0x7f, data[2] & 0x7f); - break; - case 0xb0: - synth.cc(0, data[1] & 0x7f, data[2] & 0x7f); - break; - case 0xe0: - synth.pitchWheel(0, (data[2] << 7) + data[1] - 8192); - break; + fifo.get(oscTemp, header.size); + + const char* path; + const char* sig; + const sfizz_arg_t* args; + uint8_t buffer[1024]; + if (sfizz_extract_message(oscTemp, header.size, buffer, sizeof(buffer), &path, &sig, &args) > 0) + synth.sendMessage(client, 0, path, sig, args); + } + else { + assert(false); + return; } } } @@ -494,12 +536,14 @@ tresult PLUGIN_API SfizzVstProcessor::notify(Vst::IMessage* message) const void* data = nullptr; uint32 size = 0; result = attr->getBinary("Data", data, size); - if (size < kMidiEventMaximumSize) { - if (_fifoMidiFromUi.size_free() >= sizeof(size) + size) { - _fifoMidiFromUi.put(size); - _fifoMidiFromUi.put(reinterpret_cast(data), size); - } - } + if (size < kMidiEventMaximumSize) + writeMessage(_fifoMessageFromUi, kRingIdMidi, data, size); + } + else if (!std::strcmp(id, "OscMessage")) { + const void* data = nullptr; + uint32 size = 0; + result = attr->getBinary("Data", data, size); + writeMessage(_fifoMessageFromUi, kRingIdOsc, data, size); } return result; @@ -510,6 +554,14 @@ FUnknown* SfizzVstProcessor::createInstance(void*) return static_cast(new SfizzVstProcessor); } +void SfizzVstProcessor::receiveMessage(int delay, const char* path, const char* sig, const sfizz_arg_t* args) +{ + uint8_t* oscTemp = _oscTemp.get(); + uint32_t oscSize = sfizz_prepare_message(oscTemp, kOscTempSize, path, sig, args); + if (oscSize <= kOscTempSize) + writeWorkerMessage("ReceiveMessage", oscTemp, oscSize); +} + void SfizzVstProcessor::loadSfzFileOrDefault(sfz::Sfizz& synth, const std::string& filePath) { if (!filePath.empty()) @@ -563,6 +615,12 @@ void SfizzVstProcessor::doBackgroundWork() notification->getAttributes()->setBinary("PlayState", &playState, sizeof(playState)); sendMessage(notification); } + else if (!std::strcmp(id, "ReceiveMessage")) { + Steinberg::OPtr notification { allocateMessage() }; + notification->setMessageID("ReceivedMessage"); + notification->getAttributes()->setBinary("Message", msg->payload(), msg->size); + sendMessage(notification); + } } } @@ -585,16 +643,7 @@ void SfizzVstProcessor::stopBackgroundWork() bool SfizzVstProcessor::writeWorkerMessage(const char* type, const void* data, uintptr_t size) { - RTMessage header; - header.type = type; - header.size = size; - - if (_fifoToWorker.size_free() < sizeof(header) + size) - return false; - - _fifoToWorker.put(header); - _fifoToWorker.put(static_cast(data), size); - return true; + return writeMessage(_fifoToWorker, type, data, size); } SfizzVstProcessor::RTMessagePtr SfizzVstProcessor::readWorkerMessage() @@ -631,6 +680,20 @@ bool SfizzVstProcessor::discardWorkerMessage() return true; } +bool SfizzVstProcessor::writeMessage(Ring_Buffer& fifo, const char* type, const void* data, uintptr_t size) +{ + RTMessage header; + header.type = type; + header.size = size; + + if (fifo.size_free() < sizeof(header) + size) + return false; + + fifo.put(header); + fifo.put(static_cast(data), size); + return true; +} + /* Note(jpc) Generated at random with uuidgen. Can't find docs on it... maybe it's to register somewhere? diff --git a/vst/SfizzVstProcessor.h b/vst/SfizzVstProcessor.h index 7f388621..bbbf7490 100644 --- a/vst/SfizzVstProcessor.h +++ b/vst/SfizzVstProcessor.h @@ -35,7 +35,7 @@ public: void processParameterChanges(Vst::IParameterChanges& pc); void processControllerChanges(Vst::IParameterChanges& pc); void processEvents(Vst::IEventList& events); - void processMidiFromUi(); + void processMessagesFromUi(); static int convertVelocityFromFloat(float x); tresult PLUGIN_API notify(Vst::IMessage* message) override; @@ -51,6 +51,11 @@ private: SfizzVstState _state; float _currentStretchedTuning = 0; + // client + sfz::ClientPtr _client; + std::unique_ptr _oscTemp; + void receiveMessage(int delay, const char* path, const char* sig, const sfizz_arg_t* args); + // misc static void loadSfzFileOrDefault(sfz::Sfizz& synth, const std::string& filePath); @@ -59,7 +64,7 @@ private: volatile bool _workRunning = false; Ring_Buffer _fifoToWorker; RTSemaphore _semaToWorker; - Ring_Buffer _fifoMidiFromUi; + Ring_Buffer _fifoMessageFromUi; std::mutex _processMutex; // file modification periodic checker @@ -95,6 +100,9 @@ private: // reader RTMessagePtr readWorkerMessage(); bool discardWorkerMessage(); + + // generic + static bool writeMessage(Ring_Buffer& fifo, const char* type, const void* data, uintptr_t size); }; //------------------------------------------------------------------------------