File format plugin API for Spotify?
At Spotify, we have one and a half day of ‘hackday’ ('code whatever you want’, the '20% time’ concept from Google) every sprint. Since the past few months have been a bit hectic finishing up the new 0.5.0 release, we haven’t really had time for that in a while, and thus compensated by having a whole week of crazy hacking this week. I managed to finish two features in that time; one in the iOS client (of which I will only say: Loren Brichter is my hero!), and one in the desktop client: file format plugins.
Last year, one of my hackday projects was integrating blargg’s Game Music Emu into Spotify, thus letting you play NES, GameBoy, SNES, Genesis, PC Engine and other console’s music files in Spotify. Just a few days of work, and suddenly I had the best console music player app in the world, with playqueue, playlists and whatnot. However, GME is LGPL and we statically link our dependencies, so there was no way I could release it :(
Thus, I aimed to hit two birds with one stone: by adding a file format plugin API to Spotify, I could a) enable anyone to add support for their favorite file format in Spotify (flac, mikmod, sid, …) b) let me release the GME support as an open source plugin!
However, I’ve never designed a plugin API in C before, and if we’re going to release it and support it for quite a while, it should be well designed, easy to use, and cover all the basic needs. It’s versioned, so it should be simple to add new things to the API, but changing the existing ones might be tricky. So, my question is: do you have any feedback for me on this api?
#ifndef _SPOTIFY_LOCAL_FILE_PLUGIN_API__H | |
#define _SPOTIFY_LOCAL_FILE_PLUGIN_API__H | |
#ifdef __cplusplus | |
extern "C" { | |
#endif | |
/** | |
Spotify Local File Plugin API | |
============================= | |
This file describes the API Spotify exposes for adding support for parsing | |
and playing music file formats in Spotify. | |
Building | |
-------- | |
1. Implement this API, and export the symbol SpotifyLocalFilePlaybackPluginCreate | |
(see bottom of document). | |
2. Create a folder called [your plugin name].splugin | |
3. Compile your source into a dynamic library for your platform. | |
4. Put the compiled library into | |
[name].splugin/Contents/[MacOS|Win32|{some platform}]/{name} | |
(append .dll if the platform is Windows) | |
5. Repeat 3 and 4 for each platform you wish to support. | |
(You can also just rename your dynamic library to | |
[your plugin name].splugin if you only wish to support a single platform) | |
Installing | |
---------- | |
MacOS: Install the .splugin into | |
~/Library/Application Support/Spotify/Plug-Ins/LocalFile/ | |
(or any other domain) | |
Windows: [TBD] | |
Linux: [TBD] | |
*/ | |
#include <string.h> | |
typedef unsigned char spbool; | |
enum { spfalse, sptrue }; | |
typedef unsigned char spbyte; | |
#define SP_LF_PLUGIN_API_VERSION 5 | |
/// Spotify supports the following output sound formats. | |
enum SpotifyLFSoundFormat { | |
kSoundFormat8BitsPerSample = 8, // 8 bit per sample Native Endian PCM | |
kSoundFormat16BitsPerSample = 16, // 16 bit per sample Native Endian PCM | |
kSoundFormatIEEEFloat = 17, // 32 bit per sample IEEE FLOAT PCM | |
}; | |
/// The different types of metadata that a parser can be asked about. | |
enum SPFieldType { | |
kSPFieldTypeInvalid, | |
kSPFieldTypeTitle, | |
kSPFieldTypeArtist, | |
kSPFieldTypeAlbum, | |
kSPFieldTypeAlbumArtist, | |
kSPFieldTypeComment, | |
kSPFieldTypeComposer, | |
kSPFieldTypeContent, | |
kSPFieldTypePublisher, | |
kSPFieldTypeCopyright, | |
kSPFieldTypeUrl, | |
kSPFieldTypeEncoded, | |
kSPFieldTypeTrack, | |
kSPFieldTypeDisc, | |
kSPFieldTypeYear, | |
kSPFieldTypeOrigArtist, | |
// Future versions may ask for more metadata, be prepared | |
// to fail gracefully in such a case. | |
}; | |
enum SPChannelFormat { | |
kSPMono, | |
kSPStereo, | |
}; | |
struct SpotifyLFPluginDescription; | |
/// Used to extract metadata from a file in the plugin's supported formats. | |
struct SpotifyLFParserPlugin { | |
/// Create a metadata parsing context for the given file and song. | |
/// This context will be forwarded to all metadata methods below. | |
/// @returns your internal state on success, NULL on failure. | |
void *(*create)(struct SpotifyLFPluginDescription*, const char *path, int song_index); | |
/// Close the file and destroy the context previously created with `create`. | |
void (*destroy)(struct SpotifyLFPluginDescription*, void *context); | |
/// How many songs does this single file contain? Commonly only one, | |
/// but game music files (SID etc) sometimes contain several. | |
unsigned int (*getSongCount)(struct SpotifyLFPluginDescription*, void *context); | |
/// How many sound channels does this song have? | |
/// (currently only support 1 and 2 channels) | |
enum SPChannelFormat (*getChannelFormat)(struct SpotifyLFPluginDescription*, void *context); | |
/// What does the metadata say that this file is in? | |
unsigned int (*getSampleRate)(struct SpotifyLFPluginDescription*, void *context); | |
/// How long does the metadata claim that this file is? | |
unsigned int (*getLengthInSamples)(struct SpotifyLFPluginDescription*, void *context); | |
// Field extraction methods. `Type` is the 'column' of data to fetch for this row. | |
/// Does this song in this file have this column of data? | |
spbool (*hasField)(struct SpotifyLFPluginDescription*, void *context, enum SPFieldType type); | |
/// Read a column of data from this song in this file as a string. | |
/// `readField` will be called first with a NULL `dest` to determine the | |
/// length of the string. | |
/// @returns whether the operation was successful. | |
spbool (*readField)( | |
struct SpotifyLFPluginDescription*, void *context, | |
enum SPFieldType type, | |
char *dest, | |
size_t *length | |
); | |
/// OPTIONAL: Write a column of data to this song in this file as a string. | |
/// You may leave `writeField` as NULL if you do not support editing metadata. | |
/// @returns whether the operation was successful. | |
spbool (*writeField)( | |
struct SpotifyLFPluginDescription*, void *context, | |
enum SPFieldType type, | |
const char *src, | |
size_t write_length); | |
}; | |
/// Used to decode a file in the plugin's supported formats into a buffer of samples. | |
struct SpotifyLFPlaybackPlugin { | |
/// Create a playback context for the given file and song. | |
/// This context will be forwarded to all playback methods below. | |
/// @returns your internal state on success, NULL on failure. | |
void *(*create)(struct SpotifyLFPluginDescription*, const char *path, int song_index); | |
/// Close the file, cease decoding and destroy the context previously created with `create`. | |
void (*destroy)(struct SpotifyLFPluginDescription*, void *context); | |
/// Decode at most `*destlen` bytes of audio data into the buffer `dest`. | |
/// @param dest The sample buffer to deposit your samples into. | |
/// If your data is stereo, left and right samples should be interlaced. | |
/// @param destlen Set `*destlen`to the number of bytes you have decoded when finished | |
/// @param final Set `*final = sptrue` when you have decoded all bytes in the song | |
/// @returns whether decoding was successful | |
spbool (*decode)( | |
struct SpotifyLFPluginDescription*, void *context, | |
spbyte *dest, | |
size_t *destlen, | |
spbool *final | |
); | |
/// Seek to a specific sample offset into the song, and continue decoding | |
/// from there instead. | |
/// @returns whether seeking was successful | |
spbool (*seek)(struct SpotifyLFPluginDescription*, void *context, unsigned sample); | |
/// How many bytes must the output buffer sent to `decode` be | |
/// to be able to decode a frame of samples? | |
size_t (*getMinimumOutputBufferSize)(struct SpotifyLFPluginDescription*, void *context); | |
/// Given that you now have a decoder for this song, how long can you | |
/// determine that the song *actually* is? (can be different from what | |
/// the parser thought it would be) | |
unsigned int (*getLengthInSamples)(struct SpotifyLFPluginDescription*, void *context); | |
/// Which audio format will you output in the `decode` method? | |
void (*getAudioFormat)( | |
struct SpotifyLFPluginDescription*, void *context, | |
unsigned int *samplerate, | |
enum SpotifyLFSoundFormat *format, | |
enum SPChannelFormat *channels | |
); | |
}; | |
/// This struct describes your plugin as a whole; both its parser, | |
/// player and metadata about the plugin. | |
struct SpotifyLFPluginDescription { | |
/// The API version that your plugin was built for. | |
/// Always set this to SP_LF_PLUGIN_API_VERSION. | |
unsigned int api_version; | |
/// The user-visible name of your plugin. | |
char *plugin_name; | |
unsigned int plugin_version; | |
/// NULL-terminated list of strings of file extensions (excluding | |
/// dot) this plugin adds support for. | |
const char **file_extensions; | |
/// Any context you wish to save that is persistent with this plugin. | |
void *plugin_context; | |
struct SpotifyLFPlaybackPlugin playback; | |
struct SpotifyLFParserPlugin parser; | |
}; | |
/// Convenience method to null and initialize the basics of your plugin. | |
static inline void SPLocalFilePluginInitialize( | |
struct SpotifyLFPluginDescription *plugin, | |
const char *name, | |
unsigned int version | |
) | |
{ | |
memset(plugin, 0, sizeof(struct SpotifyLFPluginDescription)); | |
plugin->plugin_name = strdup(name); | |
plugin->plugin_version = version; | |
plugin->api_version = SP_LF_PLUGIN_API_VERSION; | |
} | |
/// Implement and export this symbol from your dynamic library. Malloc | |
/// (or otherwise allocate) a plugin description, initialize it and return it. | |
/// @returns NULL to indicate failure. | |
//extern struct SpotifyLFPluginDescription *SpotifyLocalFilePlaybackPluginCreate(); | |
#if __cplusplus | |
} | |
#endif | |
#endif |