Wednesday, June 10, 2015

Using LV2 atom:Path (or how to get files safely in a plugin without a GUI)

The end of last month marked 2 years since my initial commit to my first lv2 plugins. I've learned some things, but still am picking up more little by little. I was recently reminded that I don't know it all, when falkTX pointed out another stupid mistake I'd been making that prevented my GUIs from resizing correctly.

Water under the bridge though. I'm going to try to explain out how to use lv2 to have the host make a file selection dialog for you. I want all my plugins to work purely with host generated GUIs and loading files became necessary for porting the rakarrack *tron effects. The LV2 official documentation has improved a lot, but I was still struggling, so I thought I'd post this. The example to be used is rakarrack's reverbtron. Its a sort of convolution using very simplified impulse response files (which are actually text files). Instead of a constant sample rate in the IR, you just keep the most important samples and interpolate between them.

Regardless of the details, I have a module that needs files. All I need is to pass the file's path to the modules setfile() function and it takes care of the rest. How do I get that path? The whole process involves several extensions, and the most confusing part to me was figuring out what was what. But I got it, so read on...



So lets start with the ttl. Quick recap, ttl is static data that the host loads to figure out what your plugin is all about (see the official examples for some more info). So we need to indicate to the host in this ttl that we need files. Lets create a new statement that defines our .rvb files that hold the IR data. I usually just take the URI for the plugin and tack on some extra stuff with colons. Even though its not an actual website, the whole point of a URI is to keep it universally unique so its pretty unlikely any other plugin will name anything this same string:

      <http://rakarrack.sourceforge.net/effects.html#Reverbtron:rvbfile>
      a lv2:Parameter ; 
      rdfs:label "rvb file" ; 
      rdfs:range atom:Path ; 
      .  

As you probably know already, this "sentence" in the ttl says, "I declare an lv2 parameter, labeled 'rvb file', and can be any value as long as its an atom containing a path." This is all done with URIs, but most of them are shortened using prefixes at the top of the file. Because these URIs are universal, the host knows what they mean and can interpret that as "ok, they have a parameter that will be a path" (roughly).

Now we need a port for this parameter to be passed into. Its not too different than other ports we've seen before, but we'll use an atom port like just we do for midi ports. So we have inside the plugin declaration we add:

     lv2:port [ 
              a lv2:InputPort, atom:AtomPort ; 
              lv2:index 4 ; 
              lv2:symbol "CONTROL" ; 
              lv2:name "Control" ; 
              atom:bufferType atom:Sequence ; 
              lv2:designation lv2:control ; 
              atom:supports patch:Message ; 
      ] ; 

This says the 4th port is an atom input. It has name and symbol "control". The host will hand us a pointer to a buffer full of a sequence of atoms (which are just structures of data). Its a designated port for the host to pass any control messages to (such as data for bpm sync, or our .rvb files). Finally, the port supports atoms carrying something called patch:message. What?

Patch is another extension that is used to read or manipulate properties between the host and plugin. I'm not extremely familiar with it, so I don't have any examples of what else you can do with it. Say for example is if the host wants to change the file path property of our plugin...

If you expand the prefix for patch the message URI is http://lv2plug.in/ns/ext/patch/#Message. The docs here aren't very verbose, but feel free to see what possibilities are there. The idea is that you can pass several properties without needing different message types for everything.

We want the host to know what property they can manipulate, so in our plugin declaration ttl we add:

   patch:writable <http://rakarrack.sourceforge.net/effects.html#Reverbtron:rvbfile> ;

saying that the plugin has a ".rvb file" property that the host can write. The host knows that URI means the .rvb file because we declared it before we declared the plugin.

See? This ttl stuff is pretty readable once you get the hang of it. You can see the full ttl but everything else in it, we've done before. 

So lets jump to the code. Assuming we've got the port connecting correctly, we just need to pull the atoms out of the buffer the host gives us, make sure they're the type of atom we want, then just grab the data (which should be the file path). So the action all happens in the run() part of the plugin, but we need a few things setup first. 

