BSE Plugin Development

From Testbit Wiki
Jump to: navigation, search

BSE Plugin Development

Authors: Tim Janik, Stefan Westerfeld

In this document, the basic concepts necessary for Bse plugin development are established, and an example plugin is being developed.

Getting Started

You don't need a full fledged software development environment in order build a Bse plugin, a proper Beast installation, your favourite text editor and a recent version of GCC will do fine on the technical side. Writing a synthesis plugin also requires at least some basic knowledge about digital audio mathematics (digital signal processing) and for Bse also a reasonably good idea of OO and C++. If you are new to digital audio processing in general you can find a link list to tutorials and DSP programming tips at the Beast Website. A good foundation in maths helps a lot in understanding the materiels covered.

Basic Concepts

The most central element realised in Bse for the creation and modification of audio signals is the "Module". A module almost always has one or more output signals and may have any amount of input signals. In addition, modules can define a variety of "Properties" which represent settings used to alter behaviour of a module.

Module and Voices

Bse supports multiple dynamically allocated voices internally which are represented by a single object in GUIs like the one provided by Beast. For this reason, a distinction is made in module implementations between the "Module Object" corresponding to the object displayed at the GUI, and the "Engine Modules" of which there may be many per module object, used for audio processing within the dynamic voices. Properties and channels are registered with the module object class and individual property changes are sent to the module object. The module object then passes on property changes to the engine modules, taking care of syncronization issues which arise because engine modules live and process audio data in seperate threads.

To summarize, in order to write a Bse module, we need to:

  • define the module object
  • add properties to the module object
  • add input and output channels to the module object
  • define an engine module in the module object
  • implement the data processing function of the engine module.

Defining the module

In the past, the Bse plugin API went through quite a few major rewrites to make plugin implementations as easy as possible. At this point, a standard module needs three files for a full implementation:

  1. an .idl file containing class, property and channel definitions
  2. a .cc file containing the data processing and property handling logic
  3. an icon, 64 by 64 pixels RGBA format, saved as .png file.


Writing the IDL file

Bse provides an IDL compiler for object definitions to eliminate any mechanic code fragments the programmer would need to create manually otherwise and to generate binding and glue code between various Bse components, the IDL compiler is described in detail in the Sfidl Manual.

To make use of the BSE object tree, every plugin IDL file starts out with an include statement. Then a namespace is opened within which the desired classes, structures or choices (a type of enumeration) can be defined.

For most plugins, an ordinary class definition with properties and input and output channels suffices:

#include <bse/bse.idl>                  // include BSE classes and definitions
namespace Bse { namespace Contrib {     // enter the namespace Bse::Contrib
class NotchFilter : Effect {            // derive Notchfilter from Bse::Effect
  Real    frequency;                    // a property
  OStream audio_out;                    // output signal
};
} } // Bse::Contrib

Here we have a small NotchFilter object, defined in the namespace Bse::Contrib which is the standard namespace for third-party Bse plugins, more on namespaces can be found in the Sfidl Namespaces. The object has a "frequency" property of type Real and an output channel "audio_out".


Input and output channels

In order for a module to effectively process audio, it needs input and output signal channels, unless its purely a producer for which signal inputs would be useless. Often a module needs "audio" channels and "control" channels, that is, some channels are used for audible signals, and other are used for non-audible control signals, such as volume. It is not entirely clear for every signal whether it is an audio or a control signal. Often it is its use that would best suit to provide a classification into either category but advanced knowledge of all possible uses is not available to the ordinary plugin writer. To fully support every kind of signal use, Bse doesn't make a technical distinction between so decided "audio" and "control" signals.

It is recommended however, that in order to improve usability, signals envisioned to be used primarily as audio signals are placed before other signals envisioned to be used as control signals in a class definition and therefore in generated GUIs. A good example for a module meeting this requirement is the BseAmplifier:

