diff options
author | David Robillard <d@drobilla.net> | 2011-12-23 02:41:56 +0000 |
---|---|---|
committer | David Robillard <d@drobilla.net> | 2011-12-23 02:41:56 +0000 |
commit | c5c4a6e935eecc2d2dac7bb9cccd36057b8dc123 (patch) | |
tree | aac647c5a0fcc59bfe583bb25c6b508f21db7a19 | |
parent | b760711088bb027d7c84eec7c19360d8f02680ec (diff) | |
download | lilv-c5c4a6e935eecc2d2dac7bb9cccd36057b8dc123.tar.gz lilv-c5c4a6e935eecc2d2dac7bb9cccd36057b8dc123.tar.bz2 lilv-c5c4a6e935eecc2d2dac7bb9cccd36057b8dc123.zip |
Add LilvState API for handling plugin state. This makes it simple to save and
restore plugin state both in memory and on disk, as well as save presets in a
host-sharable way since the disk format is identical to the LV2 presets format.
git-svn-id: http://svn.drobilla.net/lad/trunk/lilv@3899 a436a847-0d15-0410-975c-d299462d15a1
-rw-r--r-- | ChangeLog | 4 | ||||
-rw-r--r-- | lilv.pc.in | 2 | ||||
-rw-r--r-- | lilv/lilv.h | 166 | ||||
-rw-r--r-- | src/lilv_internal.h | 11 | ||||
-rw-r--r-- | src/plugin.c | 20 | ||||
-rw-r--r-- | src/state.c | 794 | ||||
-rw-r--r-- | src/world.c | 20 | ||||
-rw-r--r-- | src/zix/tree.c | 2 | ||||
-rw-r--r-- | wscript | 38 |
9 files changed, 1026 insertions, 31 deletions
@@ -16,6 +16,10 @@ lilv (UNRELEASED) unstable; urgency=low * Print presets in lv2info * Remove locale smashing kludges and use new serd functions for converting nodes to/from numbers. + * Add LilvState API for handling plugin state. This makes it simple to + save and restore plugin state both in memory and on disk, as well as + save presets in a host-sharable way since the disk format is identical + to the LV2 presets format. -- David Robillard <d@drobilla.net> (UNRELEASED) @@ -6,6 +6,6 @@ includedir=@INCLUDEDIR@ Name: Lilv Version: @LILV_VERSION@ Description: Simple C library for hosting LV2 plugins -Requires: lv2core +Requires: lv2core lv2-lv2plug.in-ns-ext-state Libs: -L${libdir} -l@LIB_LILV@ -ldl Cflags: -I${includedir}/lilv-@LILV_MAJOR_VERSION@ diff --git a/lilv/lilv.h b/lilv/lilv.h index ca3809d..7d04cda 100644 --- a/lilv/lilv.h +++ b/lilv/lilv.h @@ -27,6 +27,7 @@ #include <stdio.h> #include "lv2/lv2plug.in/ns/lv2core/lv2.h" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" #ifdef LILV_SHARED # ifdef __WIN32__ @@ -74,6 +75,7 @@ typedef struct LilvUIImpl LilvUI; /**< Plugin UI. */ typedef struct LilvNodeImpl LilvNode; /**< Typed Value. */ typedef struct LilvWorldImpl LilvWorld; /**< Lilv World. */ typedef struct LilvInstanceImpl LilvInstance; /**< Plugin instance. */ +typedef struct LilvStateImpl LilvState; /**< Plugin state. */ typedef void LilvIter; /**< Collection iterator */ typedef void LilvPluginClasses; /**< set<PluginClass>. */ @@ -1093,6 +1095,170 @@ lilv_port_get_scale_points(const LilvPlugin* plugin, /** @} + @name Plugin State + @{ +*/ + +/** + Free @c state. +*/ +LILV_API +void +lilv_state_free(LilvState* state); + +/** + Get the URI of the plugin @c state applies to. +*/ +LILV_API +const LilvNode* +lilv_state_get_plugin_uri(const LilvState* state); + +/** + Get the label of @c state. +*/ +LILV_API +const char* +lilv_state_get_label(const LilvState* state); + +/** + Set the label of @c state. +*/ +LILV_API +void +lilv_state_set_label(LilvState* state, + const char* label); + +/** + Load a state snapshot from @c world's RDF model. + @param subject The subject of the state description (e.g. a preset URI). + @return A new LilvState which must be freed with lilv_state_free(). +*/ +LILV_API +LilvState* +lilv_state_new_from_world(LilvWorld* world, + LV2_URID_Map* map, + const LilvNode* subject); + +/** + Load a state snapshot from a file. + @param subject The subject of the state description (e.g. a preset URI). + @param path The path of the file containing the state description. + @return A new LilvState which must be freed with lilv_state_free(). + + If @c subject is NULL, it is taken to be the URI of the file (i.e. + "<>" in Turtle). + + This function parses the file separately to create the state, it does not + parse the file into the world model, i.e. the returned state is the only + new memory consumed once this function returns. +*/ +LILV_API +LilvState* +lilv_state_new_from_file(LilvWorld* world, + LV2_URID_Map* map, + const LilvNode* subject, + const char* path); + +/** + Save state to a file. + @param unmap URID unmapper. + @param state State to save. + @param uri URI of state, may be NULL. + @param path Path of file to save state to, may be NULL. + @param manifest_path Path of manifest file to add entry to, may be NULL. + + The format of state on disk is compatible with that defined in the LV2 + preset extension, i.e. this function may be used to save presets which can + be loaded by any host. If @c path is NULL, then the default user preset + bundle (~/.lv2/presets.lv2) is used. In this case, the label of @c state + MUST be set since it is used to generate a filename. + + If @c uri is NULL, the state will be saved without an absolute URI (but + the bundle will still work correctly as a preset bundle). +*/ +LILV_API +int +lilv_state_save(LilvWorld* world, + LV2_URID_Unmap* unmap, + const LilvState* state, + const char* uri, + const char* path, + const char* manifest_path); + +/** + Function to get a port value. + @return A node the caller (lilv) takes ownership of and must free. +*/ +typedef LilvNode* (*LilvGetPortValueFunc)(const char* port_symbol, + void* user_data); + +/** + Create a new state snapshot from a plugin instance. + @param plugin The plugin this state applies to. + @param instance An instance of @c plugin. + @param flags Bitwise OR of LV2_State_Flags values. + @param features Features to pass LV2_State_Interface.save(). + @return A new LilvState which must be freed with lilv_state_free(). + + The returned state will have all properties set from the plugin, but no port + values set (since it is impossible for Lilv to do this in general). To get + a complete snapshot of plugin state, call this function, then set the + appropriate port values with lilv_state_set_port_value. + + This function may be called simultaneously with any instance function + (except discovery functions) unless the threading class of that function + explicitly disallows this. + + See <a href="http://lv2plug.in/ns/ext/state/state.h">state.h</a> from the + LV2 State extension for details on the @c flags and @c features parameters. +*/ +LILV_API +LilvState* +lilv_state_new_from_instance(const LilvPlugin* plugin, + LilvInstance* instance, + LilvGetPortValueFunc get_value, + void* user_data, + uint32_t flags, + const LV2_Feature *const * features); + +/** + Function to set a port value. +*/ +typedef void (*LilvSetPortValueFunc)(const char* port_symbol, + const LilvNode* value, + void* user_data); + +/** + Restore a plugin instance from a state snapshot. + @param state The state to restore, which must apply to the correct plugin. + @param instance An instance of the plugin @c state applies to. + @param set_value A function to set a port value (may be NULL). + @param flags Bitwise OR of LV2_State_Flags values. + @param features Features to pass LV2_State_Interface.restore(). + + This will set all the properties of @c instance to the values stored in @c + state. If @c set_value is provided, it will be called (with the given @c + user_data) to restore each port value, otherwise the host must restore the + port values itself (using lilv_state_get_port_value) in order to completely + restore @c state. + + This function is in the "instantiation" threading class, i.e. it MUST NOT be + called simultaneously with any function on the same plugin instance. + + See <a href="http://lv2plug.in/ns/ext/state/state.h">state.h</a> from the + LV2 State extension for details on the @c flags and @c features parameters. +*/ +LILV_API +void +lilv_state_restore(const LilvState* state, + LilvInstance* instance, + LilvSetPortValueFunc set_value, + void* user_data, + uint32_t flags, + const LV2_Feature *const * features); + +/** + @} @name Scale Point @{ */ diff --git a/src/lilv_internal.h b/src/lilv_internal.h index a700d6f..75e21a2 100644 --- a/src/lilv_internal.h +++ b/src/lilv_internal.h @@ -65,7 +65,6 @@ struct LilvPortImpl { LilvNodes* classes; ///< rdf:type }; - struct LilvSpecImpl { SordNode* spec; SordNode* bundle; @@ -132,6 +131,7 @@ struct LilvWorldImpl { LilvNodes* loaded_files; SordNode* dc_replaces_node; SordNode* dyn_manifest_node; + SordNode* lv2_appliesTo_node; SordNode* lv2_binary_node; SordNode* lv2_default_node; SordNode* lv2_index_node; @@ -143,6 +143,7 @@ struct LilvWorldImpl { SordNode* lv2_reportslatency_node; SordNode* lv2_specification_node; SordNode* lv2_symbol_node; + SordNode* pset_value_node; SordNode* rdf_a_node; SordNode* rdf_value_node; SordNode* rdfs_class_node; @@ -154,7 +155,6 @@ struct LilvWorldImpl { SordNode* xsd_double_node; SordNode* xsd_integer_node; LilvNode* doap_name_val; - LilvNode* lv2_applies_to_val; LilvNode* lv2_extensionData_val; LilvNode* lv2_name_val; LilvNode* lv2_optionalFeature_val; @@ -290,6 +290,13 @@ lilv_match_subject(SordIter* iter) { } static inline const SordNode* +lilv_match_predicate(SordIter* iter) { + SordQuad tup; + sord_iter_get(iter, tup); + return tup[SORD_PREDICATE]; +} + +static inline const SordNode* lilv_match_object(SordIter* iter) { SordQuad tup; sord_iter_get(iter, tup); diff --git a/src/plugin.c b/src/plugin.c index b305291..fd4b300 100644 --- a/src/plugin.c +++ b/src/plugin.c @@ -670,7 +670,7 @@ lilv_plugin_has_extension_data(const LilvPlugin* p, return false; } } - + LILV_API LilvNodes* lilv_plugin_get_extension_data(const LilvPlugin* p) @@ -833,8 +833,11 @@ LilvNodes* lilv_plugin_get_related(const LilvPlugin* plugin, const LilvNode* type) { LilvWorld* const world = plugin->world; - LilvNodes* const related = lilv_world_find_nodes( - world, NULL, world->lv2_applies_to_val, lilv_plugin_get_uri(plugin)); + LilvNodes* const related = lilv_world_query_values_internal( + world, + NULL, + world->lv2_appliesTo_node, + lilv_plugin_get_uri(plugin)->val.uri_val); if (!type) { return related; @@ -857,13 +860,6 @@ lilv_plugin_get_related(const LilvPlugin* plugin, const LilvNode* type) return matches; } -static size_t -file_sink(const void* buf, size_t len, void* stream) -{ - FILE* file = (FILE*)stream; - return fwrite(buf, 1, len, file); -} - static SerdEnv* new_lv2_env(const SerdNode* base) { @@ -910,7 +906,7 @@ lilv_plugin_write_description(LilvWorld* world, SERD_STYLE_ABBREVIATED|SERD_STYLE_CURIED, env, NULL, - file_sink, + serd_file_sink, plugin_file); // Write prefixes if this is a new file @@ -950,7 +946,7 @@ lilv_plugin_write_manifest_entry(LilvWorld* world, SERD_STYLE_ABBREVIATED|SERD_STYLE_CURIED, env, NULL, - file_sink, + serd_file_sink, manifest_file); // Write prefixes if this is a new file diff --git a/src/state.c b/src/state.c new file mode 100644 index 0000000..aae3fab --- /dev/null +++ b/src/state.c @@ -0,0 +1,794 @@ +/* + Copyright 2007-2011 David Robillard <http://drobilla.net> + + 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. +*/ + +#define _POSIX_SOURCE 1 /* for fileno */ +#define _BSD_SOURCE 1 /* for lockf */ + +#include <errno.h> +#include <stdio.h> +#include <string.h> + +#include "lilv-config.h" +#include "lilv_internal.h" + +#ifdef HAVE_LV2_STATE +# include "lv2/lv2plug.in/ns/ext/state/state.h" +#endif + +#if defined(HAVE_LOCKF) && defined(HAVE_FILENO) +# include <unistd.h> +#endif + +#ifdef HAVE_MKDIR +# include <sys/stat.h> +# include <sys/types.h> +#endif + +#define NS_ATOM "http://lv2plug.in/ns/ext/atom#" +#define NS_PSET "http://lv2plug.in/ns/ext/presets#" +#define NS_STATE "http://lv2plug.in/ns/ext/state#" + +#define USTR(s) ((const uint8_t*)(s)) + +typedef struct { + void* value; + size_t size; + uint32_t key; + uint32_t type; + uint32_t flags; +} Property; + +typedef struct { + char* symbol; + LilvNode* value; +} PortValue; + +struct LilvStateImpl { + LilvNode* plugin_uri; + Property* props; + PortValue* values; + char* label; + uint32_t num_props; + uint32_t num_values; +}; + +static int +property_cmp(const void* a, const void* b) +{ + const Property* pa = (const Property*)a; + const Property* pb = (const Property*)b; + return pa->key - pb->key; +} + +LILV_API +const LilvNode* +lilv_state_get_plugin_uri(const LilvState* state) +{ + return state->plugin_uri; +} + +static PortValue* +append_port_value(LilvState* state, + const char* port_symbol, + LilvNode* value) +{ + state->values = realloc(state->values, + (++state->num_values) * sizeof(PortValue)); + PortValue* pv = &state->values[state->num_values - 1]; + pv->symbol = lilv_strdup(port_symbol); + pv->value = value; + return pv; +} + +LILV_API +const char* +lilv_state_get_label(const LilvState* state) +{ + return state->label; +} + +LILV_API +void +lilv_state_set_label(LilvState* state, + const char* label) +{ + if (state->label) { + free(state->label); + } + + state->label = label ? lilv_strdup(label) : NULL; +} + +#ifdef HAVE_LV2_STATE +static int +store_callback(void* handle, + uint32_t key, + const void* value, + size_t size, + uint32_t type, + uint32_t flags) +{ + if (!(flags & LV2_STATE_IS_POD)) { + // TODO: A flag so we know if we can hold a reference would be nice + LILV_WARN("Non-POD property ignored.\n"); + return 1; + } + + LilvState* const state = (LilvState*)handle; + state->props = realloc(state->props, + (++state->num_props) * sizeof(Property)); + Property* const prop = &state->props[state->num_props - 1]; + + prop->value = malloc(size); + memcpy(prop->value, value, size); + + prop->size = size; + prop->key = key; + prop->type = type; + prop->flags = flags; + + return 0; +} +#endif // HAVE_LV2_STATE + +LILV_API +LilvState* +lilv_state_new_from_instance(const LilvPlugin* plugin, + LilvInstance* instance, + LilvGetPortValueFunc get_value, + void* user_data, + uint32_t flags, + const LV2_Feature *const * features) +{ + LilvWorld* const world = plugin->world; + LilvState* const state = malloc(sizeof(LilvState)); + memset(state, '\0', sizeof(LilvState)); + state->plugin_uri = lilv_node_duplicate(lilv_plugin_get_uri(plugin)); + + // Store port values + LilvNode* lv2_ControlPort = lilv_new_uri(world, LILV_URI_CONTROL_PORT); + LilvNode* lv2_InputPort = lilv_new_uri(world, LILV_URI_INPUT_PORT); + for (uint32_t i = 0; i < plugin->num_ports; ++i) { + const LilvPort* const port = plugin->ports[i]; + if (lilv_port_is_a(plugin, port, lv2_ControlPort) + && lilv_port_is_a(plugin, port, lv2_InputPort)) { + const char* sym = lilv_node_as_string(port->symbol); + append_port_value(state, sym, get_value(sym, user_data)); + } + } + lilv_node_free(lv2_ControlPort); + lilv_node_free(lv2_InputPort); + + // Store properties +#ifdef HAVE_LV2_STATE + const LV2_Descriptor* descriptor = instance->lv2_descriptor; + const LV2_State_Interface* iface = (descriptor->extension_data) + ? descriptor->extension_data(LV2_STATE_INTERFACE_URI) + : NULL; + + iface->save(instance->lv2_handle, store_callback, state, flags, features); +#endif // HAVE_LV2_STATE + + qsort(state->props, state->num_props, sizeof(Property), property_cmp); + + return state; +} + +static const void* +retrieve_callback(void* handle, + uint32_t key, + size_t* size, + uint32_t* type, + uint32_t* flags) +{ + const LilvState* const state = (LilvState*)handle; + const Property search_key = { NULL, 0, key, 0, 0 }; + const Property* const prop = (Property*)bsearch( + &search_key, state->props, state->num_props, + sizeof(Property), property_cmp); + + if (prop) { + *size = prop->size; + *type = prop->type; + *flags = prop->flags; + return prop->value; + } + return NULL; +} + +LILV_API +void +lilv_state_restore(const LilvState* state, + LilvInstance* instance, + LilvSetPortValueFunc set_value, + void* user_data, + uint32_t flags, + const LV2_Feature *const * features) +{ +#ifdef HAVE_LV2_STATE + const LV2_Descriptor* descriptor = instance->lv2_descriptor; + const LV2_State_Interface* iface = (descriptor->extension_data) + ? descriptor->extension_data(LV2_STATE_INTERFACE_URI) + : NULL; + + iface->restore(instance->lv2_handle, retrieve_callback, + (LV2_State_Handle)state, flags, features); + + if (set_value) { + for (uint32_t i = 0; i < state->num_values; ++i) { + set_value(state->values[i].symbol, + state->values[i].value, + user_data); + } + } +#endif // HAVE_LV2_STATE +} + +static SordNode* +get_one(SordModel* model, const SordNode* s, const SordNode* p) +{ + const SordQuad pat = { s, p, NULL, NULL }; + SordIter* const i = sord_find(model, pat); + SordNode* const node = i ? sord_node_copy(lilv_match_object(i)) : NULL; + sord_iter_free(i); + return node; +} + +static void +property_from_node(LilvWorld* world, + LV2_URID_Map* map, + const LilvNode* node, + Property* prop) +{ + const char* str = lilv_node_as_string(node); + switch (node->type) { + case LILV_VALUE_URI: + prop->value = malloc(sizeof(uint32_t)); + *(uint32_t*)prop->value = map->map(map->handle, str); + prop->type = map->map(map->handle, NS_ATOM "URID"); + prop->size = sizeof(uint32_t); + break; + case LILV_VALUE_BLANK: + // TODO: Hmm... + break; + case LILV_VALUE_STRING: + prop->size = strlen(str) + 1; + prop->value = malloc(prop->size); + memcpy(prop->value, str, prop->size + 1); + prop->type = map->map(map->handle, NS_ATOM "String"); + break; + case LILV_VALUE_BOOL: + prop->value = malloc(sizeof(uint32_t)); + *(uint32_t*)prop->value = lilv_node_as_bool(node) ? 1 : 0; + prop->type = map->map(map->handle, NS_ATOM "Bool"); + prop->size = sizeof(uint32_t); + break; + case LILV_VALUE_INT: + prop->value = malloc(sizeof(uint32_t)); + *(uint32_t*)prop->value = lilv_node_as_int(node); + prop->type = map->map(map->handle, NS_ATOM "Int32"); + prop->size = sizeof(uint32_t); + break; + case LILV_VALUE_FLOAT: + prop->value = malloc(sizeof(float)); + *(float*)prop->value = lilv_node_as_float(node); + prop->type = map->map(map->handle, NS_ATOM "Float"); + prop->size = sizeof(float); + break; + } + prop->flags = LV2_STATE_IS_POD|LV2_STATE_IS_PORTABLE; +} + +static LilvState* +new_state_from_model(LilvWorld* world, + LV2_URID_Map* map, + SordModel* model, + const SordNode* node) +{ + LilvState* const state = malloc(sizeof(LilvState)); + memset(state, '\0', sizeof(LilvState)); + + // Get the plugin URI this state applies to + const SordQuad pat1 = { + node, world->lv2_appliesTo_node, NULL, NULL }; + SordIter* i = sord_find(model, pat1); + if (i) { + state->plugin_uri = lilv_node_new_from_node( + world, lilv_match_object(i)); + sord_iter_free(i); + } else { + LILV_ERRORF("State %s missing lv2:appliesTo property\n", + sord_node_get_string(node)); + } + + // Get port values + const SordQuad pat2 = { node, world->lv2_port_node, NULL, NULL }; + SordIter* ports = sord_find(model, pat2); + FOREACH_MATCH(ports) { + const SordNode* port = lilv_match_object(ports); + const SordNode* label = get_one(model, port, world->rdfs_label_node); + const SordNode* symbol = get_one(model, port, world->lv2_symbol_node); + const SordNode* value = get_one(model, port, world->pset_value_node); + if (!symbol) { + LILV_ERRORF("State `%s' port missing symbol.\n", + sord_node_get_string(node)); + } else if (!value) { + LILV_ERRORF("State `%s' port `%s' missing value.\n", + sord_node_get_string(symbol), + sord_node_get_string(node)); + } else { + const char* sym = (const char*)sord_node_get_string(symbol); + LilvNode* lvalue = lilv_node_new_from_node(world, value); + append_port_value(state, sym, lvalue); + + if (label) { + lilv_state_set_label( + state, (const char*)sord_node_get_string(label)); + } + } + } + sord_iter_free(ports); + + // Get properties + SordNode* statep = sord_new_uri(world->world, USTR(NS_STATE "state")); + const SordNode* state_node = get_one(model, node, statep); + if (state_node) { + const SordQuad pat3 = { state_node, NULL, NULL }; + SordIter* props = sord_find(model, pat3); + FOREACH_MATCH(props) { + const SordNode* p = lilv_match_predicate(props); + const SordNode* o = lilv_match_object(props); + LilvNode* onode = lilv_node_new_from_node(world, o); + + Property prop = { NULL, 0, 0, 0, 0 }; + prop.key = map->map(map->handle, + (const char*)sord_node_get_string(p)); + property_from_node(world, map, onode, &prop); + if (prop.value) { + state->props = realloc( + state->props, (++state->num_props) * sizeof(Property)); + state->props[state->num_props - 1] = prop; + } + + lilv_node_free(onode); + } + sord_iter_free(props); + } + sord_node_free(world->world, statep); + + qsort(state->props, state->num_props, sizeof(Property), property_cmp); + + return state; +} + +LILV_API +LilvState* +lilv_state_new_from_world(LilvWorld* world, + LV2_URID_Map* map, + const LilvNode* node) +{ + if (!lilv_node_is_uri(node) && !lilv_node_is_blank(node)) { + LILV_ERRORF("Subject `%s' is not a URI or blank node.\n", + lilv_node_as_string(node)); + return NULL; + } + + return new_state_from_model(world, map, world->model, node->val.uri_val); +} + +LILV_API +LilvState* +lilv_state_new_from_file(LilvWorld* world, + LV2_URID_Map* map, + const LilvNode* subject, + const char* path) +{ + if (subject && !lilv_node_is_uri(subject) + && !lilv_node_is_blank(subject)) { + LILV_ERRORF("Subject `%s' is not a URI or blank node.\n", + lilv_node_as_string(subject)); + return NULL; + } + + uint8_t* uri = (uint8_t*)lilv_strjoin("file://", path, NULL); + SerdNode base = serd_node_from_string(SERD_URI, uri); + SerdEnv* env = serd_env_new(&base); + SordModel* model = sord_new(world->world, SORD_SPO, false); + SerdReader* reader = sord_new_reader(model, env, SERD_TURTLE, NULL); + + serd_reader_read_file(reader, uri); + + SordNode* subject_node = (subject) + ? subject->val.uri_val + : sord_node_from_serd_node(world->world, env, &base, NULL, NULL); + + LilvState* state = new_state_from_model(world, map, model, subject_node); + + serd_reader_free(reader); + sord_free(model); + serd_env_free(env); + free(uri); + return state; +} + +static LilvNode* +node_from_property(LilvWorld* world, const char* type, void* value, size_t size) +{ + if (!strcmp(type, NS_ATOM "String")) { + return lilv_new_string(world, (const char*)value); + } else if (!strcmp(type, NS_ATOM "Int32")) { + if (size == sizeof(int32_t)) { + return lilv_new_int(world, *(int32_t*)value); + } else { + LILV_WARNF("Int32 property <%s> has size %zu\n", type, size); + } + } else if (!strcmp(type, NS_ATOM "Float")) { + if (size == sizeof(float)) { + return lilv_new_float(world, *(float*)value); + } else { + LILV_WARNF("Float property <%s> has size %zu\n", type, size); + } + } else if (!strcmp(type, NS_ATOM "Bool")) { + if (size == sizeof(int32_t)) { + return lilv_new_bool(world, *(int32_t*)value); + } else { + LILV_WARNF("Bool property <%s> has size %zu\n", type, size); + } + } + return NULL; +} + +static void +node_to_serd(const LilvNode* node, SerdNode* value, SerdNode* type) +{ + const char* type_uri = NULL; + switch (node->type) { + case LILV_VALUE_URI: + *value = serd_node_from_string(SERD_URI, USTR(node->str_val)); + break; + case LILV_VALUE_BLANK: + *value = serd_node_from_string(SERD_BLANK, USTR(node->str_val)); + break; + default: + *value = serd_node_from_string(SERD_LITERAL, USTR(node->str_val)); + switch (node->type) { + case LILV_VALUE_BOOL: + type_uri = LILV_NS_XSD "boolean"; + break; + case LILV_VALUE_INT: + type_uri = LILV_NS_XSD "integer"; + break; + case LILV_VALUE_FLOAT: + type_uri = LILV_NS_XSD "decimal"; + break; + default: + break; + } + } + *type = (type_uri) + ? serd_node_from_string(SERD_URI, USTR(type_uri)) + : SERD_NODE_NULL; +} + +static int +add_state_to_manifest(const LilvNode* plugin_uri, + const char* manifest_path, + const char* state_uri, + const char* state_file_uri) +{ + FILE* fd = fopen((char*)manifest_path, "a"); + if (!fd) { + fprintf(stderr, "error: Failed to open %s (%s)\n", + manifest_path, strerror(errno)); + return 4; + } + + // Make path relative if it is in the same directory as manifest + const char* last_slash = strrchr(state_file_uri, '/'); + if (last_slash) { + const size_t len = last_slash - state_file_uri; + if (!strncmp(manifest_path, state_file_uri, len)) { + state_file_uri = last_slash + 1; + } + } + + SerdEnv* env = serd_env_new(NULL); + serd_env_set_prefix_from_strings(env, USTR("lv2"), USTR(LILV_NS_LV2)); + serd_env_set_prefix_from_strings(env, USTR("pset"), USTR(NS_PSET)); + serd_env_set_prefix_from_strings(env, USTR("rdfs"), USTR(LILV_NS_RDFS)); + +#if defined(HAVE_LOCKF) && defined(HAVE_FILENO) + lockf(fileno(fd), F_LOCK, 0); +#endif + + char* const manifest_uri = lilv_strjoin("file://", manifest_path, NULL); + + SerdURI base_uri; + SerdNode base = serd_node_new_uri_from_string( + (const uint8_t*)manifest_uri, NULL, &base_uri); + + SerdWriter* writer = serd_writer_new( + SERD_TURTLE, SERD_STYLE_ABBREVIATED|SERD_STYLE_CURIED, + env, &base_uri, + serd_file_sink, + fd); + + fseek(fd, 0, SEEK_END); + if (ftell(fd) == 0) { + serd_env_foreach(env, (SerdPrefixSink)serd_writer_set_prefix, writer); + } else { + fprintf(fd, "\n"); + } + + if (!state_uri) { + state_uri = state_file_uri; + } + + SerdNode s = serd_node_from_string(SERD_URI, USTR(state_uri)); + SerdNode file = serd_node_from_string(SERD_URI, USTR(state_file_uri)); + + // <state> a pset:Preset + SerdNode p = serd_node_from_string(SERD_URI, USTR(LILV_NS_RDF "type")); + SerdNode o = serd_node_from_string(SERD_CURIE, USTR("pset:Preset")); + serd_writer_write_statement(writer, 0, NULL, &s, &p, &o, NULL, NULL); + + // <state> rdfs:seeAlso <file> + p = serd_node_from_string(SERD_URI, USTR(LILV_NS_RDFS "seeAlso")); + serd_writer_write_statement(writer, 0, NULL, &s, &p, &file, NULL, NULL); + + // <state> lv2:appliesTo <plugin> + p = serd_node_from_string(SERD_URI, USTR(LILV_NS_LV2 "appliesTo")); + o = serd_node_from_string( + SERD_URI, USTR(lilv_node_as_string(plugin_uri))); + serd_writer_write_statement(writer, 0, NULL, &s, &p, &o, NULL, NULL); + + serd_writer_free(writer); + serd_node_free(&base); + +#ifdef HAVE_LOCKF + lockf(fileno(fd), F_ULOCK, 0); +#endif + + fclose(fd); + free(manifest_uri); + serd_env_free(env); + + return 0; +} + +static char* +pathify(const char* in) +{ + const size_t in_len = strlen(in); + + char* out = calloc(in_len + 1, 1); + for (size_t i = 0; i < in_len; ++i) { + char c = in[i]; + if (!((c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9'))) { + c = '-'; + } + out[i] = c; + } + return out; +} + +LILV_API +int +lilv_state_save(LilvWorld* world, + LV2_URID_Unmap* unmap, + const LilvState* state, + const char* uri, + const char* path, + const char* manifest_path) +{ + char* default_path = NULL; + char* default_manifest_path = NULL; + if (!path) { +#ifdef HAVE_MKDIR + if (!state->label) { + LILV_ERROR("Attempt to save state with no label or path.\n"); + return 1; + } + + const char* const home = getenv("HOME"); + if (!home) { + fprintf(stderr, "error: $HOME is undefined\n"); + return 2; + } + + // Create ~/.lv2/ + char* const lv2dir = lilv_strjoin(home, "/.lv2/", NULL); + if (mkdir(lv2dir, 0755) && errno != EEXIST) { + fprintf(stderr, "error: Unable to create %s (%s)\n", + lv2dir, strerror(errno)); + free(lv2dir); + return 3; + } + + // Create ~/.lv2/presets.lv2/ + char* const bundle = lilv_strjoin(lv2dir, "presets.lv2/", NULL); + if (mkdir(bundle, 0755) && errno != EEXIST) { + fprintf(stderr, "error: Unable to create %s (%s)\n", + lv2dir, strerror(errno)); + free(lv2dir); + free(bundle); + return 4; + } + + char* const filename = pathify(state->label); + default_path = lilv_strjoin(bundle, filename, ".ttl", NULL); + default_manifest_path = lilv_strjoin(bundle, "manifest.ttl", NULL); + + path = default_path; + manifest_path = default_manifest_path; + + free(lv2dir); + free(bundle); + free(filename); +#else + LILV_ERROR("Save to default state path but mkdir is unavailable.\n"); + return 1; +#endif + } + + FILE* fd = fopen(path, "w"); + if (!fd) { + fprintf(stderr, "error: Failed to open %s (%s)\n", + path, strerror(errno)); + free(default_path); + free(default_manifest_path); + return 4; + } + + SerdEnv* env = serd_env_new(NULL); + serd_env_set_prefix_from_strings(env, USTR("lv2"), USTR(LILV_NS_LV2)); + serd_env_set_prefix_from_strings(env, USTR("pset"), USTR(NS_PSET)); + serd_env_set_prefix_from_strings(env, USTR("rdfs"), USTR(LILV_NS_RDFS)); + serd_env_set_prefix_from_strings(env, USTR("state"), USTR(NS_STATE)); + + SerdNode lv2_appliesTo = serd_node_from_string( + SERD_CURIE, USTR("lv2:appliesTo")); + + const SerdNode* plugin_uri = sord_node_to_serd_node( + state->plugin_uri->val.uri_val); + + SerdNode subject = serd_node_from_string(SERD_URI, USTR(uri ? uri : "")); + + SerdWriter* writer = serd_writer_new( + SERD_TURTLE, + SERD_STYLE_ABBREVIATED|SERD_STYLE_CURIED, + env, + &SERD_URI_NULL, + serd_file_sink, + fd); + + serd_env_foreach(env, (SerdPrefixSink)serd_writer_set_prefix, writer); + + // <subject> a pset:Preset + SerdNode p = serd_node_from_string(SERD_URI, USTR(LILV_NS_RDF "type")); + SerdNode o = serd_node_from_string(SERD_CURIE, USTR("pset:Preset")); + serd_writer_write_statement(writer, 0, NULL, + &subject, &p, &o, NULL, NULL); + + // <subject> lv2:appliesTo <http://example.org/plugin> + serd_writer_write_statement(writer, 0, NULL, + &subject, + &lv2_appliesTo, + plugin_uri, NULL, NULL); + + // <subject> rdfs:label label + if (state->label) { + p = serd_node_from_string(SERD_URI, USTR(LILV_NS_RDFS "label")); + o = serd_node_from_string(SERD_LITERAL, USTR(state->label)); + serd_writer_write_statement(writer, 0, + NULL, &subject, &p, &o, NULL, NULL); + } + + // Save port values + for (uint32_t i = 0; i < state->num_values; ++i) { + PortValue* const value = &state->values[i]; + + const SerdNode port = serd_node_from_string( + SERD_BLANK, USTR(value->symbol)); + + // <> lv2:port _:symbol + p = serd_node_from_string(SERD_URI, USTR(LILV_NS_LV2 "port")); + serd_writer_write_statement(writer, SERD_ANON_O_BEGIN, + NULL, &subject, &p, &port, NULL, NULL); + + // _:symbol lv2:symbol "symbol" + p = serd_node_from_string(SERD_URI, USTR(LILV_NS_LV2 "symbol")); + o = serd_node_from_string(SERD_LITERAL, USTR(value->symbol)); + serd_writer_write_statement(writer, SERD_ANON_CONT, + NULL, &port, &p, &o, NULL, NULL); + + // _:symbol pset:value value + p = serd_node_from_string(SERD_URI, USTR(NS_PSET "value")); + SerdNode t; + node_to_serd(value->value, &o, &t); + serd_writer_write_statement(writer, SERD_ANON_CONT, NULL, + &port, &p, &o, &t, NULL); + + serd_writer_end_anon(writer, &port); + } + + // Save properties + const SerdNode state_node = serd_node_from_string(SERD_BLANK, + USTR("2state")); + if (state->num_props > 0) { + p = serd_node_from_string(SERD_URI, USTR(NS_STATE "state")); + serd_writer_write_statement(writer, SERD_ANON_O_BEGIN, NULL, + &subject, &p, &state_node, NULL, NULL); + + } + for (uint32_t i = 0; i < state->num_props; ++i) { + Property* prop = &state->props[i]; + const char* key = unmap->unmap(unmap->handle, prop->key); + const char* type = unmap->unmap(unmap->handle, prop->type); + if (!key) { + LILV_WARNF("Failed to unmap property key `%d'\n", prop->key); + } else if (!type) { + LILV_WARNF("Failed to unmap property type `%d'\n", prop->type); + } else if (!(prop->flags & LV2_STATE_IS_PORTABLE)) { + LILV_WARNF("Unable to save non-portable property <%s>\n", type); + } else { + LilvNode* const node = node_from_property( + world, type, prop->value, prop->size); + if (node) { + p = serd_node_from_string(SERD_URI, USTR(key)); + SerdNode t; + node_to_serd(node, &o, &t); + serd_writer_write_statement( + writer, SERD_ANON_CONT, NULL, + &state_node, &p, &o, &t, NULL); + } else { + LILV_WARNF("Unable to save property type <%s>\n", type); + } + } + } + if (state->num_props > 0) { + serd_writer_end_anon(writer, &state_node); + } + + // Close state file and clean up Serd + serd_writer_free(writer); + fclose(fd); + serd_env_free(env); + + if (manifest_path) { + add_state_to_manifest( + state->plugin_uri, manifest_path, uri, path); + } + + free(default_path); + free(default_manifest_path); + return 0; +} + +LILV_API +void +lilv_state_free(LilvState* state) +{ + if (state) { + lilv_node_free(state->plugin_uri); + free(state->props); + free(state->values); + free(state->label); + free(state); + } +} diff --git a/src/world.c b/src/world.c index ddcf5d1..af20bc3 100644 --- a/src/world.c +++ b/src/world.c @@ -14,7 +14,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -#define _POSIX_SOURCE 1 +#define _POSIX_SOURCE 1 /* for wordexp */ #include <assert.h> #include <errno.h> @@ -22,12 +22,13 @@ #include <string.h> #include <dirent.h> -#ifdef HAVE_WORDEXP -#include <wordexp.h> -#endif #include "lilv_internal.h" +#ifdef HAVE_WORDEXP +# include <wordexp.h> +#endif + LILV_API LilvWorld* lilv_world_new(void) @@ -48,14 +49,16 @@ lilv_world_new(void) world->loaded_files = zix_tree_new( false, lilv_resource_node_cmp, NULL, (ZixDestroyFunc)lilv_node_free); -#define NS_DYNMAN "http://lv2plug.in/ns/ext/dynmanifest#" #define NS_DCTERMS "http://purl.org/dc/terms/" +#define NS_DYNMAN "http://lv2plug.in/ns/ext/dynmanifest#" +#define NS_PSET "http://lv2plug.in/ns/ext/presets#" #define NEW_URI(uri) sord_new_uri(world->world, (const uint8_t*)uri) #define NEW_URI_VAL(uri) lilv_new_uri(world, (const char*)(uri)); world->dc_replaces_node = NEW_URI(NS_DCTERMS "replaces"); world->dyn_manifest_node = NEW_URI(NS_DYNMAN "DynManifest"); + world->lv2_appliesTo_node = NEW_URI(LILV_NS_LV2 "appliesTo"); world->lv2_binary_node = NEW_URI(LILV_NS_LV2 "binary"); world->lv2_default_node = NEW_URI(LILV_NS_LV2 "default"); world->lv2_index_node = NEW_URI(LILV_NS_LV2 "index"); @@ -67,6 +70,7 @@ lilv_world_new(void) world->lv2_reportslatency_node = NEW_URI(LILV_NS_LV2 "reportsLatency"); world->lv2_specification_node = NEW_URI(LILV_NS_LV2 "Specification"); world->lv2_symbol_node = NEW_URI(LILV_NS_LV2 "symbol"); + world->pset_value_node = NEW_URI(NS_PSET "value"); world->rdf_a_node = NEW_URI(LILV_NS_RDF "type"); world->rdf_value_node = NEW_URI(LILV_NS_RDF "value"); world->rdfs_class_node = NEW_URI(LILV_NS_RDFS "Class"); @@ -79,7 +83,6 @@ lilv_world_new(void) world->xsd_integer_node = NEW_URI(LILV_NS_XSD "integer"); world->doap_name_val = NEW_URI_VAL(LILV_NS_DOAP "name"); - world->lv2_applies_to_val = NEW_URI_VAL(LILV_NS_LV2 "appliesTo"); world->lv2_extensionData_val = NEW_URI_VAL(LILV_NS_LV2 "extensionData"); world->lv2_name_val = NEW_URI_VAL(LILV_NS_LV2 "name"); world->lv2_optionalFeature_val = NEW_URI_VAL(LILV_NS_LV2 "optionalFeature"); @@ -113,6 +116,7 @@ lilv_world_free(LilvWorld* world) sord_node_free(world->world, world->dc_replaces_node); sord_node_free(world->world, world->dyn_manifest_node); + sord_node_free(world->world, world->lv2_appliesTo_node); sord_node_free(world->world, world->lv2_binary_node); sord_node_free(world->world, world->lv2_default_node); sord_node_free(world->world, world->lv2_index_node); @@ -124,6 +128,7 @@ lilv_world_free(LilvWorld* world) sord_node_free(world->world, world->lv2_reportslatency_node); sord_node_free(world->world, world->lv2_specification_node); sord_node_free(world->world, world->lv2_symbol_node); + sord_node_free(world->world, world->pset_value_node); sord_node_free(world->world, world->rdf_a_node); sord_node_free(world->world, world->rdf_value_node); sord_node_free(world->world, world->rdfs_class_node); @@ -135,7 +140,6 @@ lilv_world_free(LilvWorld* world) sord_node_free(world->world, world->xsd_double_node); sord_node_free(world->world, world->xsd_integer_node); lilv_node_free(world->doap_name_val); - lilv_node_free(world->lv2_applies_to_val); lilv_node_free(world->lv2_extensionData_val); lilv_node_free(world->lv2_name_val); lilv_node_free(world->lv2_optionalFeature_val); @@ -585,7 +589,7 @@ expand(const char* path) wordexp_t p; if (wordexp(path, &p, 0)) { LILV_ERRORF("Error expanding path `%s'\n", path); - return lilv_strdup (path); + return lilv_strdup(path); } if (p.we_wordc == 0) { /* Literal directory path (e.g. no variables or ~) */ diff --git a/src/zix/tree.c b/src/zix/tree.c index 1be9227..1189230 100644 --- a/src/zix/tree.c +++ b/src/zix/tree.c @@ -461,7 +461,7 @@ ZIX_API void* zix_tree_get(ZixTreeIter* ti) { - return ti->data; + return ti ? ti->data : NULL; } ZIX_API @@ -8,7 +8,7 @@ import waflib.Options as Options import waflib.Logs as Logs # Version of this package (even if built as a child) -LILV_VERSION = '0.8.0' +LILV_VERSION = '0.9.0' LILV_MAJOR_VERSION = '0' # Library version (UNIX style major, minor, micro) @@ -65,19 +65,40 @@ def configure(conf): conf.line_just = 51 autowaf.configure(conf) autowaf.display_header('Lilv Configuration') + conf.env.append_unique('CFLAGS', '-std=c99') autowaf.check_pkg(conf, 'lv2core', uselib_store='LV2CORE', mandatory=True) autowaf.check_pkg(conf, 'serd-0', uselib_store='SERD', atleast_version='0.7.0', mandatory=True) autowaf.check_pkg(conf, 'sord-0', uselib_store='SORD', atleast_version='0.5.0', mandatory=True) + autowaf.check_pkg(conf, 'lv2-lv2plug.in-ns-ext-state', + uselib_store='LV2_STATE', mandatory=True) + + conf.check_cc(function_name='wordexp', + header_name='wordexp.h', + defines='_POSIX_SOURCE', + define_name='HAVE_WORDEXP', + mandatory=False) + + conf.check_cc(function_name='lockf', + header_name='unistd.h', + defines='_BSD_SOURCE', + define_name='HAVE_LOCKF', + mandatory=False) + + conf.check_cc(function_name='fileno', + header_name='stdio.h', + defines='_POSIX_SOURCE', + define_name='HAVE_FILENO', + mandatory=False) + + conf.check_cc(function_name='mkdir', + header_name=['sys/stat.h','sys/types.h'], + defines='_POSIX_SOURCE', + define_name='HAVE_MKDIR', + mandatory=False) - conf.check(function_name='wordexp', - header_name='wordexp.h', - define_name='HAVE_WORDEXP', - mandatory=False) - - conf.env.append_unique('CFLAGS', '-std=c99') autowaf.define(conf, 'LILV_VERSION', LILV_VERSION) if Options.options.dyn_manifest: autowaf.define(conf, 'LILV_DYN_MANIFEST', 1) @@ -131,6 +152,8 @@ def configure(conf): bool(conf.env['BUILD_TESTS'])) autowaf.display_msg(conf, "Dynamic manifest support", bool(conf.env['LILV_DYN_MANIFEST'])) + autowaf.display_msg(conf, "State/Preset support", + conf.is_defined('HAVE_LV2_STATE')) autowaf.display_msg(conf, "Python bindings", conf.is_defined('LILV_PYTHON')) print('') @@ -154,6 +177,7 @@ def build(bld): src/port.c src/query.c src/scalepoint.c + src/state.c src/ui.c src/util.c src/world.c |