Notes from developing an xform plugin

Erik Massop edited this page Nov 4, 2017 · 1 revision


Hello. I'm Lucas. These are notes as I come up with them. I'll write a proper introduction when I get around to it.

The Code

The first thing I was told when I asked for help writing a decoder plugin for XMMS2 was to read the friendly source. Specifically, the source for an existing decoder plugin: vorbis.c. I'm not going to reproduce the whole thing here since you can download it yourself, but let's take a look at what I learned from it.

Skipping the license section. It speaks for itself.

/**   * @file vorbisfile decoder.   * @url   */

The first time I wrote a plugin, one of the things I was told by the xmms2 dev team (coughanders_cough) was that my code was cluttered by a lot of unnecessary comments. I still try to comment my code heavily, but he suggested I put a lot of it in a blog. This line made that okay with me. I figure I'll just leave a link to all the commentary I would have liked to put in the code so it doesn't "clutter it up". I believe this particular comment from vorbis.c is in Doxygen format. I'll look up what it means later.

#include "xmms/xmms_xformplugin.h" #include "xmms/xmms_sample.h" #include "xmms/xmms_log.h" #include "xmms/xmms_medialib.h"

These, I have gathered, are important. I don't know a lot about them, but their names are fairly self-explanatory: xmms_xformplugin.h is necessary when making an xform plugin, such as a decoder plugin. xmms_sample.h has to do with sound samples and the data types and functions surrounding them in xmms2. xmms_log.h is for producing log output -- very useful for debugging. xmms_medialib.h is for interfacing with the Media Library. There's a page on that. The Medialib. Mostly for your plugin to be able to tell xmms2 about a song's metadata.

Skipping other headers. They're rather vorbis specific and not uncommon anyway.

typedef struct xmms_vorbis_data_St {    OggVorbis_File vorbisfile;    ov_callbacks callbacks;    gint current; } xmms_vorbis_data_t;

This thing right here is important. You see, an xform plugin can't keep track of data through local variables and function calls alone like a normal program often does because it has to be implemented as a set of callback functions. And those callback functions don't share a local namespace and can't send parameters to each other. And using global variables is a big no-no. So xmms2 devs came up with a way for a plugin to store persistent data away and ask for it back when the next callback function ran. This typedef struct here is part of it. You can call it anything you want because you register it with the plugin interface in your plugin setup function. It's just important to have one if your plugin needs to remember anything between function calls (and I can't think of a reason why it wouldn't need to do so). You typedef it like that so you don't have to go around typing struct xmms_vorbis_data_St whenever you want to refer to the data type.

Skipping a #define. I don't know what it does.

typedef enum { STRING, INTEGER } ptype; typedef struct {    const gchar *vname;    const gchar *xname;    ptype type; } props; /** These are the properties that we extract from the comments */ static const props properties[] = {    { "title",                XMMS_MEDIALIB_ENTRY_PROPERTY_TITLE,     STRING  },    { "artist",               XMMS_MEDIALIB_ENTRY_PROPERTY_ARTIST,    STRING  },    { "album",                XMMS_MEDIALIB_ENTRY_PROPERTY_ALBUM,     STRING  },    { "tracknumber",          XMMS_MEDIALIB_ENTRY_PROPERTY_TRACKNR,   INTEGER },    { "date",                 XMMS_MEDIALIB_ENTRY_PROPERTY_YEAR,      STRING  },    { "genre",                XMMS_MEDIALIB_ENTRY_PROPERTY_GENRE,     STRING  },    { "comment",              XMMS_MEDIALIB_ENTRY_PROPERTY_COMMENT,   STRING  },    { "discnumber",           XMMS_MEDIALIB_ENTRY_PROPERTY_PARTOFSET, INTEGER },    { "musicbrainz_albumid",  XMMS_MEDIALIB_ENTRY_PROPERTY_ALBUM_ID,  STRING  },    { "musicbrainz_artistid", XMMS_MEDIALIB_ENTRY_PROPERTY_ARTIST_ID, STRING  },    { "musicbrainz_trackid",  XMMS_MEDIALIB_ENTRY_PROPERTY_TRACK_ID,  STRING  }, };

This is a useful way of crunching metadata. I'm sure I'll have more to comment on this later.

/*  * Function prototypes  */ static gboolean xmms_vorbis_plugin_setup (xmms_xform_plugin_t *xform_plugin); static gint xmms_vorbis_read (xmms_xform_t *xform, xmms_sample_t *buf, gint len, xmms_error_t *err); static gboolean xmms_vorbis_init (xmms_xform_t *decoder); static void xmms_vorbis_destroy (xmms_xform_t *decoder); static gint64 xmms_vorbis_seek (xmms_xform_t *xform, gint64 samples, xmms_xform_seek_mode_t whence, xmms_error_t *err);

Here we go. Now we're getting into the important stuff. These functions are what do all the work in an xform plugin. The names of these functions aren't important. You can call them anything you want because, again, you register them with the plugin interface during the setup function, and you register the setup function with a macro. What's important is that they have the right return types and parameter types. My first SPC plugin didn't compile a few times because I used the wrong parameter types for the seek function. Didn't take me too long to figure it out, but it was annoying. I'll figure out where you should check the xmms2 code or documentation for the right return types and parameters later.

