LV2

From ZynthianWiki
Revision as of 16:07, 5 September 2025 by Wyleu (talk | contribs)
Jump to navigation Jump to search

1 So what is LV2?

1.1 From wikipedia...

LV2 (LADSPA Version 2) is a set of royalty-free open standards[2] for music production plug-ins and matching host applications. It includes support for the synthesis and processing of digital audio and CV,[3] events such as MIDI and OSC, and provides a free alternative to audio plug-in standards such as Virtual Studio Technology (VST) and Audio Units (AU).

1.2 And from the actual LV2 site...

LV2 is an interface for writing audio plugins in C or compatible languages, which can be dynamically loaded into many host applications. This core specification is simple and minimal, but is designed so that extensions can be defined to add more advanced features, making it possible to implement nearly any feature.

LV2 maintains a strong distinction between code and data. Plugin code is in a shared library, while data is in a companion data file written in Turtle. Code, data, and any other resources (such as waveforms) are shipped together in a bundle directory. The code contains only the executable portions of the plugin. All other data is provided in the data file(s). This makes plugin data flexible and extensible, and allows the host to do everything but run the plugin without loading or executing any code. Among other advantages, this makes hosts more robust (broken plugins can't crash a host during discovery) and allows generic tools written in any language to work with LV2 data. The LV2 specification itself is distributed in a similar way.

LV2 is an extensible framework, allowing a program to load a plugin to do some processing. Note that the terms used here are generic on purpose because LV2 allows any type of data to be exchanged between the host and the plugin.

1.3 In Summary

So there is a clear distinction between the definitions of the facilities which are held in carefully defined definition files and the actual software that accesses the facilities by reading the definition files. This way there is a controlled interaction betwenn the host and the plugin and each can be protected from the vagueries of the other.

It is a list of definitions that refer back to core definitions and these relationships are maintained in a format called RDF . . .

1.4 So what is RDF ?

Once more from wikipedia...

The Resource Description Framework (RDF) is a method to describe and exchange graph data. It was originally designed as a data model for metadata by the World Wide Web Consortium (W3C). It provides a variety of syntax notations and formats, of which the most widely used is Turtle (Terse RDF Triple Language).

It describes objects and the relationships between them . . .

RDF represents information using semantic triples, which comprise a subject, predicate, and object. Each item in the triple is expressed as a Web URI. Turtle provides a way to group three URIs to make a triple, and provides ways to abbreviate such information, for example by factoring out common portions of URIs. For example, information about Huckleberry Finn could be expressed as:

<http://example.org/books/Huckleberry_Finn>
  <http://example.org/relation/author>
  <http://example.org/person/Mark_Twain> .

In the zynthian plug in world it's primarily about the definition, allocation and mapping of audio and midi ports and parameters in the zynthian world to parameters provided by the LV2 plugin we are exchanging data with.

But first you establish some prefixes so you aren't typing out URL all the time... Here the classes that are defined by lv2 are specified ( follow the link) and similarly DOAP defines structure for a software project, and SPDX is the software package data exchange. Thus a set of definitions are described that can be understood by humans ( to a certain extent) and machines (VERY specifically)


@prefix lv2:  <http://lv2plug.in/ns/lv2core#>.
@prefix doap: <http://usefulinc.com/ns/doap#>.
@prefix spdx: <http://spdx.org/rdf/terms#>.

Then you use these prefixes to define the relationships we are concerned with...

The 'a' is an atom. More news as I get it. . .

<http://example.org/lv2/wikipediaexample/silence>
 a lv2:Plugin;
 lv2:binary <silence.so>;
 doap:name "Silence";
 doap:license spdx:GPL-3.0-or-later;
 rdfs:comment "This is an example plugin that includes an example plugin description."
 lv2:port [
   a lv2:AudioPort, lv2:OutputPort;
   lv2:index 0;
   lv2:symbol "output";
   lv2:name "Output";
 ].


1.4.1 How Do I start writing a lv2 component compatible with Zynthian?

1.4.1.1 Which zynthian components do I need to clone in Github to fully develop a plug in?

Not sure.

1.4.1.2 What do I need in code terms in addition to the normal zynthian files?

Somewhere to develop and test the code as we have described above. Now at this point we have to dip below the passive waters of Python where everything is slow, leisurely and well behaved. And embrace the proscribed world of C & C++ where code runs fast and life is cheap.

You will also need components that actually understand the concepts lv2 embodies. ( hopefully someone ( you know who you are)) will jump in and give us the history of this particular chunk of the mostly virtual universe we inhabit.

I'm working off a Grok summary on building a 50Hz & 60Hz notch filter and will attempt to ruthlessly hone this to something vaguely useful. Alternatively, I might go bell ringing.

You will need some lv3 libraries...

sudo apt install libasound2-dev lv2-dev
1.4.1.2.1 libasound2-dev

From the README.md file. The alsa-lib is a library to interface with ALSA in the Linux kernel and virtual devices using a plugin system.

Alsa Lib asound2 . . .

These are the alsa libraries, the essential glue to plug into the operating systems understanding of sound & MIDI on a Pi. Obviously this (at the moment) will only run on a Pi, so code elements at this level are aimed at that particular machine. That doesn't actually mean you can't write it on some other operating system, just be aware when you might be trying to put pears in containers designed for apples. . .

These libraries are the components that allow you to run the functionality described. But only that. If you wish to connect to them from your own programme then we need to install a little bit more, like descriptions of the hooks that those code pieces require and this is what the -dev on the end of the library name describes. Easy mistake to make it the early days, not installing the dev editions.

Why the slightly different names asound2 and alsa? Alsa has always been a bit impenetrable .

1.4.1.2.2 lv2-dev

LV2 is a plugin standard for audio systems. It defines an extensible C API for plugins, and a format for self-contained "bundle" directories that contain plugins, metadata, and other resources

LV2 components and the appropriate development files to muck around with lv2 files.

1.4.1.3 Anything else?

You are best to use a LV2 compatibleframework to construct the components to allow integration with the Linux system

There are:

  1. Juce
  2. DPF

1.4.1.4 Juce

https://github.com/rec/echomesh/blob/master/documentation/Building%20Juce%20applications%20on%20the%20Raspberry%20Pi.md

Juce is a popular and stable cross-platform open-source C++ development system that lets you write an application as a single codebase that works on Windows, OS/X and Linux.

Juce has a huge number of components relating to pretty well anything you need - GUI, audio processing, networking, etc. - and is particularly popular amongst audio software developers. You can find a lot more     information and some snappy demos on the Juce site.

It turns out that Juce supports the Raspberry Pi "out of the box" as long as you install a few libraries before you start. I created a sample application on the Mac, copied it to a Raspberry Pi and compiled it,  and it worked right the first time!

1.4.1.5 DPF

git clone --recursive https://github.com/DISTRHO/DPF.git

This is a tool to assist plug in writing....

DPF is designed to make development of new plugins an easy and enjoyable task.
It allows developers to create plugins with custom UIs using a simple C++ API.
The framework facilitates exporting various different plugin formats from the same code-base.
 DPF can build for LADSPA, DSSI, LV2, VST2, VST3 and CLAP formats.
 A JACK/Standalone mode is also available, allowing you to quickly test plugins.

Then make a directory.

mkdir SimpleGainPlugin
cd SimpleGainPlugin

Save the three files below (DistrhoPluginMain.cpp, DistrhoPluginInfo.h, Makefile) in this directory.

Lv2simple.png

1.4.1.6 So which is better?

Grok provided a comparison. For developing LV2 plugins on Zynthian, DPF is the better choice due to its native LV2 support, lightweight design, and alignment with Zynthian’s open-source, Linux-based ecosystem. It integrates seamlessly with Zynthian’s plugin system and performs well on its hardware. Use JUCE only if you need cross-platform compatibility (e.g., developing for VST3/AU alongside LV2) or prefer its polished development tools, but be prepared to optimize heavily for Zynthian’s constraints.

2 LV2 Components

2.1 A Notch Filter

2.1.1 The Definition Files

2.1.2 The Code components

2.1.2.1 DisthroPluginInfo.h
#ifndef DISTRHO_PLUGIN_INFO_H_INCLUDED
#define DISTRHO_PLUGIN_INFO_H_INCLUDED

#define DISTRHO_PLUGIN_BRAND "Wyleu"
#define DISTRHO_PLUGIN_NAME "SimpleGain"
#define DISTRHO_PLUGIN_URI "http://zynthian.org/plugins/lv2/simplegain"
#define DISTRHO_PLUGIN_HAS_UI 0
#define DISTRHO_PLUGIN_IS_RT_SAFE 1
#define DISTRHO_PLUGIN_NUM_INPUTS 2
#define DISTRHO_PLUGIN_NUM_OUTPUTS 2
#define DISTRHO_PLUGIN_WANT_LATENCY 0
#define DISTRHO_PLUGIN_WANT_PROGRAMS 0
#define DISTRHO_PLUGIN_WANT_STATE 0
#define DISTRHO_PLUGIN_WANT_TIMEPOS 0
#define DISTRHO_PLUGIN_IS_SYNTH 0

#endif
2.1.2.2 Partial Explanation of DistrhoPluginInfo.h

This defines the plugin’s metadata.

You get to define some information about you and what you are trying to do. You make decisions like am I going to use a GUI? How many inputs and outputs it will have and the expectations as how it will interact with the environment.

All wrapped in a test which is then settrue as part of programme flow. . . Very neat.

#ifndef DISTRHO_PLUGIN_INFO_H_INCLUDED
#define DISTRHO_PLUGIN_INFO_H_INCLUDED
...
#endif

The Python programmer in me would like a little indentation but such is life...

2.1.2.3 DisthroPluginMain.cpp
#include "DistrhoPlugin.hpp"

START_NAMESPACE_DPF

class SimpleGainPlugin : public Plugin {
public:
   SimpleGainPlugin() : Plugin(1, 0, 0) { // 1 parameter, 0 programs, 0 states
       gain = 1.0f; // Default gain (no change)
   }

protected:
   const char* getLabel() const override { return "SimpleGain"; }
   const char* getDescription() const override { return "A simple gain plugin for Zynthian"; }
   const char* getMaker() const override { return "YourName"; }
   const char* getLicense() const override { return "MIT"; }
   uint32_t getVersion() const override { return d_version(1, 0, 0); }
   int64_t getUniqueId() const override { return d_cconst('S', 'G', 'a', 'n'); }

   void initParameter(uint32_t index, Parameter& parameter) override {
       if (index != 0) return;
       parameter.hints = kParameterIsAutomable;
       parameter.name = "Gain";
       parameter.symbol = "gain";
       parameter.unit = "x";
       parameter.ranges.def = 1.0f;
       parameter.ranges.min = 0.0f;
       parameter.ranges.max = 2.0f;
   }

   float getParameterValue(uint32_t index) const override {
       if (index != 0) return 0.0f;
       return gain;
   }

   void setParameterValue(uint32_t index, float value) override {
       if (index != 0) return;
       gain = value;
   }

   void run(const float** inputs, float** outputs, uint32_t frames) override {
       const float* inL = inputs[0];
       const float* inR = inputs[1];
       float* outL = outputs[0];
       float* outR = outputs[1];
       for (uint32_t i = 0; i < frames; ++i) {
           outL[i] = inL[i] * gain;
           outR[i] = inR[i] * gain;
       }
   }

private:
   float gain;
   DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SimpleGainPlugin)
};

