From 8ee911e218cd1a05874818a6f98995ba6b4b7fd2 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sat, 23 Nov 2019 17:26:40 +0100 Subject: [PATCH] Add LV2 --- lv2/CMakeLists.txt | 19 ++ lv2/manifest.ttl | 8 + lv2/sfizz.c | 769 +++++++++++++++++++++++++++++++++++++++++++++ lv2/sfizz.ttl | 95 ++++++ 4 files changed, 891 insertions(+) create mode 100644 lv2/CMakeLists.txt create mode 100644 lv2/manifest.ttl create mode 100644 lv2/sfizz.c create mode 100644 lv2/sfizz.ttl diff --git a/lv2/CMakeLists.txt b/lv2/CMakeLists.txt new file mode 100644 index 00000000..b7f33978 --- /dev/null +++ b/lv2/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.11) +project(sfizz-lv2) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(sfizz REQUIRED IMPORTED_TARGET sfizz) + +# Force add relocation to the flags globally. It's ugly but what can you do... +# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC") + +# Export the compile_commands.json file +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(sfizz-lv2 SHARED sfizz.c) +set_target_properties(sfizz-lv2 PROPERTIES PREFIX "") +set_target_properties(sfizz-lv2 PROPERTIES OUTPUT_NAME "sfizz") +target_link_libraries(sfizz-lv2 sfizz) +if(UNIX) +target_compile_options(sfizz-lv2 PRIVATE -Wextra -pedantic -Wall -Werror) +endif() \ No newline at end of file diff --git a/lv2/manifest.ttl b/lv2/manifest.ttl new file mode 100644 index 00000000..deda193d --- /dev/null +++ b/lv2/manifest.ttl @@ -0,0 +1,8 @@ +@prefix lv2: . +@prefix rdfs: . + + + a lv2:Plugin ; + lv2:binary ; + rdfs:seeAlso . + diff --git a/lv2/sfizz.c b/lv2/sfizz.c new file mode 100644 index 00000000..87c09234 --- /dev/null +++ b/lv2/sfizz.c @@ -0,0 +1,769 @@ +/* + Sfizz LV2 plugin + + Copyright 2019, Paul Ferrand + + This file was based on skeleton and example code from the LV2 plugin + distribution available at http://lv2plug.in/ + + The LV2 sample plugins have the following copyright and notice, which are + extended to the current work: + Copyright 2011-2016 David Robillard + Copyright 2011 Gabriel M. Beddingfield + Copyright 2011 James Morris + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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" +#define SFIZZ_URI "http://sfztools.github.io/sfizz" +#define SFIZZ_PREFIX SFIZZ_URI "#" +#define SFIZZ__sfzFile "http://sfztools.github.io/sfizz:sfzfile" +#define SFIZZ__numVoices "http://sfztools.github.io/sfizz:numvoices" + +#define CHANNEL_MASK 0x0F +#define MIDI_CHANNEL(byte) (byte & CHANNEL_MASK) +#define MIDI_STATUS(byte) (byte & ~CHANNEL_MASK) +#define MAX_BLOCK_SIZE 8192 +#define MAX_PATH_SIZE 1024 +#define MAX_VOICES 256 +#define DEFAULT_VOICES 64 +#define UNUSED(x) (void)(x) + +typedef struct +{ + // Features + LV2_URID_Map *map; + LV2_URID_Unmap *unmap; + LV2_Worker_Schedule *worker; + LV2_Log_Log *log; + + // Ports + const LV2_Atom_Sequence *control_port; + LV2_Atom_Sequence *notify_port; + float *output_buffers[2]; + const float *volume_port; + const float *polyphony_port; + + // Atom forge + LV2_Atom_Forge forge; ///< Forge for writing atoms in run thread + LV2_Atom_Forge_Frame notify_frame; ///< Cached for worker replies + + // Logger + LV2_Log_Logger logger; + + // URIs + LV2_URID midi_event_uri; + LV2_URID options_interface_uri; + LV2_URID max_block_length_uri; + LV2_URID nominal_block_length_uri; + LV2_URID sample_rate_uri; + LV2_URID atom_object_uri; + LV2_URID atom_float_uri; + LV2_URID atom_int_uri; + LV2_URID atom_urid_uri; + LV2_URID atom_path_uri; + LV2_URID patch_set_uri; + LV2_URID patch_get_uri; + LV2_URID patch_put_uri; + LV2_URID patch_property_uri; + LV2_URID patch_value_uri; + LV2_URID patch_body_uri; + LV2_URID state_changed_uri; + LV2_URID sfizz_sfz_file_uri; + LV2_URID sfizz_num_voices_uri; + + // Sfizz related data + sfizz_synth_t *synth; + bool expect_nominal_block_length; + char sfz_file_path[MAX_PATH_SIZE]; + int num_voices; + bool changing_voices; + int max_block_size; + float sample_rate; +} sfizz_plugin_t; + +enum +{ + SFIZZ_CONTROL = 0, + SFIZZ_NOTIFY = 1, + SFIZZ_LEFT = 2, + SFIZZ_RIGHT = 3, + SFIZZ_VOLUME = 4, + SFIZZ_POLYPHONY = 5 +}; + +static void +sfizz_lv2_map_required_uris(sfizz_plugin_t *self) +{ + LV2_URID_Map *map = self->map; + self->midi_event_uri = map->map(map->handle, LV2_MIDI__MidiEvent); + self->max_block_length_uri = map->map(map->handle, LV2_BUF_SIZE__maxBlockLength); + self->nominal_block_length_uri = map->map(map->handle, LV2_BUF_SIZE__nominalBlockLength); + self->sample_rate_uri = map->map(map->handle, LV2_PARAMETERS__sampleRate); + self->atom_float_uri = map->map(map->handle, LV2_ATOM__Float); + self->atom_int_uri = map->map(map->handle, LV2_ATOM__Int); + self->atom_path_uri = map->map(map->handle, LV2_ATOM__Path); + self->atom_urid_uri = map->map(map->handle, LV2_ATOM__URID); + self->atom_object_uri = map->map(map->handle, LV2_ATOM__Object); + self->patch_set_uri = map->map(map->handle, LV2_PATCH__Set); + self->patch_get_uri = map->map(map->handle, LV2_PATCH__Get); + self->patch_put_uri = map->map(map->handle, LV2_PATCH__Put); + self->patch_body_uri = map->map(map->handle, LV2_PATCH__body); + self->patch_property_uri = map->map(map->handle, LV2_PATCH__property); + self->patch_value_uri = map->map(map->handle, LV2_PATCH__value); + self->state_changed_uri = map->map(map->handle, LV2_STATE__StateChanged); + self->sfizz_sfz_file_uri = map->map(map->handle, SFIZZ__sfzFile); + self->sfizz_num_voices_uri = map->map(map->handle, SFIZZ__numVoices); +} + +static void +connect_port(LV2_Handle instance, + uint32_t port, + void *data) +{ + sfizz_plugin_t *self = (sfizz_plugin_t *)instance; + switch (port) + { + case SFIZZ_CONTROL: + self->control_port = (const LV2_Atom_Sequence *)data; + break; + case SFIZZ_NOTIFY: + self->notify_port = (LV2_Atom_Sequence *)data; + break; + case SFIZZ_LEFT: + self->output_buffers[0] = (float *)data; + break; + case SFIZZ_RIGHT: + self->output_buffers[1] = (float *)data; + break; + case SFIZZ_VOLUME: + self->volume_port = (const float *)data; + break; + case SFIZZ_POLYPHONY: + self->polyphony_port = (const float *)data; + break; + default: + break; + } +} + +static LV2_Handle +instantiate(const LV2_Descriptor *descriptor, + double rate, + const char *path, + const LV2_Feature *const *features) +{ + UNUSED(descriptor); + UNUSED(path); + LV2_Options_Option *options; + bool supports_bounded_block_size = false; + bool options_has_block_size = false; + bool supports_fixed_block_size = false; + + // Allocate and initialise instance structure. + sfizz_plugin_t *self = (sfizz_plugin_t *)calloc(1, sizeof(sfizz_plugin_t)); + if (!self) + return NULL; + + // Set defaults + self->max_block_size = MAX_BLOCK_SIZE; + self->sample_rate = (float)rate; + self->expect_nominal_block_length = false; + self->sfz_file_path[0] = '\0'; + self->num_voices = DEFAULT_VOICES; + self->changing_voices = false; + + // Get the features from the host and populate the structure + for (const LV2_Feature *const *f = features; *f; f++) + { + // lv2_log_note(&self->logger, "Feature URI: %s\n", (**f).URI); + + if (!strcmp((**f).URI, LV2_URID__map)) + self->map = (**f).data; + + if (!strcmp((**f).URI, LV2_URID__unmap)) + self->unmap = (**f).data; + + if (!strcmp((**f).URI, LV2_BUF_SIZE__boundedBlockLength)) + supports_bounded_block_size = true; + + if (!strcmp((**f).URI, LV2_BUF_SIZE__fixedBlockLength)) + supports_fixed_block_size = true; + + if (!strcmp((**f).URI, LV2_OPTIONS__options)) + options = (**f).data; + + if (!strcmp((**f).URI, LV2_WORKER__schedule)) + self->worker = (**f).data; + + if (!strcmp((**f).URI, LV2_LOG__log)) + self->log = (**f).data; + } + + // Setup the loggers + lv2_log_logger_init(&self->logger, self->map, self->log); + + // The map feature is required + if (!self->map) + { + lv2_log_error(&self->logger, "Map feature not found, aborting...\n"); + free(self); + return NULL; + } + + // The worker feature is required + if (!self->worker) + { + lv2_log_error(&self->logger, "Worker feature not found, aborting...\n"); + free(self); + return NULL; + } + + // Map the URIs we will need + sfizz_lv2_map_required_uris(self); + + // Initialize the forge + lv2_atom_forge_init(&self->forge, self->map); + + // Check the options for the block size and sample rate parameters + if (options) + { + for (const LV2_Options_Option *opt = options; opt->value; ++opt) + { + if (opt->key == self->sample_rate_uri) + { + if (opt->type != self->atom_float_uri) + { + lv2_log_warning(&self->logger, "Got a sample rate but the type was wrong\n"); + continue; + } + self->sample_rate = *(float *)opt->value; + } + else if (!self->expect_nominal_block_length && opt->key == self->max_block_length_uri) + { + if (opt->type != self->atom_int_uri) + { + lv2_log_warning(&self->logger, "Got a max block size but the type was wrong\n"); + continue; + } + self->max_block_size = *(int *)opt->value; + options_has_block_size = true; + } + else if (opt->key == self->nominal_block_length_uri) + { + if (opt->type != self->atom_int_uri) + { + lv2_log_warning(&self->logger, "Got a nominal block size but the type was wrong\n"); + continue; + } + self->max_block_size = *(int *)opt->value; + self->expect_nominal_block_length = true; + options_has_block_size = true; + } + } + } + else + { + lv2_log_warning(&self->logger, + "No option array was given upon instantiation; will use default values\n."); + } + + // We need _some_ information on the block size + if (!supports_bounded_block_size && !supports_fixed_block_size && !options_has_block_size) + { + lv2_log_error(&self->logger, + "Bounded block size not supported and options gave no block size, aborting...\n"); + free(self); + return NULL; + } + + return (LV2_Handle)self; +} + +static void +cleanup(LV2_Handle instance) +{ + sfizz_plugin_t *self = (sfizz_plugin_t *)instance; + free(self); +} + +static void +activate(LV2_Handle instance) +{ + sfizz_plugin_t *self = (sfizz_plugin_t *)instance; + self->synth = sfizz_create_synth(); + sfizz_set_samples_per_block(self->synth, self->max_block_size); + sfizz_set_sample_rate(self->synth, self->sample_rate); + if (self->sfz_file_path && strlen(self->sfz_file_path) > 0) + { + lv2_log_note(&self->logger, "Current file is: %s\n", self->sfz_file_path); + sfizz_load_file(self->synth, self->sfz_file_path); + } +} + +static void +deactivate(LV2_Handle instance) +{ + sfizz_plugin_t *self = (sfizz_plugin_t *)instance; + sfizz_free(self->synth); +} + +static void +sfizz_lv2_handle_atom_object(sfizz_plugin_t *self, const LV2_Atom_Object *obj) +{ + const LV2_Atom *property = NULL; + lv2_atom_object_get(obj, self->patch_property_uri, &property, 0); + if (!property) + { + lv2_log_error(&self->logger, + "[handle_object] Could not get the property from the patch object, aborting.\n"); + return; + } + + if (property->type != self->atom_urid_uri) + { + lv2_log_error(&self->logger, + "[handle_object] Atom type was not a URID, aborting.\n"); + return; + } + + const uint32_t key = ((const LV2_Atom_URID *)property)->body; + const LV2_Atom *atom = NULL; + lv2_atom_object_get(obj, self->patch_value_uri, &atom, 0); + if (!atom) + { + lv2_log_error(&self->logger, "[handle_object] Error retrieving the atom, aborting.\n"); + if (self->unmap) + lv2_log_warning(&self->logger, + "Atom URI: %s\n", + self->unmap->unmap(self->unmap->handle, key)); + return; + } + + if (key == self->sfizz_sfz_file_uri) + { + + const uint32_t original_atom_size = lv2_atom_total_size((const LV2_Atom *)atom); + const uint32_t null_terminated_atom_size = original_atom_size + 1; + char atom_buffer[null_terminated_atom_size]; + memcpy(&atom_buffer, atom, original_atom_size); + atom_buffer[original_atom_size] = 0; // Null terminate the string for safety + LV2_Atom *sfz_file_path = (LV2_Atom *)&atom_buffer; + sfz_file_path->type = self->sfizz_sfz_file_uri; + + // If the parameter is different from the current one we send it through + if (strcmp(self->sfz_file_path, LV2_ATOM_BODY_CONST(sfz_file_path))) + self->worker->schedule_work(self->worker->handle, null_terminated_atom_size, sfz_file_path); + lv2_log_note(&self->logger, "[handle_object] Received an SFZ file: %s\n", (char*)LV2_ATOM_BODY_CONST(sfz_file_path)); + } + else + { + lv2_log_warning(&self->logger, "[handle_object] Unknown or unsupported object.\n"); + if (self->unmap) + lv2_log_warning(&self->logger, + "Object URI: %s\n", + self->unmap->unmap(self->unmap->handle, key)); + return; + } + + // Pfiou, we got a valid parameter, so send it to the worker +} + +static void +sfizz_lv2_process_midi_event(sfizz_plugin_t *self, const LV2_Atom_Event *ev) +{ + const uint8_t *const msg = (const uint8_t *)(ev + 1); + switch (lv2_midi_message_type(msg)) + { + case LV2_MIDI_MSG_NOTE_ON: + // lv2_log_note(&self->logger, + // "[process_midi] Received note on %d/%d at time %d\n", msg[0], msg[1], ev->time.frames); + sfizz_send_note_on(self->synth, + (int)ev->time.frames, + (int)MIDI_CHANNEL(msg[0]) + 1, + (int)msg[1], + msg[2]); + break; + case LV2_MIDI_MSG_NOTE_OFF: + // lv2_log_note(&self->logger, + // "[process_midi] Received note off %d/%d at time %d\n", msg[0], msg[1], ev->time.frames); + sfizz_send_note_off(self->synth, + (int)ev->time.frames, + (int)MIDI_CHANNEL(msg[0]) + 1, + (int)msg[1], + msg[2]); + break; + case LV2_MIDI_MSG_CONTROLLER: + // lv2_log_note(&self->logger, + // "[process_midi] Received CC %d/%d at time %d\n", msg[0], msg[1], ev->time.frames); + sfizz_send_cc(self->synth, + (int)ev->time.frames, + (int)MIDI_CHANNEL(msg[0]) + 1, + (int)msg[1], + msg[2]); + break; + default: + break; + } +} + +static void sfizz_lv2_send_file_path(sfizz_plugin_t *self) +{ + LV2_Atom_Forge_Frame frame; + 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, self->sfizz_sfz_file_uri); + lv2_atom_forge_key(&self->forge, self->patch_value_uri); + lv2_atom_forge_path(&self->forge, self->sfz_file_path, strlen(self->sfz_file_path)); + lv2_atom_forge_pop(&self->forge, &frame); +} + +static void +run(LV2_Handle instance, uint32_t sample_count) +{ + sfizz_plugin_t *self = (sfizz_plugin_t *)instance; + if (!self->control_port || !self->notify_port) + return; + + // Set up forge to write directly to notify output port. + const size_t notify_capacity = self->notify_port->atom.size; + lv2_atom_forge_set_buffer(&self->forge, (uint8_t *)self->notify_port, notify_capacity); + + // Start a sequence in the notify output port. + lv2_atom_forge_sequence_head(&self->forge, &self->notify_frame, 0); + + LV2_ATOM_SEQUENCE_FOREACH(self->control_port, ev) + { + // If the received atom is an object/patch message + if (ev->body.type == self->atom_object_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); + } + else if (obj->body.otype == self->patch_get_uri) + { + const LV2_Atom_URID *property = NULL; + lv2_atom_object_get(obj, self->patch_property_uri, &property, 0); + if (!property) // Send the full state + { + sfizz_lv2_send_file_path(self); + } + else if (property->body == self->sfizz_sfz_file_uri) + { + sfizz_lv2_send_file_path(self); + } + } + else + { + lv2_log_warning(&self->logger, "Got an Object atom but it was not supported.\n"); + if (self->unmap) + lv2_log_warning(&self->logger, + "Object URI: %s\n", + 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) + { + sfizz_lv2_process_midi_event(self, ev); + } + } + + float volume = *(self->volume_port); + sfizz_set_volume(self->synth, volume); + + int num_voices = (int)*self->polyphony_port; + if (num_voices != self->num_voices && !self->changing_voices) + { + lv2_log_note(&self->logger, "[run] Number of voices changed to %d\n", num_voices); + LV2_Atom_Int num_voices_atom; + num_voices_atom.atom.type = self->sfizz_num_voices_uri; + num_voices_atom.atom.size = sizeof(int); + num_voices_atom.body = num_voices; + if (self->worker->schedule_work(self->worker->handle, + lv2_atom_total_size((LV2_Atom*)&num_voices_atom), + &num_voices_atom) == LV2_WORKER_SUCCESS) + { + self->changing_voices = true; + } + } + + // Render the block + sfizz_render_block(self->synth, self->output_buffers, 2, (int)sample_count); +} + +static uint32_t +lv2_get_options(LV2_Handle instance, LV2_Options_Option *options) +{ + UNUSED(instance); + UNUSED(options); + // We have no options + return LV2_OPTIONS_ERR_UNKNOWN; +} + +static uint32_t +lv2_set_options(LV2_Handle instance, const LV2_Options_Option *options) +{ + sfizz_plugin_t *self = (sfizz_plugin_t *)instance; + + // Update the block size and sample rate as needed + for (const LV2_Options_Option *opt = options; opt->value; ++opt) + { + if (opt->key == self->sample_rate_uri) + { + if (opt->type != self->atom_float_uri) + { + lv2_log_warning(&self->logger, "Got a sample rate but the type was wrong\n"); + continue; + } + self->sample_rate = *(float *)opt->value; + sfizz_set_sample_rate(self->synth, self->sample_rate); + } + else if (!self->expect_nominal_block_length && opt->key == self->max_block_length_uri) + { + if (opt->type != self->atom_int_uri) + { + lv2_log_warning(&self->logger, "Got a max block size but the type was wrong\n"); + continue; + } + self->max_block_size = *(int *)opt->value; + sfizz_set_samples_per_block(self->synth, self->max_block_size); + } + else if (opt->key == self->nominal_block_length_uri) + { + if (opt->type != self->atom_int_uri) + { + lv2_log_warning(&self->logger, "Got a nominal block size but the type was wrong\n"); + continue; + } + self->max_block_size = *(int *)opt->value; + sfizz_set_samples_per_block(self->synth, self->max_block_size); + } + } + return LV2_OPTIONS_SUCCESS; +} + +static LV2_State_Status +restore(LV2_Handle instance, + LV2_State_Retrieve_Function retrieve, + LV2_State_Handle handle, + uint32_t flags, + const LV2_Feature *const *features) +{ + UNUSED(flags); + UNUSED(features); + sfizz_plugin_t *self = (sfizz_plugin_t *)instance; + + // Fetch back the saved file path, if any + size_t size; + uint32_t type; + uint32_t val_flags; + const void *value; + value = retrieve(handle, self->sfizz_sfz_file_uri, &size, &type, &val_flags); + if (value) + { + lv2_log_note(&self->logger, "Restoring the file %s\n", (const char *)value); + if (sfizz_load_file(self->synth, (const char *)value)) + strcpy(self->sfz_file_path, (const char *)value); + } + + value = retrieve(handle, self->sfizz_num_voices_uri, &size, &type, &val_flags); + if (value) + { + int num_voices = *(const int *)value; + if (num_voices > 0 && num_voices <= MAX_VOICES && num_voices != self->num_voices) + { + lv2_log_note(&self->logger, "Restoring the number of voices to %d\n", num_voices); + sfizz_set_num_voices(self->synth, num_voices); + self->num_voices = num_voices; + } + } + return LV2_STATE_SUCCESS; +} + +static LV2_State_Status +save(LV2_Handle instance, + LV2_State_Store_Function store, + LV2_State_Handle handle, + uint32_t flags, + const LV2_Feature *const *features) +{ + UNUSED(flags); + UNUSED(features); + sfizz_plugin_t *self = (sfizz_plugin_t *)instance; + // Save the file path + store(handle, + self->sfizz_sfz_file_uri, + self->sfz_file_path, + strlen(self->sfz_file_path) + 1, + self->atom_path_uri, + LV2_STATE_IS_POD); + + // Save the number of voices + store(handle, + self->sfizz_num_voices_uri, + &self->num_voices, + sizeof(int), + self->atom_int_uri, + LV2_STATE_IS_POD | LV2_STATE_IS_PORTABLE); + + return LV2_STATE_SUCCESS; +} + +// This runs in a lower priority thread +static LV2_Worker_Status +work(LV2_Handle instance, + LV2_Worker_Respond_Function respond, + LV2_Worker_Respond_Handle handle, + uint32_t size, + const void *data) +{ + sfizz_plugin_t *self = (sfizz_plugin_t *)instance; + if (!data) + { + lv2_log_error(&self->logger, "[worker] Got an empty data.\n"); + return LV2_WORKER_ERR_UNKNOWN; + } + + const LV2_Atom *atom = (const LV2_Atom *)data; + if (atom->type == self->sfizz_sfz_file_uri) + { + const char *sfz_file_path = LV2_ATOM_BODY_CONST(atom); + lv2_log_note(&self->logger, "[work] Loading file: %s\n", sfz_file_path); + sfizz_load_file(self->synth, sfz_file_path); + } + else if (atom->type == self->sfizz_num_voices_uri) + { + const int num_voices = *(const int *)LV2_ATOM_BODY_CONST(atom); + lv2_log_note(&self->logger, "[work] Changing number of voices to: %d\n", num_voices); + sfizz_set_num_voices(self->synth, num_voices); + } + else + { + lv2_log_error(&self->logger, "[worker] Got an unknown atom.\n"); + if (self->unmap) + lv2_log_error(&self->logger, + "URI: %s\n", + self->unmap->unmap(self->unmap->handle, atom->type)); + return LV2_WORKER_ERR_UNKNOWN; + } + + respond(handle, size, data); + return LV2_WORKER_SUCCESS; +} + +// This runs in the audio thread +static LV2_Worker_Status +work_response(LV2_Handle instance, + uint32_t size, + const void *data) +{ + UNUSED(size); + sfizz_plugin_t *self = (sfizz_plugin_t *)instance; + + if (!data) + return LV2_WORKER_ERR_UNKNOWN; + + const LV2_Atom *atom = (const LV2_Atom *)data; + if (atom->type == self->sfizz_sfz_file_uri) + { + const char *sfz_file_path = LV2_ATOM_BODY_CONST(atom); + strcpy(self->sfz_file_path, sfz_file_path); + lv2_log_note(&self->logger, "[work_response] File changed to: %s\n", self->sfz_file_path); + } + else if (atom->type == self->sfizz_num_voices_uri) + { + const int num_voices = *(const int *)LV2_ATOM_BODY_CONST(atom); + self->num_voices = num_voices; + self->changing_voices = false; + lv2_log_note(&self->logger, "[work_response] Number of voices changed to: %d\n", self->num_voices); + } + else + { + lv2_log_error(&self->logger, "[work_response] Got an unknown atom.\n"); + if (self->unmap) + lv2_log_error(&self->logger, + "URI: %s\n", + self->unmap->unmap(self->unmap->handle, atom->type)); + return LV2_WORKER_ERR_UNKNOWN; + } + + return LV2_WORKER_SUCCESS; +} + +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}; + + // Advertise the extensions we support + if (!strcmp(uri, LV2_OPTIONS__interface)) + return &options; + else if (!strcmp(uri, LV2_STATE__interface)) + return &state; + else if (!strcmp(uri, LV2_WORKER__interface)) + return &worker; + + return NULL; +} + +static const LV2_Descriptor descriptor = { + SFIZZ_URI, + instantiate, + connect_port, + activate, + run, + deactivate, + cleanup, + extension_data}; + +LV2_SYMBOL_EXPORT +const LV2_Descriptor * +lv2_descriptor(uint32_t index) +{ + switch (index) + { + case 0: + return &descriptor; + default: + return NULL; + } +} diff --git a/lv2/sfizz.ttl b/lv2/sfizz.ttl new file mode 100644 index 00000000..f818c27a --- /dev/null +++ b/lv2/sfizz.ttl @@ -0,0 +1,95 @@ +@prefix atom: . +@prefix doap: . +@prefix lv2: . +@prefix rdfs: . +@prefix rdf: . +@prefix urid: . +@prefix midi: . +@prefix bufsize: . +@prefix opts: . +@prefix patch: . +@prefix units: . +@prefix state: . +@prefix work: . +@prefix pprop: . +@prefix pg: . + +<#config> + a pg:Group ; + lv2:symbol "config" ; + lv2:name "Configuration" . + + + a lv2:Parameter ; + rdfs:label "SFZ file" ; + rdfs:range atom:Path . + + + a lv2:Parameter ; + rdfs:label "Polyphony" ; + rdfs:range atom:Int . + + + a doap:Project, lv2:Plugin, lv2:InstrumentPlugin; + doap:name "Sfizz" ; + lv2:requiredFeature urid:map, bufsize:boundedBlockLength, work:schedule; + rdfs:comment "SFZ sampler" ; + lv2:optionalFeature lv2:hardRTCapable, opts:options; + lv2:extensionData opts:interface, state:interface, work:interface ; + patch:writable ; + lv2:port [ + a lv2:InputPort, atom:AtomPort ; + atom:bufferType atom:Sequence ; + atom:supports patch:Message, midi:MidiEvent; + lv2:designation lv2:control ; + lv2:index 0 ; + lv2:symbol "control" ; + lv2:name "Control" + ] , [ + a lv2:OutputPort, atom:AtomPort ; + atom:bufferType atom:Sequence ; + atom:supports patch:Message ; + lv2:designation lv2:control ; + lv2:index 1 ; + lv2:symbol "notify" ; + lv2:name "Notify" ; + ] , [ + a lv2:AudioPort , lv2:OutputPort ; + lv2:index 2 ; + lv2:symbol "out_left" ; + lv2:name "Left Output" + ] , [ + a lv2:AudioPort , lv2:OutputPort ; + lv2:index 3 ; + lv2:symbol "out_right" ; + lv2:name "Right Output" + ] , [ + a lv2:InputPort, lv2:ControlPort ; + lv2:index 4 ; + lv2:symbol "volume" ; + lv2:name "Volume" ; + lv2:default 0.0 ; + lv2:minimum -80.0 ; + lv2:maximum 6.0; + lv2:portProperty pprop:notAutomatic; + units:unit units:db + ], [ + a lv2:InputPort, lv2:ControlPort ; + lv2:index 5 ; + lv2:symbol "num_voices" ; + lv2:name "Polyphony" ; + pg:group <#config>; + lv2:portProperty pprop:notAutomatic ; + lv2:portProperty pprop:expensive ; + lv2:portProperty lv2:integer; + lv2:portProperty lv2:enumeration ; + lv2:default 64 ; + lv2:minimum 8 ; + lv2:maximum 256 ; + lv2:scalePoint [ rdfs:label "8 voices"; rdf:value 8 ] ; + lv2:scalePoint [ rdfs:label "16 voices"; rdf:value 16 ] ; + lv2:scalePoint [ rdfs:label "32 voices"; rdf:value 32 ] ; + lv2:scalePoint [ rdfs:label "64 voices"; rdf:value 64 ] ; + lv2:scalePoint [ rdfs:label "128 voices"; rdf:value 128 ] ; + lv2:scalePoint [ rdfs:label "256 voices"; rdf:value 256 ] ; + ].