We need a way to recognize the URIDs that the atoms carry, and thats through the URID:map. I've described the map before, but I didn't really understand it then. Basically the host takes the URIs and says, "ok, how about from now on instead of calling that 'http://lv2plug.in/ns/ext/atom/#Object' we'll say its 4. So now whenever I pass an atom marked type 4, we both know what I mean." This mapping is generated arbitrarily by the host, they will call whatever URI any unique integer value, but you can count on it remaining unique this whole session. So always pay attention whether a function needs a URI (string) or a URID (int).

Anyway so its our plugin's responsibility to look through the host_features passed into our initialization function to get and save the mapping:
 for(i=0; host_features[i]; i++) 
    { 
        if(!strcmp(host_features[i]->;URI,LV2_URID__map)) 
        { 
            urid_map = (LV2_URID_Map *) host_features[i]->;data; 
            if(urid_map) 
            { 
                plug->URIDs.atom_Float = urid_map->map(urid_map->handle,LV2_ATOM__Float); 
                plug->URIDs.atom_Int = urid_map->map(urid_map->handle,LV2_ATOM__Int); 
                plug->URIDs.atom_Object = urid_map->map(urid_map->handle,LV2_ATOM__Object); 
                plug->URIDs.atom_Path = urid_map->map(urid_map->handle,LV2_ATOM__Path); 
                plug->URIDs.atom_URID = urid_map->map(urid_map->handle,LV2_ATOM__URID); 
                plug->URIDs.patch_Set = urid_map->map(urid_map->handle,LV2_PATCH__Set); 
                plug->URIDs.patch_property = urid_map->map(urid_map->handle,LV2_PATCH__property); 
                plug->URIDs.patch_value = urid_map->map(urid_map->handle,LV2_PATCH__value); 
                plug->URIDs.filetype_rvb = urid_map->map(urid_map->handle,RVBFILE_URI); 
            } 
        } 
    }

There in the last line I use a #define I created for the URI string (for readability):

#define RVBFILE_URI "http://rakarrack.sourceforge.net/effects.html#Reverbtron:rvbfile" 

See how that matches the URI in the ttl? One thing not shown here is that if the host does not give you a urid_map you need to abort the instantiation and return a 0 to the host. Now that we have that done (along with our other instantiation tasks not shown) then we're ready for the signal processing function, run(). When we're running the signal processing, we can look through the atom sequence and use the URIDs to find the atoms with data we want. This uses several LV2 macros, but they're pretty readable:

   LV2_ATOM_SEQUENCE_FOREACH(plug->atom_in_p, ev) 
   { 
       if (ev->body.type == plug->URIDs.atom_Object) 
       { 
           const LV2_Atom_Object* obj = (const LV2_Atom_Object*)&ev->body; 
           if (obj->body.otype == plug->URIDs.patch_Set) 
           { 
               // Get the property the set message is setting 
               const LV2_Atom* property = NULL; 
               lv2_atom_object_get(obj, plug->URIDs.patch_property, &property,  0);
               if (property && property->type == plug->URIDs.atom_URID) 
               { 
                   const uint32_t key = ((const LV2_Atom_URID*)property)->body; 
                   if (key == plug->URIDs.filetype_rvb) 
                   { 
                       // a new file! Get file path from message (the value of the set message) 
                       const LV2_Atom* file_path = NULL; 
                       lv2_atom_object_get(obj, plug->URIDs.patch_value, &file_path, 0); 
                       if (file_path) 
                       { 
                           // Load sample.  
                           strcpy(plug->revtron->Filename,LV2_ATOM_BODY_CONST(file_path)); 
                           plug->revtron->setfile(); 
                       }//got file 
                   }//property is rvb file 
               }//property type is a URID 
           }//if object is patch set 
       }//if atom is abject 
   }//each atom in sequence