class Amplifier : Effect {
  IStream audio_in1  = (_("Audio In1"), _("First audio input"));
  IStream audio_in2  = (_("Audio In2"), _("Second audio input"));
  IStream ctrl_in1   = (_("Ctrl In1"), _("First control input"));
  IStream ctrl_in2   = (_("Ctrl In2"), _("Second control input"));
  OStream audio_out  = (_("Audio Out"), _("Amplified audio output"));
};

For classes such as :mix1: or :mix2: which indicate multi channel mixer effects (see class options), the audio channels put to actual use are also the first channels provided by a module.

A Bse module may have three different types of channels:

  1. IStream - an input stream, to be connected to a single output.
  2. JStream - an input stream, to be connected to many outputs (to join).
  3. OStream - an output stream, may be connected to any number of input streams.


Property types

Bse provides a large set of property constructors. By convention, all constructors expect a translatable label as first argument, a translatable description as second argument and an option string as last argument, as described in the Sfidl Properties. The various supported options are described in the next section. Here, we give an overview of the various constructors provided by Bse, grouped by types. Note that some property constructors already add extra options to the arguments given, for instance Trigger() will automatically provide the options :trigger: and :skip-undo:.


Bool properties

Bool (label, description, default, options)
An ordinary property of type Bool. For GUIs and documentation generation, the translatable strings label and description are provided. The default value is given by default. Possible values (e.g. :trigger: which might be handy for some booleans) for options are discussed in the property options section.
Trigger (label, description, options)
This is a boolean property that's practically always FALSE. It may be set to TRUE in a trigger attempt though, so certain actions may be started in response. GUIs would usually display this option via a clickable button. All plugin's set_property() methods automatically reset trigger property values to FALSE after invoking the property_changed() method.


Real (IEEE-754 double precision floating point) properties

Real (label, description, default, minimum, maximum, stepping, options)
An ordinary property of type Real. For GUIs and documentation generation, the translatable strings label and description are provided. The default, minimum and maximum values for the Real property are given by the respective arguments. The stepping argument can be used by GUIs to provide increment/decrement editing abilities. Possible values for options are discussed in the property options section.
Perc (label, description, default, options)
Percentage property, the minimum and maximum are fixed 0 and 100 respectively, and additional options are provided for GUIs to recognize percentage properties as such.
DBVolume (label, description, default_db, minimum_db, maximum_db, options)
A DBVolume property is used for Real-valued properties with values >0 which are used as volume factors and should be displayed and editable as values in decibel. The default_db, minimum_db and maximum_db parameters are converted into linear volume factors before being interpreted as property default and limits. For example, a DBVolume("", "", 0, -6, +6, STANDARD) property will actually default to 1.0 (0 decibel) and hold values in the range +0.501 (-6 decibel) .. +1.995 (+6 decibel).
Balance (label, description, default, options)
A Balance property is similar to a normal Real property, but has a fixed minimum and maximum of -100 and +100 and defaults to 0.
Gain (label, description, default, minimum, maximum, stepping, options)
Freq (label, description, default, options)
A Real property with logarithmic scale centered around A+2. The actual range of this property in Hertz goes from 0.00005=1/20000Hz to 20000Hz.
Frequency (label, description, default, minimum, maximum, options)
A Real property with logarithmic scale centered around A+2, setup with a stepping of 10. In order for the logarithmic scale to fit the parameter ranges, the minimum frequency must be smaller than 51.9Hz (corresponds to As-1) and the maximum frequency must be larger than 15053Hz (corresponds to Ais+6).
LogScale (label, description, default, minimum, maximum, stepping, center, base, n_steps, options)
Properties of this type can be displayed by using a logarithmic scale. The arguments base and n_steps determine the logarithmic scale which starts from center*base^(-n_steps) and reaches up to center*base^(+n_steps) with center*base^(0) = center in the middle. For example, a six octave scale with center=440, base=2 and n_steps=3 would look like this:
Minimum: 440 * 2^-3 = 440 * 0.125 = 55 (A-2)
Intermediate: 440 * 2^-2 = 440 * 0.25 = 110 (A-1)
Intermediate: 440 * 2^-1 = 440 * 0.5 = 220 (A)
Center: 440 * 2^0 = 440 * 1 = 440 (A+1)
Intermediate: 440 * 2^1 = 440 * 2 = 880 (A+2)
Intermediate: 440 * 2^2 = 440 * 4 = 1760 (A+3)
Maximum: 440 * 2^3 = 440 * 8 = 3520 (A+4)