/*  * Plugin header  */ XMMS_XFORM_PLUGIN ("vorbis",                    "Vorbis Decoder", XMMS_VERSION,                    "Xiph's Ogg/Vorbis decoder",                    xmms_vorbis_plugin_setup);

This is where you first introduce your plugin to the plugin interface. XMMS_XFORM_PLUGIN is a macro that will register your plugin's setup function and tell xmms2 what your plugin actually is. I'm still fuzzy on the details of some of the parameters, but it looks like the third one really ought to be XMMS_VERSION and the fifth (last) one should be the name of your setup function.

Next comes the setup function itself, where your plugin and xmms2 get all buddy buddy.

static gboolean xmms_vorbis_plugin_setup (xmms_xform_plugin_t *xform_plugin) {    xmms_xform_methods_t methods;    XMMS_XFORM_METHODS_INIT (methods);    methods.init = xmms_vorbis_init;    methods.destroy = xmms_vorbis_destroy; = xmms_vorbis_read; = xmms_vorbis_seek;    xmms_xform_plugin_methods_set (xform_plugin, &methods);

Here's the beginning of our plugin setup function. All this "methods" stuff is how you introduce the functional parts of your plugin to xmms2. Note that XMMS_XFORM_METHODS_INIT() initializes the xmms_xform_methods_t structure and that methods.init is set equal to the function in charge of initializing an instance of your plugin. Also, just setting these members of the structure equal to your function calls is not enough. You must then call xmms_xform_plugin_methods_set() to actually give that information to xmms2.

   xmms_xform_plugin_indata_add (xform_plugin,                                  XMMS_STREAM_TYPE_MIMETYPE,                                  "application/ogg",                                  NULL);

Here we tell xmms2 what kind of plugin we are and what kind(s) of data we can handle. I'll show how more than one MIME type can be specified when I begin dealing with my very own GME plugin.

xmms_magic_add ("ogg/vorbis header",                    "application/ogg",                    "0 string OggS", ">4 byte 0",                    ">>28 string \x01vorbis", NULL);

The term "magic" as used here describes a way of recognizing a file type by the first few bytes of that file. This function teaches xmms2 how to recognize a data stream or file that your xform plugin can handle. I'm still a bit fuzzy on the details of how this particular function works, but here's what I know. The second parameter of this function must match one of the mime-type parameters of the last function. After that comes the magic, and that roughly works like this: "offset datatype value". So the first one there would be offset = 0, data type of the magic = string, magic itself = "OggS". I'll get more detailed if needed. Until then, man magic should help you.

   xmms_magic_extension_add ("application/ogg", "*.ogg");    return TRUE; }

And wrapping up our setup function is this call. It associates a mime type with a filename extension, to kind of give xmms2 a clue. Really helps to have a clue. Once we've done that, all that's left is to return TRUE. If we got this far, then everything presumably went well and we're all set up.

At this point I'd like to shift gears and take a look at how I implemented my own plugin based on the things I learned from the vorbis.c source.

My GME plugin

This is a huge helping of my GME plugin. As currently implemented, it only decodes .spc files, but that should change soon since the library itself will handle a plethora of emulated music files. You'll see how simple it is in xmms2 to create a new xform plugin if you know a bit about an existing one such as vorbis.c.

First, I made sure I had the right headers.

#include "xmms/xmms_xformplugin.h" #include "xmms/xmms_sample.h" #include "xmms/xmms_log.h" #include "xmms/xmms_medialib.h" #include "gme/gme.h"

Basically that consisted of the four plugin headers and the gme header. Since we decided to include the sources for the gme library with xmms2, I decided to put it in a subdirectory of the plugin. Hence the gme/ part. I could have called it lib/ or gmelib/ or something else, but I didn't. If it needs fixing later, I'll fix it. Next the persistent data structure and typedef.

typedef struct xmms_gme_data_St {    GString *file_contents; /* The raw data from the file. */    Music_Emu *emu;         /* An emulation instance for the GME library */ } xmms_gme_data_t;

Straightforward and simple. I'm using a GString like the modplug plugin does to avoid having to write memory management and all that stuff. The other variable, emu, is used to keep track of a particular instance of emulation we are running, and to keep the library thread-safe.

static gboolean xmms_gme_plugin_setup (xmms_xform_plugin_t *xform_plugin); static gint xmms_gme_read (xmms_xform_t *xform, xmms_sample_t *buf, gint len, xmms_error_t *err); static gboolean xmms_gme_init (xmms_xform_t *decoder); static void xmms_gme_destroy (xmms_xform_t *decoder); static gint64 xmms_gme_seek (xmms_xform_t *xform, gint64 samples, xmms_xform_seek_mode_t whence, xmms_error_t *err);

Here are my function prototypes. I just copied and pasted from vorbis.c and then searched and replaced vorbis with gme. Easy.

