Programming LV2 Plugins - Rust Edition

Foreword

This book is an effort to translate the LV2 Book by David Robillard for the rust-lv2 ecosystem. As such, the examples in this book as well as the README’s and comments are copied from the original, but the book itself has been altered to adapt for the differences between C and Rust.

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 Rust, but otherwise no special knowledge is required; the first plugin describes the basics in detail.

Each chapter corresponds to executable plugin code which can be found in the book’s Github Repository. If you prefer to read actual source code, all the content here is also available in the source code as comments.

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 provides an interface to the host written in C, but it can be written in any C-compatible language. Static data is described separately in the human and machine friendly 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:

amp/eg-amp-rs.lv2/manifest.ttl

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.

In a crate, this should be a special folder that contains the Turtle files. After the crate was build, the resulting shared object should also be copied into this folder.

URIs

LV2 makes use of URIs as globally-unique identifiers for resources. For example, the ID of the plugin described here is <https://github.com/Janonard/rust-lv2-book#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:

Subject Predicate Object
<https://github.com/Janonard/rust-lv2-book#amp> a lv2:Plugin
<https://github.com/Janonard/rust-lv2-book#amp> lv2:binary <amp.so>
<https://github.com/Janonard/rust-lv2-book#amp> rdfs:seeAlso <amp.ttl>

Firstly, <https://github.com/Janonard/rust-lv2-book#amp> is an LV2 plugin:


<https://github.com/Janonard/rust-lv2-book#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>:


<https://github.com/Janonard/rust-lv2-book#amp> lv2:binary <libamp.so> .

This line is platform-dependent since it assumes that shared objects have the .so ending. On Windows, it should be ending with .dll. 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>:


<https://github.com/Janonard/rust-lv2-book#amp> rdfs:seeAlso <amp.ttl> .

amp/eg-amp-rs.lv2/amp.ttl

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.

<https://github.com/Janonard/rust-lv2-book#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 rust-lv2-book project, which has URI https://github.com/Janonard/rust-lv2-book, and is described elsewhere. Typical plugin collections will describe the project in manifest.ttl

        lv2:project <https://github.com/Janonard/rust-lv2-book> ;

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 later.

        doap:name "Simple Amplifier (Rust Version)" ;
        doap:license <http://opensource.org/licenses/isc> ;

This tells the host that this plugin can not work “in-place”; The input and output buffers may not be the same. In this case, this isn’t technically true, but it would mean that the plugin would receive a mutable and an immutable reference to the same place in memory, which obviously isn’t allowed in Rust.

        lv2:requiredFeature lv2:inPlaceBroken ;
        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" ,
                        "收益"@ch ,
                        "Gewinn"@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"
        ] .

amp/Cargo.toml

The host does not really care in which language the code of the plugin is written, as long as the built library complies to the headers of the specifications. Therefore, every plugin is a standard Cargo crate.

[package]
name = "amp"
version = "0.2.0"
authors = ["Jan-Oliver 'Janonard' Opdenhövel <jan.opdenhoevel@protonmail.com>"]
license = "ISC"
edition = "2018"

Plugins are dynamic libraries. This setting tells cargo to export it this way.

[lib]
crate-type = ["dylib"]

Rust-lv2 is a network of individual sub-crates with different version numbers and histories. However, most plugins don’t need to deal with them directly. Instead, they use the re-export crate simply called lv2. It has an optional dependency to every sub-crate, which can be enabled via crate features.

The default feature set includes everything to create a simple plugin for audio and MIDI processing. Therefore, we don’t need to enable extra features here.

[dependencies]
lv2 = "0.5.0"

amp/src/lib.rs

Include the prelude of lv2. This includes the preludes of every sub-crate and you are strongly encouraged to use it, since many macros depend on it.

use lv2::prelude::*;

Most useful plugins will have ports for input and output data. In code, these ports are represented by a struct implementing the PortCollection trait. Internally, ports are referred to by index. These indices are assigned in ascending order, starting with 0 for the first port. The indices in amp.ttl have to match them.

#[derive(PortCollection)]
struct Ports {
    gain: InputPort<Control>,
    input: InputPort<Audio>,
    output: OutputPort<Audio>,
}

Every plugin defines a struct for the plugin instance. All persistent data associated with a plugin instance is stored here, and is available to every instance method. In this simple plugin, there is no additional instance data and therefore, this struct is empty.

The URI is the identifier for a plugin, and how the host associates this implementation in code with its description in data. If this URI does not match that used in the data files, the host will fail to load the plugin. This attribute internally implements the UriBound trait for Amp, which is also used to identify many other things in the rust-lv2 ecosystem.

#[uri("https://github.com/Janonard/rust-lv2-book#amp")]
struct Amp;

Every plugin struct implements the Plugin trait. This trait contains both the methods that are called by the hosting application and the collection types for the ports and the used host features. This plugin does not use additional host features and therefore, we set both feature collection types to (). Other plugins may define separate structs with their required and optional features and set it here.

impl Plugin for Amp {
    type Ports = Ports;

