Monday, June 17, 2013

First Impressions with LV2 Plugin Development (almost a tutorial)

Well, its been a while hasn't it? I'm actually done with my thesis. Done. Completo. Tapos na. Weird. Luckily I still have some publications from it to submit, otherwise I wouldn't have a single thing to put off. Except...we bought a  house, and it needs a new roof, and I should be working on that...

But I'm not. In fact, I've been pretty busy with trying to spend my time on unimportant things like synthesizers. I finally started writing one. It actually started when I found out someone was making LV2 midi filters that I'd been wanting to do for quite some time (to add "humanization" in Ardour 3 etc.) I contributed a  variant that used a normal distribution rather than a uniform one (using a modified Marsaglia polar method no less). Once I'd done that lv2 plugin it just broke down the wall and I felt like I could easily do a synth.

With some familiarity with the JACK API and how the Rakarrack effects work, I understood that you are handed a buffer and a number of samples (or frames) to fill it with. There are other details of setting up and connecting and all that but in the end thats the heart of the audio. The input is the same you are handed a buffer that has n frames in it that you must process/respond to. This seemed easy enough.
void run_casynth( LV2_Handle handle, uint32_t nframes)

With LV2 midi, using the LV2atom  object (which is really basically a list of objects) you get a list of events, some of which are midi, which have a timestamp of which frame in the buffer they correspond with. Cool, no problem there. But then I came to my first obstacle: URID. (huh?

Well, basically (this is a thing we run into at work) if you do everything in a human readable format you're wasting a lot of time, so instead you map it to a machine language (binary) and pass it around as that because its fast. Then on the occasion that you do need to interpret it, you use a mapping. So effectively for my synth I needed to find out where midi events get mapped to and store it, then later when I doing my RT processing, I don't have to check if its the right type, I just check if its coming from the right place. (I'm still a little unsure but thats my current understanding).

To do this takes several steps. First note that when you initialize your plugin you are handed an array of features that the host supports:
LV2_Handle init_casynth(const LV2_Descriptor *descriptor, double sample_rate, const char *bundle_path, const LV2_Feature * const* host_features)
One of the required features of every host is this URID mapping I've described. So we loop through this array until we find the URID map and then use the map to see what a midi event will come from and store it for future reference:
//get urid map value for midi events for (int i = 0; host_features[i]; i++) { if (strcmp(host_features[i]->URI, LV2_URID__map) == 0) { LV2_URID_Map *urid_map = (LV2_URID_Map *) host_features[i]->data; if (urid_map) { synth->midi_event_type = urid_map->map(urid_map->handle, LV2_MIDI__MidiEvent); break; } } }
Once thats done, then in your processing thread you go through the input buffer (called a port for LV2), make sure its a midi event, then cast that into the data type we want (a bunch of bytes):
LV2_ATOM_SEQUENCE_FOREACH(synth->midi_in_p, event) { if (event && event->body.type == synth->midi_event_type)//make sure its a midi event { message = (unsigned char*) LV2_ATOM_BODY(&(event->body)); ...
Once I got that figured out things were looking pretty good. Handling ports/buffers for things like controls are pretty straightforward. You just give it a pointer to point to its buffer value. This is done when the host calls a function for each port after the plugin instance is created:
void connect_casynth_ports(LV2_Handle handle, uint32_t port, void *data) { CASYNTH* synth = (CASYNTH*)handle; if(port == MIDI_IN) synth->midi_in_p = (LV2_Atom_Sequence*)data; else if(port == OUTPUT) synth->output_p = (float*)data; else if(port == CHANNEL) synth->channel_p = (float*)data; else if(port == MASTER_GAIN)synth->master_gain_p = (float*)data; else if(port == RULE) synth->rule_p = (float*)data; else if(port == CELL_LIFE) synth->cell_life_p = (float*)data; else if(port == INIT_CELLS) synth->init_cells_p = (float*)data; else if(port == NHARMONICS) synth->nharmonics_p = (float*)data; else if(port == HARM_MODE) synth->harmonic_mode_p = (float*)data; else if(port == WAVE) synth->wave_p = (float*)data; else if(port == ENV_A) synth->env_a_p = (float*)data; else if(port == ENV_D) synth->env_d_p = (float*)data; else if(port == ENV_B) synth->env_b_p = (float*)data; else if(port == ENV_SWL) synth->env_swl_p = (float*)data; else if(port == ENV_SUS) synth->env_sus_p = (float*)data; else if(port == ENV_R) synth->env_r_p = (float*)data; else if(port == AMOD_WAV) synth->amod_wave_p = (float*)data; else if(port == AMOD_FREQ) synth->amod_freq_p = (float*)data; else if(port == AMOD_GAIN) synth->amod_gain_p = (float*)data; else if(port == FMOD_WAV) synth->fmod_wave_p = (float*)data; else if(port == FMOD_FREQ) synth->fmod_freq_p = (float*)data; else if(port == FMOD_GAIN) synth->fmod_gain_p = (float*)data; else puts("UNKNOWN PORT YO!!"); }
Then in your RT method when you want to read the port, just dereference it:
double astep = *synth->amod_freq_p/synth->sample_rate;
Alright. That was clear enough from the examples I used. The last thing that threw me for a loop was the fact that you have to tell the host what these functions are that you want them to call. This is done through a struct like object called the descriptor:
static const LV2_Descriptor casynth_descriptor={ CASYNTH_URI, init_casynth, connect_casynth_ports, NULL,//activate run_casynth, NULL,//deactivate cleanup_casynth, NULL//extension };
It follows this specific format to describe which function is for initialization, running, activating, cleanup etc. etc. All in all it was simple enough. If I had read through the official examples I probably wouldn't have hit these pitfalls quite so hard but I muddled through.

So, you ask, what are you working on exactly?... well you'll just have to stay tuned and on the edge of your seat because I'm not going to reveal it yet! Its not ready for a release but it will be soon. And of course, I'll let you know.


David Robillard said...

Note that connect_port() is a real-time function which many hosts will call once per port every cycle. Accordingly, it is a good idea to use a switch (rather than a big nest of conditionals) to make it as fast as possible.

Re: "check where it comes from", I'm not sure what this means. You are still checking the type, it's just been mapped to an integer.

Spencer said...

You are the second one to point this out to me, perhaps I should look into it more. I wouldn't anticipate a large difference in performance between a switch and a if else if tree with a modern compiler. I wasn't aware that this happens in realtime though.

Where it comes from is the best way for me to describe it. Its a mapping from some "place" (value) on the number line of integers. You are checking the type in the linear integer space rather than the any object space (if you think from a linear algebra standpoint). Perhaps I should get a better understanding and better analogy.

Thanks for the comments.