Yirkha's stuff

foobar2000 development tutorial

Introduction

This document provides basic information about developing foobar2000 components, like setting up the development environment and building a simple component. It is assumed that the reader has at least moderate experience with C++. Knowledge of MS Visual Studio is not required, however being able to find a menu or configuration item by experimenting or googling is.

Note that this tutorial is not a comprehensive guide. It should be regarded only as a quick introduction into used tools and development environment, so that you can set it up fast and concentrate on the development itself. Also if you like to have something working, then play with it and explore the rest yourself afterwards, this document should be just what you need.


Setting up the environment

Required software

Recommended C++ compiler for component development is the latest version of Microsoft Visual Studio C++. The freely available Express edition is sufficient, the default installation even contains a bundled Windows SDK.

Other compilers are not supported and might not work, either because of differences in standards support and problems with compiling the SDK code, or because of a different ABI (multiple inheritance class layout, exceptions, etc.).

Source files needed to make a component are released under BSD license in a Software Development Kit. Download it from the official page.

SDK structure

After extracting the SDK archive to some directory, you can load whole MSVS solution by opening the file sdk-test.sln.

There are 5 projects set up by default. The most important are pfc, a shared template class library, foobar2000_SDK, containing basic interfaces for any interaction with core or other components, and foobar2000_component_client, a stub needed to build component DLLs. Your projects will be always dependent on these three.

Other included projects are foobar2000_sdk_helpers and ATL_helpers – a few classes to ease common tasks in fb2k or UI design, respectively. (To use the ATL/WTL helpers, you need to have these development libraries installed. Express versions of MSVC++ do not contain them, but it is possible to get them elsewhere on the Internet with a bit of extra work.)


Creating a component

A new project

First, create a new project in the SDK solution. Use the "Win32 Project" template and choose a name in the form foo_whatever, because only component files named like foo_*.dll are then recognized and loaded by the core. In the wizard, the only change needed is setting application type to "DLL".

Dependencies and build configuration

Delete files targetver.h and dllmain.cpp and change stdafx.h to just this:

#pragma once
#include "../foobar2000/SDK/foobar2000.h"

Right-click on the project, open "Project dependencies" dialog and check foobar2000_component_client, foobar2000_SDK and pfc.

Before setting up the build process, it would be good to have a copy of foobar2000 just for testing. Run the installer of the latest version, choose Portable mode and put it for example to the SDK folder, into a subdirectory test – that will allow us to have nice relative paths.

Now open Properties of the sample project and choose "All Configurations" from the drop-down list, so that both Debug and Release configurations are altered. Under Debugging, set Command to "$(SolutionDir)test\foobar2000.exe" and Working Directory to "$(SolutionDir)test". Under Linker > Input, add "$(SolutionDir)foobar2000/shared/shared.lib shlwapi.lib" to Additional Dependencies. Finally, go to Build Events > Post-Build Step and set Command Line to "copy "$(TargetPath)" "$(SolutionDir)\test\components\"" and Description to something descriptive like "Copying output file to test directory...".

Finally, switch to configuration for the "Debug" build only and set C/C++ > Code Generation > Runtime Library to "Multi-threaded Debug" – that's what the rest of the solution uses. Then for the "Release" configuration, set the same option to just "Multi-threaded". The default choice, "Multi-threaded DLL", generates smaller output files, but requires users to install MSVC++ redistributable package before use.

The code

Put something like this in the empty foo_whatever.cpp file:

#include "stdafx.h"

DECLARE_COMPONENT_VERSION(
  "foo_whatever",
  "0.0.1",
  "Sample foobar2000 component.\n"
  "See http://yirkha.fud.cz/progs/foobar2000/tutorial.php"
);

That will be enough to know that our component was properly built and loaded.

Test run

Everything is set up now. Make sure your project is the active StartUp project (right-click on a project to change that) and press F5 to run it. If VS asks whether to really build all dependent projects, say yes. There shouldn't be any errors; those warnings in SDK code are harmless, caused only by deprecated code calling other deprecated code. After successful build, fb2k should start up. Go to Preferences > Components and see that your useless first component is nicely listed there.


Debugging

One of the advantages of using a full-blown IDE like Visual Studio is that it provides a powerful integrated debugger. You can step through your code at runtime, inspect what data are in your variables, etc. But for an unknown reason, some people apparently don't use this feature at all. They build their components only using Release configuration, then copy the DLL manually to their foobar2000 folder (stop the music!) and try it. For debugging, they abuse console output, Win32 message boxes or even temporary text files.

The problem with that approach is twofold – it is unnecessary difficult to debug anything serious that way and it allows more error-prone code to slip through and bite you in the future. First, one well-placed breakpoint and stepping interactively through problematic part is much more productive than polluting the source with console spamming. But a debug build also includes additional runtime checks for memory access, so buffer overflows and invalid operations are detected easily. And there are numerous assert conditions throughout the code – also evaluated only in debug builds – which help to catch programming errors early.