    type InitFeatures = ();
    type AudioFeatures = ();

The new method is called by the plugin backend when it creates a new plugin instance. The host passes the plugin URI, 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 method is in the “instantiation” threading class, so no other methods on this instance will be called concurrently with it.

    fn new(_plugin_info: &PluginInfo, _features: &mut ()) -> Option<Self> {
        Some(Self)
    }

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.

    fn run(&mut self, ports: &mut Ports, _features: &mut ()) {
        let coef = if *(ports.gain) > -90.0 {
            10.0_f32.powf(*(ports.gain) * 0.05)
        } else {
            0.0
        };

        for (in_frame, out_frame) in Iterator::zip(ports.input.iter(), ports.output.iter_mut()) {
            *out_frame = in_frame * coef;
        }
    }
}

The lv2_descriptors macro creates the entry point to the plugin library. It takes structs that implement Plugin and exposes them. The host will load the library and call a generated function to find all the plugins defined in the library.

lv2_descriptors!(Amp);

MIDI Gate

This plugin demonstrates:

A key concept of LV2 that is introduced with this plugin is URID. As you’ve learned before, many things in the LV2 ecosystem are identified by URIs. However, comparing URIs isn’t nescessarily fast and the time it takes to compare URIs rises with their length. Instead, every known URI is mapped to number, a so-called URID, which is used instead of the full URI when time and space is valuable. This mapping is done by the host, which also assures that the mappings are consistent across plugins. Therefore, URIDs are also used for host-plugin or plugin-plugin communication.

midigate/eg-midigate-rs.lv2/manifest.ttl

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#> .

<https://github.com/Janonard/rust-lv2-book#midigate>
	a lv2:Plugin ;
	lv2:binary <libmidigate.so> ;
	rdfs:seeAlso <midigate.ttl> .

midigate/eg-midigate-rs.lv2/midigate.ttl

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#> .

<https://github.com/Janonard/rust-lv2-book#midigate>
	a lv2:Plugin ;
	doap:name "Example MIDI Gate (Rust Version)" ;
	doap:license <http://opensource.org/licenses/isc> ;
	lv2:project <https://github.com/Janonard/rust-lv2-book> ;
	lv2:requiredFeature urid:map , lv2:inPlaceBroken ;
	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"
	] .

midigate/Cargo.toml

The Cargo.toml file is pretty similiar too. This plugin needs to extra features, but it needs the wmidi crate, which provides the enums to handle MIDI messages.

[package]
name = "midigate"
version = "0.1.0"
authors = ["Jan-Oliver 'Janonard' Opdenhövel <jan.opdenhoevel@protonmail.com>"]
edition = "2018"

[lib]
crate-type = ["dylib"]

[dependencies]
wmidi = "3.1.0"
lv2 = "0.5.0"

midigate/src/lib.rs

Use the prelude and the wmidi crate.

use lv2::prelude::*;
use wmidi::*;

#[derive(PortCollection)]
pub struct Ports {
    control: InputPort<AtomPort>,
    input: InputPort<Audio>,
    output: OutputPort<Audio>,
}

Now, an additional host feature is needed. A feature is something that implements the Feature trait and usually wraps a certain functionality of the host; In this case mapping URIs to URIDs. The discovery and validation of features is done by the framework.

#[derive(FeatureCollection)]
pub struct Features<'a> {
    map: LV2Map<'a>,
}

Retrieving URIDs from the host isn’t guaranteed to be real-time safe or even fast. Therefore, all URIDs that may be needed should be retrieved when the plugin is instantiated. The URIDCollection trait makes this easy: It provides a single method that creates an instance of itself from the mapping feature, which can also be generated using this derive macro.

#[derive(URIDCollection)]
pub struct URIDs {
    atom: AtomURIDCollection,
    midi: MidiURIDCollection,
    unit: UnitURIDCollection,
}

#[uri("https://github.com/Janonard/rust-lv2-book#midigate")]
pub struct Midigate {
    n_active_notes: u64,
    program: u8,
    urids: URIDs,
}

impl Midigate {

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.

    fn write_output(&mut self, ports: &mut Ports, offset: usize, mut len: usize) {

check the bounds of the offset and length and cap the length, if nescessary.

        if ports.input.len() < offset + len {
            len = ports.input.len() - offset;
        }

        let active = if self.program == 0 {
            self.n_active_notes > 0
        } else {
            self.n_active_notes == 0
        };

        let input = &ports.input[offset..offset + len];
        let output = &mut ports.output[offset..offset + len];

        if active {
            output.copy_from_slice(input);
        } else {
            for frame in output.iter_mut() {
                *frame = 0.0;
            }
        }
    }
}

impl Plugin for Midigate {
    type Ports = Ports;

    type InitFeatures = Features<'static>;
    type AudioFeatures = ();

    fn new(_plugin_info: &PluginInfo, features: &mut Features<'static>) -> Option<Self> {
        Some(Self {
            n_active_notes: 0,
            program: 0,
            urids: features.map.populate_collection()?,
        })
    }