So you see we go through each atom in the sequence. If the atom is an object containing a a patch:Set message, check if the property the patch message is setting matches our file URID. If it does then get the value the patch message is setting it to (which is really .rvb file's path the user selected).

Really thats all there is to it. Except...

File IO is NOT realtime safe. It could be reading off a USB 1.0 drive. Or an i2c drive. One thats networked over a dial-up connection. You never know. So unless the Reverbtron class's setfile() function handles it through multi-threading (and it doesn't) then this plugin is not realtime safe if we leave it like this.  If you need motivation on why you should worry about being RT safe go read the classic blogpost on the subject of realtime audio programming

So LV2 conveniently has an easy way for you to do file operations or any other non-RT stuff you want safely. Its called the worker extension. You can use it to calculate Fibonacci sequences or fractals, or download the internet, or whatever you want and it won't stop your RT code. Hey, we could make it calculate PI to n decimal places... I digress. The key here is that you schedule some function to be run by the "worker" (which is really just a separate thread, but you don't need to worry about it too much). This scheduling sends it off to another "place" to calculate whatever in however much time it will take. You can pass atoms to the work function for data or to indicate why the it is being run. This way you can do different tasks even though the worker always runs the same function. Once the work is complete the worker can execute a response function that can change your plugin. This second function must be RT safe. So the worker function will load the file into a struct or some useable form (not RT), then the worker response will put the struct into the plugin (RT). All you have to do is write these two functions then the host will handle the rest.
Well, good luck! 

Just kidding. I wouldn't leave you hangin! Lets go through it together. First we need to revisit our ttl to tell the host when they load this plugin that it needs the worker extension:

    lv2:requiredFeature urid:map , worker:schedule ; 
    lv2:optionalFeature lv2:hardRTCapable ; 
    lv2:extensionData worker:interface ; 

The first line gets work:schedule added as the new required feature, optional features haven't changed, but we now need an extensionData struct for the worker (don't forget to add the necessary prefixes at the beginning of your ttl!). The work:interface indicates to the host that we'll use the extension_data() function of our plugin to hand over a struct called an LV2_Worker_Interface (surprise!). This struct has function pointers for the function you want the worker to run that doesn't have to be RT safe and the response function that does. Before now I haven't used the extension data in the dsp part of the plugin (just in the GUI), but it doesn't change too much. We need to define the struct, the functions, and put them in the plugin descriptor:

//forward declarations 
static LV2_Worker_Status revwork(LV2_Handle handle, LV2_Worker_Respond_Function respond,
                                LV2_Worker_Respond_Handle rhandle, uint32_t size, const void* data); 
static LV2_Worker_Status revwork_response(LV2_Handle handle, uint32_t size, const void* data);

static const LV2_Worker_Interface worker = { revwork, revwork_response, NULL }; 

static const void* revtron_extension_data(const char* uri) 
{ 
    if (!strcmp(uri, LV2_WORKER__interface)) { 
            return &worker; 
    } 
    return NULL; 
}

static const LV2_Descriptor revtronlv2_descriptor={ 
    REVTRONLV2_URI, 
    init_revtronlv2, 
    connect_rkrlv2_ports_w_atom, 
    0,//activate 
    run_revtronlv2, 
    0,//deactivate 
    cleanup_rkrlv2, 
    revtron_extension_data 
};

We'll get into what the work and response functions do in a bit, but for now suffice it to say they are defined, we put them in a constant struct, and pass return that when requested by the host (when the host calls revtron_extension_data(LV2_WORKER__interface);). Thats not too crazy, especially if you've read my posts on UIs where I used several extensions with extension data.

