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 /plugins | |
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.
Diffstat (limited to 'plugins')
-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 |
6 files changed, 389 insertions, 0 deletions
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 + |