    fn activate(&mut self, _features: &mut Features<'static>) {
        self.n_active_notes = 0;
        self.program = 0;
    }

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.

    fn run(&mut self, ports: &mut Ports, _: &mut ()) {
        let mut offset: usize = 0;

        let control_sequence = ports
            .control
            .read(self.urids.atom.sequence, self.urids.unit.beat)
            .unwrap();

        for (timestamp, message) in control_sequence {
            let timestamp: usize = if let Some(timestamp) = timestamp.as_frames() {
                timestamp as usize
            } else {
                continue;
            };

            let message = if let Some(message) = message.read(self.urids.midi.wmidi, ()) {
                message
            } else {
                continue;
            };

            match message {
                MidiMessage::NoteOn(_, _, _) => self.n_active_notes += 1,
                MidiMessage::NoteOff(_, _, _) => self.n_active_notes -= 1,
                MidiMessage::ProgramChange(_, program) => {
                    let program: u8 = program.into();
                    if program == 0 || program == 1 {
                        self.program = program;
                    }
                }
                _ => (),
            }

            self.write_output(ports, offset, timestamp + offset);
            offset += timestamp;
        }

        self.write_output(ports, offset, ports.input.len() - offset);
    }
}

lv2_descriptors!(Midigate);

Fifths

This plugin demonstrates simple MIDI event reading and writing.

fifths/eg-fifths-rs.lv2/manifest.ttl

@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#> .

<https://github.com/Janonard/rust-lv2-book#fifths>
	a lv2:Plugin ;
	lv2:binary <libfifths.so> ;
	rdfs:seeAlso <fifths.ttl> .

fifths/eg-fifths-rs.lv2/fifths.ttl

@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#> .

<https://github.com/Janonard/rust-lv2-book#fifths>
	a lv2:Plugin ;
	doap:name "Example Fifths (Rust Edition)" ;
	doap:license <http://opensource.org/licenses/isc> ;
    lv2:project <https://github.com/Janonard/rust-lv2-book> ;
	lv2:requiredFeature urid:map , lv2:inPlaceBroken ;
	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"
	] .

fifths/Cargo.toml

[package]
name = "fifths"
version = "0.1.0"
authors = ["Jan-Oliver 'Janonard' Opdenhövel <jan.opdenhoevel@protonmail.com>"]
edition = "2018"

[lib]
crate-type = ["dylib"]

[dependencies]
wmidi = "3.1.0"
lv2 = "0.5.0"

fifths/src/lib.rs

use lv2::prelude::*;
use wmidi::*;

#[derive(PortCollection)]
pub struct Ports {
    input: InputPort<AtomPort>,
    output: OutputPort<AtomPort>,
}

#[derive(FeatureCollection)]
pub struct Features<'a> {
    map: LV2Map<'a>,
}

#[derive(URIDCollection)]
pub struct URIDs {
    atom: AtomURIDCollection,
    midi: MidiURIDCollection,
    unit: UnitURIDCollection,
}

#[uri("https://github.com/Janonard/rust-lv2-book#fifths")]
pub struct Fifths {
    urids: URIDs,
}

impl Plugin for Fifths {
    type Ports = Ports;

    type InitFeatures = Features<'static>;
    type AudioFeatures = ();

    fn new(_plugin_info: &PluginInfo, features: &mut Features<'static>) -> Option<Self> {
        Some(Self {
            urids: features.map.populate_collection()?,
        })
    }

    fn run(&mut self, ports: &mut Ports, _: &mut ()) {

Get the reading handle of the input sequence.

        let input_sequence = ports
            .input
            .read(self.urids.atom.sequence, self.urids.unit.beat)
            .unwrap();

Initialise the output sequence and get the writing handle.

        let mut output_sequence = ports
            .output
            .init(
                self.urids.atom.sequence,
                TimeStampURID::Frames(self.urids.unit.frame),
            )
            .unwrap();

        for (timestamp, atom) in input_sequence {

Forward message to output.

            output_sequence.forward(timestamp, atom);

Retrieve the message.

            let message = if let Some(message) = atom.read(self.urids.midi.wmidi, ()) {
                message
            } else {
                continue;
            };

            match message {
                MidiMessage::NoteOn(channel, note, velocity) => {

Make a note 5th (7 semitones) higher than input.

                    if let Ok(note) = note.step(7) {

Write the fifth. Writing is done after initialization.

                        output_sequence
                            .init(
                                timestamp,
                                self.urids.midi.wmidi,
                                MidiMessage::NoteOn(channel, note, velocity),
                            )
                            .unwrap();
                        println!("Wrote a note-on");
                    }
                }
                MidiMessage::NoteOff(channel, note, velocity) => {

Do the same thing for NoteOff.

                    if let Ok(note) = note.step(7) {
                        output_sequence
                            .init(
                                timestamp,
                                self.urids.midi.wmidi,
                                MidiMessage::NoteOff(channel, note, velocity),
                            )
                            .unwrap();
                        println!("Wrote a note-off");
                    }
                }
                _ => println!("Something different!"),
            }
        }
    }
}

lv2_descriptors!(Fifths);