Sunday, February 10, 2013

LV2 and Atom communication

Situation: You're trying to write a synth or effect, and you need to communicate between your UI and the DSP parts of the plugin, and MIDI doesn't cut it: enter Atom events. I found them difficult to get to grips with, and hope that this guide eases the process of using them to achieve communication.

 

Starting out

I advise you to first read this : http://lv2plug.in/ns/ext/atom/
It is the official documentation on the Atom spec. Just read the
description. It gives a good general overview of these things called Atoms.

This is "message passing": we send an Atom event from the UI to the DSP part of the plugin. This message needs to be safe to use in a real-time context.

(Note it is assumed that the concepts of URIDs is familiar to you. If they're not, go back and read this article: http://harryhaaren.blogspot.ie/2012/06/writing-lv2-plugins-lv2-overview.html )

Step 1: Set up an LV2_Atom_Forge. The lv2_atom_forge_*   functions are how you build these events.

LV2_Atom_Forge forge;
lv2_atom_forge_init( &forge, map ); // map = LV2_URID_Map feature

Atoms

Atoms are "plain old data" or POD. They're a sequence of bytes written in a contiguous part of memory. Moving them around is possible with a single memcpy() call.

 

Writing Atoms

Understanding the URID naming convention

// we need URID's to represent functionality: There's a naming scheme here, and its *essential* to understand it. Say the functionality we want to represent is a name of a Cat (similar to the official atom example). Here eg_Cat represents the "noun" or "item" we are sending an Atom about. eg_name represents something about the eg_Cat.

something_Something represents an noun or item, while something_something (note the missing capital letter) is represents an aspect of the noun.

LV2_URID eg_Cat;
LV2_URID eg_name; 


In short classes and types are Capitalized, and nothing else is.

Code to write messages

// A frame is essentially a "holder" for data. So we put our event into a LV2_Atom_Forge_Frame. These frames allow the "appending" or adding in of data.
LV2_Atom_Forge_Frame frame;


// Here we write a "blank" atom, which contains nothing (yet). We're going to fill that blank in with some data in a minute. A blank is a dictionary of key:value pairs. The property_head is the key, and the value comes after that.
Note that the last parameter to this function represents the noun or type of item the Atom is about.
LV2_Atom* msg = (LV2_Atom*)lv2_atom_forge_blank(
                &forge, &frame, 1, uris.eg_Cat );


// then we write a "property_head": this uses a URID to describe the next bit of data coming up, which will form the value of the key:value dictionary pair.
lv2_atom_forge_property_head(&forge, uris.eg_name, 0);
 

// now we write the data, note the call to forge_string(), we're writing string data here! There's a forge_int() forge_float() etc too!
lv2_atom_forge_string(&forge, "nameOfCat", strlen("nameOfCat") );

// Popping the frame is like a closing } of a function. Its a finished event, there's nothing more to write into it.

lv2_atom_forge_pop( &forge, &frame);

 

From the UI

// To write messages, we set up a buffer:
uint8_t obj_buf[1024];

// Then we tell the forge to use that buffer

lv2_atom_forge_set_buffer(&forge, obj_buf, 1024);

// now check the "Code to write messages" heading above, that code goes here, where you write the event.

// We have a write_function (from the instantiate() call) and a controller. These are used to send Atoms back. Note that the type of event is atom_eventTransfer: This means the host should pass it directly the the input port of the plugin, and not interpret it. write_function(controller, CONTROL_PORT_NUMBER,
               lv2_atom_total_size(msg),
               uris.atom_eventTransfer, msg);



From the DSP

// Set up forge to write directly to notify output port. This means that when we create an Atom in the DSP part, we don't allocate memory, we write the Atom directly into the notify port.

const uint32_t notify_capacity = self->notify_port->atom.size;
lv2_atom_forge_set_buffer(&self->forge,
                         (uint8_t*)self->notify_port,
                          notify_capacity);
 

// Start a sequence in the notify output port
lv2_atom_forge_sequence_head(&self->forge,
                             &self->notify_frame, 0);

Now look back at the "Code to write messages" section. that's it, write the event into the Notify atom port, and done.




Reading Atoms


// Read incoming events directly from control_port, the Atom input port
LV2_ATOM_SEQUENCE_FOREACH(self->control_port, ev)
{

  // check if the type of the Atom is eg_Cat
  if (ev->body.type == self->uris.eg_Cat)

  {
    // get the object representing the rest of the data
    const LV2_Atom_Object* obj = (LV2_Atom_Object*)&ev->body;
 

    // check if the type of the data is eg_name
    if ( obj->body.otype == self->uris.eg_name )
    {

      // get the data from the body
      const LV2_Atom_Object* body = NULL;
      lv2_atom_object_get(obj, self->uris.
eg_name,
                          &body, 0);
      

      // convert it to the type it is, and use it
      string s = (char*)LV2_ATOM_BODY(body);
      cout << "Cat's name property is " << s << endl;
    }
  }
}



Conclusion

That's it. Its not hard. It just takes getting used to. Its actually a very powerful and easy way of designing a program / plugin, as it *demands* separation between the threads, which is a really good thing.

Questions or comments, let me know :) -Harry

1 comment:

  1. Hi Harry

    I've tried to convert this tutorial to read/write a LV2_Atom_Vector instead a string but fail in any instance. My conclusion is that it my be powerful, but it is far away from being easy or simple.

    greets
    hermann

    ReplyDelete