Plugin* createPlugin() {
   return new SimpleGainPlugin();
}

END_NAMESPACE_DPF
2.1.2.4 Explanation of DistrhoPluginMain.cpp

This is the main plugin implementation.

It includes the header files

#include "DistrhoPlugin.hpp"

we define a class:

class SimpleGainPlugin : public Plugin {
public:
   SimpleGainPlugin() : Plugin(1, 0, 0) { // 1 parameter, 0 programs, 0 states
       gain = 1.0f; // Default gain (no change)
   }

And within that class we set the gain to 1.0f

This means a float (A floating point number 1.0345 sorts of things not 1,2,3,4,5). Basically what it gets in it puts straight out in this case and if we substituted 1.0345f for the 1.0f the signal would get a little bit louder. A digital Patch lead!

This class will be returned as an object of type Plugin. But quite how and where Plugin is defined remains to be found....


At the bottom of the code we instantiate (generate, create, make alive...) the object and return it to the calling function.

Plugin* createPlugin() {
   return new SimpleGainPlugin();
}

So this is a little factory that makes a Plugin object that can interact with the rest of the alsa audio framework.

So what's the rest of it...

Two sections

  1. protected:
  2. private:


2.1.2.4.1 Protected:

Members declared as protected are accessible within the same class and in derived classes Some functions are declared

Firstly those that return character strings of text (char*)

getLabel() const overide { return "SimpleGain"; }
getDescription()
getMaker()
getLicense()

then a function that returns an unsigned 32 bit integer 0 -> 4 Gig

uint32_t getVersion() const override { return d_version(1, 0, 0); }

and an even more massive number 64 bit integer ( don't ask)

   int64_t getUniqueId() const override { return d_cconst('S', 'G', 'a', 'n'); }

Next we have a function that initializes some Parameters and define a range of values for that handy gain number we'd like to alter.

   void initParameter(uint32_t index, Parameter& parameter) override {
       if (index != 0) return;
       parameter.hints = kParameterIsAutomable;
       parameter.name = "Gain";
       parameter.symbol = "gain";
       parameter.unit = "x";
       parameter.ranges.def = 1.0f;
       parameter.ranges.min = 0.0f;
       parameter.ranges.max = 2.0f;

This applies extra values to the external implementation of this parameter so the two environments can pass data effectively.

index is required to have a value 0 to set these values to what are presumably defaults.

Next we have a Getter & a Setter

   float getParameterValue(uint32_t index) const override {
       if (index != 0) return 0.0f;
       return gain;
   }
   void setParameterValue(uint32_t index, float value) override {
       if (index != 0) return;
       gain = value;
   }

Again requiring a 0 index to operate.

Lastly in the protected section

   void run(const float** inputs, float** outputs, uint32_t frames) override {
       const float* inL = inputs[0];
       const float* inR = inputs[1];
       float* outL = outputs[0];
       float* outR = outputs[1];
       for (uint32_t i = 0; i < frames; ++i) {
           outL[i] = inL[i] * gain;
           outR[i] = inR[i] * gain;
       }
   }

The run function of this class, which will be called by the underlying system to process audio samples appearing at the input and generating appropriate output at the correct time.

i.e. doing the clever stuff between these two...

void run(const float** inputs, float** outputs, uint32_t frames) override

It's defined as a void so it does not return a value, interacting via the arrays passed to it.

The inputs to the tun function are a

an array of inputs 
an array of outputs 
A frame count. The number of frames in the array. 

Don't know what override does.

The arrays are allocated to variables ( notice the assumption here that we only handle the first two arrays handed to us. How it deals with other arrays is a point of discussion.)

       const float* inL = inputs[0];
       const float* inR = inputs[1];
       float* outL = outputs[0];
       float* outR = outputs[1];

So for each frame we loop a counter i with that numbered frame of audio

       for (uint32_t i = 0; i < frames; ++i) {

and then do stuff

           outL[i] = inL[i] * gain;
           outR[i] = inR[i] * gain;
       }

At the end of the process the out arrays have been filled with the adjusted values from the input arrays. . . .

And these can be handled by the framework infrastructure to throw it at the next device down the chain.


2.1.2.4.2 Private:

Members declared as private are accessible only within the same class. They cannot be accessed by derived classes, objects of the class, or external code. Used to encapsulate data and hide implementation details.

2.1.2.4.3 So where do these undefined chunks of code come from?

We haven't had any nice import functions to do this for us so how can these references to external functions be addressed?

We need to 'link' the code we have written with the development hooks of the libraries we imported earlier.

We need something to 'make' our programme. Enter the makefile . . .

2.1.2.5 makefile
#!/usr/bin/make -f

NAME=SimpleGain
DPF_PATH=../../DPF

include $(DPF_PATH)/Makefile.plugins.mk

TARGETS=lv2

all: $(TARGETS)

include $(DPF_PATH)/Makefile.base.mk
2.1.2.6 makefile what does it do ?

This attempts to builds the plugin as an LV2 bundle.

The two files mentioned here are frankly enormous. I assume these are built by code not hand but have absolutely no desire to venture into them with any exploratory desire.

I wonder if 'make' has any way of reporting the world it believes it's constructing ahead of time?

wyleu@raspberrypi:~/Code/lv2/DPF/SimpleGainPlugin $ make -n
mkdir -p /bin/SimpleGain.lv2
echo "Creating LV2 plugin for SimpleGain"
g++ /build/SimpleGain/DistrhoPluginMain_LV2.cpp.o -Wall -Wextra -pipe -MD -MP -fno-gnu- unique -fPIC -DPIC -DNDEBUG -O3 -ffast-math -fdata-sections -ffunction-sections - fvisibility=hidden -DHAVE_ALSA -DHAVE_JACK -DHAVE_RTAUDIO  -DHAVE_X11  -DHAVE_XEXT - DHAVE_XSYNC -DHAVE_OPENGL -std=gnu++11 -fvisibility-inlines-hidden -I. -I../../DPF/distrho - I../../DPF/dgl -I../../DPF/dgl/src/pugl-upstream/include -fdata-sections -ffunction-sections  -Wl,-O1,--as-needed,--gc-sections -Wl,--strip-all  -Wl,--no-undefined -ldl     -shared -Wl,-- version-script=../../DPF/utils/symbols/lv2.version -o /bin/SimpleGain.lv2/SimpleGain.so

Not sure where the line breaks and if there are any in that mouthful.

make -p generates enormous amounts of information..

make -d provides what we already know....

Live child 0x555658e367a0 (/bin/SimpleGain.lv2/SimpleGain.so) PID 51392 
Reaping winning child 0x555658e367a0 PID 51392 
Live child 0x555658e367a0 (/bin/SimpleGain.lv2/SimpleGain.so) PID 51393 
Creating LV2 plugin for SimpleGain
Reaping winning child 0x555658e367a0 PID 51393 
Live child 0x555658e367a0 (/bin/SimpleGain.lv2/SimpleGain.so) PID 51394 
/usr/bin/ld: cannot open output file /bin/SimpleGain.lv2/SimpleGain.so: Permission denied

2.2 Auto Leveller

A Place holder for information on the construction of this component and a casual implementation of some kind of document structure.

Auto Leveller
AutoLeveller GUI

2.2.1 What is it?

You can think of it as your angry sound engineer in a live situation, both during soundcheck and during the gig. Initially it will turn your output volume down to a reasonable level. It will also do that if anything unforseen happens in the chain (you add another plugin which features an integer gain multiplier and is set to gain=20). When set the right values (try the preset “Tuned”) it should level any source to a loudness level somewhere into yellow meter territory.

It also features two programs:

Init: All set to zero. It will not perform anything unless your level is above 0 dBfs, which is always bad. Tuned: Set the parameters to reasonable values: Pre Gain will boost the signal by +6 dB to compensate quiet engines like soundfonts, peak threshold is set to -4 dBfs and RMS to -18 dBFS to keep more or less transient heavy signals somewhere in yellow area of your mixer.

2.2.2 Parameters

2.2.2.1 Pre Gain (dBfs)

Adjust the pre amplification in case you deal with quieter sources (like soundfonts) you want to boost before getting into the automatic levelling.

2.2.2.2 Theshold Peak (dBfs)

Set your maximum peak levels you want to have. Setting the threshold will reset your former gain reduction so it can listen from the start again. When “Listen” is on, it will only turn the volume downwards. Recommended values are between 0 and -4 dBfs.

2.2.2.3 Theshold RMS (dBfs)

Set your maximum RMS levels you want to have. Setting the threshold will reset your former gain reduction so it can listen from the start again. When “Listen” is on, it will only turn the volume downwards. The detector is estimating RMS values of a 300ms window by a using a lowpass filter. Recommended values are between -14 and -18 dBfs.

2.2.2.4 Listen (bool)

When engaged (by default) it will perform detection and level matching. When disengaged it will keep the current gain, but will not further listen. If engaged again, the gain reduction will be reset.

3 LV2 custom control groups (ttl)

If not edited further, LV2 parameters will be presented in a manner being sorted by their parameter ID and grouped in groups of four without helpful group names. We can edit a specific file to get some more helpful named and ordered parameter groups. It is highly recommended to study other custom control ttl files before edit your own one.

3.1 Location

We start by searching for the original ttl file in the zynthian filesystem. Typically we'll find them in /usr/lib/lv2/[PLUGIN].lv2 or whereever this plugin lives. We copy the whole [PLUGIN].lv2 directory over to /zynthian/zynthian-data/lv2-custom (check the other custom ttls for reference). When we look inside, we'll find several *.so and *.ttl files. The file we are searching for is not the manifest.ttl but the other one. We can delete any other file from that directory.

On any zynthian update database regeneration zynthian will replace the original ttl file with the ones in /zynthian/zynthian-data/lv2-custom.

3.2 Editing the ttl file

3.2.1 Namespace prefixes

Most of the time we have to add some lines to the header, if not already present:

@prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix pg:   <http://lv2plug.in/ns/ext/port-groups#> .
@prefix plug: <COPY URN FROM PLUGIN BLOCK> .

These namespace prefixes act like unique identifiers for specific tasks. The URN to copy we'll find on top of the plugin block, which is the section that holds all the control ports. Just look into another custom ttl and compare the top of a plugin block with the URN set under the prefix "plug:".

3.2.2 Control groups

Add control groups like that outside the plugin block, for example right at the end of the file. Groups with higher displayPriority values will be displayed on top of your parameter group view. GROUP1 and the symbol "group1" can be named yourself (only letters, numbers or _", name "Group1 Display Name" you can also edited and will show on your UI.

plug:GROUP1
   a pg:ControlGroup ;
   lv2:name "Group1 Display Name" ;
   lv2:symbol "group1" ;
   lv2:displayPriority 3 .

plug:GROUP2
   a pg:ControlGroup ;
   lv2:name "Group2 Display Name" ;
   lv2:symbol "group2" ;
   lv2:displayPriority 2 .

...

3.2.3 Parameter assignment

Add following lines to every control port representing a parameter (contains "a lv2:ControlPort", so not the audio input and output ports), while display priority goes from higher to lower numbers as well:

pg:group plug:GROUP1 ;
lv2:displayPriority 1 ;

3.2.4 Scale Points

This is especially helpful for integer or boolean like enumeration parameters and other parameters which should have stepped controls. Here we have an example for a boolean parameter, which should only be in state "On" or "Off":

lv2:scalePoint [
   rdf:value 0.0 ;
   rdfs:label "OFF" ;
   rdfs:comment "OFF" ;
], [
   rdf:value 1.0 ;
   rdfs:label "ON" ;
   rdfs:comment "ON" ;
];

It has to be like that because typically (oftentimes, but not necessarily) parameters are represented as floating point values (0.000f to 1.000f), even if they're supposed to be boolean or integer, even if LV2 would support these kind of data. Other parameters could be of "integer logic". Let's assume the parameter "Waveform" would have the states "Saw, Sine, Square, Triange", we may assume that every floating point value from 0.0f to <0.25f represents "Saw" and so on. Then we might try this (while value and label are mandatory, comment is not):

lv2:scalePoint [
   rdf:value 0.0 ;
   rdfs:label "Saw" ;
   rdfs:comment "Saw" ;
], [
   rdf:value 0.25 ;
   rdfs:label "Sine" ;
   rdfs:comment "Sine" ;
], [
   rdf:value 0.5 ;
   rdfs:label "Square" ;
   rdfs:comment "Square" ;
], [
   rdf:value 0.75 ;
   rdfs:label "Triangle" ;
   rdfs:comment "Triangle" ;
];

3.2.5 Disable parameter from being shown on UI

Sometimes we have parameters which have no function and must not be displayed. You add the following line to the parameter definition:

lv2:portProperty pp:notOnGUI ;

3.2.6 Summary parameter definition

So a single parameter should for example look like this:

   [
       a lv2:InputPort, lv2:ControlPort ;
       lv2:index 8 ;
       lv2:symbol "delaytimesync" ;
       lv2:name "Host Sync" ;
       lv2:default 0.473684 ;
       lv2:minimum 0.0 ;
       lv2:maximum 1.0 ;
       pg:group plug:DELAY ;
       lv2:displayPriority 2 ;
       lv2:scalePoint [
           rdf:value 0.0 ;
           rdfs:label "Free" ;
           rdfs:comment "Free" ;
       ], [
           rdf:value 0.052631 ;
           rdfs:label "1/16" ;
           rdfs:comment "1/16" ;
       ], [
           rdf:value 0.105263 ;
           rdfs:label "1/8" ;
           rdfs:comment "1/8" ;
       ], [
           rdf:value 0.157895 ;
           rdfs:label "1/4" ;
           rdfs:comment "1/4" ;
       ], [
           rdf:value 0.211053 ;
           rdfs:label "1/2" ;
           rdfs:comment "1/2" ;
       ], [
           rdf:value 0.263158 ;
           rdfs:label "1/1" ;
           rdfs:comment "1/1" ;
       ], [
           rdf:value 0.315789 ;
           rdfs:label "2/1" ;
           rdfs:comment "2/1" ;
       ], [
           rdf:value 0.368421 ;
           rdfs:label "1/16." ;
           rdfs:comment "1/16." ;
       ], [
           rdf:value 0.421052 ;
           rdfs:label "1/8." ;
           rdfs:comment "1/8." ;
       ], [
           rdf:value 0.473684 ;
           rdfs:label "1/4." ;
           rdfs:comment "1/4." ;
       ], [
           rdf:value 0.526316 ;
           rdfs:label "1/2." ;
           rdfs:comment "1/2." ;
       ], [
           rdf:value 0.578947 ;
           rdfs:label "1/1." ;
           rdfs:comment "1/1." ;
       ], [
           rdf:value 0.631579 ;
           rdfs:label "2/1." ;
           rdfs:comment "2/1." ;
       ], [
           rdf:value 0.684211 ;
           rdfs:label "1/16T" ;
           rdfs:comment "1/16T" ;
       ], [
           rdf:value 0.736842 ;
           rdfs:label "1/8T" ;
           rdfs:comment "1/8T" ;
       ], [
           rdf:value 0.789474 ;
           rdfs:label "1/4T" ;
           rdfs:comment "1/4T" ;
       ], [
           rdf:value 0.842105 ;
           rdfs:label "1/2T" ;
           rdfs:comment "1/2T" ;
       ], [
           rdf:value 0.894736 ;
           rdfs:label "1/1T" ;
           rdfs:comment "1/1T" ;
       ], [
           rdf:value 0.947368 ;
           rdfs:label "2/1T" ;
           rdfs:comment "2/1T" ;
       ], [
           rdf:value 1.0 ;
           rdfs:label "-" ;
           rdfs:comment "-" ;
       ];
   ] ,

The bold line is the one we started to insert things. You typically should only edit the "name" value, which is the display label and the newly added values. In this case we added the control to the previously added group named "DELAY", added the displayPriority, so that it will be displayed within the group above all parameters with value <2 and below paramters with value >2. Then we added stepped controls for delay times defined by subdivisions of the host tempo, where the values represent 1/19, 2/19, 3/19... Continuous parameters typically do not need scale points at all, rather they would become stepped parameters as well if we'd add them.

3.3 Recommended workflow

Instead of just copying the ttl over to the respective directory and edit it for your own use you can make things easier for the devs to implement your custom feature and share it with everybody this way:

  • Fork the zynthian/zynthian-data repositories vangelis branch
  • Checkout to that branch in your zynthian
  • Develop using your fork
  • Make a pull request