Int (signed 32bit) properties

Int (label, description, default, minimum, maximum, stepping, options)
An ordinary property of type Int. For GUIs and documentation generation, the translatable strings label and description are provided. The default, minimum and maximum values for the Int property are given by the respective arguments. The stepping argument can be used by GUIs to provide increment/decrement editing abilities. Possible values for options are discussed in the property options section.
UInt (label, description, default, options)
Similar to Int() where the minimum and maximum are fixed at 0 and 2147483647.
Octave (label, description, default, options)
An integer property ranging from -4 to +6 with variable default.
FineTune (label, description, options)
Note (label, description, default, options)
Int property within 0 and 131 (sometimes also uses 132 to denote no/unspecified/unparsable note). For the default value, Bse provides a constant KAMMER_NOTE and a set of macros taking an octave argument: NOTE_C(octave), NOTE_Cis(octave), NOTE_Des(octave), NOTE_D(octave), NOTE_Dis(octave), NOTE_Es(octave), NOTE_E(octave), NOTE_F(octave), NOTE_Fis(octave), NOTE_Ges(octave), NOTE_G(octave), NOTE_Gis(octave), NOTE_As(octave), NOTE_A(octave), NOTE_Ais(octave), NOTE_Bes(octave), NOTE_B(octave).


Other properties

Num (label, description, default, minimum, maximum, stepping, options)
A signed 64bit numeric property similar to Int covering the integer range -9223372036854775808 til 9223372036854775807.
String (label, description, default, options)
String property, the String() constructor is used to provide a translatable label and translatable description and to define the default string value. For some strings it makes sense to provide additional options such as :searchpath:.
Choice (label, description, default, options)
Choice property, the Choice() constructor is used to provide a translatable label and translatable description and to define the default choice value.
BBlock (label, description, options)
Byte block property, the BBlock() constructor is used to provide a label and description.
FBlock (label, description, options)
Float value block property, the FBlock() constructor is used to provide a label and description.
Record (label, description, options)
Record type property, the Record() constructor is used to provide a translatable label and translatable description for properties that have the type of a previously defined record (see Sfidl Composite Types).
Sequence (label, description, options)
Sequence type property, the Sequence() constructor is used to provide a translatable label and translatable description for properties that have the type of a previously defined sequence (see Sfidl Composite Types).
Object (label, description, options)
Object property, the Object() constructor is used to provide a translatable label and translatable description for properties that have the type of a previously defined object (see Sfidl Classes).


Property options

A special cpability of IDL properties is behavioural adjustment through an added option string. Several options can be combined in such a string by concatenating them with ":", and options can be enabled or disabled by postfixing them with "+" or "-". The following lists describe the property options currently supported (defined) by Bse.

Property options from GParamSpec:

:r:
the property is readable (same as G_PARAM_READABLE)
:w:
the property is writable (same as G_PARAM_WRITABLE)
:construct:
the property is writable (same as G_PARAM_CONSTRUCT)
:construct-only:
the property is writable (same as G_PARAM_CONSTRUCT_ONLY)
:lax-validation:
the property is writable (same as G_PARAM_LAX_VALIDATION)

BSE core options:

:S:
the property is serializable
:f:
float indicator, reduce serialization precision to IEEE 754 Single Precision Floating Point
:skip-default:
do not serialize property if its set to its default value
:skip-undo:
do not record property changes to undo/redo mechanism
:unprepared:
the property is writable only for unprepared objects

GUI Options:

