diff options
author | David Robillard <d@drobilla.net> | 2013-02-10 03:21:20 +0000 |
---|---|---|
committer | David Robillard <d@drobilla.net> | 2013-02-10 03:21:20 +0000 |
commit | 2deb34ec442772f5f68c1818a290748ac5a1ed36 (patch) | |
tree | 0e377eb79f909b83c3c69692328df642b86201c5 | |
parent | 401d1e9591633dc5c47ccb8e341b63a423c0ee76 (diff) | |
download | lv2-2deb34ec442772f5f68c1818a290748ac5a1ed36.tar.xz |
Add lv2:Bank, lv2:Program, and lv2:program for describing programs, including but not limited to MIDI programs.
-rw-r--r-- | lv2/lv2plug.in/ns/lv2core/lv2core.doap.ttl | 6 | ||||
-rw-r--r-- | lv2/lv2plug.in/ns/lv2core/lv2core.ttl | 56 | ||||
-rw-r--r-- | plugins/eg-midigate.lv2/README.txt | 10 | ||||
-rw-r--r-- | plugins/eg-midigate.lv2/manifest.ttl.in | 10 | ||||
-rw-r--r-- | plugins/eg-midigate.lv2/midigate.c | 223 | ||||
-rw-r--r-- | plugins/eg-midigate.lv2/midigate.ttl | 80 | ||||
l--------- | plugins/eg-midigate.lv2/waf | 1 | ||||
-rw-r--r-- | plugins/eg-midigate.lv2/wscript | 65 | ||||
-rw-r--r-- | wscript | 2 |
9 files changed, 449 insertions, 4 deletions
diff --git a/lv2/lv2plug.in/ns/lv2core/lv2core.doap.ttl b/lv2/lv2plug.in/ns/lv2core/lv2core.doap.ttl index 4fe9f3a..b4eb5c3 100644 --- a/lv2/lv2plug.in/ns/lv2core/lv2core.doap.ttl +++ b/lv2/lv2plug.in/ns/lv2core/lv2core.doap.ttl @@ -16,12 +16,14 @@ <http://drobilla.net/drobilla#me> ; doap:maintainer <http://drobilla.net/drobilla#me> ; doap:release [ - doap:revision "9.1" ; - doap:created "2013-01-10" ; + doap:revision "9.2" ; + doap:created "2013-02-09" ; dcs:changeset [ dcs:item [ rdfs:label "Add lv2:EnvelopePlugin class." ] , [ + rdfs:label "Add lv2:Bank, lv2:Program, and lv2:program for describing programs, including but not limited to MIDI programs." + ] , [ rdfs:label "Add lv2:control for designating primary event-based control ports." ] , [ rdfs:label "Set range of lv2:designation to lv2:Designation." diff --git a/lv2/lv2plug.in/ns/lv2core/lv2core.ttl b/lv2/lv2plug.in/ns/lv2core/lv2core.ttl index 8bfd6ec..be968f4 100644 --- a/lv2/lv2plug.in/ns/lv2core/lv2core.ttl +++ b/lv2/lv2plug.in/ns/lv2core/lv2core.ttl @@ -122,6 +122,19 @@ which of these classes the functions belong, define new classes for them, or otherwise precisely describe their threading rules.</p> """ . +lv2:Bank + a rdfs:Class , + owl:Class ; + rdfs:label "Bank" ; + lv2:documentation """ +<p>A bank of programs. See lv2:Program for details.</p> + +<p>A Bank MUST have an rdfs:label giving a short label suitable for +presentation in a user interface, and SHOULD have an rdfs:comment if a more +detailed description is available. For MIDI compatibility, a bank SHOULD have +an lv2:index within [0,127].</p> +""" . + lv2:PluginBase a rdfs:Class , owl:Class ; @@ -219,6 +232,31 @@ necessarily refer to the same port on all plugins with a given URI (i.e. the index for a port may differ between plugin binaries).</p> """ . +lv2:Program + a rdfs:Class , + owl:Class ; + rdfs:label "Program" ; + lv2:documentation """ +<p>A plugin configuration or mode. This can describe MIDI programs, but is +more general. Implementations MAY support changing programs by any +mechanism.</p> + +<p>A program MUST have an rdfs:label giving a short label suitable for +presentation in a user interface, and SHOULD have an rdfs:comment if a more +detailed description is available. For MIDI compatibility, a program SHOULD +have an lv2:index within [0,127].</p> + +<p>Note that a program is an internal plugin mode, not necessarily a complete +snapshot of plugin state (which is a <q>preset</q>). The currently active +program is a parameter, lv2:program. Programs allow plugins to implement +internal configurations opaque to the host; in particular, a program does not +define control input values. The distinction is analogous to hardware that +supports program changes but also has physical controls that can not be changed +by software. However, a program MAY be associated with a preset, or one +resource may be both a program and a preset, to support completely changing a +plugin instance's state via a program change.</p> +""" . + lv2:InputPort a rdfs:Class , owl:Class ; @@ -277,6 +315,23 @@ versa. Hosts SHOULD take care to prevent data from a CVPort port from being used as audio.</p> """ . +lv2:bank + a rdf:Property , + owl:ObjectProperty , + lv2:Parameter ; + rdfs:domain lv2:PluginBase ; + rdfs:range lv2:Bank ; + rdfs:label "bank" ; + rdfs:comment "A supported bank of programs, or the currently active bank." . + +lv2:program + a rdf:Property , + owl:ObjectProperty , + lv2:Parameter ; + rdfs:range lv2:Program ; + rdfs:label "program" ; + rdfs:comment "A program in a bank, or the currently active program." . + lv2:port a rdf:Property , owl:ObjectProperty ; @@ -312,7 +367,6 @@ lv2:minorVersion rdfs:range xsd:nonNegativeInteger ; rdfs:label "minor version" ; lv2:documentation """ - <p>The minor version of an LV2 Resource. This, along with lv2:microVersion, is used to distinguish between different versions of the <q>same</q> resource, e.g. to load only the bundle with the most recent version of a plugin. An LV2 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..3b74bfc --- /dev/null +++ b/plugins/eg-midigate.lv2/midigate.c @@ -0,0 +1,223 @@ +/* + Copyright 2013 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/midi/midi.h" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2.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; + + struct { + LV2_URID midi_MidiEvent; + } uris; + + unsigned n_active_notes; + unsigned program; +} Midigate; + +static LV2_Handle +instantiate(const LV2_Descriptor* descriptor, + double rate, + const char* bundle_path, + const LV2_Feature* const* features) +{ + /** Scan features array for the URID feature we need. */ + LV2_URID_Map* map = NULL; + for (int i = 0; features[i]; ++i) { + if (!strcmp(features[i]->URI, LV2_URID__map)) { + map = (LV2_URID_Map*)features[i]->data; + break; + } + } + if (!map) { + /** + No URID feature given. This is a host bug since we require this + feature, but should be handled gracefully anyway. + */ + return NULL; + } + + Midigate* self = (Midigate*)calloc(1, sizeof(Midigate)); + self->map = map; + self->uris.midi_MidiEvent = map->map(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. + + 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: + --self->n_active_notes; + 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 = 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..59ac815 --- /dev/null +++ b/plugins/eg-midigate.lv2/midigate.ttl @@ -0,0 +1,80 @@ +# 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 ; +# Describe program banks so the host can automate and/or present a user +# interface. Describing supported programs (or any other MIDI event) is not +# required, but is a good idea since it allows hosts to make better use of +# plugins. This plugin has a single bank of two programs, which have a +# (mandatory) label, and an (optional) comment to describe their meaning in +# more detail. +# +# Both programs and the bank have an index, which corresponds to the MIDI bank +# and program numbers that will activate them. Since there are other ways to +# change programs (not used here), an index is not strictly required, but must +# be present to support program changes from MIDI. + lv2:bank [ + rdfs:label "Default" ; + lv2:index 0 ; + lv2:program [ + lv2:index 0 ; + rdfs:label "Normal" ; + rdfs:comment "Input is passed through if notes are active." + ] , [ + lv2:index 1 ; + rdfs:label "Inverted" ; + rdfs:comment "Input is passed through if no notes are active." + ] + ] ; +# 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..44336af --- /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') + autowaf.set_options(opt) + +def configure(conf): + conf.load('compiler_c') + autowaf.configure(conf) + autowaf.set_c99_mode(conf) + autowaf.display_header('Midigate Configuration') + + 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 + @@ -14,7 +14,7 @@ import waflib.Scripting as Scripting # Variables for 'waf dist' APPNAME = 'lv2' -VERSION = '1.3.1' +VERSION = '1.3.2' # Mandatory variables top = '.' |