LV2

From ZynthianWiki
Revision as of 16:05, 5 September 2025 by Wyleu (talk | contribs) (→‎Listen (bool))
Jump to navigation Jump to search

1 LV2 Components

1.1 A Notch Filter

1.1.1 The Definition Files

1.1.2 The Code components

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

1.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
1.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:


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


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

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

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

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

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

1.2.2 Parameters

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

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

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

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

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

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

2.2 Editing the ttl file

2.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:".

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

...

2.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 ;

2.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" ;
];

2.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 ;

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

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