:G:
the property should be represented by GUIs
:ro:
for GUI representation purposes, the property should be considered read-only (non-editable)
:trigger:
a hint to display the property with a trigger button
:radio:
a hint to display the property with a radio button
:dial:
a hint to display the property with dial knob
:scale:
a hint to display the property with a scale adjustment
:log-scale:
a hint to display the property with a logarithmic scale adjustment
:db-value:
a hint indicating a decibel valued property (as opposed to a property that is real valued but for which dB values should be displayed)
:db-volume:
a hint indicating a dB volume scale for a dB or real valued property
:db-range:
a hint indicating a dB scale with smooth curvature for a dB or real valued property
:searchpath:
a hint indicating a property consisting of colon seperated directory list
:filename:
a hint indicating a property consisting of a filename
:rgb:
a hint indicating a property consisting of an RGB color value
:hex:
a hint indicating a preference for base 16 in numeric editing
:item-sequence:
the property type is a sequence of item objects
:note-sequence:
the property type is a sequence of notes

Predefined options sets:

READWRITE
property is readable and writable (:r:w:)
GUI
property is READWRITE and GUI representable (:r:w:G:)
GUI_RDONLY
property is GUI but non-editable (:r:w:G:ro:)
GUI_READABLE
property is readable and GUI representable but not writable (:r:G:)
STORAGE
property is READWRITE and serializable (:r:w:S:)
STANDARD
property is READWRITE, STORAGE and GUI (:r:w:S:G:)
STANDARD_RDONLY
property is READWRITE, STORAGE, GUI_RDONLY (:r:w:S:G:ro:)

Extra option sets (mostly for convenience and readability):

SKIP_DEFAULT
skip default value serialization (:skip-default:)
SKIP_UNDO
property changes are not undo/redo recorded (:skip-undo:)


Class options

The property option mechanism described in the last section has been adapted for classes at some point. Currently, applications of this feature a very rare, but expected to increase in the future. As such, the recognized option set for classes also is very limited at the moment.

Predefined class options:

:unstable:
code is (still) unstable and enabled only by --devel
:deprecated:
code is deprecated and scheduled for removal, enabled only by --devel
:mix1:
this class implements a mono channel mixer effect (also see channels)
:mix2:
this class implements a stereo channel mixer effect
:mix5.1:
this class implements a 5.1 channel mixer effect


Adding a PNG icon

Every GUI representable object should have an icon eventually. Once an image file exists in the desired format, it is easily integrated into a class definition:

class NotchFilter : Effect {
  Info    icon      = "notch.png";	// use the image notch.png as class icon
};

Images to be used as class icons should meet a few requirements:

  1. Size: the class icon should be 64 pixels wide and 64 pixels heigh.
  2. Colors: the image should be provided in RGBA format with transparent background.
  3. Format: the image should be provided in a common image format, PNG is fully supported by sfidl.1.


Changing the icon

Writing the C++ file

Module object methods

Engine module methods

An example notch filter

Integrating presets

Storing custom data

Additional Methods

Compiling and installing a plugin

After writing a plugin, to use it in beast, it needs to be compiled and installed. Here is a brief example which assumes that the idl file is called bseportamento.idl, and the implementation is the C++ source bseportamento.cc.

You can get the source code of the plugin from the bugtracker, at http://bugzilla.gnome.org/show_bug.cgi?id=353137. The plugin wasn't designed for learning to write plugins, though, we'll publish a "real" example plugin at some later point in time.

 sfidl --plugin bseportamento.idl > bseportamento.genidl.hh
 g++ -O2 -g -Wall -shared -fPIC -o bseportamento.so bseportamento.cc `pkg-config --cflags --libs bse`
 mkdir -p ~/beast/plugins
 cp bseportamento.so ~/beast/plugins

Line by line walk through:

  1. The "sfidl" line is compiling your IDL file into a C++ header, that provides the necessary definitions for implementing the plugin. You need to repeat this step whenever you change the IDL file.
  2. The "g++" line is compiling the C++ source and the generated C++ header to a binary. Note that you need to have libbse installed, along with its pkg-config file, for this step to work. The pkg-config file might be provided by a separate package of your distribution, such as beast-dev.
  3. The "mkdir" line is creating a directory for the plugin.
  4. The final step copies the binary plugin to beasts plugin path.