Because even if you didn't know how, I just described it above, so please, do the development on debug builds. It won't make you magically write awesome and completely error-free programs, but it's much easier to catch and resolve bugs that way.


Some examples of basic usage

About services

Most interaction in foobar2000 is accomplished using services, which is an extensible object-oriented API similar to, for instance, COM. Each service interface has an assigned GUID and each service instance implements reference counting mechanism. Some services are automatically registered during initialization, some are dynamically instantiated during runtime.

To do something useful, each component registers services it provides and the core calls them afterwards – input services when opening a file, dsp services during playback, mainmenu_command when showing a menu, etc. Even the DECLARE_COMPONENT_VERSION() macro we used above just creates a componentversion service.

“Being called” – Implementing services

For example, let's just print a message to console at startup. For that, we will create an initquit service, which has a simple interface with only two methods, called at startup and shutdown. Add the following code to the main file:

class test : public initquit {
  public:
    virtual void on_init()
    {
      console::formatter() << "'Hello world!', says " << core_version_info::g_get_version_string() << ".";
    }

    virtual void on_quit()
    {
    }
};

That's our implementation class for the service. To let the core see it, add this:

initquit_factory_t<test> g_foo;

Instantiating a service factory adds it to a global list of services in this DLL, so it can be enumerated by relevant code in the main application and instantiated whenever someone needs to call it. It also determines the reference counting method used – in this case a single static instance (actually hidden by the helper derived type initquit_factory_t here).

When you build and run the project, you will find your message in the console.

In a similar fashion, you can create a service to provide alternate UI (see user_interface service), get notified about playback state (start, stop, new track, volume, … – see play_callback_static), changes in media library (library_callback) and so on. Note that the latter two callbacks and many other can also be registered dynamically during runtime using their respective manager.

“Calling others” – Instantiating services

On the other hand, to call functions from other parts of the program, you need to find the service you need, get its instance and invoke its methods. For the most common usage of services, there is a handy template class – standard_api_ptr_t. It automatically finds the service according to its template parameter, validates there is exactly one such service and acts as a smart pointer to its instance until out of scope.

For an illustration, let's print additional info in our example from the previous chapter: current volume and number of playlists. Add the following code to the on_init method:

    virtual void on_init()
    {
      …

      // Get play_control service
      static_api_ptr_t<playback_control> pc;
      // Then call its method to get the information we need
      console::formatter() << "Volume: " << pfc::format_float(pc->get_volume(), 0, 2) << " dB";

      // Similarly with a different service and function
      static_api_ptr_t<playlist_manager> pm;
      console::formatter() << "Playlist count: " << pm->get_playlist_count();

      // A simple call - this will start playback immediately after startup
      pc->start();
    }

Build, run and see that it does what it should. Services can be instantiated and called like this almost everywhere, except during early initialization and late shutdown (particularly in constructors and destructors of static global objects).

Also note that many API functions can be called only from the main thread and will fail or throw an exception when invoked incorrectly. To circumvent that, defer the action into a service class implementing main_thread_callback, which will be then scheduled to run using main_thread_callback_manager whenever needed.


Getting more information

First, for questions regarding C++, use your favorite book or Internet. Similarly, for Windows programming topics, use local help installed with Windows SDK, MSDN or other online source. It is not possible to teach you all this from the ground up in a few paragraphs just because you decided to write a plugin for an application built on those technologies.

Anyway, at the time of writing, there was no comprehensive documentation available for the SDK. The easiest and most resourceful way for me has always been simply searching in the SDK files. One can find not only the bare interfaces that way, but also a lot of useful hints and important notes in the comments. Also if you look around, you might find some neat helper class or method which can save you a lot of time. With the "Go to definition/declaration" features of the IDE, it is even possible to jump around quite fast.

For another way how to learn by yourself, check out foosion's site. He provides a browsable SDK reference generated by Doxygen and a few component tutorials. Although for example the DSP tutorial target a specific outcome, it naturally contains a lot of useful comments and explanations about general subjects as well. I strongly recommend you to download and read these guides thoroughly.

Of course, source code of random components released on the Internet can be used to get a clue about a specific task (or copy-paste half of the functionality) too. But just because someone else does something, don't automatically assume it is the right way. I've seen a lot of broken code by others and while I'm not saying it was intentional, this is a chapter about learning resources, so beware.

Then there is the official foobar2000 Development forum, hosted on Hydrogenaudio. Searching or posting a question there (in that order!) means it will be read by a lot of experienced people, including the application's author himself. But note that trivial and clueless queries will be probably ignored due to lack of interest. Also why wait a day for someone to reply than simply use one of the methods above?

Another way is to join #foobar2000 channel on Freenode. As on any other IRC channel, there are a lot of friendly people eager to spend their time resolving your problems, so don't hesitate and visit us.


Random other tips