XMMS_XFORM_PLUGIN ("gme",                    "Game Music decoder", XMMS_VERSION,                    "Game Music Emulator music decoder",                    xmms_gme_plugin_setup);

Here's the plugin header. Last parameter matches my setup function's name.

static gboolean xmms_gme_plugin_setup (xmms_xform_plugin_t *xform_plugin) {    xmms_xform_methods_t methods;    XMMS_XFORM_METHODS_INIT (methods);    methods.init = xmms_gme_init;    methods.destroy = xmms_gme_destroy; = xmms_gme_read; = xmms_gme_seek;    xmms_xform_plugin_methods_set (xform_plugin, &methods);

The beginning of the setup function. Again, I just copied and pasted this part then searched for vorbis and replaced with gme.

   /* todo: add other mime types */    xmms_xform_plugin_indata_add (xform_plugin,                                  XMMS_STREAM_TYPE_MIMETYPE,                                  "application/x-spc",                                  NULL);

I'll add more mime-types later. I'd especially like to know if there's an actual mime type out there for SPC700 files. How does one go about registering a mime type anyway?

   /* todo: add other magic */    xmms_magic_add ("SPC header",                    "application/x-spc",                    "0 string SNES",                     NULL);

I'll add more magic later. This one could even be more specific. The beginning string of SPC files is longer than this, and I wonder if there are other files out there that start with "SNES"...

   /* todo: add other file extensions */    xmms_magic_extension_add ("application/x-spc", "*.spc");    return TRUE; }

I'll add more file extensions later. At some point I might want to add support for .rsn files, but that'd require a rar library, which is slightly hard to get open source.

static gboolean xmms_gme_init (xmms_xform_t *xform) {    xmms_gme_data_t *data;    gint filesize;    g_return_val_if_fail (xform, FALSE);    data = g_new0 (xmms_gme_data_t, 1);    xmms_xform_private_data_set (xform, data);

Start off our init function. We need to fail if we didn't get our xform properly. Then we initialize our persistent data with g_new0. This xform is how we keep our persistent data, so we need to associate the two, and xmms_xform_private_data_set is how we tell xmms2 that we are doing so.

   xmms_xform_outdata_type_add (xform,                                 XMMS_STREAM_TYPE_MIMETYPE,                                 "audio/pcm",                               /* PCM samples */                                 XMMS_STREAM_TYPE_FMT_FORMAT,                                 XMMS_SAMPLE_FORMAT_S16,                /* 16-bit signed */                                 XMMS_STREAM_TYPE_FMT_CHANNELS,                                 2,                                         /* stereo */                                 XMMS_STREAM_TYPE_FMT_SAMPLERATE,                                 32000,                                     /* 32 kHz */                                 XMMS_STREAM_TYPE_END);

This function call describes to xmms2 what kind of data your plugin outputs. It doesn't have to be audio/pcm. It could be a type of data handled by some other plugin. If that's the case, xmms2 will chain your plugin together with whatever other plugins are necessary to get a stream it can send to an output plugin.

   data->file_contents = g_string_new ("");    for (;;)    {        xmms_error_t error;        gchar buf[4096];        gint ret;        ret = xmms_xform_read (xform, buf, sizeof (buf), &error);        if (ret == -1)        {            XMMS_DBG ("Error reading emulated music data");            return FALSE;        }        if (ret == 0)        {            break;        }        g_string_append_len (data->file_contents, buf, ret);    }

This part is all about reading the data from file or wherever xmms2 got it. I copied the basic form of it from modplug.c and modified basically only the error message and the name of the GString where we store the data.

   if (init_error = gme_open_data(data->file_contents->str, data->file_contents->len, &data->emu, 32000))    {        XMMS_DBG ("gme_open_data returned an error: %s", init_error);        return FALSE;    }

This is GME-specific stuff. The if line loads the data into the library's Music_emu data structure for processing and grabs any error messages along the way. If there was a message, we send that to debug output and return FALSE to say we failed initialization.

   return TRUE; }

We're done with initialization.

static void xmms_gme_destroy (xmms_xform_t *xform) {    xmms_gme_data_t *data;    g_return_if_fail (xform);    data = xmms_xform_private_data_get (xform);    g_return_if_fail (data);

Here's the start of our destroy function. Look at those first four lines. You'll get used to seeing this at the beginning of a function. Or something like it. It's how we get our persistent data back. Unless your plugin is crazy or has some weird plugin intuition, you'll need this at the beginning of destroy, read, and seek.

   if (data->file_contents)        g_string_free (data->file_contents, TRUE);    gme_delete (data->emu); }

Typical memory management and stuff. We're just giving back what we borrowed. That's all there is to our destroy function.

Status of this page and my plugin

So far I've got the following implemented and documented:

  • Headers
  • Function prototypes
  • Persistent data structure typedef
  • The plugin header
  • xmms_gme_plugin_setup()
  • xmms_gme_init()
  • xmms_gme_destroy()

That leaves:

  • xmms_gme_read()
  • xmms_gme_seek()

I'll have more here soon. Everyone watch this space!

Clone this wiki locally
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.