diff options
Diffstat (limited to 'plugins')
52 files changed, 5772 insertions, 0 deletions
diff --git a/plugins/README.txt b/plugins/README.txt new file mode 100644 index 0000000..361460d --- /dev/null +++ b/plugins/README.txt @@ -0,0 +1,26 @@ += Programming LV2 Plugins = +David Robillard <d@drobilla.net> +:Author Initials: DER +:toc: +:website: http://lv2plug.in/ +:doctype: book + +== Introduction == + +This is a series of well-documented example plugins that demonstrate the various features of LV2. +Starting with the most basic plugin possible, +each adds new functionality and explains the features used from a high level perspective. + +API and vocabulary reference documentation explains details, +but not the ``big picture''. +This book is intended to complement the reference documentation by providing good reference implementations of plugins, +while also conveying a higher-level understanding of LV2. + +The chapters/plugins are arranged so that each builds incrementally on its predecessor. +Reading this book front to back is a good way to become familiar with modern LV2 programming. +The reader is expected to be familiar with C, but otherwise no special knowledge is required; +the first plugin describes the basics in detail. + +This book is compiled from plugin source code into a single document for pleasant reading and ease of reference. +Each chapter corresponds to executable plugin code which can be found in the +plugins+ directory of the LV2 distribution. +If you prefer to read actual source code, all the content here is also available in the source code as comments. diff --git a/plugins/eg-amp.lv2/README.txt b/plugins/eg-amp.lv2/README.txt new file mode 100644 index 0000000..41683d3 --- /dev/null +++ b/plugins/eg-amp.lv2/README.txt @@ -0,0 +1,19 @@ +== Simple Amplifier == + +This plugin is a simple example of a basic LV2 plugin with no additional features. +It has audio ports which contain an array of `float`, +and a control port which contains a single `float`. + +LV2 plugins are defined in two parts: code and data. +The code is written in C, or any C compatible language such as C++. +Static data is described separately in the human and machine friendly http://www.w3.org/TeamSubmission/turtle/[Turtle] syntax. + +Generally, the goal is to keep code minimal, +and describe as much as possible in the static data. +There are several advantages to this approach: + + * Hosts can discover and inspect plugins without loading or executing any plugin code. + * Plugin data can be used from a wide range of generic tools like scripting languages and command line utilities. + * The standard data model allows the use of existing vocabularies to describe plugins and related information. + * The language is extensible, so authors may describe any data without requiring changes to the LV2 specification. + * Labels and documentation are translatable, and available to hosts for display in user interfaces. diff --git a/plugins/eg-amp.lv2/amp.c b/plugins/eg-amp.lv2/amp.c new file mode 100644 index 0000000..4aef8d4 --- /dev/null +++ b/plugins/eg-amp.lv2/amp.c @@ -0,0 +1,229 @@ +/* + Copyright 2006-2016 David Robillard <d@drobilla.net> + Copyright 2006 Steve Harris <steve@plugin.org.uk> + + 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 standard C headers */ +#include <math.h> +#include <stdlib.h> + +/** + LV2 headers are based on the URI of the specification they come from, so a + consistent convention can be used even for unofficial extensions. The URI + of the core LV2 specification is <http://lv2plug.in/ns/lv2core>, by + replacing `http:/` with `lv2` any header in the specification bundle can be + included, in this case `lv2.h`. +*/ +#include "lv2/lv2plug.in/ns/lv2core/lv2.h" + +/** + The URI is the identifier for a plugin, and how the host associates this + implementation in code with its description in data. In this plugin it is + only used once in the code, but defining the plugin URI at the top of the + file is a good convention to follow. If this URI does not match that used + in the data files, the host will fail to load the plugin. +*/ +#define AMP_URI "http://lv2plug.in/plugins/eg-amp" + +/** + In code, ports are referred to by index. An enumeration of port indices + should be defined for readability. +*/ +typedef enum { + AMP_GAIN = 0, + AMP_INPUT = 1, + AMP_OUTPUT = 2 +} PortIndex; + +/** + Every plugin defines a private structure for the plugin instance. All data + associated with a plugin instance is stored here, and is available to + every instance method. In this simple plugin, only port buffers need to be + stored, since there is no additional instance data. +*/ +typedef struct { + // Port buffers + const float* gain; + const float* input; + float* output; +} Amp; + +/** + The `instantiate()` function is called by the host to create a new plugin + instance. The host passes the plugin descriptor, sample rate, and bundle + path for plugins that need to load additional resources (e.g. waveforms). + The features parameter contains host-provided features defined in LV2 + extensions, but this simple plugin does not use any. + + This function is in the ``instantiation'' threading class, so no other + methods on this instance will be called concurrently with it. +*/ +static LV2_Handle +instantiate(const LV2_Descriptor* descriptor, + double rate, + const char* bundle_path, + const LV2_Feature* const* features) +{ + Amp* amp = (Amp*)calloc(1, sizeof(Amp)); + + return (LV2_Handle)amp; +} + +/** + The `connect_port()` method is called by the host to connect a particular + port to a buffer. The plugin must store the data location, but data may not + be accessed except in run(). + + This method is in the ``audio'' threading class, and is called in the same + context as run(). +*/ +static void +connect_port(LV2_Handle instance, + uint32_t port, + void* data) +{ + Amp* amp = (Amp*)instance; + + switch ((PortIndex)port) { + case AMP_GAIN: + amp->gain = (const float*)data; + break; + case AMP_INPUT: + amp->input = (const float*)data; + break; + case AMP_OUTPUT: + amp->output = (float*)data; + break; + } +} + +/** + The `activate()` method is called by the host to initialise and prepare the + plugin instance for running. The plugin must reset all internal state + except for buffer locations set by `connect_port()`. Since this plugin has + no other internal state, this method does nothing. + + This method is in the ``instantiation'' threading class, so no other + methods on this instance will be called concurrently with it. +*/ +static void +activate(LV2_Handle instance) +{ +} + +/** Define a macro for converting a gain in dB to a coefficient. */ +#define DB_CO(g) ((g) > -90.0f ? powf(10.0f, (g) * 0.05f) : 0.0f) + +/** + The `run()` method is the main process function of the plugin. It processes + a block of audio in the audio context. Since this plugin is + `lv2:hardRTCapable`, `run()` must be real-time safe, so blocking (e.g. with + a mutex) or memory allocation are not allowed. +*/ +static void +run(LV2_Handle instance, uint32_t n_samples) +{ + const Amp* amp = (const Amp*)instance; + + const float gain = *(amp->gain); + const float* const input = amp->input; + float* const output = amp->output; + + const float coef = DB_CO(gain); + + for (uint32_t pos = 0; pos < n_samples; pos++) { + output[pos] = input[pos] * coef; + } +} + +/** + The `deactivate()` method is the counterpart to `activate()`, and is called by + the host after running the plugin. It indicates that the host will not call + `run()` again until another call to `activate()` and is mainly useful for more + advanced plugins with ``live'' characteristics such as those with auxiliary + processing threads. As with `activate()`, this plugin has no use for this + information so this method does nothing. + + This method is in the ``instantiation'' threading class, so no other + methods on this instance will be called concurrently with it. +*/ +static void +deactivate(LV2_Handle instance) +{ +} + +/** + Destroy a plugin instance (counterpart to `instantiate()`). + + This method is in the ``instantiation'' threading class, so no other + methods on this instance will be called concurrently with it. +*/ +static void +cleanup(LV2_Handle instance) +{ + free(instance); +} + +/** + The `extension_data()` function returns any extension data supported by the + plugin. Note that this is not an instance method, but a function on the + plugin descriptor. It is usually used by plugins to implement additional + interfaces. This plugin does not have any extension data, so this function + returns NULL. + + This method is in the ``discovery'' threading class, so no other functions + or methods in this plugin library will be called concurrently with it. +*/ +static const void* +extension_data(const char* uri) +{ + return NULL; +} + +/** + Every plugin must define an `LV2_Descriptor`. It is best to define + descriptors statically to avoid leaking memory and non-portable shared + library constructors and destructors to clean up properly. +*/ +static const LV2_Descriptor descriptor = { + AMP_URI, + instantiate, + connect_port, + activate, + run, + deactivate, + cleanup, + extension_data +}; + +/** + The `lv2_descriptor()` function is the entry point to the plugin library. The + host will load the library and call this function repeatedly with increasing + indices to find all the plugins defined in the library. The index is not an + indentifier, the URI of the returned descriptor is used to determine the + identify of the plugin. + + This method is in the ``discovery'' threading class, so no other functions + or methods in this plugin library will be called concurrently with it. +*/ +LV2_SYMBOL_EXPORT +const LV2_Descriptor* +lv2_descriptor(uint32_t index) +{ + switch (index) { + case 0: return &descriptor; + default: return NULL; + } +} diff --git a/plugins/eg-amp.lv2/amp.ttl b/plugins/eg-amp.lv2/amp.ttl new file mode 100644 index 0000000..9f522a1 --- /dev/null +++ b/plugins/eg-amp.lv2/amp.ttl @@ -0,0 +1,90 @@ +# The full description of the plugin is in this file, which is linked to from +# `manifest.ttl`. This is done so the host only needs to scan the relatively +# small `manifest.ttl` files to quickly discover all plugins. + +@prefix doap: <http://usefulinc.com/ns/doap#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix units: <http://lv2plug.in/ns/extensions/units#> . + +# First the type of the plugin is described. All plugins must explicitly list +# `lv2:Plugin` as a type. A more specific type should also be given, where +# applicable, so hosts can present a nicer UI for loading plugins. Note that +# this URI is the identifier of the plugin, so if it does not match the one in +# `manifest.ttl`, the host will not discover the plugin data at all. +<http://lv2plug.in/plugins/eg-amp> + a lv2:Plugin , + lv2:AmplifierPlugin ; +# Plugins are associated with a project, where common information like +# developers, home page, and so on are described. This plugin is part of the +# LV2 project, which has URI <http://lv2plug.in/ns/lv2>, and is described +# elsewhere. Typical plugin collections will describe the project in +# manifest.ttl + lv2:project <http://lv2plug.in/ns/lv2> ; +# Every plugin must have a name, described with the doap:name property. +# Translations to various languages can be added by putting a language tag +# after strings as shown. + doap:name "Simple Amplifier" , + "简单放大器"@zh , + "Einfacher Verstärker"@de , + "Simple Amplifier"@en-gb , + "Amplificador Simple"@es , + "Amplificateur de Base"@fr , + "Amplificatore Semplice"@it , + "簡単なアンプ"@jp , + "Просто Усилитель"@ru ; + doap:license <http://opensource.org/licenses/isc> ; + lv2:optionalFeature lv2:hardRTCapable ; + lv2:port [ +# Every port must have at least two types, one that specifies direction +# (lv2:InputPort or lv2:OutputPort), and another to describe the data type. +# This port is a lv2:ControlPort, which means it contains a single float. + a lv2:InputPort , + lv2:ControlPort ; + lv2:index 0 ; + lv2:symbol "gain" ; + lv2:name "Gain" , + "收益"@zh , + "Verstärkung"@de , + "Gain"@en-gb , + "Aumento"@es , + "Gain"@fr , + "Guadagno"@it , + "利益"@jp , + "Увеличение"@ru ; +# An lv2:ControlPort should always describe its default value, and usually a +# minimum and maximum value. Defining a range is not strictly required, but +# should be done wherever possible to aid host support, particularly for UIs. + lv2:default 0.0 ; + lv2:minimum -90.0 ; + lv2:maximum 24.0 ; +# Ports can describe units and control detents to allow better UI generation +# and host automation. + units:unit units:db ; + lv2:scalePoint [ + rdfs:label "+5" ; + rdf:value 5.0 + ] , [ + rdfs:label "0" ; + rdf:value 0.0 + ] , [ + rdfs:label "-5" ; + rdf:value -5.0 + ] , [ + rdfs:label "-10" ; + rdf:value -10.0 + ] + ] , [ + a lv2:AudioPort , + lv2:InputPort ; + lv2:index 1 ; + lv2:symbol "in" ; + lv2:name "In" + ] , [ + a lv2:AudioPort , + lv2:OutputPort ; + lv2:index 2 ; + lv2:symbol "out" ; + lv2:name "Out" + ] . diff --git a/plugins/eg-amp.lv2/manifest.ttl.in b/plugins/eg-amp.lv2/manifest.ttl.in new file mode 100644 index 0000000..4a22f95 --- /dev/null +++ b/plugins/eg-amp.lv2/manifest.ttl.in @@ -0,0 +1,68 @@ +# LV2 plugins are installed in a ``bundle'', a directory with a standard +# structure. Each bundle has a Turtle file named `manifest.ttl` which lists +# the contents of the bundle. +# +# Hosts typically read the manifest of every installed bundle to discover +# plugins on start-up, so it should be as small as possible for performance +# reasons. Details that are only useful if the host chooses to load the plugin +# are stored in other files and linked to from `manifest.ttl`. +# +# ==== URIs ==== +# +# LV2 makes use of URIs as globally-unique identifiers for resources. For +# example, the ID of the plugin described here is +# `<http://lv2plug.in/plugins/eg-amp>`. Note that URIs are only used as +# identifiers and don't necessarily imply that something can be accessed at +# that address on the web (though that may be the case). +# +# ==== Namespace Prefixes ==== +# +# Turtle files contain many URIs, but prefixes can be defined to improve +# readability. For example, with the `lv2:` prefix below, `lv2:Plugin` can be +# written instead of `<http://lv2plug.in/ns/lv2core#Plugin>`. + +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . + +# ==== Describing a Plugin ==== + +# Turtle files contain a set of ``statements'' which describe resources. +# This file contains 3 statements: +# [options="header"] +# |================================================================ +# | Subject | Predicate | Object +# | <http://lv2plug.in/plugins/eg-amp> | a | lv2:Plugin +# | <http://lv2plug.in/plugins/eg-amp> | lv2:binary | <amp.so> +# | <http://lv2plug.in/plugins/eg-amp> | rdfs:seeAlso | <amp.ttl> +# |================================================================ + +# Firstly, `<http://lv2plug.in/plugins/eg-amp>` is an LV2 plugin: +<http://lv2plug.in/plugins/eg-amp> a lv2:Plugin . + +# The predicate ```a`'' is a Turtle shorthand for `rdf:type`. + +# The binary of that plugin can be found at `<amp.ext>`: +<http://lv2plug.in/plugins/eg-amp> lv2:binary <amp@LIB_EXT@> . + +# This file is a template; the token `@LIB_EXT@` is replaced by the build +# system with the appropriate extension for the current platform before +# installation. For example, in the output `manifest.ttl`, the binary would be +# listed as `<amp.so>`. Relative URIs in manifests are relative to the bundle +# directory, so this refers to a binary with the given name in the same +# directory as this manifest. + +# Finally, more information about this plugin can be found in `<amp.ttl>`: +<http://lv2plug.in/plugins/eg-amp> rdfs:seeAlso <amp.ttl> . + +# ==== Abbreviation ==== +# +# This file shows these statements individually for instructive purposes, but +# the subject `<http://lv2plug.in/plugins/eg-amp>` is repetitive. Turtle +# allows the semicolon to be used as a delimiter that repeats the previous +# subject. For example, this manifest would more realistically be written like +# so: + +<http://lv2plug.in/plugins/eg-amp> + a lv2:Plugin ; + lv2:binary <amp@LIB_EXT@> ; + rdfs:seeAlso <amp.ttl> . diff --git a/plugins/eg-amp.lv2/waf b/plugins/eg-amp.lv2/waf new file mode 120000 index 0000000..59a1ac9 --- /dev/null +++ b/plugins/eg-amp.lv2/waf @@ -0,0 +1 @@ +../../waf
\ No newline at end of file diff --git a/plugins/eg-amp.lv2/wscript b/plugins/eg-amp.lv2/wscript new file mode 100644 index 0000000..04efb0a --- /dev/null +++ b/plugins/eg-amp.lv2/wscript @@ -0,0 +1,67 @@ +#!/usr/bin/env python +from waflib.extras import autowaf as autowaf +import re + +# Variables for 'waf dist' +APPNAME = 'eg-amp.lv2' +VERSION = '1.0.0' + +# Mandatory variables +top = '.' +out = 'build' + +def options(opt): + opt.load('compiler_c') + opt.load('lv2') + autowaf.set_options(opt) + +def configure(conf): + autowaf.display_header('Amp Configuration') + conf.load('compiler_c', cache=True) + conf.load('lv2', cache=True) + conf.load('autowaf', cache=True) + + if not autowaf.is_child(): + autowaf.check_pkg(conf, 'lv2', uselib_store='LV2') + + conf.check(features='c cshlib', lib='m', uselib_store='M', mandatory=False) + + autowaf.display_msg(conf, 'LV2 bundle directory', conf.env.LV2DIR) + print('') + +def build(bld): + bundle = 'eg-amp.lv2' + + # Make a pattern for shared objects without the 'lib' prefix + module_pat = re.sub('^lib', '', bld.env.cshlib_PATTERN) + module_ext = module_pat[module_pat.rfind('.'):] + + # Build manifest.ttl by substitution (for portable lib extension) + bld(features = 'subst', + source = 'manifest.ttl.in', + target = '%s/%s' % (bundle, 'manifest.ttl'), + install_path = '${LV2DIR}/%s' % bundle, + LIB_EXT = module_ext) + + # Copy other data files to build bundle (build/eg-amp.lv2) + for i in ['amp.ttl']: + bld(features = 'subst', + is_copy = True, + source = i, + target = '%s/%s' % (bundle, i), + install_path = '${LV2DIR}/%s' % bundle) + + # Use LV2 headers from parent directory if building as a sub-project + includes = None + if autowaf.is_child: + includes = '../..' + + # Build plugin library + obj = bld(features = 'c cshlib', + source = 'amp.c', + name = 'amp', + target = '%s/amp' % bundle, + install_path = '${LV2DIR}/%s' % bundle, + uselib = 'M LV2', + includes = includes) + obj.env.cshlib_PATTERN = module_pat diff --git a/plugins/eg-fifths.lv2/README.txt b/plugins/eg-fifths.lv2/README.txt new file mode 100644 index 0000000..2154321 --- /dev/null +++ b/plugins/eg-fifths.lv2/README.txt @@ -0,0 +1,3 @@ +== Fifths == + +This plugin demonstrates simple MIDI event reading and writing. diff --git a/plugins/eg-fifths.lv2/fifths.c b/plugins/eg-fifths.lv2/fifths.c new file mode 100644 index 0000000..0141fa2 --- /dev/null +++ b/plugins/eg-fifths.lv2/fifths.c @@ -0,0 +1,196 @@ +/* + LV2 Fifths Example Plugin + Copyright 2014-2016 David Robillard <d@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. +*/ + +#include <math.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#ifndef __cplusplus +# include <stdbool.h> +#endif + +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "lv2/lv2plug.in/ns/ext/log/logger.h" +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" +#include "lv2/lv2plug.in/ns/ext/patch/patch.h" +#include "lv2/lv2plug.in/ns/ext/state/state.h" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2_util.h" + +#include "./uris.h" + +enum { + FIFTHS_IN = 0, + FIFTHS_OUT = 1 +}; + +typedef struct { + // Features + LV2_URID_Map* map; + LV2_Log_Logger logger; + + // Ports + const LV2_Atom_Sequence* in_port; + LV2_Atom_Sequence* out_port; + + // URIs + FifthsURIs uris; +} Fifths; + +static void +connect_port(LV2_Handle instance, + uint32_t port, + void* data) +{ + Fifths* self = (Fifths*)instance; + switch (port) { + case FIFTHS_IN: + self->in_port = (const LV2_Atom_Sequence*)data; + break; + case FIFTHS_OUT: + self->out_port = (LV2_Atom_Sequence*)data; + break; + default: + break; + } +} + +static LV2_Handle +instantiate(const LV2_Descriptor* descriptor, + double rate, + const char* path, + const LV2_Feature* const* features) +{ + // Allocate and initialise instance structure. + Fifths* self = (Fifths*)calloc(1, sizeof(Fifths)); + if (!self) { + return NULL; + } + + // Scan host features for URID map + const char* missing = lv2_features_query( + features, + LV2_LOG__log, &self->logger.log, false, + LV2_URID__map, &self->map, true, + NULL); + lv2_log_logger_set_map(&self->logger, self->map); + if (missing) { + lv2_log_error(&self->logger, "Missing feature <%s>\n", missing); + free(self); + return NULL; + } + + map_fifths_uris(self->map, &self->uris); + + return (LV2_Handle)self; +} + +static void +cleanup(LV2_Handle instance) +{ + free(instance); +} + +static void +run(LV2_Handle instance, + uint32_t sample_count) +{ + Fifths* self = (Fifths*)instance; + FifthsURIs* uris = &self->uris; + + // Struct for a 3 byte MIDI event, used for writing notes + typedef struct { + LV2_Atom_Event event; + uint8_t msg[3]; + } MIDINoteEvent; + + // Initially self->out_port contains a Chunk with size set to capacity + + // Get the capacity + const uint32_t out_capacity = self->out_port->atom.size; + + // Write an empty Sequence header to the output + lv2_atom_sequence_clear(self->out_port); + self->out_port->atom.type = self->in_port->atom.type; + + // Read incoming events + LV2_ATOM_SEQUENCE_FOREACH(self->in_port, ev) { + if (ev->body.type == uris->midi_Event) { + const uint8_t* const msg = (const uint8_t*)(ev + 1); + switch (lv2_midi_message_type(msg)) { + case LV2_MIDI_MSG_NOTE_ON: + case LV2_MIDI_MSG_NOTE_OFF: + // Forward note to output + lv2_atom_sequence_append_event( + self->out_port, out_capacity, ev); + + if (msg[1] <= 127 - 7) { + // Make a note one 5th (7 semitones) higher than input + MIDINoteEvent fifth; + + // Could simply do fifth.event = *ev here instead... + fifth.event.time.frames = ev->time.frames; // Same time + fifth.event.body.type = ev->body.type; // Same type + fifth.event.body.size = ev->body.size; // Same size + + fifth.msg[0] = msg[0]; // Same status + fifth.msg[1] = msg[1] + 7; // Pitch up 7 semitones + fifth.msg[2] = msg[2]; // Same velocity + + // Write 5th event + lv2_atom_sequence_append_event( + self->out_port, out_capacity, &fifth.event); + } + break; + default: + // Forward all other MIDI events directly + lv2_atom_sequence_append_event( + self->out_port, out_capacity, ev); + break; + } + } + } +} + +static const void* +extension_data(const char* uri) +{ + return NULL; +} + +static const LV2_Descriptor descriptor = { + EG_FIFTHS_URI, + instantiate, + connect_port, + NULL, // activate, + run, + NULL, // 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/plugins/eg-fifths.lv2/fifths.ttl b/plugins/eg-fifths.lv2/fifths.ttl new file mode 100644 index 0000000..7f58a33 --- /dev/null +++ b/plugins/eg-fifths.lv2/fifths.ttl @@ -0,0 +1,30 @@ +@prefix atom: <http://lv2plug.in/ns/ext/atom#> . +@prefix doap: <http://usefulinc.com/ns/doap#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix urid: <http://lv2plug.in/ns/ext/urid#> . +@prefix midi: <http://lv2plug.in/ns/ext/midi#> . + +<http://lv2plug.in/plugins/eg-fifths> + a lv2:Plugin ; + doap:name "Example Fifths" ; + doap:license <http://opensource.org/licenses/isc> ; + lv2:project <http://lv2plug.in/ns/lv2> ; + lv2:requiredFeature urid:map ; + lv2:optionalFeature lv2:hardRTCapable ; + lv2:port [ + a lv2:InputPort , + atom:AtomPort ; + atom:bufferType atom:Sequence ; + atom:supports midi:MidiEvent ; + lv2:index 0 ; + lv2:symbol "in" ; + lv2:name "In" + ] , [ + a lv2:OutputPort , + atom:AtomPort ; + atom:bufferType atom:Sequence ; + atom:supports midi:MidiEvent ; + lv2:index 1 ; + lv2:symbol "out" ; + lv2:name "Out" + ] . diff --git a/plugins/eg-fifths.lv2/manifest.ttl.in b/plugins/eg-fifths.lv2/manifest.ttl.in new file mode 100644 index 0000000..f87f2c1 --- /dev/null +++ b/plugins/eg-fifths.lv2/manifest.ttl.in @@ -0,0 +1,8 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix ui: <http://lv2plug.in/ns/extensions/ui#> . + +<http://lv2plug.in/plugins/eg-fifths> + a lv2:Plugin ; + lv2:binary <fifths@LIB_EXT@> ; + rdfs:seeAlso <fifths.ttl> . diff --git a/plugins/eg-fifths.lv2/uris.h b/plugins/eg-fifths.lv2/uris.h new file mode 100644 index 0000000..361334c --- /dev/null +++ b/plugins/eg-fifths.lv2/uris.h @@ -0,0 +1,53 @@ +/* + LV2 Fifths Example Plugin + Copyright 2014-2015 David Robillard <d@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. +*/ + +#ifndef FIFTHS_URIS_H +#define FIFTHS_URIS_H + +#include "lv2/lv2plug.in/ns/ext/log/log.h" +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" +#include "lv2/lv2plug.in/ns/ext/state/state.h" + +#define EG_FIFTHS_URI "http://lv2plug.in/plugins/eg-fifths" + +typedef struct { + LV2_URID atom_Path; + LV2_URID atom_Resource; + LV2_URID atom_Sequence; + LV2_URID atom_URID; + LV2_URID atom_eventTransfer; + LV2_URID midi_Event; + LV2_URID patch_Set; + LV2_URID patch_property; + LV2_URID patch_value; +} FifthsURIs; + +static inline void +map_fifths_uris(LV2_URID_Map* map, FifthsURIs* uris) +{ + uris->atom_Path = map->map(map->handle, LV2_ATOM__Path); + uris->atom_Resource = map->map(map->handle, LV2_ATOM__Resource); + uris->atom_Sequence = map->map(map->handle, LV2_ATOM__Sequence); + uris->atom_URID = map->map(map->handle, LV2_ATOM__URID); + uris->atom_eventTransfer = map->map(map->handle, LV2_ATOM__eventTransfer); + uris->midi_Event = map->map(map->handle, LV2_MIDI__MidiEvent); + uris->patch_Set = map->map(map->handle, LV2_PATCH__Set); + uris->patch_property = map->map(map->handle, LV2_PATCH__property); + uris->patch_value = map->map(map->handle, LV2_PATCH__value); +} + +#endif /* FIFTHS_URIS_H */ diff --git a/plugins/eg-fifths.lv2/waf b/plugins/eg-fifths.lv2/waf new file mode 120000 index 0000000..59a1ac9 --- /dev/null +++ b/plugins/eg-fifths.lv2/waf @@ -0,0 +1 @@ +../../waf
\ No newline at end of file diff --git a/plugins/eg-fifths.lv2/wscript b/plugins/eg-fifths.lv2/wscript new file mode 100644 index 0000000..f5c1808 --- /dev/null +++ b/plugins/eg-fifths.lv2/wscript @@ -0,0 +1,65 @@ +#!/usr/bin/env python +from waflib.extras import autowaf as autowaf +import re + +# Variables for 'waf dist' +APPNAME = 'eg-fifths.lv2' +VERSION = '1.0.0' + +# Mandatory variables +top = '.' +out = 'build' + +def options(opt): + opt.load('compiler_c') + opt.load('lv2') + autowaf.set_options(opt) + +def configure(conf): + autowaf.display_header('Fifths Configuration') + conf.load('compiler_c', cache=True) + conf.load('lv2', cache=True) + conf.load('autowaf', cache=True) + + if not autowaf.is_child(): + autowaf.check_pkg(conf, 'lv2', atleast_version='1.2.1', uselib_store='LV2') + + autowaf.display_msg(conf, 'LV2 bundle directory', conf.env.LV2DIR) + print('') + +def build(bld): + bundle = 'eg-fifths.lv2' + + # Make a pattern for shared objects without the 'lib' prefix + module_pat = re.sub('^lib', '', bld.env.cshlib_PATTERN) + module_ext = module_pat[module_pat.rfind('.'):] + + # Build manifest.ttl by substitution (for portable lib extension) + bld(features = 'subst', + source = 'manifest.ttl.in', + target = '%s/%s' % (bundle, 'manifest.ttl'), + install_path = '${LV2DIR}/%s' % bundle, + LIB_EXT = module_ext) + + # Copy other data files to build bundle (build/eg-fifths.lv2) + for i in ['fifths.ttl']: + bld(features = 'subst', + is_copy = True, + source = i, + target = '%s/%s' % (bundle, i), + install_path = '${LV2DIR}/%s' % bundle) + + # Use LV2 headers from parent directory if building as a sub-project + includes = ['.'] + if autowaf.is_child: + includes += ['../..'] + + # Build plugin library + obj = bld(features = 'c cshlib', + source = 'fifths.c', + name = 'fifths', + target = '%s/fifths' % bundle, + install_path = '${LV2DIR}/%s' % bundle, + use = 'LV2', + includes = includes) + obj.env.cshlib_PATTERN = module_pat diff --git a/plugins/eg-metro.lv2/README.txt b/plugins/eg-metro.lv2/README.txt new file mode 100644 index 0000000..5e9a84a --- /dev/null +++ b/plugins/eg-metro.lv2/README.txt @@ -0,0 +1,9 @@ +== Metronome == + +This plugin demonstrates tempo synchronisation by clicking on every beat. The +host sends this information to the plugin as events, so an event with new time +and tempo information will be received whenever there is a change. + +Time is assumed to continue rolling at the tempo and speed defined by the last +received tempo event, even across cycles, until a new tempo event is received +or the plugin is deactivated. diff --git a/plugins/eg-metro.lv2/manifest.ttl.in b/plugins/eg-metro.lv2/manifest.ttl.in new file mode 100644 index 0000000..bd93f66 --- /dev/null +++ b/plugins/eg-metro.lv2/manifest.ttl.in @@ -0,0 +1,7 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . + +<http://lv2plug.in/plugins/eg-metro> + a lv2:Plugin ; + lv2:binary <metro@LIB_EXT@> ; + rdfs:seeAlso <metro.ttl> . diff --git a/plugins/eg-metro.lv2/metro.c b/plugins/eg-metro.lv2/metro.c new file mode 100644 index 0000000..05d7004 --- /dev/null +++ b/plugins/eg-metro.lv2/metro.c @@ -0,0 +1,355 @@ +/* + LV2 Metronome Example Plugin + Copyright 2012-2016 David Robillard <d@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. +*/ + +#include <math.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#ifndef __cplusplus +# include <stdbool.h> +#endif + +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "lv2/lv2plug.in/ns/ext/log/logger.h" +#include "lv2/lv2plug.in/ns/ext/time/time.h" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2_util.h" + +#ifndef M_PI +# define M_PI 3.14159265 +#endif + +#define EG_METRO_URI "http://lv2plug.in/plugins/eg-metro" + +typedef struct { + LV2_URID atom_Blank; + LV2_URID atom_Float; + LV2_URID atom_Object; + LV2_URID atom_Path; + LV2_URID atom_Resource; + LV2_URID atom_Sequence; + LV2_URID time_Position; + LV2_URID time_barBeat; + LV2_URID time_beatsPerMinute; + LV2_URID time_speed; +} MetroURIs; + +static const double attack_s = 0.005; +static const double decay_s = 0.075; + +enum { + METRO_CONTROL = 0, + METRO_OUT = 1 +}; + +/** During execution this plugin can be in one of 3 states: */ +typedef enum { + STATE_ATTACK, // Envelope rising + STATE_DECAY, // Envelope lowering + STATE_OFF // Silent +} State; + +/** + This plugin must keep track of more state than previous examples to be able + to render audio. The basic idea is to generate a single cycle of a sine + wave which is conceptually played continuously. The 'tick' is generated by + enveloping the amplitude so there is a short attack/decay peak around a + tick, and silence the rest of the time. + + This example uses a simple AD envelope with fixed parameters. A more + sophisticated implementation might use a more advanced envelope and allow + the user to modify these parameters, the frequency of the wave, and so on. +*/ +typedef struct { + LV2_URID_Map* map; // URID map feature + LV2_Log_Logger logger; // Logger API + MetroURIs uris; // Cache of mapped URIDs + + struct { + LV2_Atom_Sequence* control; + float* output; + } ports; + + // Variables to keep track of the tempo information sent by the host + double rate; // Sample rate + float bpm; // Beats per minute (tempo) + float speed; // Transport speed (usually 0=stop, 1=play) + + uint32_t elapsed_len; // Frames since the start of the last click + uint32_t wave_offset; // Current play offset in the wave + State state; // Current play state + + // One cycle of a sine wave + float* wave; + uint32_t wave_len; + + // Envelope parameters + uint32_t attack_len; + uint32_t decay_len; +} Metro; + +static void +connect_port(LV2_Handle instance, + uint32_t port, + void* data) +{ + Metro* self = (Metro*)instance; + + switch (port) { + case METRO_CONTROL: + self->ports.control = (LV2_Atom_Sequence*)data; + break; + case METRO_OUT: + self->ports.output = (float*)data; + break; + default: + break; + } +} + +/** + The activate() method resets the state completely, so the wave offset is + zero and the envelope is off. +*/ +static void +activate(LV2_Handle instance) +{ + Metro* self = (Metro*)instance; + + self->elapsed_len = 0; + self->wave_offset = 0; + self->state = STATE_OFF; +} + +/** + This plugin does a bit more work in instantiate() than the previous + examples. The tempo updates from the host contain several URIs, so those + are mapped, and the sine wave to be played needs to be generated based on + the current sample rate. +*/ +static LV2_Handle +instantiate(const LV2_Descriptor* descriptor, + double rate, + const char* path, + const LV2_Feature* const* features) +{ + Metro* self = (Metro*)calloc(1, sizeof(Metro)); + if (!self) { + return NULL; + } + + // Scan host features for URID map + const char* missing = lv2_features_query( + features, + LV2_LOG__log, &self->logger.log, false, + LV2_URID__map, &self->map, true, + NULL); + lv2_log_logger_set_map(&self->logger, self->map); + if (missing) { + lv2_log_error(&self->logger, "Missing feature <%s>\n", missing); + free(self); + return NULL; + } + + // Map URIS + MetroURIs* const uris = &self->uris; + LV2_URID_Map* const map = self->map; + uris->atom_Blank = map->map(map->handle, LV2_ATOM__Blank); + uris->atom_Float = map->map(map->handle, LV2_ATOM__Float); + uris->atom_Object = map->map(map->handle, LV2_ATOM__Object); + uris->atom_Path = map->map(map->handle, LV2_ATOM__Path); + uris->atom_Resource = map->map(map->handle, LV2_ATOM__Resource); + uris->atom_Sequence = map->map(map->handle, LV2_ATOM__Sequence); + uris->time_Position = map->map(map->handle, LV2_TIME__Position); + uris->time_barBeat = map->map(map->handle, LV2_TIME__barBeat); + uris->time_beatsPerMinute = map->map(map->handle, LV2_TIME__beatsPerMinute); + uris->time_speed = map->map(map->handle, LV2_TIME__speed); + + // Initialise instance fields + self->rate = rate; + self->bpm = 120.0f; + self->attack_len = (uint32_t)(attack_s * rate); + self->decay_len = (uint32_t)(decay_s * rate); + self->state = STATE_OFF; + + // Generate one cycle of a sine wave at the desired frequency + const double freq = 440.0 * 2.0; + const double amp = 0.5; + self->wave_len = (uint32_t)(rate / freq); + self->wave = (float*)malloc(self->wave_len * sizeof(float)); + for (uint32_t i = 0; i < self->wave_len; ++i) { + self->wave[i] = (float)(sin(i * 2 * M_PI * freq / rate) * amp); + } + + return (LV2_Handle)self; +} + +static void +cleanup(LV2_Handle instance) +{ + free(instance); +} + +/** + Play back audio for the range [begin..end) relative to this cycle. This is + called by run() in-between events to output audio up until the current time. +*/ +static void +play(Metro* self, uint32_t begin, uint32_t end) +{ + float* const output = self->ports.output; + const uint32_t frames_per_beat = 60.0f / self->bpm * self->rate; + + if (self->speed == 0.0f) { + memset(output, 0, (end - begin) * sizeof(float)); + return; + } + + for (uint32_t i = begin; i < end; ++i) { + switch (self->state) { + case STATE_ATTACK: + // Amplitude increases from 0..1 until attack_len + output[i] = self->wave[self->wave_offset] * + self->elapsed_len / (float)self->attack_len; + if (self->elapsed_len >= self->attack_len) { + self->state = STATE_DECAY; + } + break; + case STATE_DECAY: + // Amplitude decreases from 1..0 until attack_len + decay_len + output[i] = 0.0f; + output[i] = self->wave[self->wave_offset] * + (1 - ((self->elapsed_len - self->attack_len) / + (float)self->decay_len)); + if (self->elapsed_len >= self->attack_len + self->decay_len) { + self->state = STATE_OFF; + } + break; + case STATE_OFF: + output[i] = 0.0f; + } + + // We continuously play the sine wave regardless of envelope + self->wave_offset = (self->wave_offset + 1) % self->wave_len; + + // Update elapsed time and start attack if necessary + if (++self->elapsed_len == frames_per_beat) { + self->state = STATE_ATTACK; + self->elapsed_len = 0; + } + } +} + +/** + Update the current position based on a host message. This is called by + run() when a time:Position is received. +*/ +static void +update_position(Metro* self, const LV2_Atom_Object* obj) +{ + const MetroURIs* uris = &self->uris; + + // Received new transport position/speed + LV2_Atom *beat = NULL, *bpm = NULL, *speed = NULL; + lv2_atom_object_get(obj, + uris->time_barBeat, &beat, + uris->time_beatsPerMinute, &bpm, + uris->time_speed, &speed, + NULL); + if (bpm && bpm->type == uris->atom_Float) { + // Tempo changed, update BPM + self->bpm = ((LV2_Atom_Float*)bpm)->body; + } + if (speed && speed->type == uris->atom_Float) { + // Speed changed, e.g. 0 (stop) to 1 (play) + self->speed = ((LV2_Atom_Float*)speed)->body; + } + if (beat && beat->type == uris->atom_Float) { + // Received a beat position, synchronise + // This hard sync may cause clicks, a real plugin would be more graceful + const float frames_per_beat = 60.0f / self->bpm * self->rate; + const float bar_beats = ((LV2_Atom_Float*)beat)->body; + const float beat_beats = bar_beats - floorf(bar_beats); + self->elapsed_len = beat_beats * frames_per_beat; + if (self->elapsed_len < self->attack_len) { + self->state = STATE_ATTACK; + } else if (self->elapsed_len < self->attack_len + self->decay_len) { + self->state = STATE_DECAY; + } else { + self->state = STATE_OFF; + } + } +} + +static void +run(LV2_Handle instance, uint32_t sample_count) +{ + Metro* self = (Metro*)instance; + const MetroURIs* uris = &self->uris; + + // Work forwards in time frame by frame, handling events as we go + const LV2_Atom_Sequence* in = self->ports.control; + uint32_t last_t = 0; + for (const LV2_Atom_Event* ev = lv2_atom_sequence_begin(&in->body); + !lv2_atom_sequence_is_end(&in->body, in->atom.size, ev); + ev = lv2_atom_sequence_next(ev)) { + + // Play the click for the time slice from last_t until now + play(self, last_t, ev->time.frames); + + // Check if this event is an Object + // (or deprecated Blank to tolerate old hosts) + if (ev->body.type == uris->atom_Object || + ev->body.type == uris->atom_Blank) { + const LV2_Atom_Object* obj = (const LV2_Atom_Object*)&ev->body; + if (obj->body.otype == uris->time_Position) { + // Received position information, update + update_position(self, obj); + } + } + + // Update time for next iteration and move to next event + last_t = ev->time.frames; + } + + // Play for remainder of cycle + play(self, last_t, sample_count); +} + +static const LV2_Descriptor descriptor = { + EG_METRO_URI, + instantiate, + connect_port, + activate, + run, + NULL, // deactivate, + cleanup, + NULL, // 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/plugins/eg-metro.lv2/metro.ttl b/plugins/eg-metro.lv2/metro.ttl new file mode 100644 index 0000000..8b4af3d --- /dev/null +++ b/plugins/eg-metro.lv2/metro.ttl @@ -0,0 +1,30 @@ +@prefix atom: <http://lv2plug.in/ns/ext/atom#> . +@prefix doap: <http://usefulinc.com/ns/doap#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix time: <http://lv2plug.in/ns/ext/time#> . +@prefix urid: <http://lv2plug.in/ns/ext/urid#> . + +<http://lv2plug.in/plugins/eg-metro> + a lv2:Plugin ; + doap:name "Example Metronome" ; + doap:license <http://opensource.org/licenses/isc> ; + lv2:project <http://lv2plug.in/ns/lv2> ; + lv2:requiredFeature urid:map ; + lv2:optionalFeature lv2:hardRTCapable ; + lv2:port [ + a lv2:InputPort , + atom:AtomPort ; + atom:bufferType atom:Sequence ; +# Since this port supports time:Position, the host knows to deliver time and +# tempo information + atom:supports time:Position ; + lv2:index 0 ; + lv2:symbol "control" ; + lv2:name "Control" ; + ] , [ + a lv2:AudioPort , + lv2:OutputPort ; + lv2:index 1 ; + lv2:symbol "out" ; + lv2:name "Out" ; + ] . diff --git a/plugins/eg-metro.lv2/waf b/plugins/eg-metro.lv2/waf new file mode 120000 index 0000000..59a1ac9 --- /dev/null +++ b/plugins/eg-metro.lv2/waf @@ -0,0 +1 @@ +../../waf
\ No newline at end of file diff --git a/plugins/eg-metro.lv2/wscript b/plugins/eg-metro.lv2/wscript new file mode 100644 index 0000000..467a069 --- /dev/null +++ b/plugins/eg-metro.lv2/wscript @@ -0,0 +1,66 @@ +#!/usr/bin/env python +from waflib.extras import autowaf as autowaf +import re + +# Variables for 'waf dist' +APPNAME = 'eg-metro.lv2' +VERSION = '1.0.0' + +# Mandatory variables +top = '.' +out = 'build' + +def options(opt): + opt.load('compiler_c') + opt.load('lv2') + autowaf.set_options(opt) + +def configure(conf): + autowaf.display_header('Metro Configuration') + conf.load('compiler_c', cache=True) + conf.load('lv2', cache=True) + conf.load('autowaf', cache=True) + + if not autowaf.is_child(): + autowaf.check_pkg(conf, 'lv2', atleast_version='0.2.0', uselib_store='LV2') + + conf.check(features='c cshlib', lib='m', uselib_store='M', mandatory=False) + + autowaf.display_msg(conf, 'LV2 bundle directory', conf.env.LV2DIR) + print('') + +def build(bld): + bundle = 'eg-metro.lv2' + + # Make a pattern for shared objects without the 'lib' prefix + module_pat = re.sub('^lib', '', bld.env.cshlib_PATTERN) + module_ext = module_pat[module_pat.rfind('.'):] + + # Build manifest.ttl by substitution (for portable lib extension) + bld(features = 'subst', + source = 'manifest.ttl.in', + target = '%s/%s' % (bundle, 'manifest.ttl'), + install_path = '${LV2DIR}/%s' % bundle, + LIB_EXT = module_ext) + + # Copy other data files to build bundle (build/eg-metro.lv2) + bld(features = 'subst', + is_copy = True, + source = 'metro.ttl', + target = '%s/metro.ttl' % bundle, + install_path = '${LV2DIR}/%s' % bundle) + + # Use LV2 headers from parent directory if building as a sub-project + includes = ['.'] + if autowaf.is_child: + includes += ['../..'] + + # Build plugin library + obj = bld(features = 'c cshlib', + source = 'metro.c', + name = 'metro', + target = '%s/metro' % bundle, + install_path = '${LV2DIR}/%s' % bundle, + use = ['M', 'LV2'], + includes = includes) + obj.env.cshlib_PATTERN = module_pat diff --git a/plugins/eg-midigate.lv2/README.txt b/plugins/eg-midigate.lv2/README.txt new file mode 100644 index 0000000..8f4a0f0 --- /dev/null +++ b/plugins/eg-midigate.lv2/README.txt @@ -0,0 +1,10 @@ +== MIDI Gate == + +This plugin demonstrates: + + * Receiving MIDI input + + * Processing audio based on MIDI events with sample accuracy + + * Supporting MIDI programs which the host can control/automate, or present a + user interface for with human readable labels diff --git a/plugins/eg-midigate.lv2/manifest.ttl.in b/plugins/eg-midigate.lv2/manifest.ttl.in new file mode 100644 index 0000000..d32f1dc --- /dev/null +++ b/plugins/eg-midigate.lv2/manifest.ttl.in @@ -0,0 +1,10 @@ +# The manifest.ttl file follows the same template as the previous example. + +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix ui: <http://lv2plug.in/ns/extensions/ui#> . + +<http://lv2plug.in/plugins/eg-midigate> + a lv2:Plugin ; + lv2:binary <midigate@LIB_EXT@> ; + rdfs:seeAlso <midigate.ttl> . diff --git a/plugins/eg-midigate.lv2/midigate.c b/plugins/eg-midigate.lv2/midigate.c new file mode 100644 index 0000000..a967384 --- /dev/null +++ b/plugins/eg-midigate.lv2/midigate.c @@ -0,0 +1,238 @@ +/* + Copyright 2013-2016 David Robillard <d@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. +*/ + +#include <stdio.h> + +#include <math.h> +#include <stdlib.h> + +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "lv2/lv2plug.in/ns/ext/log/logger.h" +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2_util.h" + +#define MIDIGATE_URI "http://lv2plug.in/plugins/eg-midigate" + +typedef enum { + MIDIGATE_CONTROL = 0, + MIDIGATE_IN = 1, + MIDIGATE_OUT = 2 +} PortIndex; + +typedef struct { + // Port buffers + const LV2_Atom_Sequence* control; + const float* in; + float* out; + + // Features + LV2_URID_Map* map; + LV2_Log_Logger logger; + + struct { + LV2_URID midi_MidiEvent; + } uris; + + unsigned n_active_notes; + unsigned program; // 0 = normal, 1 = inverted +} Midigate; + +static LV2_Handle +instantiate(const LV2_Descriptor* descriptor, + double rate, + const char* bundle_path, + const LV2_Feature* const* features) +{ + Midigate* self = (Midigate*)calloc(1, sizeof(Midigate)); + if (!self) { + return NULL; + } + + // Scan host features for URID map + const char* missing = lv2_features_query( + features, + LV2_LOG__log, &self->logger.log, false, + LV2_URID__map, &self->map, true, + NULL); + lv2_log_logger_set_map(&self->logger, self->map); + if (missing) { + lv2_log_error(&self->logger, "Missing feature <%s>\n", missing); + free(self); + return NULL; + } + + self->uris.midi_MidiEvent = self->map->map( + self->map->handle, LV2_MIDI__MidiEvent); + + return (LV2_Handle)self; +} + +static void +connect_port(LV2_Handle instance, + uint32_t port, + void* data) +{ + Midigate* self = (Midigate*)instance; + + switch ((PortIndex)port) { + case MIDIGATE_CONTROL: + self->control = (const LV2_Atom_Sequence*)data; + break; + case MIDIGATE_IN: + self->in = (const float*)data; + break; + case MIDIGATE_OUT: + self->out = (float*)data; + break; + } +} + +static void +activate(LV2_Handle instance) +{ + Midigate* self = (Midigate*)instance; + self->n_active_notes = 0; + self->program = 0; +} + +/** + A function to write a chunk of output, to be called from run(). If the gate + is high, then the input will be passed through for this chunk, otherwise + silence is written. +*/ +static void +write_output(Midigate* self, uint32_t offset, uint32_t len) +{ + const bool active = (self->program == 0) + ? (self->n_active_notes > 0) + : (self->n_active_notes == 0); + if (active) { + memcpy(self->out + offset, self->in + offset, len * sizeof(float)); + } else { + memset(self->out + offset, 0, len * sizeof(float)); + } +} + +/** + This plugin works through the cycle in chunks starting at offset zero. The + +offset+ represents the current time within this this cycle, so + the output from 0 to +offset+ has already been written. + + MIDI events are read in a loop. In each iteration, the number of active + notes (on note on and note off) or the program (on program change) is + updated, then the output is written up until the current event time. Then + +offset+ is updated and the next event is processed. After the loop the + final chunk from the last event to the end of the cycle is emitted. + + There is currently no standard way to describe MIDI programs in LV2, so the + host has no way of knowing that these programs exist and should be presented + to the user. A future version of LV2 will address this shortcoming. + + This pattern of iterating over input events and writing output along the way + is a common idiom for writing sample accurate output based on event input. + + Note that this simple example simply writes input or zero for each sample + based on the gate. A serious implementation would need to envelope the + transition to avoid aliasing. +*/ +static void +run(LV2_Handle instance, uint32_t sample_count) +{ + Midigate* self = (Midigate*)instance; + uint32_t offset = 0; + + LV2_ATOM_SEQUENCE_FOREACH(self->control, ev) { + if (ev->body.type == self->uris.midi_MidiEvent) { + const uint8_t* const msg = (const uint8_t*)(ev + 1); + switch (lv2_midi_message_type(msg)) { + case LV2_MIDI_MSG_NOTE_ON: + ++self->n_active_notes; + break; + case LV2_MIDI_MSG_NOTE_OFF: + if (self->n_active_notes > 0) { + --self->n_active_notes; + } + break; + case LV2_MIDI_MSG_CONTROLLER: + if (msg[1] == LV2_MIDI_CTL_ALL_NOTES_OFF) { + self->n_active_notes = 0; + } + break; + case LV2_MIDI_MSG_PGM_CHANGE: + if (msg[1] == 0 || msg[1] == 1) { + self->program = msg[1]; + } + break; + default: break; + } + } + + write_output(self, offset, ev->time.frames - offset); + offset = (uint32_t)ev->time.frames; + } + + write_output(self, offset, sample_count - offset); +} + +/** + We have no resources to free on deactivation. + Note that the next call to activate will re-initialise the state, namely + self->n_active_notes, so there is no need to do so here. +*/ +static void +deactivate(LV2_Handle instance) +{} + +static void +cleanup(LV2_Handle instance) +{ + free(instance); +} + +/** + This plugin also has no extension data to return. +*/ +static const void* +extension_data(const char* uri) +{ + return NULL; +} + +static const LV2_Descriptor descriptor = { + MIDIGATE_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/plugins/eg-midigate.lv2/midigate.ttl b/plugins/eg-midigate.lv2/midigate.ttl new file mode 100644 index 0000000..e14a329 --- /dev/null +++ b/plugins/eg-midigate.lv2/midigate.ttl @@ -0,0 +1,56 @@ +# The same set of namespace prefixes with two additions for LV2 extensions this +# plugin uses: atom and urid. + +@prefix atom: <http://lv2plug.in/ns/ext/atom#> . +@prefix doap: <http://usefulinc.com/ns/doap#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix midi: <http://lv2plug.in/ns/ext/midi#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix urid: <http://lv2plug.in/ns/ext/urid#> . + +<http://lv2plug.in/plugins/eg-midigate> + a lv2:Plugin ; + doap:name "Example MIDI Gate" ; + doap:license <http://opensource.org/licenses/isc> ; + lv2:project <http://lv2plug.in/ns/lv2> ; + lv2:requiredFeature urid:map ; + lv2:optionalFeature lv2:hardRTCapable ; +# This plugin has three ports. There is an audio input and output as before, +# as well as a new AtomPort. An AtomPort buffer contains an Atom, which is a +# generic container for any type of data. In this case, we want to receive +# MIDI events, so the (mandatory) +atom:bufferType+ is atom:Sequence, which is +# a series of events with time stamps. +# +# Events themselves are also generic and can contain any type of data, but in +# this case we are only interested in MIDI events. The (optional) +# +atom:supports+ property describes which event types are supported. Though +# not required, this information should always be given so the host knows what +# types of event it can expect the plugin to understand. +# +# The (optional) +lv2:designation+ of this port is +lv2:control+, which +# indicates that this is the "main" control port where the host should send +# events it expects to configure the plugin, in this case changing the MIDI +# program. This is necessary since it is possible to have several MIDI input +# ports, though typically it is best to have one. + lv2:port [ + a lv2:InputPort , + atom:AtomPort ; + atom:bufferType atom:Sequence ; + atom:supports midi:MidiEvent ; + lv2:designation lv2:control ; + lv2:index 0 ; + lv2:symbol "control" ; + lv2:name "Control" + ] , [ + a lv2:AudioPort , + lv2:InputPort ; + lv2:index 1 ; + lv2:symbol "in" ; + lv2:name "In" + ] , [ + a lv2:AudioPort , + lv2:OutputPort ; + lv2:index 2 ; + lv2:symbol "out" ; + lv2:name "Out" + ] . diff --git a/plugins/eg-midigate.lv2/waf b/plugins/eg-midigate.lv2/waf new file mode 120000 index 0000000..59a1ac9 --- /dev/null +++ b/plugins/eg-midigate.lv2/waf @@ -0,0 +1 @@ +../../waf
\ No newline at end of file diff --git a/plugins/eg-midigate.lv2/wscript b/plugins/eg-midigate.lv2/wscript new file mode 100644 index 0000000..933ae7a --- /dev/null +++ b/plugins/eg-midigate.lv2/wscript @@ -0,0 +1,65 @@ +#!/usr/bin/env python +from waflib.extras import autowaf as autowaf +import re + +# Variables for 'waf dist' +APPNAME = 'eg-midigate.lv2' +VERSION = '1.0.0' + +# Mandatory variables +top = '.' +out = 'build' + +def options(opt): + opt.load('compiler_c') + opt.load('lv2') + autowaf.set_options(opt) + +def configure(conf): + autowaf.display_header('Midigate Configuration') + conf.load('compiler_c', cache=True) + conf.load('lv2', cache=True) + conf.load('autowaf', cache=True) + + if not autowaf.is_child(): + autowaf.check_pkg(conf, 'lv2', uselib_store='LV2') + + autowaf.display_msg(conf, 'LV2 bundle directory', conf.env.LV2DIR) + print('') + +def build(bld): + bundle = 'eg-midigate.lv2' + + # Make a pattern for shared objects without the 'lib' prefix + module_pat = re.sub('^lib', '', bld.env.cshlib_PATTERN) + module_ext = module_pat[module_pat.rfind('.'):] + + # Build manifest.ttl by substitution (for portable lib extension) + bld(features = 'subst', + source = 'manifest.ttl.in', + target = '%s/%s' % (bundle, 'manifest.ttl'), + install_path = '${LV2DIR}/%s' % bundle, + LIB_EXT = module_ext) + + # Copy other data files to build bundle (build/eg-midigate.lv2) + for i in ['midigate.ttl']: + bld(features = 'subst', + is_copy = True, + source = i, + target = '%s/%s' % (bundle, i), + install_path = '${LV2DIR}/%s' % bundle) + + # Use LV2 headers from parent directory if building as a sub-project + includes = None + if autowaf.is_child: + includes = '../..' + + # Build plugin library + obj = bld(features = 'c cshlib', + source = 'midigate.c', + name = 'midigate', + target = '%s/midigate' % bundle, + install_path = '${LV2DIR}/%s' % bundle, + uselib = 'LV2', + includes = includes) + obj.env.cshlib_PATTERN = module_pat diff --git a/plugins/eg-params.lv2/README.txt b/plugins/eg-params.lv2/README.txt new file mode 100644 index 0000000..acf90c1 --- /dev/null +++ b/plugins/eg-params.lv2/README.txt @@ -0,0 +1,21 @@ +== Params == + +The basic LV2 mechanism for controls is +http://lv2plug.in/ns/lv2core#ControlPort[lv2:ControlPort], inherited from +LADSPA. Control ports are problematic because they are not sample accurate, +support only one type (`float`), and require that plugins poll to know when a +control has changed. + +Parameters can be used instead to address these issues. Parameters can be +thought of as properties of a plugin instance; they are identified by URI and +have a value of any type. This deliberately meshes with the concept of plugin +state defined by the http://lv2plug.in/ns/ext/state[LV2 state extension]. +The state extension allows plugins to save and restore their parameters (along +with other internal state information, if necessary). + +Parameters are accessed and manipulated using messages sent via a sequence +port. The http://lv2plug.in/ns/ext/patch[LV2 patch extension] defines the +standard messages for working with parameters. Typically, only two are used +for simple plugins: http://lv2plug.in/ns/ext/patch#Set[patch:Set] sets a +parameter to some value, and http://lv2plug.in/ns/ext/patch#Get[patch:Get] +requests that the plugin send a description of its parameters. diff --git a/plugins/eg-params.lv2/manifest.ttl.in b/plugins/eg-params.lv2/manifest.ttl.in new file mode 100644 index 0000000..913de7c --- /dev/null +++ b/plugins/eg-params.lv2/manifest.ttl.in @@ -0,0 +1,7 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . + +<http://lv2plug.in/plugins/eg-params> + a lv2:Plugin ; + lv2:binary <params@LIB_EXT@> ; + rdfs:seeAlso <params.ttl> . diff --git a/plugins/eg-params.lv2/params.c b/plugins/eg-params.lv2/params.c new file mode 100644 index 0000000..3df6652 --- /dev/null +++ b/plugins/eg-params.lv2/params.c @@ -0,0 +1,526 @@ +/* + LV2 Parameter Example Plugin + Copyright 2014-2016 David Robillard <d@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. +*/ + +#include <math.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#ifndef __cplusplus +# include <stdbool.h> +#endif + +#include "lv2/lv2plug.in/ns/ext/atom/forge.h" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "lv2/lv2plug.in/ns/ext/log/logger.h" +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" +#include "lv2/lv2plug.in/ns/ext/patch/patch.h" +#include "lv2/lv2plug.in/ns/ext/state/state.h" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2_util.h" + +#include "state_map.h" + +#define MAX_STRING 1024 + +#define EG_PARAMS_URI "http://lv2plug.in/plugins/eg-params" + +#define N_PROPS 9 + +typedef struct { + LV2_URID plugin; + LV2_URID atom_Path; + LV2_URID atom_Sequence; + LV2_URID atom_URID; + LV2_URID atom_eventTransfer; + LV2_URID eg_spring; + LV2_URID midi_Event; + LV2_URID patch_Get; + LV2_URID patch_Set; + LV2_URID patch_Put; + LV2_URID patch_body; + LV2_URID patch_subject; + LV2_URID patch_property; + LV2_URID patch_value; +} URIs; + +typedef struct { + LV2_Atom_Int aint; + LV2_Atom_Long along; + LV2_Atom_Float afloat; + LV2_Atom_Double adouble; + LV2_Atom_Bool abool; + LV2_Atom astring; + char string[MAX_STRING]; + LV2_Atom apath; + char path[MAX_STRING]; + LV2_Atom_Float lfo; + LV2_Atom_Float spring; +} State; + +static inline void +map_uris(LV2_URID_Map* map, URIs* uris) +{ + uris->plugin = map->map(map->handle, EG_PARAMS_URI); + + uris->atom_Path = map->map(map->handle, LV2_ATOM__Path); + uris->atom_Sequence = map->map(map->handle, LV2_ATOM__Sequence); + uris->atom_URID = map->map(map->handle, LV2_ATOM__URID); + uris->atom_eventTransfer = map->map(map->handle, LV2_ATOM__eventTransfer); + uris->eg_spring = map->map(map->handle, EG_PARAMS_URI "#spring"); + uris->midi_Event = map->map(map->handle, LV2_MIDI__MidiEvent); + uris->patch_Get = map->map(map->handle, LV2_PATCH__Get); + uris->patch_Set = map->map(map->handle, LV2_PATCH__Set); + uris->patch_Put = map->map(map->handle, LV2_PATCH__Put); + uris->patch_body = map->map(map->handle, LV2_PATCH__body); + uris->patch_subject = map->map(map->handle, LV2_PATCH__subject); + uris->patch_property = map->map(map->handle, LV2_PATCH__property); + uris->patch_value = map->map(map->handle, LV2_PATCH__value); +} + +enum { + PARAMS_IN = 0, + PARAMS_OUT = 1 +}; + +typedef struct { + // Features + LV2_URID_Map* map; + LV2_URID_Unmap* unmap; + LV2_Log_Logger log; + + // Forge for creating atoms + LV2_Atom_Forge forge; + + // Ports + const LV2_Atom_Sequence* in_port; + LV2_Atom_Sequence* out_port; + + // URIs + URIs uris; + + // Plugin state + StateMapItem props[N_PROPS]; + State state; + + // Buffer for making strings from URIDs if unmap is not provided + char urid_buf[12]; +} Params; + +static void +connect_port(LV2_Handle instance, + uint32_t port, + void* data) +{ + Params* self = (Params*)instance; + switch (port) { + case PARAMS_IN: + self->in_port = (const LV2_Atom_Sequence*)data; + break; + case PARAMS_OUT: + self->out_port = (LV2_Atom_Sequence*)data; + break; + default: + break; + } +} + +static LV2_Handle +instantiate(const LV2_Descriptor* descriptor, + double rate, + const char* path, + const LV2_Feature* const* features) +{ + // Allocate instance + Params* self = (Params*)calloc(1, sizeof(Params)); + if (!self) { + return NULL; + } + + // Get host features + const char* missing = lv2_features_query( + features, + LV2_LOG__log, &self->log.log, false, + LV2_URID__map, &self->map, true, + LV2_URID__unmap, &self->unmap, false, + NULL); + lv2_log_logger_set_map(&self->log, self->map); + if (missing) { + lv2_log_error(&self->log, "Missing feature <%s>\n", missing); + free(self); + return NULL; + } + + // Map URIs and initialise forge + map_uris(self->map, &self->uris); + lv2_atom_forge_init(&self->forge, self->map); + + // Initialise state dictionary + State* state = &self->state; + state_map_init( + self->props, self->map, self->map->handle, + EG_PARAMS_URI "#int", STATE_MAP_INIT(Int, &state->aint), + EG_PARAMS_URI "#long", STATE_MAP_INIT(Long, &state->along), + EG_PARAMS_URI "#float", STATE_MAP_INIT(Float, &state->afloat), + EG_PARAMS_URI "#double", STATE_MAP_INIT(Double, &state->adouble), + EG_PARAMS_URI "#bool", STATE_MAP_INIT(Bool, &state->abool), + EG_PARAMS_URI "#string", STATE_MAP_INIT(String, &state->astring), + EG_PARAMS_URI "#path", STATE_MAP_INIT(Path, &state->apath), + EG_PARAMS_URI "#lfo", STATE_MAP_INIT(Float, &state->lfo), + EG_PARAMS_URI "#spring", STATE_MAP_INIT(Float, &state->spring), + NULL); + + return (LV2_Handle)self; +} + +static void +cleanup(LV2_Handle instance) +{ + free(instance); +} + +/** Helper function to unmap a URID if possible. */ +static const char* +unmap(Params* self, LV2_URID urid) +{ + if (self->unmap) { + return self->unmap->unmap(self->unmap->handle, urid); + } else { + snprintf(self->urid_buf, sizeof(self->urid_buf), "%u", urid); + return self->urid_buf; + } +} + +static LV2_State_Status +check_type(Params* self, + LV2_URID key, + LV2_URID type, + LV2_URID required_type) +{ + if (type != required_type) { + lv2_log_trace( + &self->log, "Bad type <%s> for <%s> (needs <%s>)\n", + unmap(self, type), + unmap(self, key), + unmap(self, required_type)); + return LV2_STATE_ERR_BAD_TYPE; + } + return LV2_STATE_SUCCESS; +} + +static LV2_State_Status +set_parameter(Params* self, + LV2_URID key, + uint32_t size, + LV2_URID type, + const void* body, + bool from_state) +{ + // Look up property in state dictionary + const StateMapItem* entry = state_map_find(self->props, N_PROPS, key); + if (!entry) { + lv2_log_trace(&self->log, "Unknown parameter <%s>\n", unmap(self, key)); + return LV2_STATE_ERR_NO_PROPERTY; + } + + // Ensure given type matches property's type + if (check_type(self, key, type, entry->value->type)) { + return LV2_STATE_ERR_BAD_TYPE; + } + + // Set property value in state dictionary + lv2_log_trace(&self->log, "Set <%s>\n", entry->uri); + memcpy(entry->value + 1, body, size); + entry->value->size = size; + return LV2_STATE_SUCCESS; +} + +static const LV2_Atom* +get_parameter(Params* self, LV2_URID key) +{ + const StateMapItem* entry = state_map_find(self->props, N_PROPS, key); + if (entry) { + lv2_log_trace(&self->log, "Get <%s>\n", entry->uri); + return entry->value; + } + + lv2_log_trace(&self->log, "Unknown parameter <%s>\n", unmap(self, key)); + return NULL; +} + +static LV2_State_Status +write_param_to_forge(LV2_State_Handle handle, + uint32_t key, + const void* value, + size_t size, + uint32_t type, + uint32_t flags) +{ + LV2_Atom_Forge* forge = (LV2_Atom_Forge*)handle; + + if (!lv2_atom_forge_key(forge, key) || + !lv2_atom_forge_atom(forge, size, type) || + !lv2_atom_forge_write(forge, value, size)) { + return LV2_STATE_ERR_UNKNOWN; + } + + return LV2_STATE_SUCCESS; +} + +static void +store_prop(Params* self, + LV2_State_Map_Path* map_path, + LV2_State_Status* save_status, + LV2_State_Store_Function store, + LV2_State_Handle handle, + LV2_URID key, + const LV2_Atom* value) +{ + LV2_State_Status st; + if (map_path && value->type == self->uris.atom_Path) { + // Map path to abstract path for portable storage + const char* path = (const char*)(value + 1); + char* apath = map_path->abstract_path(map_path->handle, path); + st = store(handle, + key, + apath, + strlen(apath) + 1, + self->uris.atom_Path, + LV2_STATE_IS_POD | LV2_STATE_IS_PORTABLE); + free(apath); + } else { + // Store simple property + st = store(handle, + key, + value + 1, + value->size, + value->type, + LV2_STATE_IS_POD | LV2_STATE_IS_PORTABLE); + } + + if (save_status && !*save_status) { + *save_status = st; + } +} + +/** + State save method. + + This is used in the usual way when called by the host to save plugin state, + but also internally for writing messages in the audio thread by passing a + "store" function which actually writes the description to the forge. +*/ +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) +{ + Params* self = (Params*)instance; + LV2_State_Map_Path* map_path = (LV2_State_Map_Path*)lv2_features_data( + features, LV2_STATE__mapPath); + + LV2_State_Status st = LV2_STATE_SUCCESS; + for (unsigned i = 0; i < N_PROPS; ++i) { + StateMapItem* prop = &self->props[i]; + store_prop(self, map_path, &st, store, handle, prop->urid, prop->value); + } + + return st; +} + +static void +retrieve_prop(Params* self, + LV2_State_Status* restore_status, + LV2_State_Retrieve_Function retrieve, + LV2_State_Handle handle, + LV2_URID key) +{ + // Retrieve value from saved state + size_t vsize; + uint32_t vtype; + uint32_t vflags; + const void* value = retrieve(handle, key, &vsize, &vtype, &vflags); + + // Set plugin instance state + const LV2_State_Status st = value + ? set_parameter(self, key, vsize, vtype, value, true) + : LV2_STATE_ERR_NO_PROPERTY; + + if (!*restore_status) { + *restore_status = st; // Set status if there has been no error yet + } +} + +/** State restore method. */ +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) +{ + Params* self = (Params*)instance; + LV2_State_Status st = LV2_STATE_SUCCESS; + + for (unsigned i = 0; i < N_PROPS; ++i) { + retrieve_prop(self, &st, retrieve, handle, self->props[i].urid); + } + + return st; +} + +static inline bool +subject_is_plugin(Params* self, const LV2_Atom_URID* subject) +{ + // This simple plugin only supports one subject: itself + return (!subject || (subject->atom.type == self->uris.atom_URID && + subject->body == self->uris.plugin)); +} + +static void +run(LV2_Handle instance, uint32_t sample_count) +{ + Params* self = (Params*)instance; + URIs* uris = &self->uris; + + // Initially, self->out_port contains a Chunk with size set to capacity + // Set up forge to write directly to output port + const uint32_t out_capacity = self->out_port->atom.size; + lv2_atom_forge_set_buffer(&self->forge, + (uint8_t*)self->out_port, + out_capacity); + + // Start a sequence in the output port + LV2_Atom_Forge_Frame out_frame; + lv2_atom_forge_sequence_head(&self->forge, &out_frame, 0); + + // Read incoming events + LV2_ATOM_SEQUENCE_FOREACH(self->in_port, ev) { + const LV2_Atom_Object* obj = (const LV2_Atom_Object*)&ev->body; + if (obj->body.otype == uris->patch_Set) { + // Get the property and value of the set message + const LV2_Atom_URID* subject = NULL; + const LV2_Atom_URID* property = NULL; + const LV2_Atom* value = NULL; + lv2_atom_object_get(obj, + uris->patch_subject, (const LV2_Atom**)&subject, + uris->patch_property, (const LV2_Atom**)&property, + uris->patch_value, &value, + 0); + if (!subject_is_plugin(self, subject)) { + lv2_log_error(&self->log, "Set for unknown subject\n"); + } else if (!property) { + lv2_log_error(&self->log, "Set with no property\n"); + } else if (property->atom.type != uris->atom_URID) { + lv2_log_error(&self->log, "Set property is not a URID\n"); + } else { + // Set property to the given value + const LV2_URID key = property->body; + set_parameter(self, key, value->size, value->type, value + 1, false); + } + } else if (obj->body.otype == uris->patch_Get) { + // Get the property of the get message + const LV2_Atom_URID* subject = NULL; + const LV2_Atom_URID* property = NULL; + lv2_atom_object_get(obj, + uris->patch_subject, (const LV2_Atom**)&subject, + uris->patch_property, (const LV2_Atom**)&property, + 0); + if (!subject_is_plugin(self, subject)) { + lv2_log_error(&self->log, "Get with unknown subject\n"); + } else if (!property) { + // Get with no property, emit complete state + lv2_atom_forge_frame_time(&self->forge, ev->time.frames); + LV2_Atom_Forge_Frame pframe; + lv2_atom_forge_object(&self->forge, &pframe, 0, uris->patch_Put); + lv2_atom_forge_key(&self->forge, uris->patch_body); + + LV2_Atom_Forge_Frame bframe; + lv2_atom_forge_object(&self->forge, &bframe, 0, 0); + save(self, write_param_to_forge, &self->forge, 0, NULL); + + lv2_atom_forge_pop(&self->forge, &bframe); + lv2_atom_forge_pop(&self->forge, &pframe); + } else if (property->atom.type != uris->atom_URID) { + lv2_log_error(&self->log, "Get property is not a URID\n"); + } else { + // Get for a specific property + const LV2_URID key = property->body; + const LV2_Atom* value = get_parameter(self, key); + if (value) { + lv2_atom_forge_frame_time(&self->forge, ev->time.frames); + LV2_Atom_Forge_Frame frame; + lv2_atom_forge_object(&self->forge, &frame, 0, uris->patch_Set); + lv2_atom_forge_key(&self->forge, uris->patch_property); + lv2_atom_forge_urid(&self->forge, property->body); + store_prop(self, NULL, NULL, write_param_to_forge, &self->forge, + uris->patch_value, value); + lv2_atom_forge_pop(&self->forge, &frame); + } + } + } else { + lv2_log_trace(&self->log, "Unknown object type <%s>\n", + unmap(self, obj->body.otype)); + } + } + + if (self->state.spring.body > 0.0f) { + const float spring = self->state.spring.body; + self->state.spring.body = (spring >= 0.001) ? spring - 0.001 : 0.0; + lv2_atom_forge_frame_time(&self->forge, 0); + LV2_Atom_Forge_Frame frame; + lv2_atom_forge_object(&self->forge, &frame, 0, uris->patch_Set); + + lv2_atom_forge_key(&self->forge, uris->patch_property); + lv2_atom_forge_urid(&self->forge, uris->eg_spring); + lv2_atom_forge_key(&self->forge, uris->patch_value); + lv2_atom_forge_float(&self->forge, self->state.spring.body); + + lv2_atom_forge_pop(&self->forge, &frame); + } + + lv2_atom_forge_pop(&self->forge, &out_frame); +} + +static const void* +extension_data(const char* uri) +{ + static const LV2_State_Interface state = { save, restore }; + if (!strcmp(uri, LV2_STATE__interface)) { + return &state; + } + return NULL; +} + +static const LV2_Descriptor descriptor = { + EG_PARAMS_URI, + instantiate, + connect_port, + NULL, // activate, + run, + NULL, // deactivate, + cleanup, + extension_data +}; + +LV2_SYMBOL_EXPORT const LV2_Descriptor* +lv2_descriptor(uint32_t index) +{ + return (index == 0) ? &descriptor : NULL; +} diff --git a/plugins/eg-params.lv2/params.ttl b/plugins/eg-params.lv2/params.ttl new file mode 100644 index 0000000..dbcf6aa --- /dev/null +++ b/plugins/eg-params.lv2/params.ttl @@ -0,0 +1,126 @@ +@prefix atom: <http://lv2plug.in/ns/ext/atom#> . +@prefix doap: <http://usefulinc.com/ns/doap#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix param: <http://lv2plug.in/ns/ext/parameters#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix plug: <http://lv2plug.in/plugins/eg-params#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix state: <http://lv2plug.in/ns/ext/state#> . +@prefix urid: <http://lv2plug.in/ns/ext/urid#> . +@prefix xsd: <http://www.w3.org/2001/XMLSchema#> . + +# An existing parameter or RDF property can be used as a parameter. The LV2 +# parameters extension <http://lv2plug.in/ns/ext/parameters> defines many +# common audio parameters. Where possible, existing parameters should be used +# so hosts can intelligently control plugins. + +# If no suitable parameter exists, one can be defined for the plugin like so: + +plug:int + a lv2:Parameter ; + rdfs:label "int" ; + rdfs:range atom:Int . + +plug:long + a lv2:Parameter ; + rdfs:label "long" ; + rdfs:range atom:Long . + +plug:float + a lv2:Parameter ; + rdfs:label "float" ; + rdfs:range atom:Float . + +plug:double + a lv2:Parameter ; + rdfs:label "double" ; + rdfs:range atom:Double . + +plug:bool + a lv2:Parameter ; + rdfs:label "bool" ; + rdfs:range atom:Bool . + +plug:string + a lv2:Parameter ; + rdfs:label "string" ; + rdfs:range atom:String . + +plug:path + a lv2:Parameter ; + rdfs:label "path" ; + rdfs:range atom:Path . + +plug:lfo + a lv2:Parameter ; + rdfs:label "LFO" ; + rdfs:range atom:Float ; + lv2:minimum -1.0 ; + lv2:maximum 1.0 . + +plug:spring + a lv2:Parameter ; + rdfs:label "spring" ; + rdfs:range atom:Float . + +# Most of the plugin description is similar to the others we have seen, but +# this plugin has only two ports, for receiving and sending messages used to +# manipulate and access parameters. +<http://lv2plug.in/plugins/eg-params> + a lv2:Plugin , + lv2:UtilityPlugin ; + doap:name "Example Parameters" ; + doap:license <http://opensource.org/licenses/isc> ; + lv2:project <http://lv2plug.in/ns/lv2> ; + lv2:requiredFeature urid:map ; + lv2:optionalFeature lv2:hardRTCapable , + state:loadDefaultState ; + lv2:extensionData state:interface ; + lv2:port [ + a lv2:InputPort , + atom:AtomPort ; + atom:bufferType atom:Sequence ; + atom:supports patch:Message ; + lv2:designation lv2:control ; + lv2:index 0 ; + lv2:symbol "in" ; + lv2:name "In" + ] , [ + a lv2:OutputPort , + atom:AtomPort ; + atom:bufferType atom:Sequence ; + atom:supports patch:Message ; + lv2:designation lv2:control ; + lv2:index 1 ; + lv2:symbol "out" ; + lv2:name "Out" + ] ; +# The plugin must list all parameters that can be written (e.g. changed by the +# user) as patch:writable: + patch:writable plug:int , + plug:long , + plug:float , + plug:double , + plug:bool , + plug:string , + plug:path , + plug:spring ; +# Similarly, parameters that may change internally must be listed as patch:readable, +# meaning to host should watch for changes to the parameter's value: + patch:readable plug:lfo , + plug:spring ; +# Parameters map directly to properties of the plugin's state. So, we can +# specify initial parameter values with the state:state property. The +# state:loadDefaultState feature (required above) requires that the host loads +# the default state after instantiation but before running the plugin. + state:state [ + plug:int 0 ; + plug:long "0"^^xsd:long ; + plug:float 0.1234 ; + plug:double "0e0"^^xsd:double ; + plug:bool false ; + plug:string "Hello, world" ; + plug:path <params.ttl> ; + plug:spring 0.0 ; + plug:lfo 0.0 + ] . diff --git a/plugins/eg-params.lv2/state_map.h b/plugins/eg-params.lv2/state_map.h new file mode 100644 index 0000000..cd97a80 --- /dev/null +++ b/plugins/eg-params.lv2/state_map.h @@ -0,0 +1,110 @@ +/* + LV2 State Map + Copyright 2016 David Robillard <d@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. +*/ + +#include <stdlib.h> + +/** Entry in an array that serves as a dictionary of properties. */ +typedef struct { + const char* uri; + LV2_URID urid; + LV2_Atom* value; +} StateMapItem; + +/** Comparator for StateMapItems sorted by URID. */ +static int +state_map_cmp(const void* a, const void* b) +{ + const StateMapItem* ka = (const StateMapItem*)a; + const StateMapItem* kb = (const StateMapItem*)b; + if (ka->urid < kb->urid) { + return -1; + } else if (kb->urid < ka->urid) { + return 1; + } + return 0; +} + +/** Helper macro for terse state map initialisation. */ +#define STATE_MAP_INIT(type, ptr) \ + (LV2_ATOM__ ## type), \ + (sizeof(*ptr) - sizeof(LV2_Atom)), \ + (ptr) + +/** + Initialise a state map. + + The variable parameters list must be NULL terminated, and is a sequence of + const char* uri, const char* type, uint32_t size, LV2_Atom* value. The + value must point to a valid atom that resides elsewhere, the state map is + only an index and does not contain actual state values. The macro + STATE_MAP_INIT can be used to make simpler code when state is composed of + standard atom types, for example: + + struct Plugin { + LV2_URID_Map* map; + StateMapItem props[3]; + // ... + }; + + state_map_init( + self->props, self->map, self->map->handle, + PLUG_URI "#gain", STATE_MAP_INIT(Float, &state->gain), + PLUG_URI "#offset", STATE_MAP_INIT(Int, &state->offset), + PLUG_URI "#file", STATE_MAP_INIT(Path, &state->file), + NULL); +*/ +static void +state_map_init(StateMapItem dict[], + LV2_URID_Map* map, + LV2_URID_Map_Handle handle, + /* const char* uri, const char* type, uint32_t size, LV2_Atom* value */ ...) +{ + // Set dict entries from parameters + unsigned i = 0; + va_list args; + va_start(args, handle); + for (const char* uri; (uri = va_arg(args, const char*)); ++i) { + const char* type = va_arg(args, const char*); + const uint32_t size = va_arg(args, uint32_t); + LV2_Atom* const value = va_arg(args, LV2_Atom*); + dict[i].uri = uri; + dict[i].urid = map->map(map->handle, uri); + dict[i].value = value; + dict[i].value->size = size; + dict[i].value->type = map->map(map->handle, type); + } + va_end(args); + + // Sort for fast lookup by URID by state_map_find() + qsort(dict, i, sizeof(StateMapItem), state_map_cmp); +} + +/** + Retrieve an item from a state map by URID. + + This takes O(lg(n)) time, and is useful for implementing generic property + access with little code, for example to respond to patch:Get messages for a + specific property. +*/ +static StateMapItem* +state_map_find(StateMapItem dict[], uint32_t n_entries, LV2_URID urid) +{ + const StateMapItem key = { NULL, urid, NULL }; + return (StateMapItem*)bsearch( + &key, dict, n_entries, sizeof(StateMapItem), state_map_cmp); +} + diff --git a/plugins/eg-params.lv2/wscript b/plugins/eg-params.lv2/wscript new file mode 100644 index 0000000..60dcee4 --- /dev/null +++ b/plugins/eg-params.lv2/wscript @@ -0,0 +1,65 @@ +#!/usr/bin/env python +from waflib.extras import autowaf as autowaf +import re + +# Variables for 'waf dist' +APPNAME = 'eg-params.lv2' +VERSION = '1.0.0' + +# Mandatory variables +top = '.' +out = 'build' + +def options(opt): + opt.load('compiler_c') + opt.load('lv2') + autowaf.set_options(opt) + +def configure(conf): + autowaf.display_header('Params Configuration') + conf.load('compiler_c', cache=True) + conf.load('lv2', cache=True) + conf.load('autowaf', cache=True) + + if not autowaf.is_child(): + autowaf.check_pkg(conf, 'lv2', atleast_version='1.12.1', uselib_store='LV2') + + autowaf.display_msg(conf, 'LV2 bundle directory', conf.env.LV2DIR) + print('') + +def build(bld): + bundle = 'eg-params.lv2' + + # Make a pattern for shared objects without the 'lib' prefix + module_pat = re.sub('^lib', '', bld.env.cshlib_PATTERN) + module_ext = module_pat[module_pat.rfind('.'):] + + # Build manifest.ttl by substitution (for portable lib extension) + bld(features = 'subst', + source = 'manifest.ttl.in', + target = '%s/%s' % (bundle, 'manifest.ttl'), + install_path = '${LV2DIR}/%s' % bundle, + LIB_EXT = module_ext) + + # Copy other data files to build bundle (build/eg-params.lv2) + for i in ['params.ttl']: + bld(features = 'subst', + is_copy = True, + source = i, + target = '%s/%s' % (bundle, i), + install_path = '${LV2DIR}/%s' % bundle) + + # Use LV2 headers from parent directory if building as a sub-project + includes = ['.'] + if autowaf.is_child: + includes += ['../..'] + + # Build plugin library + obj = bld(features = 'c cshlib', + source = 'params.c', + name = 'params', + target = '%s/params' % bundle, + install_path = '${LV2DIR}/%s' % bundle, + use = 'LV2', + includes = includes) + obj.env.cshlib_PATTERN = module_pat diff --git a/plugins/eg-sampler.lv2/README.txt b/plugins/eg-sampler.lv2/README.txt new file mode 100644 index 0000000..8d136fa --- /dev/null +++ b/plugins/eg-sampler.lv2/README.txt @@ -0,0 +1,14 @@ +== Sampler == + +This plugin loads a single sample from a .wav file and plays it back when a MIDI +note on is received. Any sample on the system can be loaded via another event. +A Gtk UI is included which does this, but the host can as well. + +This plugin illustrates: + +- UI <==> Plugin communication via events +- Use of the worker extension for non-realtime tasks (sample loading) +- Use of the log extension to print log messages via the host +- Saving plugin state via the state extension +- Dynamic plugin control via the same properties saved to state +- Network-transparent waveform display with incremental peak transmission diff --git a/plugins/eg-sampler.lv2/atom_sink.h b/plugins/eg-sampler.lv2/atom_sink.h new file mode 100644 index 0000000..ae3df30 --- /dev/null +++ b/plugins/eg-sampler.lv2/atom_sink.h @@ -0,0 +1,40 @@ +/* + Copyright 2016 David Robillard <d@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. +*/ + +/** + A forge sink that writes to an atom buffer. + + It is assumed that the handle points to an LV2_Atom large enough to store + the forge output. The forged result is in the body of the buffer atom. +*/ +static LV2_Atom_Forge_Ref +atom_sink(LV2_Atom_Forge_Sink_Handle handle, const void* buf, uint32_t size) +{ + LV2_Atom* atom = (LV2_Atom*)handle; + const uint32_t offset = lv2_atom_total_size(atom); + memcpy((char*)atom + offset, buf, size); + atom->size += size; + return offset; +} + +/** + Dereference counterpart to atom_sink(). +*/ +static LV2_Atom* +atom_sink_deref(LV2_Atom_Forge_Sink_Handle handle, LV2_Atom_Forge_Ref ref) +{ + return (LV2_Atom*)((char*)handle + ref); +} diff --git a/plugins/eg-sampler.lv2/click.wav b/plugins/eg-sampler.lv2/click.wav Binary files differnew file mode 100644 index 0000000..520a18c --- /dev/null +++ b/plugins/eg-sampler.lv2/click.wav diff --git a/plugins/eg-sampler.lv2/manifest.ttl.in b/plugins/eg-sampler.lv2/manifest.ttl.in new file mode 100644 index 0000000..8a01428 --- /dev/null +++ b/plugins/eg-sampler.lv2/manifest.ttl.in @@ -0,0 +1,19 @@ +# Unlike the previous examples, this manifest lists more than one resource: the +# plugin as usual, and the UI. The descriptions are similar, but have +# different types, so the host can decide from this file alone whether or not +# it is interested, and avoid following the `rdfs:seeAlso` link if not (though +# in this case both are described in the same file). + +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix ui: <http://lv2plug.in/ns/extensions/ui#> . + +<http://lv2plug.in/plugins/eg-sampler> + a lv2:Plugin ; + lv2:binary <sampler@LIB_EXT@> ; + rdfs:seeAlso <sampler.ttl> . + +<http://lv2plug.in/plugins/eg-sampler#ui> + a ui:GtkUI ; + ui:binary <sampler_ui@LIB_EXT@> ; + rdfs:seeAlso <sampler.ttl> . diff --git a/plugins/eg-sampler.lv2/peaks.h b/plugins/eg-sampler.lv2/peaks.h new file mode 100644 index 0000000..69688b8 --- /dev/null +++ b/plugins/eg-sampler.lv2/peaks.h @@ -0,0 +1,270 @@ +/* + LV2 audio peaks utilities + Copyright 2016 David Robillard <d@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. +*/ + +/** + This file defines utilities for sending and receiving audio peaks for + waveform display. The functionality is divided into two objects: + PeaksSender, for sending peaks updates from the plugin, and PeaksReceiver, + for receiving such updates and caching the peaks. + + This allows peaks for a waveform of any size at any resolution to be + requested, with reasonably sized incremental updates sent over plugin ports. +*/ + +#ifndef PEAKS_H_INCLUDED +#define PEAKS_H_INCLUDED + +#include <math.h> + +#include "lv2/lv2plug.in/ns/ext/atom/forge.h" + +#define PEAKS_URI "http://lv2plug.in/ns/peaks#" +#define PEAKS__PeakUpdate PEAKS_URI "PeakUpdate" +#define PEAKS__magnitudes PEAKS_URI "magnitudes" +#define PEAKS__offset PEAKS_URI "offset" +#define PEAKS__total PEAKS_URI "total" + +#ifndef MIN +# define MIN(a, b) (((a) < (b)) ? (a) : (b)) +#endif +#ifndef MAX +# define MAX(a, b) (((a) > (b)) ? (a) : (b)) +#endif + +typedef struct { + LV2_URID atom_Float; + LV2_URID atom_Int; + LV2_URID atom_Vector; + LV2_URID peaks_PeakUpdate; + LV2_URID peaks_magnitudes; + LV2_URID peaks_offset; + LV2_URID peaks_total; +} PeaksURIs; + +typedef struct { + PeaksURIs uris; ///< URIDs used in protocol + const float* samples; ///< Sample data + uint32_t n_samples; ///< Total number of samples + uint32_t n_peaks; ///< Total number of peaks + uint32_t current_offset; ///< Current peak offset + bool sending; ///< True iff currently sending +} PeaksSender; + +typedef struct { + PeaksURIs uris; ///< URIDs used in protocol + float* peaks; ///< Received peaks, or zeroes + uint32_t n_peaks; ///< Total number of peaks +} PeaksReceiver; + +/** + Map URIs used in the peaks protocol. +*/ +static inline void +peaks_map_uris(PeaksURIs* uris, LV2_URID_Map* map) +{ + uris->atom_Float = map->map(map->handle, LV2_ATOM__Float); + uris->atom_Int = map->map(map->handle, LV2_ATOM__Int); + uris->atom_Vector = map->map(map->handle, LV2_ATOM__Vector); + uris->peaks_PeakUpdate = map->map(map->handle, PEAKS__PeakUpdate); + uris->peaks_magnitudes = map->map(map->handle, PEAKS__magnitudes); + uris->peaks_offset = map->map(map->handle, PEAKS__offset); + uris->peaks_total = map->map(map->handle, PEAKS__total); +} + +/** + Initialise peaks sender. The new sender is inactive and will do nothing + when `peaks_sender_send()` is called, until a transmission is started with + `peaks_sender_start()`. +*/ +static inline PeaksSender* +peaks_sender_init(PeaksSender* sender, LV2_URID_Map* map) +{ + memset(sender, 0, sizeof(*sender)); + peaks_map_uris(&sender->uris, map); + return sender; +} + +/** + Prepare to start a new peaks transmission. After this is called, the peaks + can be sent with successive calls to `peaks_sender_send()`. +*/ +static inline void +peaks_sender_start(PeaksSender* sender, + const float* samples, + uint32_t n_samples, + uint32_t n_peaks) +{ + sender->samples = samples; + sender->n_samples = n_samples; + sender->n_peaks = n_peaks; + sender->current_offset = 0; + sender->sending = true; +} + +/** + Forge a message which sends a range of peaks. Writes a peaks:PeakUpdate + object to `forge`, like: + + [source,n3] + ---- + [] + a peaks:PeakUpdate ; + peaks:offset 256 ; + peaks:total 1024 ; + peaks:magnitudes [ 0.2f, 0.3f, ... ] . + ---- +*/ +static inline bool +peaks_sender_send(PeaksSender* sender, + LV2_Atom_Forge* forge, + uint32_t n_frames, + uint32_t offset) +{ + const PeaksURIs* uris = &sender->uris; + if (!sender->sending || sender->current_offset >= sender->n_peaks) { + return sender->sending = false; + } + + // Start PeakUpdate object + lv2_atom_forge_frame_time(forge, offset); + LV2_Atom_Forge_Frame frame; + lv2_atom_forge_object(forge, &frame, 0, uris->peaks_PeakUpdate); + + // eg:offset = OFFSET + lv2_atom_forge_key(forge, uris->peaks_offset); + lv2_atom_forge_int(forge, sender->current_offset); + + // eg:total = TOTAL + lv2_atom_forge_key(forge, uris->peaks_total); + lv2_atom_forge_int(forge, sender->n_peaks); + + // eg:magnitudes = Vector<Float>(PEAK, PEAK, ...) + lv2_atom_forge_key(forge, uris->peaks_magnitudes); + LV2_Atom_Forge_Frame vec_frame; + lv2_atom_forge_vector_head( + forge, &vec_frame, sizeof(float), uris->atom_Float); + + // Calculate how many peaks to send this update + const int chunk_size = MAX(1, sender->n_samples / sender->n_peaks); + const uint32_t space = forge->size - forge->offset; + const uint32_t remaining = sender->n_peaks - sender->current_offset; + const int n_update = MIN(remaining, + MIN(n_frames / 4, space / sizeof(float))); + + // Calculate peak (maximum magnitude) for each chunk + for (int i = 0; i < n_update; ++i) { + const int start = (sender->current_offset + i) * chunk_size; + float peak = 0.0f; + for (int j = 0; j < chunk_size; ++j) { + peak = fmaxf(peak, fabsf(sender->samples[start + j])); + } + lv2_atom_forge_float(forge, peak); + } + + // Finish message + lv2_atom_forge_pop(forge, &vec_frame); + lv2_atom_forge_pop(forge, &frame); + + sender->current_offset += n_update; + return true; +} + +/** + Initialise a peaks receiver. The receiver stores an array of all peaks, + which is updated incrementally with peaks_receiver_receive(). +*/ +static inline PeaksReceiver* +peaks_receiver_init(PeaksReceiver* receiver, LV2_URID_Map* map) +{ + memset(receiver, 0, sizeof(*receiver)); + peaks_map_uris(&receiver->uris, map); + return receiver; +} + +/** + Clear stored peaks and free all memory. This should be called when the + peaks are to be updated with a different audio source. +*/ +static inline void +peaks_receiver_clear(PeaksReceiver* receiver) +{ + free(receiver->peaks); + receiver->peaks = NULL; + receiver->n_peaks = 0; +} + +/** + Handle PeakUpdate message. + + The stored peaks array is updated with the slice of peaks in `update`, + resizing if necessary while preserving contents. + + Returns 0 if peaks have been updated, negative on error. +*/ +static inline int +peaks_receiver_receive(PeaksReceiver* receiver, const LV2_Atom_Object* update) +{ + const PeaksURIs* uris = &receiver->uris; + + // Get properties of interest from update + const LV2_Atom_Int* offset = NULL; + const LV2_Atom_Int* total = NULL; + const LV2_Atom_Vector* peaks = NULL; + lv2_atom_object_get_typed(update, + uris->peaks_offset, &offset, uris->atom_Int, + uris->peaks_total, &total, uris->atom_Int, + uris->peaks_magnitudes, &peaks, uris->atom_Vector, + 0); + + if (!offset || !total || !peaks || + peaks->body.child_type != uris->atom_Float) { + return -1; // Invalid update + } + + const uint32_t n = (uint32_t)total->body; + if (receiver->n_peaks != n) { + // Update is for a different total number of peaks, resize + receiver->peaks = (float*)realloc(receiver->peaks, n * sizeof(float)); + if (receiver->n_peaks > 0 && receiver->n_peaks < n) { + /* The peaks array is being expanded. Copy the old peaks, + duplicating each as necessary to fill the new peaks buffer. + This preserves the current peaks so that the peaks array can be + reasonably drawn at any time, but the resolution will increase + as new updates arrive. */ + const int n_per = n / receiver->n_peaks; + for (int i = n - 1; i >= 0; --i) { + receiver->peaks[i] = receiver->peaks[i / n_per]; + } + } else if (receiver->n_peaks > 0) { + /* The peak array is being shrunk. Similar to the above. */ + const int n_per = receiver->n_peaks / n; + for (int i = n - 1; i >= 0; --i) { + receiver->peaks[i] = receiver->peaks[i * n_per]; + } + } + receiver->n_peaks = n; + } + + // Copy vector contents to corresponding range in peaks array + memcpy(receiver->peaks + offset->body, + peaks + 1, + peaks->atom.size - sizeof(LV2_Atom_Vector_Body)); + + return 0; +} + +#endif // PEAKS_H_INCLUDED diff --git a/plugins/eg-sampler.lv2/sampler.c b/plugins/eg-sampler.lv2/sampler.c new file mode 100644 index 0000000..aa8a1c1 --- /dev/null +++ b/plugins/eg-sampler.lv2/sampler.c @@ -0,0 +1,603 @@ +/* + LV2 Sampler Example Plugin + Copyright 2011-2016 David Robillard <d@drobilla.net> + Copyright 2011 Gabriel M. Beddingfield <gabriel@teuton.org> + Copyright 2011 James Morris <jwm.art.net@gmail.com> + + 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 <math.h> +#include <stdlib.h> +#include <string.h> +#ifndef __cplusplus +# include <stdbool.h> +#endif + +#include <sndfile.h> + +#include "lv2/lv2plug.in/ns/ext/atom/forge.h" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "lv2/lv2plug.in/ns/ext/log/log.h" +#include "lv2/lv2plug.in/ns/ext/log/logger.h" +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" +#include "lv2/lv2plug.in/ns/ext/patch/patch.h" +#include "lv2/lv2plug.in/ns/ext/state/state.h" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" +#include "lv2/lv2plug.in/ns/ext/worker/worker.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2_util.h" + +#include "atom_sink.h" +#include "peaks.h" +#include "uris.h" + +enum { + SAMPLER_CONTROL = 0, + SAMPLER_NOTIFY = 1, + SAMPLER_OUT = 2 +}; + +typedef struct { + SF_INFO info; // Info about sample from sndfile + float* data; // Sample data in float + char* path; // Path of file + uint32_t path_len; // Length of path +} Sample; + +typedef struct { + // Features + LV2_URID_Map* map; + LV2_Worker_Schedule* schedule; + LV2_Log_Logger logger; + + // Ports + const LV2_Atom_Sequence* control_port; + LV2_Atom_Sequence* notify_port; + float* output_port; + + // Communication utilities + LV2_Atom_Forge_Frame notify_frame; ///< Cached for worker replies + LV2_Atom_Forge forge; ///< Forge for writing atoms in run thread + PeaksSender psend; ///< Audio peaks sender + + // URIs + SamplerURIs uris; + + // Playback state + Sample* sample; + uint32_t frame_offset; + float gain; + sf_count_t frame; + bool play; + bool activated; + bool sample_changed; +} Sampler; + +/** + An atom-like message used internally to apply/free samples. + + This is only used internally to communicate with the worker, it is never + sent to the outside world via a port since it is not POD. It is convenient + to use an Atom header so actual atoms can be easily sent through the same + ringbuffer. +*/ +typedef struct { + LV2_Atom atom; + Sample* sample; +} SampleMessage; + +/** + Load a new sample and return it. + + Since this is of course not a real-time safe action, this is called in the + worker thread only. The sample is loaded and returned only, plugin state is + not modified. +*/ +static Sample* +load_sample(LV2_Log_Logger* logger, const char* path) +{ + lv2_log_trace(logger, "Loading %s\n", path); + + const size_t path_len = strlen(path); + Sample* const sample = (Sample*)calloc(1, sizeof(Sample)); + SF_INFO* const info = &sample->info; + SNDFILE* const sndfile = sf_open(path, SFM_READ, info); + float* data = NULL; + bool error = true; + if (!sndfile || !info->frames) { + lv2_log_error(logger, "Failed to open %s\n", path); + } else if (info->channels != 1) { + lv2_log_error(logger, "%s has %d channels\n", path, info->channels); + } else if (!(data = (float*)malloc(sizeof(float) * info->frames))) { + lv2_log_error(logger, "Failed to allocate memory for sample\n"); + } else { + error = false; + } + + if (error) { + free(sample); + free(data); + sf_close(sndfile); + return NULL; + } + + sf_seek(sndfile, 0ul, SEEK_SET); + sf_read_float(sndfile, data, info->frames); + sf_close(sndfile); + + // Fill sample struct and return it + sample->data = data; + sample->path = (char*)malloc(path_len + 1); + sample->path_len = (uint32_t)path_len; + memcpy(sample->path, path, path_len + 1); + + return sample; +} + +static void +free_sample(Sampler* self, Sample* sample) +{ + if (sample) { + lv2_log_trace(&self->logger, "Freeing %s\n", sample->path); + free(sample->path); + free(sample->data); + free(sample); + } +} + +/** + Do work in a non-realtime thread. + + This is called for every piece of work scheduled in the audio thread using + self->schedule->schedule_work(). A reply can be sent back to the audio + thread using the provided `respond` function. +*/ +static LV2_Worker_Status +work(LV2_Handle instance, + LV2_Worker_Respond_Function respond, + LV2_Worker_Respond_Handle handle, + uint32_t size, + const void* data) +{ + Sampler* self = (Sampler*)instance; + const LV2_Atom* atom = (const LV2_Atom*)data; + if (atom->type == self->uris.eg_freeSample) { + // Free old sample + const SampleMessage* msg = (const SampleMessage*)data; + free_sample(self, msg->sample); + } else if (atom->type == self->forge.Object) { + // Handle set message (load sample). + const LV2_Atom_Object* obj = (const LV2_Atom_Object*)data; + const char* path = read_set_file(&self->uris, obj); + if (!path) { + lv2_log_error(&self->logger, "Malformed set file request\n"); + return LV2_WORKER_ERR_UNKNOWN; + } + + // Load sample. + Sample* sample = load_sample(&self->logger, path); + if (sample) { + // Send new sample to run() to be applied + respond(handle, sizeof(sample), &sample); + } + } + + return LV2_WORKER_SUCCESS; +} + +/** + Handle a response from work() in the audio thread. + + When running normally, this will be called by the host after run(). When + freewheeling, this will be called immediately at the point the work was + scheduled. +*/ +static LV2_Worker_Status +work_response(LV2_Handle instance, + uint32_t size, + const void* data) +{ + Sampler* self = (Sampler*)instance; + Sample* old_sample = self->sample; + Sample* new_sample = *(Sample*const*)data; + + // Install the new sample + self->sample = *(Sample*const*)data; + + // Schedule work to free the old sample + SampleMessage msg = { { sizeof(Sample*), self->uris.eg_freeSample }, + old_sample }; + self->schedule->schedule_work(self->schedule->handle, sizeof(msg), &msg); + + // Send a notification that we're using a new sample + lv2_atom_forge_frame_time(&self->forge, self->frame_offset); + write_set_file(&self->forge, &self->uris, + new_sample->path, + new_sample->path_len); + + return LV2_WORKER_SUCCESS; +} + +static void +connect_port(LV2_Handle instance, + uint32_t port, + void* data) +{ + Sampler* self = (Sampler*)instance; + switch (port) { + case SAMPLER_CONTROL: + self->control_port = (const LV2_Atom_Sequence*)data; + break; + case SAMPLER_NOTIFY: + self->notify_port = (LV2_Atom_Sequence*)data; + break; + case SAMPLER_OUT: + self->output_port = (float*)data; + break; + default: + break; + } +} + +static LV2_Handle +instantiate(const LV2_Descriptor* descriptor, + double rate, + const char* path, + const LV2_Feature* const* features) +{ + // Allocate and initialise instance structure. + Sampler* self = (Sampler*)calloc(1, sizeof(Sampler)); + if (!self) { + return NULL; + } + + // Get host features + const char* missing = lv2_features_query( + features, + LV2_LOG__log, &self->logger.log, false, + LV2_URID__map, &self->map, true, + LV2_WORKER__schedule, &self->schedule, true, + NULL); + lv2_log_logger_set_map(&self->logger, self->map); + if (missing) { + lv2_log_error(&self->logger, "Missing feature <%s>\n", missing); + free(self); + return NULL; + } + + // Map URIs and initialise forge + map_sampler_uris(self->map, &self->uris); + lv2_atom_forge_init(&self->forge, self->map); + peaks_sender_init(&self->psend, self->map); + + self->gain = 1.0; + + return (LV2_Handle)self; +} + +static void +cleanup(LV2_Handle instance) +{ + Sampler* self = (Sampler*)instance; + free_sample(self, self->sample); + free(self); +} + +static void +activate(LV2_Handle instance) +{ + ((Sampler*)instance)->activated = true; +} + +static void +deactivate(LV2_Handle instance) +{ + ((Sampler*)instance)->activated = false; +} + +/** Define a macro for converting a gain in dB to a coefficient. */ +#define DB_CO(g) ((g) > -90.0f ? powf(10.0f, (g) * 0.05f) : 0.0f) + +/** + Handle an incoming event in the audio thread. + + This performs any actions triggered by an event, such as the start of sample + playback, a sample change, or responding to requests from the UI. +*/ +static void +handle_event(Sampler* self, LV2_Atom_Event* ev) +{ + SamplerURIs* uris = &self->uris; + PeaksURIs* peaks_uris = &self->psend.uris; + + if (ev->body.type == uris->midi_Event) { + const uint8_t* const msg = (const uint8_t*)(ev + 1); + switch (lv2_midi_message_type(msg)) { + case LV2_MIDI_MSG_NOTE_ON: + self->frame = 0; + self->play = true; + break; + default: + break; + } + } else if (lv2_atom_forge_is_object_type(&self->forge, ev->body.type)) { + const LV2_Atom_Object* obj = (const LV2_Atom_Object*)&ev->body; + if (obj->body.otype == uris->patch_Set) { + // Get the property and value of the set message + const LV2_Atom* property = NULL; + const LV2_Atom* value = NULL; + lv2_atom_object_get(obj, + uris->patch_property, &property, + uris->patch_value, &value, + 0); + if (!property) { + lv2_log_error(&self->logger, "Set message with no property\n"); + return; + } else if (property->type != uris->atom_URID) { + lv2_log_error(&self->logger, "Set property is not a URID\n"); + return; + } + + const uint32_t key = ((const LV2_Atom_URID*)property)->body; + if (key == uris->eg_sample) { + // Sample change, send it to the worker. + lv2_log_trace(&self->logger, "Scheduling sample change\n"); + self->schedule->schedule_work(self->schedule->handle, + lv2_atom_total_size(&ev->body), + &ev->body); + } else if (key == uris->param_gain) { + // Gain change + if (value->type == uris->atom_Float) { + self->gain = DB_CO(((LV2_Atom_Float*)value)->body); + } + } + } else if (obj->body.otype == uris->patch_Get && self->sample) { + const LV2_Atom_URID* accept = NULL; + const LV2_Atom_Int* n_peaks = NULL; + lv2_atom_object_get_typed( + obj, + uris->patch_accept, &accept, uris->atom_URID, + peaks_uris->peaks_total, &n_peaks, peaks_uris->atom_Int, 0); + if (accept && accept->body == peaks_uris->peaks_PeakUpdate) { + // Received a request for peaks, prepare for transmission + peaks_sender_start(&self->psend, + self->sample->data, + self->sample->info.frames, + n_peaks->body); + } else { + // Received a get message, emit our state (probably to UI) + lv2_atom_forge_frame_time(&self->forge, self->frame_offset); + write_set_file(&self->forge, &self->uris, + self->sample->path, + self->sample->path_len); + } + } else { + lv2_log_trace(&self->logger, + "Unknown object type %d\n", obj->body.otype); + } + } else { + lv2_log_trace(&self->logger, + "Unknown event type %d\n", ev->body.type); + } + +} + +/** + Output audio for a slice of the current cycle. +*/ +static void +render(Sampler* self, uint32_t start, uint32_t end) +{ + float* output = self->output_port; + + if (self->play && self->sample) { + // Start/continue writing sample to output + for (; start < end; ++start) { + output[start] = self->sample->data[self->frame] * self->gain; + if (++self->frame == self->sample->info.frames) { + self->play = false; // Reached end of sample + break; + } + } + } + + // Write silence to remaining buffer + for (; start < end; ++start) { + output[start] = 0.0f; + } +} + +static void +run(LV2_Handle instance, uint32_t sample_count) +{ + Sampler* self = (Sampler*)instance; + + // Set up forge to write directly to notify output port. + const uint32_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); + + // Send update to UI if sample has changed due to state restore + if (self->sample_changed) { + lv2_atom_forge_frame_time(&self->forge, 0); + write_set_file(&self->forge, &self->uris, + self->sample->path, + self->sample->path_len); + self->sample_changed = false; + } + + // Iterate over incoming events, emitting audio along the way + self->frame_offset = 0; + LV2_ATOM_SEQUENCE_FOREACH(self->control_port, ev) { + // Render output up to the time of this event + render(self, self->frame_offset, ev->time.frames); + + /* Update current frame offset to this event's time. This is stored in + the instance because it is used for sychronous worker event + execution. This allows a sample load event to be executed with + sample accuracy when running in a non-realtime context (such as + exporting a session). */ + self->frame_offset = ev->time.frames; + + // Process this event + handle_event(self, ev); + } + + // Use available space after any emitted events to send peaks + peaks_sender_send(&self->psend, &self->forge, sample_count, self->frame_offset); + + // Render output for the rest of the cycle past the last event + render(self, self->frame_offset, sample_count); +} + +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) +{ + Sampler* self = (Sampler*)instance; + if (!self->sample) { + return LV2_STATE_SUCCESS; + } + + LV2_State_Map_Path* map_path = (LV2_State_Map_Path*)lv2_features_data( + features, LV2_STATE__mapPath); + if (!map_path) { + return LV2_STATE_ERR_NO_FEATURE; + } + + // Map absolute sample path to an abstract state path + char* apath = map_path->abstract_path(map_path->handle, self->sample->path); + + // Store eg:sample = abstract path + store(handle, + self->uris.eg_sample, + apath, + strlen(apath) + 1, + self->uris.atom_Path, + LV2_STATE_IS_POD | LV2_STATE_IS_PORTABLE); + + free(apath); + return LV2_STATE_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) +{ + Sampler* self = (Sampler*)instance; + + // Get host features + LV2_Worker_Schedule* schedule = NULL; + LV2_State_Map_Path* paths = NULL; + const char* missing = lv2_features_query( + features, + LV2_STATE__mapPath, &paths, true, + LV2_WORKER__schedule, &schedule, false, + NULL); + if (missing) { + lv2_log_error(&self->logger, "Missing feature <%s>\n", missing); + return LV2_STATE_ERR_NO_FEATURE; + } + + // Get eg:sample from state + size_t size; + uint32_t type; + uint32_t valflags; + const void* value = retrieve(handle, self->uris.eg_sample, + &size, &type, &valflags); + if (!value) { + lv2_log_error(&self->logger, "Missing eg:sample\n"); + return LV2_STATE_ERR_NO_PROPERTY; + } else if (type != self->uris.atom_Path) { + lv2_log_error(&self->logger, "Non-path eg:sample\n"); + return LV2_STATE_ERR_BAD_TYPE; + } + + // Map abstract state path to absolute path + const char* apath = (const char*)value; + char* path = paths->absolute_path(paths->handle, apath); + + // Replace current sample with the new one + if (!self->activated || !schedule) { + // No scheduling available, load sample immediately + lv2_log_trace(&self->logger, "Synchronous restore\n"); + Sample* sample = load_sample(&self->logger, path); + if (sample) { + free_sample(self, self->sample); + self->sample = sample; + self->sample_changed = true; + } + } else { + // Schedule sample to be loaded by the provided worker + lv2_log_trace(&self->logger, "Scheduling restore\n"); + LV2_Atom_Forge forge; + LV2_Atom* buf = (LV2_Atom*)calloc(1, strlen(path) + 128); + lv2_atom_forge_init(&forge, self->map); + lv2_atom_forge_set_sink(&forge, atom_sink, atom_sink_deref, buf); + write_set_file(&forge, &self->uris, path, strlen(path)); + + const uint32_t msg_size = lv2_atom_pad_size(buf->size); + schedule->schedule_work(self->schedule->handle, msg_size, buf + 1); + free(buf); + } + + free(path); + + return LV2_STATE_SUCCESS; +} + +static const void* +extension_data(const char* uri) +{ + static const LV2_State_Interface state = { save, restore }; + static const LV2_Worker_Interface worker = { work, work_response, NULL }; + if (!strcmp(uri, LV2_STATE__interface)) { + return &state; + } else if (!strcmp(uri, LV2_WORKER__interface)) { + return &worker; + } + return NULL; +} + +static const LV2_Descriptor descriptor = { + EG_SAMPLER_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/plugins/eg-sampler.lv2/sampler.ttl b/plugins/eg-sampler.lv2/sampler.ttl new file mode 100644 index 0000000..92570e5 --- /dev/null +++ b/plugins/eg-sampler.lv2/sampler.ttl @@ -0,0 +1,71 @@ +@prefix atom: <http://lv2plug.in/ns/ext/atom#> . +@prefix doap: <http://usefulinc.com/ns/doap#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix state: <http://lv2plug.in/ns/ext/state#> . +@prefix ui: <http://lv2plug.in/ns/extensions/ui#> . +@prefix urid: <http://lv2plug.in/ns/ext/urid#> . +@prefix work: <http://lv2plug.in/ns/ext/worker#> . +@prefix param: <http://lv2plug.in/ns/ext/parameters#> . + +<http://lv2plug.in/plugins/eg-sampler#sample> + a lv2:Parameter ; + rdfs:label "sample" ; + rdfs:range atom:Path . + +<http://lv2plug.in/plugins/eg-sampler> + a lv2:Plugin ; + doap:name "Exampler" ; + doap:license <http://opensource.org/licenses/isc> ; + lv2:project <http://lv2plug.in/ns/lv2> ; + lv2:requiredFeature state:loadDefaultState , + urid:map , + work:schedule ; + lv2:optionalFeature lv2:hardRTCapable , + state:threadSafeRestore ; + lv2:extensionData state:interface , + work:interface ; + ui:ui <http://lv2plug.in/plugins/eg-sampler#ui> ; + patch:writable <http://lv2plug.in/plugins/eg-sampler#sample> , + param:gain ; + lv2:port [ + a lv2:InputPort , + atom:AtomPort ; + atom:bufferType atom:Sequence ; + atom:supports <http://lv2plug.in/ns/ext/midi#MidiEvent> , + patch:Message ; + 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" ; + lv2:name "Out" + ] ; + state:state [ + <http://lv2plug.in/plugins/eg-sampler#sample> <click.wav> ; + param:gain 1.0 + ] . + +<http://lv2plug.in/plugins/eg-sampler#ui> + a ui:GtkUI ; + lv2:requiredFeature urid:map ; + lv2:extensionData ui:showInterface ; + ui:portNotification [ + ui:plugin <http://lv2plug.in/plugins/eg-sampler> ; + lv2:symbol "notify" ; + ui:notifyType atom:Blank + ] . diff --git a/plugins/eg-sampler.lv2/sampler_ui.c b/plugins/eg-sampler.lv2/sampler_ui.c new file mode 100644 index 0000000..ac4601a --- /dev/null +++ b/plugins/eg-sampler.lv2/sampler_ui.c @@ -0,0 +1,383 @@ +/* + LV2 Sampler Example Plugin UI + Copyright 2011-2016 David Robillard <d@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. +*/ + +#include <stdlib.h> + +#include <gtk/gtk.h> + +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" +#include "lv2/lv2plug.in/ns/ext/atom/forge.h" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "lv2/lv2plug.in/ns/ext/log/logger.h" +#include "lv2/lv2plug.in/ns/ext/patch/patch.h" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" +#include "lv2/lv2plug.in/ns/extensions/ui/ui.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2_util.h" + +#include "peaks.h" +#include "uris.h" + +#define SAMPLER_UI_URI "http://lv2plug.in/plugins/eg-sampler#ui" + +#define MIN_CANVAS_W 128 +#define MIN_CANVAS_H 80 + +typedef struct { + LV2_Atom_Forge forge; + LV2_URID_Map* map; + LV2_Log_Logger logger; + SamplerURIs uris; + PeaksReceiver precv; + + LV2UI_Write_Function write; + LV2UI_Controller controller; + + GtkWidget* box; + GtkWidget* play_button; + GtkWidget* file_button; + GtkWidget* button_box; + GtkWidget* canvas; + GtkWidget* window; /* For optional show interface. */ + + uint32_t width; + uint32_t requested_n_peaks; + char* filename; + + uint8_t forge_buf[1024]; +} SamplerUI; + +static void +on_file_set(GtkFileChooserButton* widget, void* handle) +{ + SamplerUI* ui = (SamplerUI*)handle; + + // Get the filename from the file chooser + char* filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget)); + + // Write a set message to the plugin to load new file + lv2_atom_forge_set_buffer(&ui->forge, ui->forge_buf, sizeof(ui->forge_buf)); + LV2_Atom* msg = (LV2_Atom*)write_set_file(&ui->forge, &ui->uris, + filename, strlen(filename)); + + ui->write(ui->controller, 0, lv2_atom_total_size(msg), + ui->uris.atom_eventTransfer, + msg); + + g_free(filename); +} + +static void +on_play_clicked(GtkFileChooserButton* widget, void* handle) +{ + SamplerUI* ui = (SamplerUI*)handle; + struct { + LV2_Atom atom; + uint8_t msg[3]; + } note_on; + + note_on.atom.type = ui->uris.midi_Event; + note_on.atom.size = 3; + note_on.msg[0] = LV2_MIDI_MSG_NOTE_ON; + note_on.msg[1] = 60; + note_on.msg[2] = 60; + ui->write(ui->controller, 0, sizeof(note_on), + ui->uris.atom_eventTransfer, + ¬e_on); +} + +static void +request_peaks(SamplerUI* ui, uint32_t n_peaks) +{ + if (n_peaks == ui->requested_n_peaks) { + return; + } + + lv2_atom_forge_set_buffer(&ui->forge, ui->forge_buf, sizeof(ui->forge_buf)); + + LV2_Atom_Forge_Frame frame; + lv2_atom_forge_object(&ui->forge, &frame, 0, ui->uris.patch_Get); + lv2_atom_forge_key(&ui->forge, ui->uris.patch_accept); + lv2_atom_forge_urid(&ui->forge, ui->precv.uris.peaks_PeakUpdate); + lv2_atom_forge_key(&ui->forge, ui->precv.uris.peaks_total); + lv2_atom_forge_int(&ui->forge, n_peaks); + lv2_atom_forge_pop(&ui->forge, &frame); + + LV2_Atom* msg = lv2_atom_forge_deref(&ui->forge, frame.ref); + ui->write(ui->controller, 0, lv2_atom_total_size(msg), + ui->uris.atom_eventTransfer, + msg); + + ui->requested_n_peaks = n_peaks; +} + +/** Set Cairo color to a GDK color (to follow Gtk theme). */ +static void +cairo_set_source_gdk(cairo_t* cr, const GdkColor* color) +{ + cairo_set_source_rgb( + cr, color->red / 65535.0, color->green / 65535.0, color->blue / 65535.0); + +} + +static gboolean +on_canvas_expose(GtkWidget* widget, GdkEventExpose* event, gpointer data) +{ + SamplerUI* ui = (SamplerUI*)data; + + GtkAllocation size; + gtk_widget_get_allocation(widget, &size); + + ui->width = size.width; + if ((uint32_t)ui->width > 2 * ui->requested_n_peaks) { + request_peaks(ui, 2 * ui->requested_n_peaks); + } + + cairo_t* cr = gdk_cairo_create(gtk_widget_get_window(widget)); + + cairo_set_line_width(cr, 1.0); + cairo_translate(cr, 0.5, 0.5); + + const int mid_y = size.height / 2; + + const float* const peaks = ui->precv.peaks; + const int32_t n_peaks = ui->precv.n_peaks; + if (peaks) { + // Draw waveform + const double scale = size.width / ((double)n_peaks - 1.0f); + + // Start at left origin + cairo_move_to(cr, 0, mid_y); + + // Draw line through top peaks + for (int i = 0; i < n_peaks; ++i) { + const float peak = peaks[i]; + cairo_line_to(cr, i * scale, mid_y + (peak / 2.0f) * size.height); + } + + // Continue through bottom peaks + for (int i = n_peaks - 1; i >= 0; --i) { + const float peak = peaks[i]; + cairo_line_to(cr, i * scale, mid_y - (peak / 2.0f) * size.height); + } + + // Close shape + cairo_line_to(cr, 0, mid_y); + + cairo_set_source_gdk(cr, widget->style->mid); + cairo_fill_preserve(cr); + + cairo_set_source_gdk(cr, widget->style->fg); + cairo_stroke(cr); + } + + cairo_destroy(cr); + return TRUE; +} + +static LV2UI_Handle +instantiate(const LV2UI_Descriptor* descriptor, + const char* plugin_uri, + const char* bundle_path, + LV2UI_Write_Function write_function, + LV2UI_Controller controller, + LV2UI_Widget* widget, + const LV2_Feature* const* features) +{ + SamplerUI* ui = (SamplerUI*)calloc(1, sizeof(SamplerUI)); + if (!ui) { + return NULL; + } + + ui->write = write_function; + ui->controller = controller; + ui->width = MIN_CANVAS_W; + *widget = NULL; + + // Get host features + const char* missing = lv2_features_query( + features, + LV2_LOG__log, &ui->logger.log, false, + LV2_URID__map, &ui->map, true, + NULL); + lv2_log_logger_set_map(&ui->logger, ui->map); + if (missing) { + lv2_log_error(&ui->logger, "Missing feature <%s>\n", missing); + free(ui); + return NULL; + } + + // Map URIs and initialise forge + map_sampler_uris(ui->map, &ui->uris); + lv2_atom_forge_init(&ui->forge, ui->map); + peaks_receiver_init(&ui->precv, ui->map); + + // Construct Gtk UI + ui->box = gtk_vbox_new(FALSE, 4); + ui->play_button = gtk_button_new_with_label("▶"); + ui->canvas = gtk_drawing_area_new(); + ui->button_box = gtk_hbox_new(FALSE, 4); + ui->file_button = gtk_file_chooser_button_new( + "Load Sample", GTK_FILE_CHOOSER_ACTION_OPEN); + gtk_widget_set_size_request(ui->canvas, MIN_CANVAS_W, MIN_CANVAS_H); + gtk_container_set_border_width(GTK_CONTAINER(ui->box), 4); + gtk_box_pack_start(GTK_BOX(ui->box), ui->canvas, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(ui->box), ui->button_box, FALSE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(ui->button_box), ui->play_button, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(ui->button_box), ui->file_button, TRUE, TRUE, 0); + + g_signal_connect(ui->file_button, "file-set", + G_CALLBACK(on_file_set), ui); + + g_signal_connect(ui->play_button, "clicked", + G_CALLBACK(on_play_clicked), ui); + + g_signal_connect(G_OBJECT(ui->canvas), "expose_event", + G_CALLBACK(on_canvas_expose), ui); + + // Request state (filename) from plugin + lv2_atom_forge_set_buffer(&ui->forge, ui->forge_buf, sizeof(ui->forge_buf)); + LV2_Atom_Forge_Frame frame; + LV2_Atom* msg = (LV2_Atom*)lv2_atom_forge_object( + &ui->forge, &frame, 0, ui->uris.patch_Get); + lv2_atom_forge_pop(&ui->forge, &frame); + + ui->write(ui->controller, 0, lv2_atom_total_size(msg), + ui->uris.atom_eventTransfer, + msg); + + *widget = ui->box; + + return ui; +} + +static void +cleanup(LV2UI_Handle handle) +{ + SamplerUI* ui = (SamplerUI*)handle; + gtk_widget_destroy(ui->box); + gtk_widget_destroy(ui->play_button); + gtk_widget_destroy(ui->canvas); + gtk_widget_destroy(ui->button_box); + gtk_widget_destroy(ui->file_button); + free(ui); +} + +static void +port_event(LV2UI_Handle handle, + uint32_t port_index, + uint32_t buffer_size, + uint32_t format, + const void* buffer) +{ + SamplerUI* ui = (SamplerUI*)handle; + if (format == ui->uris.atom_eventTransfer) { + const LV2_Atom* atom = (const LV2_Atom*)buffer; + if (lv2_atom_forge_is_object_type(&ui->forge, atom->type)) { + const LV2_Atom_Object* obj = (const LV2_Atom_Object*)atom; + if (obj->body.otype == ui->uris.patch_Set) { + const char* path = read_set_file(&ui->uris, obj); + if (path && (!ui->filename || strcmp(path, ui->filename))) { + g_free(ui->filename); + ui->filename = g_strdup(path); + gtk_file_chooser_set_filename( + GTK_FILE_CHOOSER(ui->file_button), path); + peaks_receiver_clear(&ui->precv); + ui->requested_n_peaks = 0; + request_peaks(ui, ui->width / 2 * 2); + } else if (!path) { + lv2_log_warning(&ui->logger, "Set message has no path\n"); + } + } else if (obj->body.otype == ui->precv.uris.peaks_PeakUpdate) { + if (!peaks_receiver_receive(&ui->precv, obj)) { + gtk_widget_queue_draw(ui->canvas); + } + } + } else { + lv2_log_error(&ui->logger, "Unknown message type\n"); + } + } else { + lv2_log_warning(&ui->logger, "Unknown port event format\n"); + } +} + +/* Optional non-embedded UI show interface. */ +static int +ui_show(LV2UI_Handle handle) +{ + SamplerUI* ui = (SamplerUI*)handle; + + int argc = 0; + gtk_init(&argc, NULL); + + ui->window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_container_add(GTK_CONTAINER(ui->window), ui->box); + gtk_widget_show_all(ui->window); + gtk_window_present(GTK_WINDOW(ui->window)); + + return 0; +} + +/* Optional non-embedded UI hide interface. */ +static int +ui_hide(LV2UI_Handle handle) +{ + return 0; +} + +/* Idle interface for optional non-embedded UI. */ +static int +ui_idle(LV2UI_Handle handle) +{ + SamplerUI* ui = (SamplerUI*)handle; + if (ui->window) { + gtk_main_iteration(); + } + return 0; +} + +static const void* +extension_data(const char* uri) +{ + static const LV2UI_Show_Interface show = { ui_show, ui_hide }; + static const LV2UI_Idle_Interface idle = { ui_idle }; + if (!strcmp(uri, LV2_UI__showInterface)) { + return &show; + } else if (!strcmp(uri, LV2_UI__idleInterface)) { + return &idle; + } + return NULL; +} + +static const LV2UI_Descriptor descriptor = { + SAMPLER_UI_URI, + instantiate, + cleanup, + port_event, + extension_data +}; + +LV2_SYMBOL_EXPORT +const LV2UI_Descriptor* +lv2ui_descriptor(uint32_t index) +{ + switch (index) { + case 0: + return &descriptor; + default: + return NULL; + } +} diff --git a/plugins/eg-sampler.lv2/uris.h b/plugins/eg-sampler.lv2/uris.h new file mode 100644 index 0000000..9e44cf4 --- /dev/null +++ b/plugins/eg-sampler.lv2/uris.h @@ -0,0 +1,147 @@ +/* + LV2 Sampler Example Plugin + Copyright 2011-2016 David Robillard <d@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. +*/ + +#ifndef SAMPLER_URIS_H +#define SAMPLER_URIS_H + +#include "lv2/lv2plug.in/ns/ext/log/log.h" +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" +#include "lv2/lv2plug.in/ns/ext/state/state.h" +#include "lv2/lv2plug.in/ns/ext/parameters/parameters.h" + +#define EG_SAMPLER_URI "http://lv2plug.in/plugins/eg-sampler" +#define EG_SAMPLER__applySample EG_SAMPLER_URI "#applySample" +#define EG_SAMPLER__freeSample EG_SAMPLER_URI "#freeSample" +#define EG_SAMPLER__sample EG_SAMPLER_URI "#sample" + +typedef struct { + LV2_URID atom_Float; + LV2_URID atom_Path; + LV2_URID atom_Resource; + LV2_URID atom_Sequence; + LV2_URID atom_URID; + LV2_URID atom_eventTransfer; + LV2_URID eg_applySample; + LV2_URID eg_freeSample; + LV2_URID eg_sample; + LV2_URID midi_Event; + LV2_URID param_gain; + LV2_URID patch_Get; + LV2_URID patch_Set; + LV2_URID patch_accept; + LV2_URID patch_property; + LV2_URID patch_value; +} SamplerURIs; + +static inline void +map_sampler_uris(LV2_URID_Map* map, SamplerURIs* uris) +{ + uris->atom_Float = map->map(map->handle, LV2_ATOM__Float); + uris->atom_Path = map->map(map->handle, LV2_ATOM__Path); + uris->atom_Resource = map->map(map->handle, LV2_ATOM__Resource); + uris->atom_Sequence = map->map(map->handle, LV2_ATOM__Sequence); + uris->atom_URID = map->map(map->handle, LV2_ATOM__URID); + uris->atom_eventTransfer = map->map(map->handle, LV2_ATOM__eventTransfer); + uris->eg_applySample = map->map(map->handle, EG_SAMPLER__applySample); + uris->eg_freeSample = map->map(map->handle, EG_SAMPLER__freeSample); + uris->eg_sample = map->map(map->handle, EG_SAMPLER__sample); + uris->midi_Event = map->map(map->handle, LV2_MIDI__MidiEvent); + uris->param_gain = map->map(map->handle, LV2_PARAMETERS__gain); + uris->patch_Get = map->map(map->handle, LV2_PATCH__Get); + uris->patch_Set = map->map(map->handle, LV2_PATCH__Set); + uris->patch_accept = map->map(map->handle, LV2_PATCH__accept); + uris->patch_property = map->map(map->handle, LV2_PATCH__property); + uris->patch_value = map->map(map->handle, LV2_PATCH__value); +} + +/** + Write a message like the following to `forge`: + [source,n3] + ---- + [] + a patch:Set ; + patch:property eg:sample ; + patch:value </home/me/foo.wav> . + ---- +*/ +static inline LV2_Atom_Forge_Ref +write_set_file(LV2_Atom_Forge* forge, + const SamplerURIs* uris, + const char* filename, + const uint32_t filename_len) +{ + LV2_Atom_Forge_Frame frame; + LV2_Atom_Forge_Ref set = lv2_atom_forge_object( + forge, &frame, 0, uris->patch_Set); + + lv2_atom_forge_key(forge, uris->patch_property); + lv2_atom_forge_urid(forge, uris->eg_sample); + lv2_atom_forge_key(forge, uris->patch_value); + lv2_atom_forge_path(forge, filename, filename_len); + + lv2_atom_forge_pop(forge, &frame); + return set; +} + +/** + Get the file path from `obj` which is a message like: + [source,n3] + ---- + [] + a patch:Set ; + patch:property eg:sample ; + patch:value </home/me/foo.wav> . + ---- +*/ +static inline const char* +read_set_file(const SamplerURIs* uris, + const LV2_Atom_Object* obj) +{ + if (obj->body.otype != uris->patch_Set) { + fprintf(stderr, "Ignoring unknown message type %d\n", obj->body.otype); + return NULL; + } + + /* Get property URI. */ + const LV2_Atom* property = NULL; + lv2_atom_object_get(obj, uris->patch_property, &property, 0); + if (!property) { + fprintf(stderr, "Malformed set message has no body.\n"); + return NULL; + } else if (property->type != uris->atom_URID) { + fprintf(stderr, "Malformed set message has non-URID property.\n"); + return NULL; + } else if (((const LV2_Atom_URID*)property)->body != uris->eg_sample) { + fprintf(stderr, "Set message for unknown property.\n"); + return NULL; + } + + /* Get value. */ + const LV2_Atom* value = NULL; + lv2_atom_object_get(obj, uris->patch_value, &value, 0); + if (!value) { + fprintf(stderr, "Malformed set message has no value.\n"); + return NULL; + } else if (value->type != uris->atom_Path) { + fprintf(stderr, "Set message value is not a Path.\n"); + return NULL; + } + + return (const char*)LV2_ATOM_BODY_CONST(value); +} + +#endif /* SAMPLER_URIS_H */ diff --git a/plugins/eg-sampler.lv2/waf b/plugins/eg-sampler.lv2/waf new file mode 120000 index 0000000..59a1ac9 --- /dev/null +++ b/plugins/eg-sampler.lv2/waf @@ -0,0 +1 @@ +../../waf
\ No newline at end of file diff --git a/plugins/eg-sampler.lv2/wscript b/plugins/eg-sampler.lv2/wscript new file mode 100644 index 0000000..3656edc --- /dev/null +++ b/plugins/eg-sampler.lv2/wscript @@ -0,0 +1,82 @@ +#!/usr/bin/env python +from waflib.extras import autowaf as autowaf +import re + +# Variables for 'waf dist' +APPNAME = 'eg-sampler.lv2' +VERSION = '1.0.0' + +# Mandatory variables +top = '.' +out = 'build' + +def options(opt): + opt.load('compiler_c') + opt.load('lv2') + autowaf.set_options(opt) + +def configure(conf): + autowaf.display_header('Sampler Configuration') + conf.load('compiler_c', cache=True) + conf.load('lv2', cache=True) + conf.load('autowaf', cache=True) + + if not autowaf.is_child(): + autowaf.check_pkg(conf, 'lv2', atleast_version='1.2.1', uselib_store='LV2') + + autowaf.check_pkg(conf, 'sndfile', uselib_store='SNDFILE', + atleast_version='1.0.0', mandatory=True) + autowaf.check_pkg(conf, 'gtk+-2.0', uselib_store='GTK2', + atleast_version='2.18.0', mandatory=False) + conf.check(features='c cshlib', lib='m', uselib_store='M', mandatory=False) + + autowaf.display_msg(conf, 'LV2 bundle directory', conf.env.LV2DIR) + print('') + +def build(bld): + bundle = 'eg-sampler.lv2' + + # Make a pattern for shared objects without the 'lib' prefix + module_pat = re.sub('^lib', '', bld.env.cshlib_PATTERN) + module_ext = module_pat[module_pat.rfind('.'):] + + # Build manifest.ttl by substitution (for portable lib extension) + bld(features = 'subst', + source = 'manifest.ttl.in', + target = '%s/%s' % (bundle, 'manifest.ttl'), + install_path = '${LV2DIR}/%s' % bundle, + LIB_EXT = module_ext) + + # Copy other data files to build bundle (build/eg-sampler.lv2) + for i in ['sampler.ttl', 'click.wav']: + bld(features = 'subst', + is_copy = True, + source = i, + target = '%s/%s' % (bundle, i), + install_path = '${LV2DIR}/%s' % bundle) + + # Use LV2 headers from parent directory if building as a sub-project + includes = ['.'] + if autowaf.is_child: + includes += ['../..'] + + # Build plugin library + obj = bld(features = 'c cshlib', + source = 'sampler.c', + name = 'sampler', + target = '%s/sampler' % bundle, + install_path = '${LV2DIR}/%s' % bundle, + use = ['M', 'SNDFILE', 'LV2'], + includes = includes) + obj.env.cshlib_PATTERN = module_pat + + # Build UI library + if bld.env.HAVE_GTK2: + obj = bld(features = 'c cshlib', + source = 'sampler_ui.c', + name = 'sampler_ui', + target = '%s/sampler_ui' % bundle, + install_path = '${LV2DIR}/%s' % bundle, + use = ['GTK2', 'LV2'], + includes = includes) + obj.env.cshlib_PATTERN = module_pat diff --git a/plugins/eg-scope.lv2/README.txt b/plugins/eg-scope.lv2/README.txt new file mode 100644 index 0000000..122794c --- /dev/null +++ b/plugins/eg-scope.lv2/README.txt @@ -0,0 +1,32 @@ +== Simple Oscilloscope == + +This plugin displays the waveform of an incoming audio signal using a simple +GTK+Cairo GUI. + +This plugin illustrates: + +- UI <==> Plugin communication via http://lv2plug.in/ns/ext/atom/[LV2 Atom] events +- Atom vector usage and resize-port extension +- Save/Restore UI state by communicating state to backend +- Saving simple key/value state via the http://lv2plug.in/ns/ext/state/[LV2 State] extension +- Cairo drawing and partial exposure + +This plugin intends to outline the basics for building visualization plugins +that rely on atom communication. The UI looks like an oscilloscope, but is not +a real oscilloscope implementation: + +- There is no display synchronisation, results will depend on LV2 host. +- It displays raw audio samples, which a proper scope must not do. +- The display itself just connects min/max line segments. +- No triggering or synchronization. +- No labels, no scale, no calibration, no markers, no numeric readout, etc. + +Addressing these issues is beyond the scope of this example. + +Please see http://lac.linuxaudio.org/2013/papers/36.pdf for scope design, +https://wiki.xiph.org/Videos/Digital_Show_and_Tell for background information, +and http://lists.lv2plug.in/pipermail/devel-lv2plug.in/2013-November/000545.html +for general LV2 related conceptual criticism regarding real-time visualizations. + +A proper oscilloscope based on this example can be found at +https://github.com/x42/sisco.lv2 diff --git a/plugins/eg-scope.lv2/examploscope.c b/plugins/eg-scope.lv2/examploscope.c new file mode 100644 index 0000000..d672d25 --- /dev/null +++ b/plugins/eg-scope.lv2/examploscope.c @@ -0,0 +1,421 @@ +/* + Copyright 2016 David Robillard <d@drobilla.net> + Copyright 2013 Robin Gareus <robin@gareus.org> + + 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 <stdio.h> +#include <stdlib.h> +#include <stdint.h> + +#include "lv2/lv2plug.in/ns/ext/log/log.h" +#include "lv2/lv2plug.in/ns/ext/log/logger.h" +#include "lv2/lv2plug.in/ns/ext/state/state.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2_util.h" + +#include "./uris.h" + +/** + ==== Private Plugin Instance Structure ==== + + In addition to the usual port buffers and features, this plugin stores the + state of the UI here, so it can be opened and closed without losing the + current settings. The UI state is communicated between the plugin and the + UI using atom messages via a sequence port, similarly to MIDI I/O. +*/ +typedef struct { + // Port buffers + float* input[2]; + float* output[2]; + const LV2_Atom_Sequence* control; + LV2_Atom_Sequence* notify; + + // Atom forge and URI mapping + LV2_URID_Map* map; + ScoLV2URIs uris; + LV2_Atom_Forge forge; + LV2_Atom_Forge_Frame frame; + + // Log feature and convenience API + LV2_Log_Logger logger; + + // Instantiation settings + uint32_t n_channels; + double rate; + + // UI state + bool ui_active; + bool send_settings_to_ui; + float ui_amp; + uint32_t ui_spp; +} EgScope; + +/** ==== Port Indices ==== */ +typedef enum { + SCO_CONTROL = 0, // Event input + SCO_NOTIFY = 1, // Event output + SCO_INPUT0 = 2, // Audio input 0 + SCO_OUTPUT0 = 3, // Audio output 0 + SCO_INPUT1 = 4, // Audio input 1 (stereo variant) + SCO_OUTPUT1 = 5, // Audio input 2 (stereo variant) +} PortIndex; + +/** ==== Instantiate Method ==== */ +static LV2_Handle +instantiate(const LV2_Descriptor* descriptor, + double rate, + const char* bundle_path, + const LV2_Feature* const* features) +{ + (void)descriptor; // Unused variable + (void)bundle_path; // Unused variable + + // Allocate and initialise instance structure. + EgScope* self = (EgScope*)calloc(1, sizeof(EgScope)); + if (!self) { + return NULL; + } + + // Get host features + const char* missing = lv2_features_query( + features, + LV2_LOG__log, &self->logger.log, false, + LV2_URID__map, &self->map, true, + NULL); + lv2_log_logger_set_map(&self->logger, self->map); + if (missing) { + lv2_log_error(&self->logger, "Missing feature <%s>\n", missing); + free(self); + return NULL; + } + + // Decide which variant to use depending on the plugin URI + if (!strcmp(descriptor->URI, SCO_URI "#Stereo")) { + self->n_channels = 2; + } else if (!strcmp(descriptor->URI, SCO_URI "#Mono")) { + self->n_channels = 1; + } else { + free(self); + return NULL; + } + + // Initialise local variables + self->ui_active = false; + self->send_settings_to_ui = false; + self->rate = rate; + + // Set default UI settings + self->ui_spp = 50; + self->ui_amp = 1.0; + + // Map URIs and initialise forge/logger + map_sco_uris(self->map, &self->uris); + lv2_atom_forge_init(&self->forge, self->map); + + return (LV2_Handle)self; +} + +/** ==== Connect Port Method ==== */ +static void +connect_port(LV2_Handle handle, + uint32_t port, + void* data) +{ + EgScope* self = (EgScope*)handle; + + switch ((PortIndex)port) { + case SCO_CONTROL: + self->control = (const LV2_Atom_Sequence*)data; + break; + case SCO_NOTIFY: + self->notify = (LV2_Atom_Sequence*)data; + break; + case SCO_INPUT0: + self->input[0] = (float*)data; + break; + case SCO_OUTPUT0: + self->output[0] = (float*)data; + break; + case SCO_INPUT1: + self->input[1] = (float*)data; + break; + case SCO_OUTPUT1: + self->output[1] = (float*)data; + break; + } +} + +/** + ==== Utility Function: `tx_rawaudio` ==== + + This function forges a message for sending a vector of raw data. The object + is a http://lv2plug.in/ns/ext/atom#Blank[Blank] with a few properties, like: + [source,n3] + -------- + [] + a sco:RawAudio ; + sco:channelID 0 ; + sco:audioData [ 0.0, 0.0, ... ] . + -------- + + where the value of the `sco:audioData` property, `[ 0.0, 0.0, ... ]`, is a + http://lv2plug.in/ns/ext/atom#Vector[Vector] of + http://lv2plug.in/ns/ext/atom#Float[Float]. +*/ +static void +tx_rawaudio(LV2_Atom_Forge* forge, + ScoLV2URIs* uris, + const int32_t channel, + const size_t n_samples, + const float* data) +{ + LV2_Atom_Forge_Frame frame; + + // Forge container object of type 'RawAudio' + lv2_atom_forge_frame_time(forge, 0); + lv2_atom_forge_object(forge, &frame, 0, uris->RawAudio); + + // Add integer 'channelID' property + lv2_atom_forge_key(forge, uris->channelID); + lv2_atom_forge_int(forge, channel); + + // Add vector of floats 'audioData' property + lv2_atom_forge_key(forge, uris->audioData); + lv2_atom_forge_vector( + forge, sizeof(float), uris->atom_Float, n_samples, data); + + // Close off object + lv2_atom_forge_pop(forge, &frame); +} + +/** ==== Run Method ==== */ +static void +run(LV2_Handle handle, uint32_t n_samples) +{ + EgScope* self = (EgScope*)handle; + + /* Ensure notify port buffer is large enough to hold all audio-samples and + configuration settings. A minimum size was requested in the .ttl file, + but check here just to be sure. + + TODO: Explain these magic numbers. + */ + const size_t size = (sizeof(float) * n_samples + 64) * self->n_channels; + const uint32_t space = self->notify->atom.size; + if (space < size + 128) { + /* Insufficient space, report error and do nothing. Note that a + real-time production plugin mustn't call log functions in run(), but + this can be useful for debugging and example purposes. + */ + lv2_log_error(&self->logger, "Buffer size is insufficient\n"); + return; + } + + // Prepare forge buffer and initialize atom-sequence + lv2_atom_forge_set_buffer(&self->forge, (uint8_t*)self->notify, space); + lv2_atom_forge_sequence_head(&self->forge, &self->frame, 0); + + /* Send settings to UI + + The plugin can continue to run while the UI is closed and re-opened. + The state and settings of the UI are kept here and transmitted to the UI + every time it asks for them or if the user initializes a 'load preset'. + */ + if (self->send_settings_to_ui && self->ui_active) { + self->send_settings_to_ui = false; + // Forge container object of type 'ui_state' + LV2_Atom_Forge_Frame frame; + lv2_atom_forge_frame_time(&self->forge, 0); + lv2_atom_forge_object(&self->forge, &frame, 0, self->uris.ui_State); + + // Add UI state as properties + lv2_atom_forge_key(&self->forge, self->uris.ui_spp); + lv2_atom_forge_int(&self->forge, self->ui_spp); + lv2_atom_forge_key(&self->forge, self->uris.ui_amp); + lv2_atom_forge_float(&self->forge, self->ui_amp); + lv2_atom_forge_key(&self->forge, self->uris.param_sampleRate); + lv2_atom_forge_float(&self->forge, self->rate); + lv2_atom_forge_pop(&self->forge, &frame); + } + + // Process incoming events from GUI + if (self->control) { + const LV2_Atom_Event* ev = lv2_atom_sequence_begin( + &(self->control)->body); + // For each incoming message... + while (!lv2_atom_sequence_is_end( + &self->control->body, self->control->atom.size, ev)) { + // If the event is an atom:Blank object + if (lv2_atom_forge_is_object_type(&self->forge, ev->body.type)) { + const LV2_Atom_Object* obj = (const LV2_Atom_Object*)&ev->body; + if (obj->body.otype == self->uris.ui_On) { + // If the object is a ui-on, the UI was activated + self->ui_active = true; + self->send_settings_to_ui = true; + } else if (obj->body.otype == self->uris.ui_Off) { + // If the object is a ui-off, the UI was closed + self->ui_active = false; + } else if (obj->body.otype == self->uris.ui_State) { + // If the object is a ui-state, it's the current UI settings + const LV2_Atom* spp = NULL; + const LV2_Atom* amp = NULL; + lv2_atom_object_get(obj, self->uris.ui_spp, &spp, + self->uris.ui_amp, &, + 0); + if (spp) { + self->ui_spp = ((const LV2_Atom_Int*)spp)->body; + } + if (amp) { + self->ui_amp = ((const LV2_Atom_Float*)amp)->body; + } + } + } + ev = lv2_atom_sequence_next(ev); + } + } + + // Process audio data + for (uint32_t c = 0; c < self->n_channels; ++c) { + if (self->ui_active) { + // If UI is active, send raw audio data to UI + tx_rawaudio(&self->forge, &self->uris, c, n_samples, self->input[c]); + } + // If not processing audio in-place, forward audio + if (self->input[c] != self->output[c]) { + memcpy(self->output[c], self->input[c], sizeof(float) * n_samples); + } + } + + // Close off sequence + lv2_atom_forge_pop(&self->forge, &self->frame); +} + +static void +cleanup(LV2_Handle handle) +{ + free(handle); +} + + +/** + ==== State Methods ==== + + This plugin's state consists of two basic properties: one `int` and one + `float`. No files are used. Note these values are POD, but not portable, + since different machines may have a different integer endianness or floating + point format. However, since standard Atom types are used, a good host will + be able to save them portably as text anyway. +*/ +static LV2_State_Status +state_save(LV2_Handle instance, + LV2_State_Store_Function store, + LV2_State_Handle handle, + uint32_t flags, + const LV2_Feature* const* features) +{ + EgScope* self = (EgScope*)instance; + if (!self) { + return LV2_STATE_SUCCESS; + } + + store(handle, self->uris.ui_spp, + (void*)&self->ui_spp, sizeof(uint32_t), + self->uris.atom_Int, + LV2_STATE_IS_POD); + + store(handle, self->uris.ui_amp, + (void*)&self->ui_amp, sizeof(float), + self->uris.atom_Float, + LV2_STATE_IS_POD); + + return LV2_STATE_SUCCESS; +} + +static LV2_State_Status +state_restore(LV2_Handle instance, + LV2_State_Retrieve_Function retrieve, + LV2_State_Handle handle, + uint32_t flags, + const LV2_Feature* const* features) +{ + EgScope* self = (EgScope*)instance; + + size_t size; + uint32_t type; + uint32_t valflags; + + const void* spp = retrieve( + handle, self->uris.ui_spp, &size, &type, &valflags); + if (spp && size == sizeof(uint32_t) && type == self->uris.atom_Int) { + self->ui_spp = *((const uint32_t*)spp); + self->send_settings_to_ui = true; + } + + const void* amp = retrieve( + handle, self->uris.ui_amp, &size, &type, &valflags); + if (amp && size == sizeof(float) && type == self->uris.atom_Float) { + self->ui_amp = *((const float*)amp); + self->send_settings_to_ui = true; + } + + return LV2_STATE_SUCCESS; +} + +static const void* +extension_data(const char* uri) +{ + static const LV2_State_Interface state = { state_save, state_restore }; + if (!strcmp(uri, LV2_STATE__interface)) { + return &state; + } + return NULL; +} + +/** ==== Plugin Descriptors ==== */ +static const LV2_Descriptor descriptor_mono = { + SCO_URI "#Mono", + instantiate, + connect_port, + NULL, + run, + NULL, + cleanup, + extension_data +}; + +static const LV2_Descriptor descriptor_stereo = { + SCO_URI "#Stereo", + instantiate, + connect_port, + NULL, + run, + NULL, + cleanup, + extension_data +}; + +LV2_SYMBOL_EXPORT +const LV2_Descriptor* +lv2_descriptor(uint32_t index) +{ + switch (index) { + case 0: + return &descriptor_mono; + case 1: + return &descriptor_stereo; + default: + return NULL; + } +} diff --git a/plugins/eg-scope.lv2/examploscope.ttl.in b/plugins/eg-scope.lv2/examploscope.ttl.in new file mode 100644 index 0000000..0b76962 --- /dev/null +++ b/plugins/eg-scope.lv2/examploscope.ttl.in @@ -0,0 +1,130 @@ +@prefix atom: <http://lv2plug.in/ns/ext/atom#> . +@prefix bufsz: <http://lv2plug.in/ns/ext/buf-size#> . +@prefix doap: <http://usefulinc.com/ns/doap#> . +@prefix foaf: <http://xmlns.com/foaf/0.1/> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix ui: <http://lv2plug.in/ns/extensions/ui#> . +@prefix urid: <http://lv2plug.in/ns/ext/urid#> . +@prefix rsz: <http://lv2plug.in/ns/ext/resize-port#> . +@prefix state: <http://lv2plug.in/ns/ext/state#> . +@prefix egscope: <http://lv2plug.in/plugins/eg-scope#> . + +<http://gareus.org/rgareus#me> + a foaf:Person ; + foaf:name "Robin Gareus" ; + foaf:mbox <mailto:robin@gareus.org> ; + foaf:homepage <http://gareus.org/> . + +<http://lv2plug.in/plugins/eg-scope> + a doap:Project ; + doap:maintainer <http://gareus.org/rgareus#me> ; + doap:name "Example Scope" . + +egscope:Mono + a lv2:Plugin, lv2:AnalyserPlugin ; + doap:name "Example Scope (Mono)" ; + lv2:project <http://lv2plug.in/plugins/eg-scope> ; + doap:license <http://usefulinc.com/doap/licenses/gpl> ; + lv2:requiredFeature urid:map ; + lv2:optionalFeature lv2:hardRTCapable ; + lv2:extensionData state:interface ; + ui:ui egscope:ui ; + lv2:port [ + a atom:AtomPort , + lv2:InputPort ; + atom:bufferType atom:Sequence ; + lv2:designation lv2:control ; + lv2:index 0 ; + lv2:symbol "control" ; + lv2:name "Control" + ] , [ + a atom:AtomPort , + lv2:OutputPort ; + atom:bufferType atom:Sequence ; + lv2:designation lv2:control ; + lv2:index 1 ; + lv2:symbol "notify" ; + lv2:name "Notify" ; + # 8192 * sizeof(float) + LV2-Atoms + rsz:minimumSize 32832; + ] , [ + a lv2:AudioPort , + lv2:InputPort ; + lv2:index 2 ; + lv2:symbol "in" ; + lv2:name "In" + ] , [ + a lv2:AudioPort , + lv2:OutputPort ; + lv2:index 3 ; + lv2:symbol "out" ; + lv2:name "Out" + ] . + + +egscope:Stereo + a lv2:Plugin, lv2:AnalyserPlugin ; + doap:name "Example Scope (Stereo)" ; + lv2:project <http://lv2plug.in/plugins/eg-scope> ; + doap:license <http://usefulinc.com/doap/licenses/gpl> ; + lv2:requiredFeature urid:map ; + lv2:optionalFeature lv2:hardRTCapable ; + lv2:extensionData state:interface ; + ui:ui egscope:ui ; + lv2:port [ + a atom:AtomPort , + lv2:InputPort ; + atom:bufferType atom:Sequence ; + lv2:designation lv2:control ; + lv2:index 0 ; + lv2:symbol "control" ; + lv2:name "Control" + ] , [ + a atom:AtomPort , + lv2:OutputPort ; + atom:bufferType atom:Sequence ; + lv2:designation lv2:control ; + lv2:index 1 ; + lv2:symbol "notify" ; + lv2:name "Notify" ; + rsz:minimumSize 65664; + ] , [ + a lv2:AudioPort , + lv2:InputPort ; + lv2:index 2 ; + lv2:symbol "in0" ; + lv2:name "InL" + ] , [ + a lv2:AudioPort , + lv2:OutputPort ; + lv2:index 3 ; + lv2:symbol "out0" ; + lv2:name "OutL" + ] , [ + a lv2:AudioPort , + lv2:InputPort ; + lv2:index 4 ; + lv2:symbol "in1" ; + lv2:name "InR" + ] , [ + a lv2:AudioPort , + lv2:OutputPort ; + lv2:index 5 ; + lv2:symbol "out1" ; + lv2:name "OutR" + ] . + + +egscope:ui + a ui:GtkUI ; + lv2:requiredFeature urid:map ; + ui:portNotification [ + ui:plugin egscope:Mono ; + lv2:symbol "notify" ; + ui:notifyType atom:Blank + ] , [ + ui:plugin egscope:Stereo ; + lv2:symbol "notify" ; + ui:notifyType atom:Blank + ] . diff --git a/plugins/eg-scope.lv2/examploscope_ui.c b/plugins/eg-scope.lv2/examploscope_ui.c new file mode 100644 index 0000000..e2723c6 --- /dev/null +++ b/plugins/eg-scope.lv2/examploscope_ui.c @@ -0,0 +1,662 @@ +/* + Copyright 2013 Robin Gareus <robin@gareus.org> + + 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 <stdio.h> +#include <stdlib.h> + +#include <cairo.h> +#include <gtk/gtk.h> + +#include "lv2/lv2plug.in/ns/extensions/ui/ui.h" +#include "./uris.h" + +// Drawing area size +#define DAWIDTH (640) +#define DAHEIGHT (200) + +/** + Max continuous points on path. Many short-path segments are + expensive|inefficient long paths are not supported by all surfaces (usually + its a miter - not point - limit, depending on used cairo backend) +*/ +#define MAX_CAIRO_PATH (128) + +/** + Representation of the raw audio-data for display (min | max) values for a + given 'index' position. +*/ +typedef struct { + float data_min[DAWIDTH]; + float data_max[DAWIDTH]; + + uint32_t idx; + uint32_t sub; +} ScoChan; + +typedef struct { + LV2_Atom_Forge forge; + LV2_URID_Map* map; + ScoLV2URIs uris; + + LV2UI_Write_Function write; + LV2UI_Controller controller; + + GtkWidget* hbox; + GtkWidget* vbox; + GtkWidget* sep[2]; + GtkWidget* darea; + GtkWidget* btn_pause; + GtkWidget* lbl_speed; + GtkWidget* lbl_amp; + GtkWidget* spb_speed; + GtkWidget* spb_amp; + GtkAdjustment* spb_speed_adj; + GtkAdjustment* spb_amp_adj; + + ScoChan chn[2]; + uint32_t stride; + uint32_t n_channels; + bool paused; + float rate; + bool updating; +} EgScopeUI; + + +/** Send current UI settings to backend. */ +static void +send_ui_state(LV2UI_Handle handle) +{ + EgScopeUI* ui = (EgScopeUI*)handle; + const float gain = gtk_spin_button_get_value(GTK_SPIN_BUTTON(ui->spb_amp)); + + // Use local buffer on the stack to build atom + uint8_t obj_buf[1024]; + lv2_atom_forge_set_buffer(&ui->forge, obj_buf, sizeof(obj_buf)); + + // Start a ui:State object + LV2_Atom_Forge_Frame frame; + LV2_Atom* msg = (LV2_Atom*)lv2_atom_forge_object( + &ui->forge, &frame, 0, ui->uris.ui_State); + + // msg[samples-per-pixel] = integer + lv2_atom_forge_key(&ui->forge, ui->uris.ui_spp); + lv2_atom_forge_int(&ui->forge, ui->stride); + + // msg[amplitude] = float + lv2_atom_forge_key(&ui->forge, ui->uris.ui_amp); + lv2_atom_forge_float(&ui->forge, gain); + + // Finish ui:State object + lv2_atom_forge_pop(&ui->forge, &frame); + + // Send message to plugin port '0' + ui->write(ui->controller, + 0, + lv2_atom_total_size(msg), + ui->uris.atom_eventTransfer, + msg); +} + +/** Notify backend that UI is closed. */ +static void +send_ui_disable(LV2UI_Handle handle) +{ + EgScopeUI* ui = (EgScopeUI*)handle; + send_ui_state(handle); + + uint8_t obj_buf[64]; + lv2_atom_forge_set_buffer(&ui->forge, obj_buf, sizeof(obj_buf)); + + LV2_Atom_Forge_Frame frame; + LV2_Atom* msg = (LV2_Atom*)lv2_atom_forge_object( + &ui->forge, &frame, 0, ui->uris.ui_Off); + lv2_atom_forge_pop(&ui->forge, &frame); + ui->write(ui->controller, + 0, + lv2_atom_total_size(msg), + ui->uris.atom_eventTransfer, + msg); +} + +/** + Notify backend that UI is active. + + The plugin should send state and enable data transmission. +*/ +static void +send_ui_enable(LV2UI_Handle handle) +{ + EgScopeUI* ui = (EgScopeUI*)handle; + + uint8_t obj_buf[64]; + lv2_atom_forge_set_buffer(&ui->forge, obj_buf, sizeof(obj_buf)); + + LV2_Atom_Forge_Frame frame; + LV2_Atom* msg = (LV2_Atom*)lv2_atom_forge_object( + &ui->forge, &frame, 0, ui->uris.ui_On); + lv2_atom_forge_pop(&ui->forge, &frame); + ui->write(ui->controller, + 0, + lv2_atom_total_size(msg), + ui->uris.atom_eventTransfer, + msg); +} + +/** Gtk widget callback. */ +static gboolean +on_cfg_changed(GtkWidget* widget, gpointer data) +{ + EgScopeUI* ui = (EgScopeUI*)data; + if (!ui->updating) { + // Only send UI state if the change is from user interaction + send_ui_state(data); + } + return TRUE; +} + +/** + Gdk drawing area draw callback. + + Called in Gtk's main thread and uses Cairo to draw the data. +*/ +static gboolean +on_expose_event(GtkWidget* widget, GdkEventExpose* ev, gpointer data) +{ + EgScopeUI* ui = (EgScopeUI*)data; + const float gain = gtk_spin_button_get_value(GTK_SPIN_BUTTON(ui->spb_amp)); + + // Get cairo type for the gtk window + cairo_t* cr; + cr = gdk_cairo_create(ui->darea->window); + + // Limit cairo-drawing to exposed area + cairo_rectangle(cr, ev->area.x, ev->area.y, ev->area.width, ev->area.height); + cairo_clip(cr); + + // Clear background + cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 1.0); + cairo_rectangle(cr, 0, 0, DAWIDTH, DAHEIGHT * ui->n_channels); + cairo_fill(cr); + + cairo_set_line_width(cr, 1.0); + + const uint32_t start = ev->area.x; + const uint32_t end = ev->area.x + ev->area.width; + + assert(start < DAWIDTH); + assert(end <= DAWIDTH); + assert(start < end); + + for (uint32_t c = 0; c < ui->n_channels; ++c) { + ScoChan* chn = &ui->chn[c]; + + /* Drawing area Y-position of given sample-value. + * Note: cairo-pixel at 0 spans -0.5 .. +0.5, hence (DAHEIGHT / 2.0 -0.5) + * also the cairo Y-axis points upwards (hence 'minus value') + * + * == ( DAHEIGHT * (CHN) // channel offset + * + (DAHEIGHT / 2) - 0.5 // vertical center -- '0' + * - (DAHEIGHT / 2) * (VAL) * (GAIN) + * ) + */ + const float chn_y_offset = DAHEIGHT * c + DAHEIGHT * 0.5f - 0.5f; + const float chn_y_scale = DAHEIGHT * 0.5f * gain; + +#define CYPOS(VAL) (chn_y_offset - (VAL) * chn_y_scale) + + cairo_save(cr); + + /* Restrict drawing to current channel area, don't bleed drawing into + neighboring channels. */ + cairo_rectangle(cr, 0, DAHEIGHT * c, DAWIDTH, DAHEIGHT); + cairo_clip(cr); + + // Set color of wave-form + cairo_set_source_rgba(cr, 0.0, 1.0, 0.0, 1.0); + + /* This is a somewhat 'smart' mechanism to plot audio data using + alternating up/down line-directions. It works well for both cases: + 1 pixel <= 1 sample and 1 pixel represents more than 1 sample, but + is not ideal for either. */ + if (start == chn->idx) { + cairo_move_to(cr, start - 0.5, CYPOS(0)); + } else { + cairo_move_to(cr, start - 0.5, CYPOS(chn->data_max[start])); + } + + uint32_t pathlength = 0; + for (uint32_t i = start; i < end; ++i) { + if (i == chn->idx) { + continue; + } else if (i % 2) { + cairo_line_to(cr, i - .5, CYPOS(chn->data_min[i])); + cairo_line_to(cr, i - .5, CYPOS(chn->data_max[i])); + ++pathlength; + } else { + cairo_line_to(cr, i - .5, CYPOS(chn->data_max[i])); + cairo_line_to(cr, i - .5, CYPOS(chn->data_min[i])); + ++pathlength; + } + + /** Limit the max cairo path length. This is an optimization trade + off: too short path: high load CPU/GPU load. too-long path: + bad anti-aliasing, or possibly lost points */ + if (pathlength > MAX_CAIRO_PATH) { + pathlength = 0; + cairo_stroke(cr); + if (i % 2) { + cairo_move_to(cr, i - .5, CYPOS(chn->data_max[i])); + } else { + cairo_move_to(cr, i - .5, CYPOS(chn->data_min[i])); + } + } + } + + if (pathlength > 0) { + cairo_stroke(cr); + } + + // Draw current position vertical line if display is slow + if (ui->stride >= ui->rate / 4800.0f || ui->paused) { + cairo_set_source_rgba(cr, .9, .2, .2, .6); + cairo_move_to(cr, chn->idx - .5, DAHEIGHT * c); + cairo_line_to(cr, chn->idx - .5, DAHEIGHT * (c + 1)); + cairo_stroke(cr); + } + + // Undo the 'clipping' restriction + cairo_restore(cr); + + // Channel separator + if (c > 0) { + cairo_set_source_rgba(cr, .5, .5, .5, 1.0); + cairo_move_to(cr, 0, DAHEIGHT * c - .5); + cairo_line_to(cr, DAWIDTH, DAHEIGHT * c - .5); + cairo_stroke(cr); + } + + // Zero scale line + cairo_set_source_rgba(cr, .3, .3, .7, .5); + cairo_move_to(cr, 0, DAHEIGHT * (c + .5) - .5); + cairo_line_to(cr, DAWIDTH, DAHEIGHT * (c + .5) - .5); + cairo_stroke(cr); + } + + cairo_destroy(cr); + return TRUE; +} + +/** + Parse raw audio data and prepare for later drawing. + + Note this is a toy example, which is really a waveform display, not an + oscilloscope. A serious scope would not display samples as is. + + Signals above ~ 1/10 of the sampling-rate will not yield a useful visual + display and result in a rather unintuitive representation of the actual + waveform. + + Ideally the audio-data would be buffered and upsampled here and after that + written in a display buffer for later use. + + For more information, see + https://wiki.xiph.org/Videos/Digital_Show_and_Tell + http://lac.linuxaudio.org/2013/papers/36.pdf + and https://github.com/x42/sisco.lv2 +*/ +static int +process_channel(EgScopeUI* ui, + ScoChan* chn, + const size_t n_elem, + float const* data, + uint32_t* idx_start, + uint32_t* idx_end) +{ + int overflow = 0; + *idx_start = chn->idx; + for (size_t i = 0; i < n_elem; ++i) { + if (data[i] < chn->data_min[chn->idx]) { + chn->data_min[chn->idx] = data[i]; + } + if (data[i] > chn->data_max[chn->idx]) { + chn->data_max[chn->idx] = data[i]; + } + if (++chn->sub >= ui->stride) { + chn->sub = 0; + chn->idx = (chn->idx + 1) % DAWIDTH; + if (chn->idx == 0) { + ++overflow; + } + chn->data_min[chn->idx] = 1.0; + chn->data_max[chn->idx] = -1.0; + } + } + *idx_end = chn->idx; + return overflow; +} + +/** + Called via port_event() which is called by the host, typically at a rate of + around 25 FPS. +*/ +static void +update_scope(EgScopeUI* ui, + const int32_t channel, + const size_t n_elem, + float const* data) +{ + // Never trust input data which could lead to application failure. + if (channel < 0 || (uint32_t)channel > ui->n_channels) { + return; + } + + // Update state in sync with 1st channel + if (channel == 0) { + ui->stride = gtk_spin_button_get_value(GTK_SPIN_BUTTON(ui->spb_speed)); + const bool paused = gtk_toggle_button_get_active( + GTK_TOGGLE_BUTTON(ui->btn_pause)); + + if (paused != ui->paused) { + ui->paused = paused; + gtk_widget_queue_draw(ui->darea); + } + } + if (ui->paused) { + return; + } + + uint32_t idx_start; // Display pixel start + uint32_t idx_end; // Display pixel end + int overflow; // Received more audio-data than display-pixel + + // Process this channel's audio-data for display + ScoChan* chn = &ui->chn[channel]; + overflow = process_channel(ui, chn, n_elem, data, &idx_start, &idx_end); + + // Signal gtk's main thread to redraw the widget after the last channel + if ((uint32_t)channel + 1 == ui->n_channels) { + if (overflow > 1) { + // Redraw complete widget + gtk_widget_queue_draw(ui->darea); + } else if (idx_end > idx_start) { + // Redraw area between start -> end pixel + gtk_widget_queue_draw_area(ui->darea, idx_start - 2, 0, 3 + + idx_end - idx_start, + DAHEIGHT * ui->n_channels); + } else if (idx_end < idx_start) { + // Wrap-around: redraw area between 0->start AND end->right-end + gtk_widget_queue_draw_area( + ui->darea, + idx_start - 2, 0, + 3 + DAWIDTH - idx_start, DAHEIGHT * ui->n_channels); + gtk_widget_queue_draw_area( + ui->darea, + 0, 0, + idx_end + 1, DAHEIGHT * ui->n_channels); + } + } +} + +static LV2UI_Handle +instantiate(const LV2UI_Descriptor* descriptor, + const char* plugin_uri, + const char* bundle_path, + LV2UI_Write_Function write_function, + LV2UI_Controller controller, + LV2UI_Widget* widget, + const LV2_Feature* const* features) +{ + EgScopeUI* ui = (EgScopeUI*)calloc(1, sizeof(EgScopeUI)); + + if (!ui) { + fprintf(stderr, "EgScope.lv2 UI: out of memory\n"); + return NULL; + } + + ui->map = NULL; + *widget = NULL; + + if (!strcmp(plugin_uri, SCO_URI "#Mono")) { + ui->n_channels = 1; + } else if (!strcmp(plugin_uri, SCO_URI "#Stereo")) { + ui->n_channels = 2; + } else { + free(ui); + return NULL; + } + + for (int i = 0; features[i]; ++i) { + if (!strcmp(features[i]->URI, LV2_URID_URI "#map")) { + ui->map = (LV2_URID_Map*)features[i]->data; + } + } + + if (!ui->map) { + fprintf(stderr, "EgScope.lv2 UI: Host does not support urid:map\n"); + free(ui); + return NULL; + } + + // Initialize private data structure + ui->write = write_function; + ui->controller = controller; + + ui->vbox = NULL; + ui->hbox = NULL; + ui->darea = NULL; + ui->stride = 25; + ui->paused = false; + ui->rate = 48000; + + ui->chn[0].idx = 0; + ui->chn[0].sub = 0; + ui->chn[1].idx = 0; + ui->chn[1].sub = 0; + memset(ui->chn[0].data_min, 0, sizeof(float) * DAWIDTH); + memset(ui->chn[0].data_max, 0, sizeof(float) * DAWIDTH); + memset(ui->chn[1].data_min, 0, sizeof(float) * DAWIDTH); + memset(ui->chn[1].data_max, 0, sizeof(float) * DAWIDTH); + + map_sco_uris(ui->map, &ui->uris); + lv2_atom_forge_init(&ui->forge, ui->map); + + // Setup UI + ui->hbox = gtk_hbox_new(FALSE, 0); + ui->vbox = gtk_vbox_new(FALSE, 0); + + ui->darea = gtk_drawing_area_new(); + gtk_widget_set_size_request(ui->darea, DAWIDTH, DAHEIGHT * ui->n_channels); + + ui->lbl_speed = gtk_label_new("Samples/Pixel"); + ui->lbl_amp = gtk_label_new("Amplitude"); + + ui->sep[0] = gtk_hseparator_new(); + ui->sep[1] = gtk_label_new(""); + ui->btn_pause = gtk_toggle_button_new_with_label("Pause"); + + ui->spb_speed_adj = (GtkAdjustment*)gtk_adjustment_new( + 25.0, 1.0, 1000.0, 1.0, 5.0, 0.0); + ui->spb_speed = gtk_spin_button_new(ui->spb_speed_adj, 1.0, 0); + + ui->spb_amp_adj = (GtkAdjustment*)gtk_adjustment_new( + 1.0, 0.1, 6.0, 0.1, 1.0, 0.0); + ui->spb_amp = gtk_spin_button_new(ui->spb_amp_adj, 0.1, 1); + + gtk_box_pack_start(GTK_BOX(ui->hbox), ui->darea, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(ui->hbox), ui->vbox, FALSE, FALSE, 4); + + gtk_box_pack_start(GTK_BOX(ui->vbox), ui->lbl_speed, FALSE, FALSE, 2); + gtk_box_pack_start(GTK_BOX(ui->vbox), ui->spb_speed, FALSE, FALSE, 2); + gtk_box_pack_start(GTK_BOX(ui->vbox), ui->sep[0], FALSE, FALSE, 8); + gtk_box_pack_start(GTK_BOX(ui->vbox), ui->lbl_amp, FALSE, FALSE, 2); + gtk_box_pack_start(GTK_BOX(ui->vbox), ui->spb_amp, FALSE, FALSE, 2); + gtk_box_pack_start(GTK_BOX(ui->vbox), ui->sep[1], TRUE, FALSE, 8); + gtk_box_pack_start(GTK_BOX(ui->vbox), ui->btn_pause, FALSE, FALSE, 2); + + g_signal_connect(G_OBJECT(ui->darea), "expose_event", + G_CALLBACK(on_expose_event), ui); + g_signal_connect(G_OBJECT(ui->spb_amp), "value-changed", + G_CALLBACK(on_cfg_changed), ui); + g_signal_connect(G_OBJECT(ui->spb_speed), "value-changed", + G_CALLBACK(on_cfg_changed), ui); + + *widget = ui->hbox; + + /* Send UIOn message to plugin, which will request state and enable message + transmission. */ + send_ui_enable(ui); + + return ui; +} + +static void +cleanup(LV2UI_Handle handle) +{ + EgScopeUI* ui = (EgScopeUI*)handle; + /* Send UIOff message to plugin, which will save state and disable message + * transmission. */ + send_ui_disable(ui); + gtk_widget_destroy(ui->darea); + free(ui); +} + +static int +recv_raw_audio(EgScopeUI* ui, const LV2_Atom_Object* obj) +{ + const LV2_Atom* chan_val = NULL; + const LV2_Atom* data_val = NULL; + const int n_props = lv2_atom_object_get( + obj, + ui->uris.channelID, &chan_val, + ui->uris.audioData, &data_val, + NULL); + + if (n_props != 2 || + chan_val->type != ui->uris.atom_Int || + data_val->type != ui->uris.atom_Vector) { + // Object does not have the required properties with correct types + fprintf(stderr, "eg-scope.lv2 UI error: Corrupt audio message\n"); + return 1; + } + + // Get the values we need from the body of the property value atoms + const int32_t chn = ((const LV2_Atom_Int*)chan_val)->body; + const LV2_Atom_Vector* vec = (const LV2_Atom_Vector*)data_val; + if (vec->body.child_type != ui->uris.atom_Float) { + return 1; // Vector has incorrect element type + } + + // Number of elements = (total size - header size) / element size + const size_t n_elem = ((data_val->size - sizeof(LV2_Atom_Vector_Body)) + / sizeof(float)); + + // Float elements immediately follow the vector body header + const float* data = (const float*)(&vec->body + 1); + + // Update display + update_scope(ui, chn, n_elem, data); + return 0; +} + +static int +recv_ui_state(EgScopeUI* ui, const LV2_Atom_Object* obj) +{ + const LV2_Atom* spp_val = NULL; + const LV2_Atom* amp_val = NULL; + const LV2_Atom* rate_val = NULL; + const int n_props = lv2_atom_object_get( + obj, + ui->uris.ui_spp, &spp_val, + ui->uris.ui_amp, &_val, + ui->uris.param_sampleRate, &rate_val, + NULL); + + if (n_props != 3 || + spp_val->type != ui->uris.atom_Int || + amp_val->type != ui->uris.atom_Float || + rate_val->type != ui->uris.atom_Float) { + // Object does not have the required properties with correct types + fprintf(stderr, "eg-scope.lv2 UI error: Corrupt state message\n"); + return 1; + } + + // Get the values we need from the body of the property value atoms + const int32_t spp = ((const LV2_Atom_Int*)spp_val)->body; + const float amp = ((const LV2_Atom_Float*)amp_val)->body; + const float rate = ((const LV2_Atom_Float*)rate_val)->body; + + // Disable transmission and update UI + ui->updating = true; + gtk_spin_button_set_value(GTK_SPIN_BUTTON(ui->spb_speed), spp); + gtk_spin_button_set_value(GTK_SPIN_BUTTON(ui->spb_amp), amp); + ui->updating = false; + ui->rate = rate; + + return 0; +} + +/** + Receive data from the DSP-backend. + + This is called by the host, typically at a rate of around 25 FPS. + + Ideally this happens regularly and with relatively low latency, but there + are no hard guarantees about message delivery. +*/ +static void +port_event(LV2UI_Handle handle, + uint32_t port_index, + uint32_t buffer_size, + uint32_t format, + const void* buffer) +{ + EgScopeUI* ui = (EgScopeUI*)handle; + const LV2_Atom* atom = (const LV2_Atom*)buffer; + + /* Check type of data received + * - format == 0: Control port event (float) + * - format > 0: Message (atom) + */ + if (format == ui->uris.atom_eventTransfer && + lv2_atom_forge_is_object_type(&ui->forge, atom->type)) { + const LV2_Atom_Object* obj = (const LV2_Atom_Object*)atom; + if (obj->body.otype == ui->uris.RawAudio) { + recv_raw_audio(ui, obj); + } else if (obj->body.otype == ui->uris.ui_State) { + recv_ui_state(ui, obj); + } + } +} + +static const LV2UI_Descriptor descriptor = { + SCO_URI "#ui", + instantiate, + cleanup, + port_event, + NULL +}; + +LV2_SYMBOL_EXPORT +const LV2UI_Descriptor* +lv2ui_descriptor(uint32_t index) +{ + switch (index) { + case 0: + return &descriptor; + default: + return NULL; + } +} diff --git a/plugins/eg-scope.lv2/manifest.ttl.in b/plugins/eg-scope.lv2/manifest.ttl.in new file mode 100644 index 0000000..a64aff1 --- /dev/null +++ b/plugins/eg-scope.lv2/manifest.ttl.in @@ -0,0 +1,21 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix ui: <http://lv2plug.in/ns/extensions/ui#> . + +# ==== Mono plugin variant ==== +<http://lv2plug.in/plugins/eg-scope#Mono> + a lv2:Plugin ; + lv2:binary <examploscope@LIB_EXT@> ; + rdfs:seeAlso <examploscope.ttl> . + +# ==== Stereo plugin variant ==== +<http://lv2plug.in/plugins/eg-scope#Stereo> + a lv2:Plugin ; + lv2:binary <examploscope@LIB_EXT@> ; + rdfs:seeAlso <examploscope.ttl> . + +# ==== Gtk 2.0 UI ==== +<http://lv2plug.in/plugins/eg-scope#ui> + a ui:GtkUI ; + ui:binary <examploscope_ui@LIB_EXT@> ; + rdfs:seeAlso <examploscope.ttl> . diff --git a/plugins/eg-scope.lv2/uris.h b/plugins/eg-scope.lv2/uris.h new file mode 100644 index 0000000..7c13c06 --- /dev/null +++ b/plugins/eg-scope.lv2/uris.h @@ -0,0 +1,71 @@ +/* + Copyright 2013 Robin Gareus <robin@gareus.org> + + 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. +*/ + +#ifndef SCO_URIS_H +#define SCO_URIS_H + +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" +#include "lv2/lv2plug.in/ns/ext/atom/forge.h" +#include "lv2/lv2plug.in/ns/ext/parameters/parameters.h" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" + +#define SCO_URI "http://lv2plug.in/plugins/eg-scope" + +typedef struct { + // URIs defined in LV2 specifications + LV2_URID atom_Vector; + LV2_URID atom_Float; + LV2_URID atom_Int; + LV2_URID atom_eventTransfer; + LV2_URID param_sampleRate; + + /* URIs defined for this plugin. It is best to re-use existing URIs as + much as possible, but plugins may need more vocabulary specific to their + needs. These are used as types and properties for plugin:UI + communication, as well as for saving state. */ + LV2_URID RawAudio; + LV2_URID channelID; + LV2_URID audioData; + LV2_URID ui_On; + LV2_URID ui_Off; + LV2_URID ui_State; + LV2_URID ui_spp; + LV2_URID ui_amp; +} ScoLV2URIs; + +static inline void +map_sco_uris(LV2_URID_Map* map, ScoLV2URIs* uris) +{ + uris->atom_Vector = map->map(map->handle, LV2_ATOM__Vector); + uris->atom_Float = map->map(map->handle, LV2_ATOM__Float); + uris->atom_Int = map->map(map->handle, LV2_ATOM__Int); + uris->atom_eventTransfer = map->map(map->handle, LV2_ATOM__eventTransfer); + uris->param_sampleRate = map->map(map->handle, LV2_PARAMETERS__sampleRate); + + /* Note the convention that URIs for types are capitalized, and URIs for + everything else (mainly properties) are not, just as in LV2 + specifications. */ + uris->RawAudio = map->map(map->handle, SCO_URI "#RawAudio"); + uris->audioData = map->map(map->handle, SCO_URI "#audioData"); + uris->channelID = map->map(map->handle, SCO_URI "#channelID"); + uris->ui_On = map->map(map->handle, SCO_URI "#UIOn"); + uris->ui_Off = map->map(map->handle, SCO_URI "#UIOff"); + uris->ui_State = map->map(map->handle, SCO_URI "#UIState"); + uris->ui_spp = map->map(map->handle, SCO_URI "#ui-spp"); + uris->ui_amp = map->map(map->handle, SCO_URI "#ui-amp"); +} + +#endif /* SCO_URIS_H */ diff --git a/plugins/eg-scope.lv2/wscript b/plugins/eg-scope.lv2/wscript new file mode 100644 index 0000000..3fb0687 --- /dev/null +++ b/plugins/eg-scope.lv2/wscript @@ -0,0 +1,74 @@ +#!/usr/bin/env python +from waflib.extras import autowaf as autowaf +import re + +# Variables for 'waf dist' +APPNAME = 'eg-scope.lv2' +VERSION = '1.0.0' + +# Mandatory variables +top = '.' +out = 'build' + +def options(opt): + opt.load('compiler_c') + opt.load('lv2') + autowaf.set_options(opt) + +def configure(conf): + autowaf.display_header('Scope Configuration') + conf.load('compiler_c', cache=True) + conf.load('lv2', cache=True) + conf.load('autowaf', cache=True) + + if not autowaf.is_child(): + autowaf.check_pkg(conf, 'lv2', atleast_version='1.2.1', uselib_store='LV2') + + autowaf.check_pkg(conf, 'cairo', uselib_store='CAIRO', + atleast_version='1.8.10', mandatory=True) + autowaf.check_pkg(conf, 'gtk+-2.0', uselib_store='GTK2', + atleast_version='2.18.0', mandatory=False) + + autowaf.display_msg(conf, 'LV2 bundle directory', conf.env.LV2DIR) + print('') + +def build(bld): + bundle = 'eg-scope.lv2' + + # Make a pattern for shared objects without the 'lib' prefix + module_pat = re.sub('^lib', '', bld.env.cshlib_PATTERN) + module_ext = module_pat[module_pat.rfind('.'):] + + # Build manifest.ttl by substitution (for portable lib extension) + for i in ['manifest.ttl', 'examploscope.ttl']: + bld(features = 'subst', + source = i + '.in', + target = '%s/%s' % (bundle, i), + install_path = '${LV2DIR}/%s' % bundle, + LIB_EXT = module_ext) + + # Use LV2 headers from parent directory if building as a sub-project + includes = ['.'] + if autowaf.is_child: + includes += ['../..'] + + # Build plugin library + obj = bld(features = 'c cshlib', + source = 'examploscope.c', + name = 'examploscope', + target = '%s/examploscope' % bundle, + install_path = '${LV2DIR}/%s' % bundle, + use = 'LV2', + includes = includes) + obj.env.cshlib_PATTERN = module_pat + + # Build UI library + if bld.env.HAVE_GTK2: + obj = bld(features = 'c cshlib', + source = 'examploscope_ui.c', + name = 'examploscope_ui', + target = '%s/examploscope_ui' % bundle, + install_path = '${LV2DIR}/%s' % bundle, + use = 'GTK2 CAIRO LV2', + includes = includes) + obj.env.cshlib_PATTERN = module_pat diff --git a/plugins/literasc.py b/plugins/literasc.py new file mode 100755 index 0000000..f0b8cc4 --- /dev/null +++ b/plugins/literasc.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Literasc, a simple literate programming tool for C, C++, and Turtle. +# Copyright 2012 David Robillard <d@drobilla.net> +# +# Unlike many LP tools, this tool uses normal source code as input, there is no +# tangle/weave and no special file format. The literate parts of the program +# are written in comments, which are emitted as paragraphs of regular text +# interleaved with code. Asciidoc is both the comment and output syntax. + +import os +import re +import sys + +def format_text(text): + 'Format a text (comment) fragment and return it as a marked up string' + return '\n\n' + re.sub('\n *', '\n', text.strip()) + '\n\n' + +def format_code(lang, code): + if code.strip() == '': + return code + + head = '[source,%s]' % lang + sep = '-' * len(head) + '\n' + return head + '\n' + sep + code.strip('\n') + '\n' + sep + +def format_c_source(filename, file): + output = '=== %s ===\n' % os.path.basename(filename) + chunk = '' + prev_c = 0 + in_comment = False + in_comment_start = False + n_stars = 0 + code = '' + for line in file: + code += line + + # Skip initial license comment + if code[0:2] == '/*': + code = code[code.find('*/') + 2:] + + for c in code: + if prev_c == '/' and c == '*': + in_comment_start = True + n_stars = 1 + elif in_comment_start: + if c == '*': + n_stars += 1 + else: + if n_stars > 1: + output += format_code('c', chunk[0:len(chunk)-1]) + chunk = '' + in_comment = True + else: + chunk += '*' + c + in_comment_start = False + elif in_comment and prev_c == '*' and c == '/': + if n_stars > 1: + output += format_text(chunk[0:len(chunk)-1]) + else: + output += format_code('c', '/* ' + chunk[0:len(chunk)-1] + '*/') + in_comment = False + in_comment_start = False + chunk = '' + elif in_comment_start and c == '*': + n_stars += 1 + else: + chunk += c + prev_c = c + + return output + format_code('c', chunk) + +def format_ttl_source(filename, file): + output = '=== %s ===\n' % os.path.basename(filename) + + in_comment = False + chunk = '' + for line in file: + is_comment = line.strip().startswith('#') + if in_comment: + if is_comment: + chunk += line.strip().lstrip('# ') + ' \n' + else: + output += format_text(chunk) + in_comment = False + chunk = line + else: + if is_comment: + output += format_code('n3', chunk) + in_comment = True + chunk = line.strip().lstrip('# ') + ' \n' + else: + chunk += line + + if in_comment: + return output + format_text(chunk) + else: + return output + format_code('n3', chunk) + +def gen(out, filenames): + for filename in filenames: + file = open(filename) + if not file: + sys.stderr.write('Failed to open file %s\n' % filename) + continue + + if filename.endswith('.c') or filename.endswith('.h'): + out.write(format_c_source(filename, file)) + elif filename.endswith('.ttl') or filename.endswith('.ttl.in'): + out.write(format_ttl_source(filename, file)) + elif filename.endswith('.txt'): + for line in file: + out.write(line) + out.write('\n') + else: + sys.stderr.write("Unknown source format `%s'" % ( + filename[filename.find('.'):])) + + file.close() + +if __name__ == "__main__": + if len(sys.argv) < 2: + sys.stderr.write('Usage: %s FILENAME...\n' % sys.argv[1]) + sys.exit(1) + + gen(sys.argv[1:]) diff --git a/plugins/wscript b/plugins/wscript new file mode 100644 index 0000000..f5f6571 --- /dev/null +++ b/plugins/wscript @@ -0,0 +1,45 @@ +#!/usr/bin/env python +import os + +from waflib.extras import autowaf as autowaf +import waflib.Logs as Logs + +import literasc + +def confgure(conf): + pass + +def bld_book_src(task): + filenames = [] + for i in task.inputs: + filenames += [i.abspath()] + + literasc.gen(open(task.outputs[0].abspath(), 'w'), filenames) + +def build(bld): + files = [bld.path.find_node('README.txt')] + for i in ['eg-amp.lv2', + 'eg-midigate.lv2', + 'eg-fifths.lv2', + 'eg-metro.lv2', + 'eg-sampler.lv2', + 'eg-scope.lv2', + 'eg-params.lv2']: + files += bld.path.ant_glob('%s/*.txt' % i) + files += bld.path.ant_glob('%s/manifest.ttl*' % i) + files += bld.path.ant_glob('%s/*.ttl' % i) + files += bld.path.ant_glob('%s/*.c' % i) + files += bld.path.ant_glob('%s/*.h' % i) + + # Compile book sources into book.txt asciidoc source + bld(rule = bld_book_src, + source = files, + target = 'book.txt') + + # Run asciidoc to generate book.html + stylesdir = bld.path.find_node('../doc/').abspath() + pygments_style = bld.path.find_node('../doc/style.css').abspath() + bld(rule = 'asciidoc -a stylesdir=%s -a source-highlighter=pygments -a pygments-style=%s -b html -o ${TGT} ${SRC}' % ( + stylesdir, pygments_style), + source = 'book.txt', + target = 'book.html') |