diff --git a/cmake/LV2Config.cmake b/cmake/LV2Config.cmake index ab7a1652..11330812 100644 --- a/cmake/LV2Config.cmake +++ b/cmake/LV2Config.cmake @@ -40,3 +40,59 @@ else() set(LV2PLUGIN_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/lib/lv2" CACHE STRING "Install destination for LV2 bundle [default: ${CMAKE_INSTALL_PREFIX}/lib/lv2]") endif() + +include(StringUtility) + +function(sfizz_lv2_generate_controllers_ttl FILE) + file(WRITE "${FILE}" "# LV2 parameters for SFZ controllers +@prefix atom: . +@prefix lv2: . +@prefix midi: . +@prefix patch: . +@prefix rdfs: . +@prefix sfizz: <${LV2PLUGIN_URI}#> . +") + math(EXPR _j "${SFIZZ_NUM_CCS}-1") + foreach(_i RANGE "${_j}") + string_left_pad(_i "${_i}" 3 0) + file(APPEND "${FILE}" " +sfizz:cc${_i} + a lv2:Parameter ; + rdfs:label \"Controller ${_i}\" ; + rdfs:range atom:Float") + + if(_i LESS 128) + math(EXPR _digit1 "${_i}>>4") + math(EXPR _digit2 "${_i}&15") + string(SUBSTRING "0123456789ABCDEF" "${_digit1}" 1 _digit1) + string(SUBSTRING "0123456789ABCDEF" "${_digit2}" 1 _digit2) + file(APPEND "${FILE}" " ; + midi:binding \"B0${_digit1}${_digit2}00\"^^midi:MidiEvent . +") + else() + file(APPEND "${FILE}" " . +") + endif() + endforeach() + + file(APPEND "${FILE}" " +<${LV2PLUGIN_URI}> + a lv2:Plugin ; +") + + file(APPEND "${FILE}" " patch:readable sfizz:cc000") + foreach(_i RANGE 1 "${_j}") + string_left_pad(_i "${_i}" 3 0) + file(APPEND "${FILE}" ", sfizz:cc${_i}") + endforeach() + file(APPEND "${FILE}" " ; +") + + file(APPEND "${FILE}" " patch:writable sfizz:cc000") + foreach(_i RANGE 1 "${_j}") + string_left_pad(_i "${_i}" 3 0) + file(APPEND "${FILE}" ", sfizz:cc${_i}") + endforeach() + file(APPEND "${FILE}" " . +") +endfunction() diff --git a/cmake/StringUtility.cmake b/cmake/StringUtility.cmake new file mode 100644 index 00000000..00c5ba5c --- /dev/null +++ b/cmake/StringUtility.cmake @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: BSD-2-Clause + +function(string_left_pad VAR INPUT LENGTH FILLCHAR) + set(_output "${INPUT}") + string(LENGTH "${_output}" _length) + while(_length LESS "${LENGTH}") + set(_output "${FILLCHAR}${_output}") + string(LENGTH "${_output}" _length) + endwhile() + set("${VAR}" "${_output}" PARENT_SCOPE) +endfunction() diff --git a/plugins/editor/src/editor/Editor.cpp b/plugins/editor/src/editor/Editor.cpp index e8bcf7b7..914181ae 100644 --- a/plugins/editor/src/editor/Editor.cpp +++ b/plugins/editor/src/editor/Editor.cpp @@ -306,6 +306,12 @@ void Editor::close() } } +void Editor::sendQueuedOSC(const char* path, const char* sig, const sfizz_arg_t* args) +{ + Impl& impl = *impl_; + impl.sendQueuedOSC(path, sig, args); +} + void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) { switch (id) { @@ -476,12 +482,18 @@ void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) else if (editIdIsKeyswitchLabel(id)) { updateSWLastLabel(keyswitchLabelForEditId(id), v.to_string().c_str()); } + else if (editIdIsCC(id)) { + updateCCValue(unsigned(ccForEditId(id)), v.to_float()); + } else if (editIdIsCCUsed(id)) { - updateCCUsed(ccUsedForEditId(id), v.to_float() != 0); + bool used = v.to_float() != 0; + updateCCUsed(ccUsedForEditId(id), used); // TODO(jpc) remove value requests, when implementing CC automation - char pathBuf[256]; - sprintf(pathBuf, "/cc%u/value", ccUsedForEditId(id)); - sendQueuedOSC(pathBuf, "", nullptr); + if (used) { + char pathBuf[256]; + sprintf(pathBuf, "/cc%u/value", ccUsedForEditId(id)); + sendQueuedOSC(pathBuf, "", nullptr); + } } else if (editIdIsCCDefault(id)) { updateCCDefaultValue(ccDefaultForEditId(id), v.to_float()); @@ -1610,13 +1622,8 @@ void Editor::Impl::updateMemoryUsed(uint64_t mem) void Editor::Impl::performCCValueChange(unsigned cc, float value) { - // TODO(jpc) CC as parameters and automation - - char pathBuf[256]; - sprintf(pathBuf, "/cc%u/value", cc); - sfizz_arg_t args[1]; - args[0].f = value; - sendQueuedOSC(pathBuf, "f", args); + EditorController& ctrl = *ctrl_; + ctrl.uiSendValue(editIdForCC(int(cc)), value); } void Editor::Impl::performCCBeginEdit(unsigned cc) diff --git a/plugins/editor/src/editor/Editor.h b/plugins/editor/src/editor/Editor.h index 0c0ae1de..4345426c 100644 --- a/plugins/editor/src/editor/Editor.h +++ b/plugins/editor/src/editor/Editor.h @@ -5,6 +5,7 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #pragma once +#include #include class EditorController; @@ -24,6 +25,9 @@ public: void open(CFrame& frame); void close(); + // TODO(jpc) remove me after doing parameter automations + void sendQueuedOSC(const char* path, const char* sig, const sfizz_arg_t* args); + private: struct Impl; std::unique_ptr impl_; diff --git a/plugins/lv2/CMakeLists.txt b/plugins/lv2/CMakeLists.txt index 3f9a7247..2417e263 100644 --- a/plugins/lv2/CMakeLists.txt +++ b/plugins/lv2/CMakeLists.txt @@ -82,6 +82,9 @@ if(SFIZZ_USE_VCPKG OR SFIZZ_STATIC_DEPENDENCIES OR CMAKE_CXX_COMPILER_ID MATCHES file(COPY "lgpl-3.0.txt" DESTINATION ${PROJECT_BINARY_DIR}) endif() +# Generate controllers.ttl +sfizz_lv2_generate_controllers_ttl("${PROJECT_BINARY_DIR}/controllers.ttl") + # Copy resource files into the bundle set(LV2_RESOURCES DefaultInstrument.sfz diff --git a/plugins/lv2/manifest.ttl.in b/plugins/lv2/manifest.ttl.in index ab407d33..3c5a130c 100644 --- a/plugins/lv2/manifest.ttl.in +++ b/plugins/lv2/manifest.ttl.in @@ -5,7 +5,7 @@ <@LV2PLUGIN_URI@> a lv2:Plugin ; lv2:binary ; - rdfs:seeAlso <@PROJECT_NAME@.ttl> . + rdfs:seeAlso <@PROJECT_NAME@.ttl>, . @LV2PLUGIN_IF_ENABLE_UI@<@LV2PLUGIN_URI@#ui> @LV2PLUGIN_IF_ENABLE_UI@ a ui:@LV2_UI_TYPE@ ; diff --git a/plugins/lv2/sfizz.cpp b/plugins/lv2/sfizz.cpp index a0c94a0b..aaeab58a 100644 --- a/plugins/lv2/sfizz.cpp +++ b/plugins/lv2/sfizz.cpp @@ -42,6 +42,8 @@ #include #include +#include + #define CHANNEL_MASK 0x0F #define MIDI_CHANNEL(byte) (byte & CHANNEL_MASK) #define MIDI_STATUS(byte) (byte & ~CHANNEL_MASK) @@ -201,6 +203,9 @@ connect_port(LV2_Handle instance, case SFIZZ_NOTIFY: self->notify_port = (LV2_Atom_Sequence *)data; break; + case SFIZZ_AUTOMATE: + self->automate_port = (LV2_Atom_Sequence *)data; + break; case SFIZZ_LEFT: self->output_buffers[0] = (float *)data; break; @@ -320,11 +325,12 @@ sfizz_lv2_receive_message(void* data, int delay, const char* path, const char* s if (osc_size > OSC_TEMP_SIZE) return; + LV2_Atom_Forge* forge = &self->forge_notify; 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); + lv2_atom_forge_frame_time(forge, 0) && + lv2_atom_forge_atom(forge, osc_size, self->sfizz_osc_blob_uri) && + lv2_atom_forge_raw(forge, osc_temp, osc_size); + lv2_atom_forge_pad(forge, osc_size); (void)write_ok; } @@ -426,7 +432,8 @@ instantiate(const LV2_Descriptor *descriptor, sfizz_lv2_map_required_uris(self); // Initialize the forge - lv2_atom_forge_init(&self->forge, self->map); + lv2_atom_forge_init(&self->forge_notify, self->map); + lv2_atom_forge_init(&self->forge_automate, self->map); lv2_atom_forge_init(&self->forge_secondary, self->map); // Check the options for the block size and sample rate parameters @@ -476,6 +483,10 @@ instantiate(const LV2_Descriptor *descriptor, return NULL; } + self->ccmap = sfizz_lv2_ccmap_create(self->map); + self->cc_current = new float[sfz::config::numCCs](); + self->ccauto = new absl::optional[sfz::config::numCCs]; + self->synth = sfizz_create_synth(); self->client = sfizz_create_client(self); self->synth_mutex = spin_mutex_create(); @@ -501,6 +512,9 @@ cleanup(LV2_Handle instance) spin_mutex_destroy(self->synth_mutex); sfizz_delete_client(self->client); sfizz_free(self->synth); + delete[] self->ccauto; + delete[] self->cc_current; + sfizz_lv2_ccmap_free(self->ccmap); delete self; } @@ -521,24 +535,42 @@ deactivate(LV2_Handle instance) } static void -sfizz_lv2_send_file_path(sfizz_plugin_t *self, LV2_URID urid, const char *path) +sfizz_lv2_send_file_path(sfizz_plugin_t *self, LV2_Atom_Forge* forge, LV2_URID urid, const char *path) { LV2_Atom_Forge_Frame frame; bool write_ok = - lv2_atom_forge_frame_time(&self->forge, 0) && - lv2_atom_forge_object(&self->forge, &frame, 0, self->patch_set_uri) && - lv2_atom_forge_key(&self->forge, self->patch_property_uri) && - lv2_atom_forge_urid(&self->forge, urid) && - lv2_atom_forge_key(&self->forge, self->patch_value_uri) && - lv2_atom_forge_path(&self->forge, path, (uint32_t)strlen(path)); + lv2_atom_forge_frame_time(forge, 0) && + lv2_atom_forge_object(forge, &frame, 0, self->patch_set_uri) && + lv2_atom_forge_key(forge, self->patch_property_uri) && + lv2_atom_forge_urid(forge, urid) && + lv2_atom_forge_key(forge, self->patch_value_uri) && + lv2_atom_forge_path(forge, path, (uint32_t)strlen(path)); if (write_ok) - lv2_atom_forge_pop(&self->forge, &frame); + lv2_atom_forge_pop(forge, &frame); } static void -sfizz_lv2_handle_atom_object(sfizz_plugin_t *self, const LV2_Atom_Object *obj) +sfizz_lv2_send_controller(sfizz_plugin_t *self, LV2_Atom_Forge* forge, unsigned cc, float value) +{ + LV2_URID urid = sfizz_lv2_ccmap_map(self->ccmap, int(cc)); + LV2_Atom_Forge_Frame frame; + + bool write_ok = + lv2_atom_forge_frame_time(forge, 0) && + lv2_atom_forge_object(forge, &frame, 0, self->patch_set_uri) && + lv2_atom_forge_key(forge, self->patch_property_uri) && + lv2_atom_forge_urid(forge, urid) && + lv2_atom_forge_key(forge, self->patch_value_uri) && + lv2_atom_forge_float(forge, value); + + if (write_ok) + lv2_atom_forge_pop(forge, &frame); +} + +static void +sfizz_lv2_handle_atom_object(sfizz_plugin_t *self, int delay, const LV2_Atom_Object *obj) { const LV2_Atom *property = NULL; lv2_atom_object_get(obj, self->patch_property_uri, &property, 0); @@ -575,7 +607,15 @@ sfizz_lv2_handle_atom_object(sfizz_plugin_t *self, const LV2_Atom_Object *obj) char body[MAX_PATH_SIZE]; } sfizz_path_atom_buffer_t; - if (key == self->sfizz_sfz_file_uri) + int cc = sfizz_lv2_ccmap_unmap(self->ccmap, key); + if (cc != -1) { + if (atom->type == self->atom_float_uri && atom->size == sizeof(float)) { + float value = *(const float *)LV2_ATOM_BODY_CONST(atom); + sfizz_send_hdcc(self->synth, delay, cc, value); + self->cc_current[cc] = value; + } + } + else if (key == self->sfizz_sfz_file_uri) { LV2_Atom_Forge *forge = &self->forge_secondary; sfizz_path_atom_buffer_t buffer; @@ -629,12 +669,14 @@ sfizz_lv2_process_midi_event(sfizz_plugin_t *self, const LV2_Atom_Event *ev) (int)msg[1], msg[2]); break; - case LV2_MIDI_MSG_CONTROLLER: + // Note(jpc) CC must be mapped by host, not handled here. + // See LV2 midi:binding. + /*case LV2_MIDI_MSG_CONTROLLER: sfizz_send_cc(self->synth, (int)ev->time.frames, (int)msg[1], msg[2]); - break; + break;*/ case LV2_MIDI_MSG_CHANNEL_PRESSURE: sfizz_send_aftertouch(self->synth, (int)ev->time.frames, @@ -770,7 +812,7 @@ static void run(LV2_Handle instance, uint32_t sample_count) { sfizz_plugin_t *self = (sfizz_plugin_t *)instance; - assert(self->control_port && self->notify_port); + assert(self->control_port && self->notify_port && self->automate_port); if (!spin_mutex_trylock(self->synth_mutex)) { @@ -779,13 +821,18 @@ run(LV2_Handle instance, uint32_t sample_count) return; } - // Set up forge to write directly to notify output port. + // Set up dedicated forges to write on their respective ports. const size_t notify_capacity = self->notify_port->atom.size; - lv2_atom_forge_set_buffer(&self->forge, (uint8_t *)self->notify_port, notify_capacity); + lv2_atom_forge_set_buffer(&self->forge_notify, (uint8_t *)self->notify_port, notify_capacity); + const size_t automate_capacity = self->automate_port->atom.size; + lv2_atom_forge_set_buffer(&self->forge_automate, (uint8_t *)self->automate_port, automate_capacity); - // Start a sequence in the notify output port. + // Start sequences in the respective output ports. LV2_Atom_Forge_Frame notify_frame; - if (!lv2_atom_forge_sequence_head(&self->forge, ¬ify_frame, 0)) + if (!lv2_atom_forge_sequence_head(&self->forge_notify, ¬ify_frame, 0)) + assert(false); + LV2_Atom_Forge_Frame automate_frame; + if (!lv2_atom_forge_sequence_head(&self->forge_automate, &automate_frame, 0)) assert(false); LV2_ATOM_SEQUENCE_FOREACH(self->control_port, ev) @@ -796,9 +843,10 @@ run(LV2_Handle instance, uint32_t sample_count) if (ev->body.type == self->atom_object_uri || ev->body.type == self->atom_blank_uri) { const LV2_Atom_Object *obj = (const LV2_Atom_Object *)&ev->body; + if (obj->body.otype == self->patch_set_uri) { - sfizz_lv2_handle_atom_object(self, obj); + sfizz_lv2_handle_atom_object(self, delay, obj); } else if (obj->body.otype == self->patch_get_uri) { @@ -806,16 +854,25 @@ run(LV2_Handle instance, uint32_t sample_count) lv2_atom_object_get(obj, self->patch_property_uri, &property, 0); if (!property) // Send the full state { - sfizz_lv2_send_file_path(self, self->sfizz_sfz_file_uri, self->sfz_file_path); - sfizz_lv2_send_file_path(self, self->sfizz_scala_file_uri, self->scala_file_path); + sfizz_lv2_send_file_path(self, &self->forge_notify, self->sfizz_sfz_file_uri, self->sfz_file_path); + sfizz_lv2_send_file_path(self, &self->forge_notify, self->sfizz_scala_file_uri, self->scala_file_path); + + for (unsigned cc = 0; cc < sfz::config::numCCs; ++cc) + sfizz_lv2_send_controller(self, &self->forge_notify, cc, self->cc_current[cc]); } else if (property->body == self->sfizz_sfz_file_uri) { - sfizz_lv2_send_file_path(self, self->sfizz_sfz_file_uri, self->sfz_file_path); + sfizz_lv2_send_file_path(self, &self->forge_notify, self->sfizz_sfz_file_uri, self->sfz_file_path); } else if (property->body == self->sfizz_scala_file_uri) { - sfizz_lv2_send_file_path(self, self->sfizz_scala_file_uri, self->scala_file_path); + sfizz_lv2_send_file_path(self, &self->forge_notify, self->sfizz_scala_file_uri, self->scala_file_path); + } + else + { + int cc = sfizz_lv2_ccmap_unmap(self->ccmap, property->body); + if (cc != -1) + sfizz_lv2_send_controller(self, &self->forge_notify, unsigned(cc), self->cc_current[cc]); } } else if (obj->body.otype == self->time_position_uri) @@ -949,17 +1006,29 @@ run(LV2_Handle instance, uint32_t sample_count) sfizz_render_block(self->synth, self->output_buffers, 2, (int)sample_count); // Request OSC updates - sfizz_send_message(self->synth, self->client, 0, "/cc/changed~", "", nullptr); sfizz_send_message(self->synth, self->client, 0, "/sw/last/current", "", nullptr); - spin_mutex_unlock(self->synth_mutex); - if (self->midnam && self->must_update_midnam.exchange(0)) { self->midnam->update(self->midnam->handle); } - lv2_atom_forge_pop(&self->forge, ¬ify_frame); + if (self->have_ccauto) + { + for (unsigned cc = 0; cc < sfz::config::numCCs; ++cc) { + absl::optional value = self->ccauto[cc]; + if (value) { + sfizz_lv2_send_controller(self, &self->forge_automate, cc, *value); + self->ccauto[cc] = absl::nullopt; + } + } + self->have_ccauto = false; + } + + spin_mutex_unlock(self->synth_mutex); + + lv2_atom_forge_pop(&self->forge_notify, ¬ify_frame); + lv2_atom_forge_pop(&self->forge_automate, &automate_frame); } static uint32_t @@ -1063,6 +1132,7 @@ sfizz_lv2_update_sfz_info(sfizz_plugin_t *self) { const std::string blob = getDescriptionBlob(self->synth); + // Update description blob that UI can fetch, thread-safely uint32_t size = uint32_t(blob.size()); uint8_t *data = new uint8_t[size]; memcpy(data, blob.data(), size); @@ -1075,6 +1145,18 @@ sfizz_lv2_update_sfz_info(sfizz_plugin_t *self) self->sfz_blob_mutex->unlock(); delete[] old_data; + + // + const InstrumentDescription desc = parseDescriptionBlob(blob); + for (unsigned cc = 0; cc < sfz::config::numCCs; ++cc) { + if (desc.ccUsed.test(cc)) { + // Mark all the used CCs for automation with default values + self->ccauto[cc] = desc.ccDefault[cc]; + self->have_ccauto = true; + // Update the current CCs + self->cc_current[cc] = desc.ccDefault[cc]; + } + } } static bool @@ -1220,6 +1302,17 @@ restore(LV2_Handle instance, self->oversampling = oversampling; } + // Collect all CC values present in the state + std::unique_ptr[]> cc_values( + new absl::optional[sfz::config::numCCs]); + + for (unsigned cc = 0; cc < sfz::config::numCCs; ++cc) { + LV2_URID urid = sfizz_lv2_ccmap_map(self->ccmap, int(cc)); + value = retrieve(handle, urid, &size, &type, &val_flags); + if (value && type == self->atom_float_uri) + cc_values[cc] = *(const float *)value; + } + // Sync the parameters to the synth spin_mutex_lock(self->synth_mutex); @@ -1258,6 +1351,20 @@ restore(LV2_Handle instance, lv2_log_note(&self->logger, "[sfizz] Restoring the oversampling to %d\n", self->oversampling); sfizz_set_oversampling_factor(self->synth, self->oversampling); + // Override default automation values with these from the state file + for (unsigned cc = 0; cc < sfz::config::numCCs; ++cc) { + absl::optional value = cc_values[cc]; + if (value) { + // Set CC in the synth + sfizz_send_hdcc(self->synth, 0, int(cc), *value); + // Mark CCs for automation with state values + self->ccauto[cc] = *value; + self->have_ccauto = true; + // Update the current CCs + self->cc_current[cc] = *value; + } + } + spin_mutex_unlock(self->synth_mutex); return status; @@ -1327,7 +1434,7 @@ save(LV2_Handle instance, &self->num_voices, sizeof(int), self->atom_int_uri, - LV2_STATE_IS_POD | LV2_STATE_IS_PORTABLE); + LV2_STATE_IS_POD); // Save the preload size store(handle, @@ -1335,7 +1442,7 @@ save(LV2_Handle instance, &self->preload_size, sizeof(unsigned int), self->atom_int_uri, - LV2_STATE_IS_POD | LV2_STATE_IS_PORTABLE); + LV2_STATE_IS_POD); // Save the preload size store(handle, @@ -1343,7 +1450,25 @@ save(LV2_Handle instance, &self->oversampling, sizeof(int), self->atom_int_uri, - LV2_STATE_IS_POD | LV2_STATE_IS_PORTABLE); + LV2_STATE_IS_POD); + + // Save the CCs (used only) + self->sfz_blob_mutex->lock(); + const InstrumentDescription desc = parseDescriptionBlob( + absl::string_view((const char*)self->sfz_blob_data, self->sfz_blob_size)); + self->sfz_blob_mutex->unlock(); + + for (unsigned cc = 0; cc < sfz::config::numCCs; ++cc) { + if (desc.ccUsed.test(cc)) { + LV2_URID urid = sfizz_lv2_ccmap_map(self->ccmap, int(cc)); + store(handle, + urid, + &self->cc_current[cc], + sizeof(float), + self->atom_float_uri, + LV2_STATE_IS_POD); + } + } return LV2_STATE_SUCCESS; } diff --git a/plugins/lv2/sfizz.ttl.in b/plugins/lv2/sfizz.ttl.in index 45494016..112a970c 100644 --- a/plugins/lv2/sfizz.ttl.in +++ b/plugins/lv2/sfizz.ttl.in @@ -104,29 +104,38 @@ midnam:update a lv2:Feature . a lv2:OutputPort, atom:AtomPort ; atom:bufferType atom:Sequence ; 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 ; + a lv2:OutputPort, atom:AtomPort ; + atom:bufferType atom:Sequence ; + atom:supports patch:Message ; + lv2:designation lv2:control ; lv2:index 2 ; + lv2:symbol "automate" ; + lv2:name "Automate", + "Automatisation"@fr ; + rsz:minimumSize 65536 ; + ] , [ + a lv2:AudioPort, lv2:OutputPort ; + lv2:index 3 ; lv2:symbol "out_left" ; lv2:name "Left Output", "Sortie gauche"@fr , "Uscita Sinistra"@it ] , [ a lv2:AudioPort, lv2:OutputPort ; - lv2:index 3 ; + lv2:index 4 ; lv2:symbol "out_right" ; lv2:name "Right Output", "Sortie droite"@fr , "Uscita Destra"@it ] , [ a lv2:InputPort, lv2:ControlPort ; - lv2:index 4 ; + lv2:index 5 ; lv2:symbol "volume" ; lv2:name "Volume" ; lv2:default 0.0 ; @@ -135,7 +144,7 @@ midnam:update a lv2:Feature . units:unit units:db ] , [ a lv2:InputPort, lv2:ControlPort ; - lv2:index 5 ; + lv2:index 6 ; lv2:symbol "num_voices" ; lv2:name "Polyphony", "Polyphonie"@fr , @@ -180,7 +189,7 @@ midnam:update a lv2:Feature . ] ; ] , [ a lv2:InputPort, lv2:ControlPort ; - lv2:index 6 ; + lv2:index 7 ; lv2:symbol "oversampling" ; lv2:name "Internal oversampling factor", "Facteur de suréchantillonnage interne"@fr , @@ -215,7 +224,7 @@ midnam:update a lv2:Feature . ] ; ] , [ a lv2:InputPort, lv2:ControlPort ; - lv2:index 7 ; + lv2:index 8 ; lv2:symbol "preload_size" ; lv2:name "Preload size", "Taille préchargée"@fr , @@ -258,7 +267,7 @@ midnam:update a lv2:Feature . ] ; ] , [ a lv2:InputPort, lv2:ControlPort ; - lv2:index 8 ; + lv2:index 9 ; lv2:symbol "freewheeling" ; lv2:name "Freewheeling", "En roue libre (freewheeling)"@fr , @@ -270,7 +279,7 @@ midnam:update a lv2:Feature . lv2:maximum 1 ; ] , [ a lv2:InputPort, lv2:ControlPort ; - lv2:index 9 ; + lv2:index 10 ; lv2:symbol "scala_root_key" ; lv2:name "Scala root key", "Tonalité de base Scala"@fr , @@ -283,7 +292,7 @@ midnam:update a lv2:Feature . units:unit units:midiNote ] , [ a lv2:InputPort, lv2:ControlPort ; - lv2:index 10 ; + lv2:index 11 ; lv2:symbol "tuning_frequency" ; lv2:name "Tuning frequency", "Fréquence d'accordage"@fr , @@ -295,7 +304,7 @@ midnam:update a lv2:Feature . units:unit units:hz ] , [ a lv2:InputPort, lv2:ControlPort ; - lv2:index 11 ; + lv2:index 12 ; lv2:symbol "stretched_tuning" ; lv2:name "Stretched tuning", "Accordage étiré"@fr , @@ -307,7 +316,7 @@ midnam:update a lv2:Feature . units:unit units:coef ] , [ a lv2:OutputPort, lv2:ControlPort ; - lv2:index 12 ; + lv2:index 13 ; lv2:symbol "active_voices" ; lv2:name "Active voices", "Voix utilisées"@fr ; @@ -318,7 +327,7 @@ midnam:update a lv2:Feature . lv2:maximum 256 ; ] , [ a lv2:OutputPort, lv2:ControlPort ; - lv2:index 13 ; + lv2:index 14 ; lv2:symbol "num_curves" ; lv2:name "Number of curves", "Nombre de courbes"@fr ; @@ -329,7 +338,7 @@ midnam:update a lv2:Feature . lv2:maximum 65535 ; ] , [ a lv2:OutputPort, lv2:ControlPort ; - lv2:index 14 ; + lv2:index 15 ; lv2:symbol "num_masters" ; lv2:name "Number of masters", "Nombre de maîtres"@fr ; @@ -340,7 +349,7 @@ midnam:update a lv2:Feature . lv2:maximum 65535 ; ] , [ a lv2:OutputPort, lv2:ControlPort ; - lv2:index 15 ; + lv2:index 16 ; lv2:symbol "num_groups" ; lv2:name "Number of groups", "Nombre de groupes"@fr ; @@ -351,7 +360,7 @@ midnam:update a lv2:Feature . lv2:maximum 65535 ; ] , [ a lv2:OutputPort, lv2:ControlPort ; - lv2:index 16 ; + lv2:index 17 ; lv2:symbol "num_regions" ; lv2:name "Number of regions", "Nombre de régions"@fr ; @@ -362,7 +371,7 @@ midnam:update a lv2:Feature . lv2:maximum 65535 ; ] , [ a lv2:OutputPort, lv2:ControlPort ; - lv2:index 17 ; + lv2:index 18 ; lv2:symbol "num_samples" ; lv2:name "Number of samples", "Nombre d'échantillons"@fr ; @@ -371,4 +380,6 @@ midnam:update a lv2:Feature . lv2:default 0 ; lv2:minimum 0 ; lv2:maximum 65535 ; - ] . + ] ; + + rdfs:seeAlso . diff --git a/plugins/lv2/sfizz_lv2.h b/plugins/lv2/sfizz_lv2.h index 68cb03be..678f0c02 100644 --- a/plugins/lv2/sfizz_lv2.h +++ b/plugins/lv2/sfizz_lv2.h @@ -49,22 +49,23 @@ enum { SFIZZ_CONTROL = 0, SFIZZ_NOTIFY = 1, - SFIZZ_LEFT = 2, - SFIZZ_RIGHT = 3, - SFIZZ_VOLUME = 4, - SFIZZ_POLYPHONY = 5, - SFIZZ_OVERSAMPLING = 6, - SFIZZ_PRELOAD = 7, - SFIZZ_FREEWHEELING = 8, - SFIZZ_SCALA_ROOT_KEY = 9, - SFIZZ_TUNING_FREQUENCY = 10, - SFIZZ_STRETCH_TUNING = 11, - SFIZZ_ACTIVE_VOICES = 12, - SFIZZ_NUM_CURVES = 13, - SFIZZ_NUM_MASTERS = 14, - SFIZZ_NUM_GROUPS = 15, - SFIZZ_NUM_REGIONS = 16, - SFIZZ_NUM_SAMPLES = 17, + SFIZZ_AUTOMATE = 2, + SFIZZ_LEFT = 3, + SFIZZ_RIGHT = 4, + SFIZZ_VOLUME = 5, + SFIZZ_POLYPHONY = 6, + SFIZZ_OVERSAMPLING = 7, + SFIZZ_PRELOAD = 8, + SFIZZ_FREEWHEELING = 9, + SFIZZ_SCALA_ROOT_KEY = 10, + SFIZZ_TUNING_FREQUENCY = 11, + SFIZZ_STRETCH_TUNING = 12, + SFIZZ_ACTIVE_VOICES = 13, + SFIZZ_NUM_CURVES = 14, + SFIZZ_NUM_MASTERS = 15, + SFIZZ_NUM_GROUPS = 16, + SFIZZ_NUM_REGIONS = 17, + SFIZZ_NUM_SAMPLES = 18, }; // For use with instance-access @@ -85,3 +86,14 @@ struct sfizz_plugin_t; bool sfizz_lv2_fetch_description( sfizz_plugin_t *self, const int *serial, uint8_t **descp, uint32_t *sizep, int *serialp); + +// Mapping URID to CC and vice-versa +struct sfizz_lv2_ccmap; +sfizz_lv2_ccmap *sfizz_lv2_ccmap_create(LV2_URID_Map* map); +void sfizz_lv2_ccmap_free(sfizz_lv2_ccmap *ccmap); +LV2_URID sfizz_lv2_ccmap_map(const sfizz_lv2_ccmap *ccmap, int cc); +int sfizz_lv2_ccmap_unmap(const sfizz_lv2_ccmap *ccmap, LV2_URID urid); + +struct sfizz_lv2_ccmap_delete { + void operator()(sfizz_lv2_ccmap* ccmap) const noexcept { sfizz_lv2_ccmap_free(ccmap); } +}; diff --git a/plugins/lv2/sfizz_lv2_common.cpp b/plugins/lv2/sfizz_lv2_common.cpp index ea6562bb..7881db1d 100644 --- a/plugins/lv2/sfizz_lv2_common.cpp +++ b/plugins/lv2/sfizz_lv2_common.cpp @@ -6,6 +6,7 @@ #include "sfizz_lv2.h" #include "sfizz_lv2_plugin.h" +#include "sfizz/Config.h" bool sfizz_lv2_fetch_description( sfizz_plugin_t *self, const int *serial, @@ -29,3 +30,81 @@ bool sfizz_lv2_fetch_description( return true; } + +struct sfizz_lv2_ccmap { + LV2_URID *cc_to_urid; + int *urid_to_cc; + LV2_URID min_cc_urid; + LV2_URID max_cc_urid; +}; + +sfizz_lv2_ccmap * +sfizz_lv2_ccmap_create(LV2_URID_Map* map) +{ + // store the associations between CC number and parameter URID + // construct it trivially in the form of a tabulated perfect hash function + + LV2_URID* cc_to_urid = new LV2_URID[sfz::config::numCCs]; + int* urid_to_cc; + int urid_to_cc_size; + + LV2_URID min_cc_urid {}; + LV2_URID max_cc_urid {}; + for (int cc = 0; cc < sfz::config::numCCs; ++cc) { + char name[256]; + sprintf(name, SFIZZ_URI "#cc%03d", cc); + LV2_URID urid = map->map(map->handle, name); + if (cc == 0) { + min_cc_urid = urid; + max_cc_urid = urid; + } + else { + min_cc_urid = (urid < min_cc_urid) ? urid : min_cc_urid; + max_cc_urid = (urid > max_cc_urid) ? urid : max_cc_urid; + } + cc_to_urid[cc] = urid; + } + + urid_to_cc_size = max_cc_urid - min_cc_urid + 1; + urid_to_cc = new int[urid_to_cc_size]; + + for (int i = 0; i < urid_to_cc_size; ++i) + urid_to_cc[i] = -1; + + for (int cc = 0; cc < sfz::config::numCCs; ++cc) { + LV2_URID urid = cc_to_urid[cc]; + urid_to_cc[urid - min_cc_urid] = cc; + } + + sfizz_lv2_ccmap *self = new sfizz_lv2_ccmap; + self->cc_to_urid = cc_to_urid; + self->urid_to_cc = urid_to_cc; + self->min_cc_urid = min_cc_urid; + self->max_cc_urid = max_cc_urid; + return self; +} + +void sfizz_lv2_ccmap_free(sfizz_lv2_ccmap *ccmap) +{ + if (ccmap) { + delete[] ccmap->cc_to_urid; + delete[] ccmap->urid_to_cc; + delete ccmap; + } +} + +LV2_URID sfizz_lv2_ccmap_map(const sfizz_lv2_ccmap *ccmap, int cc) +{ + LV2_URID urid = 0; + if (cc >= 0 && cc < sfz::config::numCCs) + urid = ccmap->cc_to_urid[cc]; + return urid; +} + +int sfizz_lv2_ccmap_unmap(const sfizz_lv2_ccmap *ccmap, LV2_URID urid) +{ + int cc = -1; + if (urid >= ccmap->min_cc_urid && urid <= ccmap->max_cc_urid) + cc = ccmap->urid_to_cc[urid - ccmap->min_cc_urid]; + return cc; +} diff --git a/plugins/lv2/sfizz_lv2_plugin.h b/plugins/lv2/sfizz_lv2_plugin.h index 0d05a00b..348e417d 100644 --- a/plugins/lv2/sfizz_lv2_plugin.h +++ b/plugins/lv2/sfizz_lv2_plugin.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -31,6 +32,7 @@ struct sfizz_plugin_t // Ports const LV2_Atom_Sequence *control_port {}; LV2_Atom_Sequence *notify_port {}; + LV2_Atom_Sequence *automate_port {}; float *output_buffers[2] {}; const float *volume_port {}; const float *polyphony_port {}; @@ -48,7 +50,8 @@ struct sfizz_plugin_t float *num_samples_port {}; // Atom forge - LV2_Atom_Forge forge {}; ///< Forge for writing atoms in run thread + LV2_Atom_Forge forge_notify {}; ///< Forge for writing notification atoms in run thread + LV2_Atom_Forge forge_automate {}; ///< Forge for writing automation atoms in run thread LV2_Atom_Forge forge_secondary {}; ///< Forge for writing into other buffers // Logger @@ -92,6 +95,9 @@ struct sfizz_plugin_t LV2_URID time_beats_per_minute_uri {}; LV2_URID time_speed_uri {}; + // CC parameters + sfizz_lv2_ccmap* ccmap {}; + // Sfizz related data sfizz_synth_t *synth {}; sfizz_client_t *client {}; @@ -108,6 +114,7 @@ struct sfizz_plugin_t int sample_counter {}; float sample_rate {}; std::atomic must_update_midnam {}; + volatile bool must_automate_cc {}; // Current instrument description std::mutex *sfz_blob_mutex {}; @@ -115,6 +122,14 @@ struct sfizz_plugin_t const uint8_t *volatile sfz_blob_data {}; volatile uint32_t sfz_blob_size {}; + // Current CC values in the synth (synchronized by `synth_mutex`) + // updated by hdcc or file load + float *cc_current {}; + + // CC queued for automation on next run(). (synchronized by `synth_mutex`) + absl::optional* ccauto {}; + volatile bool have_ccauto {}; + // Timing data int bar {}; double bar_beat {}; diff --git a/plugins/lv2/sfizz_ui.cpp b/plugins/lv2/sfizz_ui.cpp index d6073939..581f373f 100644 --- a/plugins/lv2/sfizz_ui.cpp +++ b/plugins/lv2/sfizz_ui.cpp @@ -102,6 +102,7 @@ struct sfizz_ui_t : EditorController, VSTGUIEditorInterface { LV2_Atom_Forge atom_forge; LV2_URID atom_event_transfer_uri; LV2_URID atom_object_uri; + LV2_URID atom_float_uri; LV2_URID atom_path_uri; LV2_URID atom_urid_uri; LV2_URID midi_event_uri; @@ -112,6 +113,7 @@ struct sfizz_ui_t : EditorController, VSTGUIEditorInterface { LV2_URID sfizz_sfz_file_uri; LV2_URID sfizz_scala_file_uri; LV2_URID sfizz_osc_blob_uri; + std::unique_ptr ccmap; uint8_t osc_temp[OSC_TEMP_SIZE]; alignas(LV2_Atom) uint8_t atom_temp[ATOM_TEMP_SIZE]; @@ -201,6 +203,7 @@ instantiate(const LV2UI_Descriptor *descriptor, lv2_atom_forge_init(forge, map); self->atom_event_transfer_uri = map->map(map->handle, LV2_ATOM__eventTransfer); self->atom_object_uri = map->map(map->handle, LV2_ATOM__Object); + self->atom_float_uri = map->map(map->handle, LV2_ATOM__Float); self->atom_path_uri = map->map(map->handle, LV2_ATOM__Path); self->atom_urid_uri = map->map(map->handle, LV2_ATOM__URID); self->midi_event_uri = map->map(map->handle, LV2_MIDI__MidiEvent); @@ -211,6 +214,7 @@ instantiate(const LV2UI_Descriptor *descriptor, 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); + self->ccmap.reset(sfizz_lv2_ccmap_create(map)); // set up the resource path // * on Linux, this is determined by going 2 folders back from the SO path @@ -348,7 +352,15 @@ port_event(LV2UI_Handle ui, const LV2_URID prop_uri = reinterpret_cast(prop)->body; auto *value_body = reinterpret_cast(LV2_ATOM_BODY_CONST(value)); - if (prop_uri == self->sfizz_sfz_file_uri && value->type == self->atom_path_uri) { + int cc = sfizz_lv2_ccmap_unmap(self->ccmap.get(), prop_uri); + + if (cc != -1) { + if (value->type == self->atom_float_uri) { + float ccvalue = *reinterpret_cast(value_body); + self->uiReceiveValue(editIdForCC(cc), ccvalue); + } + } + else if (prop_uri == self->sfizz_sfz_file_uri && value->type == self->atom_path_uri) { std::string path(value_body, strnlen(value_body, value->size)); self->uiReceiveValue(EditId::SfzFile, path); } @@ -525,6 +537,22 @@ void sfizz_ui_t::uiSendValue(EditId id, const EditValue& v) } }; + auto sendController = [this](LV2_URID property, float value) { + LV2_Atom_Forge *forge = &atom_forge; + LV2_Atom_Forge_Frame frame; + 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) && + lv2_atom_forge_key(forge, patch_value_uri) && + lv2_atom_forge_float(forge, value)) + { + lv2_atom_forge_pop(forge, &frame); + write(con, SFIZZ_CONTROL, lv2_atom_total_size(atom), atom_event_transfer_uri, atom); + } + }; + switch (id) { case EditId::Volume: sendFloat(SFIZZ_VOLUME, v.to_float()); @@ -554,6 +582,11 @@ void sfizz_ui_t::uiSendValue(EditId id, const EditValue& v) sendPath(sfizz_scala_file_uri, v.to_string()); break; default: + if (editIdIsCC(id)) { + int cc = ccForEditId(id); + LV2_URID urid = sfizz_lv2_ccmap_map(ccmap.get(), cc); + sendController(urid, v.to_float()); + } break; } } diff --git a/plugins/vst/SfizzVstEditor.cpp b/plugins/vst/SfizzVstEditor.cpp index decc6f43..eff7462f 100644 --- a/plugins/vst/SfizzVstEditor.cpp +++ b/plugins/vst/SfizzVstEditor.cpp @@ -329,7 +329,18 @@ void SfizzVstEditor::processNoteEventQueue() /// void SfizzVstEditor::uiSendValue(EditId id, const EditValue& v) { - if (id == EditId::SfzFile) + if (editIdIsCC(id)) { + int cc = ccForEditId(id); + // TODO(jpc) CC as parameters and automation + if (Editor* editor = editor_.get()) { + char pathBuf[256]; + sprintf(pathBuf, "/cc%u/value", cc); + sfizz_arg_t args[1]; + args[0].f = v.to_float(); + editor->sendQueuedOSC(pathBuf, "f", args); + } + } + else if (id == EditId::SfzFile) loadSfzFile(v.to_string()); else if (id == EditId::ScalaFile) loadScalaFile(v.to_string());