We next need to add an LV2_Worker_Schedule* member to our plugin for this will be the way to schedule the non-RT work to run. We also need to get the LV2_Worker_Schedule* that will come from the host during initialization. So:

   for(i=0; host_features[i]; i++) 
    { 
        if(!strcmp(host_features[i]->URI),LV2_WORKER__schedule) 
        { 
            plug->;scheduler = (LV2_Worker_Schedule*)host_features[i]->;data; 
        } 
        else if(!strcmp(host_features[i]->URI,LV2_URID__map)) 
        { 
            //urid map stuff from earlier 
...

Now that we have that, we can use it in the run() function. The code is the same part as above but in the deepest part of the if tree it changes to:
 
//going through the atoms in the sequence 
lv2_atom_object_get(obj, plug->URIDs.patch_property, &property, 0); 
if (property && property->type == plug->URIDs.atom_URID) 
{ 
     const uint32_t key = ((const LV2_Atom_URID*)property)->;body; 
     if (key == plug->URIDs.filetype_rvb) 
     { 
          // a new file! pass the atom to the worker thread to load it 
          plug->scheduler->schedule_work(plug->scheduler->handle, lv2_atom_total_size(&ev->body), &ev->t;body);) 
     }//property is rvb file 
}//property is URID 
...

So instead of pulling the file path string out of the atom and calling setfile(),  we just pass the atom to the worker. Now though, we have to be careful of multi-threading issues. If the worker is in the middle of loading the new file data while the run() executes, you might get some crazy behavior, reading half written variables etc. So I have to change the Reverbtron class to have 2 functions: loadfile() and applyfile(). The first will do the file operations and create a struct with the important data. applyfile() in turn will take the new struct and swap out the old one. loadfile() will be used in the work function, and the other in the RT-safe worker reply. This keeps everything clean and threadsafe. I'm not puting the load and apply code here because its very application specific, but thats the general idea. You can go look at the repo for the full code.

Once I have that in place I can write the function that the worker will run:

static LV2_Worker_Status revwork(LV2_Handle handle, LV2_Worker_Respond_Function respond, 
                                  LV2_Worker_Respond_Handle rhandle, uint32_t size, const void* data)
{

    RKRLV2* plug = (RKRLV2*)handle;
    LV2_Atom_Object* obj = (LV2_Atom_Object*)data;
    const LV2_Atom* file_path;

    //work was scheduled to load a new file
    lv2_atom_object_get(obj, plug->URIDs.patch_value, &file_path, 0);
    if (file_path && file_path->type == plug->URIDs.atom_Path)
    {
        // Load file.
        char* path = (char*)LV2_ATOM_BODY_CONST(file_path);
        RvbFile filedata = plug->revtron->loadfile(path);
        respond(rhandle,sizeof(RvbFile),(void*)&filedata);
    }//got file
    else
        return LV2_WORKER_ERR_UNKNOWN;
            
    return LV2_WORKER_SUCCESS;
}

So we take that atom and just pull out the patch value, run it through loadfile() which returns a struct that holds all the important data from the file. We then pass the struct to the respond function, which looks like so:

static LV2_Worker_Status revwork_response(LV2_Handle handle, uint32_t size, const void* data)
{
    RKRLV2* plug = (RKRLV2*)handle;
    plug->revtron->applyfile((RvbFile*)data);
    return LV2_WORKER_SUCCESS;
}

This simply takes the struct with the filedata and applies it. This response function is actually run between run() calls of your plugin so you will have predictable results (you don't have to worry about the worker finishing and changing things in the middle of your signal processing). Originally I was trying to allocate the struct in loadfile() so that applyfile() just has to swap pointers, but then you must schedule the worker thread again to delete the old struct (delete/free() are not RT safe either). The other obstacle is that when the host is running these functions it copies the data that you pass, so you have to make take care you are manipulating the data you think you are when you try to pass pointers. It can be done (the official example does this), but I realized it made the code more complex than it needs to be. Premature optimization is the root of all evil. If profiling shows the extra memory copying is too expensive I'll worry about it then. For this example and many cases, just use plain old data (POD).

So, we're done, right? Unfortunately, no. If you want the user to be able to use your plugin, they'll need to be able to load it up in their session and when they reload the session it has the same file selected. This has to be done through yet another extension: state.

The state extension mechanics are similar to the worker extension, the host will pass in the state URI to extension_data() and we must return a special struct that tells the host what function to call to save and to restore the current state. Since the only state of the plugin that's not controlled by the usual controlPorts is the .rvb file thats all we need to save. So first we change the .ttl:

    lv2:requiredFeature urid:map, worker:schedule ;
    lv2:optionalFeature lv2:hardRTCapable, state:loadDefaultState ;
    lv2:extensionData worker:interface, state:interface ;


And in the extension data, add the state interface below what we did for worker:

static const LV2_Worker_Interface worker = { revwork, revwork_response, NULL };
static const LV2_State_Interface state_iface = { revsave, revrestore };

static const void* revtron_extension_data(const char* uri)
{
    if (!strcmp(uri, LV2_STATE__interface))
    {
        return &state_iface;
    }
    else if (!strcmp(uri, LV2_WORKER__interface))
    {
        return &worker;
    }
    return NULL;
}


for this extension we won't need any special mappings or anything in initialization. This is pretty much stuff we've done before, but what does the save function do? Well, something like:

static LV2_State_Status revsave(LV2_Handle handle, LV2_State_Store_Function  store, LV2_State_Handle state_handle,
        uint32_t flags, const LV2_Feature* const* features)
{
    RKRLV2* plug = (RKRLV2*)handle;
    LV2_State_Map_Path* map_path = NULL;
    for (int i = 0; features[i]; ++i) 
    {
        if (!strcmp(features[i]->URI, LV2_STATE__mapPath)) 
        {
            map_path = (LV2_State_Map_Path*)features[i]->data;
        }
    }

    char* abstractpath = map_path->abstract_path(map_path->handle, plug->revtron->File.Filename);

    store(state_handle, plug->URIDs.filetype_rvb, abstractpath, strlen(plug->revtron->File.Filename) + 1,
           plug->URIDs.atom_Path, LV2_STATE_IS_POD | LV2_STATE_IS_PORTABLE);

    free(abstractpath);

    return LV2_STATE_SUCCESS;
}


File paths are a little unique because you don't want to store an absolute path, otherwise it won't be very portable (i.e. a preset on your machine probably won't work on mine). So there's a special part of the state extension that generates an abstract path that is more portable. Finally the big moment is calling store which comes from the state extension. You just pass in the URID for whatever you are saving, then the data you are saving, followed by the size and data type URID. The rest is just boilerplate code that you can more or less just paste and forget in most cases.

From save, restore ends up being pretty predictable:

static LV2_State_Status revrestore(LV2_Handle handle, LV2_State_Retrieve_Function retrieve,
        LV2_State_Handle state_handle, uint32_t flags, const LV2_Feature* const* features)
{
    RKRLV2* plug = (RKRLV2*)handle;

    size_t   size;
    uint32_t type;
    uint32_t valflags;

    const void* value = retrieve( state_handle, plug->URIDs.filetype_rvb, &size, &type, &valflags);

    if (value) 
    {
        char* path = (char*)value;
        RvbFile f = plug->revtron->loadfile(path);
        plug->revtron->applyfile(f);
    }

    return LV2_STATE_SUCCESS;
} 

On this side, just a call to retrieve which will find the data that was saved with our URID and return it.

So FINALLY we've gone from not being able to have users load files to an unsafe way to load files (patch extension), to a safe way (worker extension), to a way that allows us to reload the previous setting (state extension). Not too bad.

Hopefully this was as helpful to you as it was to me as I was writing it. I was looking through the examples, but couldn't really parse out what parts applied to my plugin and what parts were for this extension or that one. I had to break it down like this to really understand whats going on.

1 comment:

The Caretaker said...

Brilliant, thank you! Good LV2 info is very hard to find. This saved me hours, maybe days.