lsp-plugins
lsp-plugins copied to clipboard
MIDI Utilities Feature Request
@sadko4u I've made the email an issue for ease of communication and discussion.
I have a mockup of what features would be included in this plugin. This is not how the plugin will be styled, but just what features and GUI widgets for each.
Where do I start this feature?
First of all, I think, you should describe a concept of plugin. What each control is designed for and how it will affect the MIDI data transformations. First of all, you should create a branch from 'devel' branch to start development. When you finish development, we'll merge your branch into 'devel', and after release it will migrate into 'master'. First of all, you need to understand the architecture of LSP. Here's short information I've sent to @CrocoDuckoDucks in a private email when he started plugin development.
If you have questions we may discuss them in chat mode. First of all you have to understand how do plugins work. I've down a pair of drawings that describe the architecture and main principles.
Figure 1: LSP Architecture.
You may see here different hosts that utilize different plugin interfaces like LADSPA, LV2, VST, etc. For each plugin interface there is a thin wrapper in LSP core architecture. Currently you have no need to study how do wrappers work (but it would be a good option for deeper understanding). The only thing you should know that all wrappers utilize methods of plugin_t interface. All methods provided by plugin_t interface implement all necessary functionality of plugin. Then, we have inheritance interface: we declare different classes that inherit from plugin_t interface and override methods of this interface. So instead of calling interface methods, thanks to virtual functions, wrappers call methods of derived classes. So we get different plugin modules like comp_delay_mono, comp_delay_stereo, etc. All these modules have common parts that are implemented as separate functional blocks called Core DSP Modules.
Now, we need to study how do wrappers interact with plugin modules. At this point we should understand plugin model and data flows.
Figure 2: Plugin model. This figure shows the generalized plugin model from the point of view of the Host. Plugin has set of audio inputs, set of audio outputs, it also has input parameters to control the workflow of plugin, and set of output parameters (often called meters) to interact with user and display useful information. The overall data flow between host and plugin consists of similar cycles that can be divided into several periods.
Figure 3. Plugin lifecycle.
As you see, there's nothing special, all plugins have similar lifecycle, independent from what host is running them and what plugin interface (LV2, VST, etc...) is used. Each plugin has initialization stage, finalization stage and main processing cycle stage. The common plugin interface between wrappers and plugin_t generalizes this behaviour.
Now we can draw the interaction diagram in terms of plugin_t interface:
Figure 4. Plugin interaction diagram.
Here you can see what does user do with host and how this reflects on the plugin interface.
We see that when user creates a plugin, host-wrapper interface calls set of methods: executes
constructor, performs plugin initialization, reports current sample rate to plugin, activates plugin.
Then it does regular job at his own cycle, calls main processing method process() and displays
output values of plugin (meters) to user. When user tries to change one or more input parameters,
host notifies plugin about change of input parameters by calling update_settings() method.
When user disables plugin, deactivated() method is called.
When user enables plugin again, activated() method is called and host does job as usual.
When user deletes plugin, host first deactivates plugin by calling deactivated() method, then
destroys plugin instance by calling destroy() method and then calls destructor.
The only question left is: what about parameters and buffers? How are they passed? When I developed the wrapper layer, I decided that the best solution is to incapsulate each parameter, each meter and each audio buffer into separate data unit called 'port'. That allows to solve problems that different plugin formats have different parameter and audio data passing mechanisms. So there are only 3 basic methods to pass/retrieve data from the host to plugin and back:
- getValue() - this method is called by plugin when it wants to retrieve current value of parameter from host.
- setValue() - this method is called by plugin when it wants to pass current value of meter to host.
- getBuffer() - this method is called by plugin when it wants to retrieve audio buffer or another complex data from host. The only rule is: plugin should know what data type is stored in port.
So, in reality we can draw the process() method more detailed:
Figure 5. The process() method.
We can see that, when calling process(), wrapper passes number of samples that will contain all audio buffers. Plugin can read values from ports that are stored in vPorts container (this is protected member of plugin_t class). All data operations are performed by using getValue(), getBuffer() and setValue() methods. Also wrapper does some cool job with input data: it detects if the state of input parameters has changed and triggers update_settings() method when at least one of parameters has changed, so we have no need to read parameters on each iteration of process(). This we can do once in update_settings() method.
So now we know how does plugin work but... How do we describe ports? Simple, by providing metadata for each plugin. Metadata contains complete description of all ports used by plugin and their types. When wrapper instantiates plugin, it reads plugin's metadata and creates corresponding ports for parameter and data transport. So when init() method is called, plugin has already vPorts member collection filled with complete list of ports.
Now let's practice. For example, we want to implement a stereo plugin that adjusts gain. First, we describe list of port:
static const port_t test_ports[] =
{
PORTS_STEREO_PLUGIN,
AMP_GAIN10("gain", "Output gain", 1.0f),
PORTS_END
};
This may look strange, but most of this are macros that are expanded to:
{ PORT_NAME_INPUT_L, "Input L", U_NONE, R_AUDIO, F_IN, 0, 0, 0, 0, 0, 0 },
{ PORT_NAME_INPUT_R, "Input R", U_NONE, R_AUDIO, F_IN, 0, 0, 0, 0, 0, 0 },
{ PORT_NAME_OUTPUT_L, "Output L", U_NONE, R_AUDIO, F_OUT, 0, 0, 0, 0, 0, 0 },
{ PORT_NAME_OUTPUT_R, "Output R", U_NONE, R_AUDIO, F_OUT, 0, 0, 0, 0, 0, 0 },
{ "gain", "Output gain", U_GAIN_AMP, R_CONTROL, F_IN | F_LOG | F_UPPER | F_LOWER | F_STEP, 0, 10.0f, 1.0f, 0.1, 0, 0 },
NULL
Now we create complete description of plugin:
struct test_plugin_metadata
{
static const plugin_metadata_t metadata;
};
const plugin_metadata_t test_plugin_metadata::metadata =
{
"TEST",
"TEST",
"TEST",
&developers::v_sadovnikov,
"test_plugin",
"TEST",
LSP_LADSPA_BASE + 1001,
LSP_VERSION(1, 0, 0),
test_classes,
test_ports,
stereo_plugin_port_groups
};
OK, now we need to create derivatve from plugin_t.
namespace lsp
{
class test_plugin: public plugin_t, public test_plugin_metadata
{
protected:
IPort *pIn[2]; // Input ports
IPort *pOut[2]; // Output ports
IPort *pGain; // Gain port
float fGain; // Gain value
public:
test_plugin();
virtual ~test_plugin();
public:
virtual void init(IWrapper *wrapper);
virtual void process(size_t samples);
virtual void update_settings();
};
}
And here it's implementation:
namespace lsp
{
test_plugin::test_plugin(): plugin_t(metadata)
{
for (size_t i=0; i<2; ++i)
{
pIn[i] = NULL;
pOut[i] = NULL;
}
pGain = NULL;
fGain = 1.0f;
}
test_plugin::~test_plugin()
{
}
void test_plugin::update_settings()
{
fGain = pGain->getValue();
}
void test_plugin::init(IWrapper *wrapper)
{
plugin_t::init(wrapper);
// Remember pointers to ports
size_t port_id = 0;
for (size_t i=0; i<2; ++i)
pIn[i] = vPorts[port_id++];
for (size_t i=0; i<2; ++i)
pOut[i] = vPorts[port_id++];
pGain = vPorts[port_id++];
}
void test_plugin::process(size_t samples)
{
for (size_t i=0; i<2; ++i)
{
// Get data buffers
float *in = pIn[i]->getBuffer<float>();
if (in == NULL)
continue;
float *out = pOut[i]->getBuffer<float>();
if (out == NULL)
continue;
// dsp::scale - optimized version for
// *(out++) = *(in++) * fGain x samples times
dsp::scale(out, in, fGain, samples);
}
}
}
Nice! We got plugin. Now we should tell that it exists (currently without UI). We do this by describing module capabilities:
// Test plugin
MOD_PLUGIN(test_plugin)
Nice! If we have right written all includes, we can perform now:
> make clean && make && make install
Related links:
plugin_t interface: https://github.com/sadko4u/lsp-plugins/blob/devel/include/core/plugin.h https://github.com/sadko4u/lsp-plugins/blob/devel/src/core/plugin.cpp
One of the simplest plugins example: https://github.com/sadko4u/lsp-plugins/blob/devel/include/plugins/comp_delay.h https://github.com/sadko4u/lsp-plugins/blob/devel/src/plugins/comp_delay.cpp
It's metadata: https://github.com/sadko4u/lsp-plugins/blob/devel/include/metadata/comp_delay.h https://github.com/sadko4u/lsp-plugins/blob/devel/src/metadata/comp_delay.cpp
Plugin definition as a module: https://github.com/sadko4u/lsp-plugins/blob/devel/include/metadata/modules.h
@sadko4u why don't you turn these useful docs into a wiki page here on GitHub? It would be great for anyone willing to contribute to your plug-in suite.
why don't you turn these useful docs into a wiki page here on GitHub? It would be great for anyone willing to contribute to your plug-in suite.
Yes, definitely it would be a good feature. I think I'll put the documentation to the wiki a little bit later.