Airsonic-Advanced on NixOS with libopenmpt and libgme transcoding

April 26, 2022

Airsonic is a web-based music player. I’ve tinkered around with a lot of options over the years and keep coming back to Airsonic; the UI is kind of clunky, but it has a bunch of features that I can’t seem to find anywhere else without serious compromises. In particular:

  • it lets me browse the music collection based on filesystem layout, rather than relying solely on embedded metadata (my collection is well tagged in general, but the filesystem layout encodes some information not present in the tags, and some of my music is in formats that don’t support tags at all)
  • it defaults to playing music on the client through the browser, rather than being a remote control for speakers attached to the server
  • it has a mobile client that supports downloading music for later offline play

In my experience, its biggest functional deficiency (apart from not knowing how to order podcasts, which is easily solved by using an external podcast manager like gpodder and importing them as a music, rather than podcast, library in Airsonic) is that there are a lot of formats I want to play, like tracker files, that are not supported by Airsonic out of the box. In fairness, they aren’t supported by most other music servers either — the only exception I know of is MPD — but it would be nice to have a solution.

Historically, my solution has been to use a shell script to convert my entire library to mp3s offline, and import that in airsonic. Recently, though, I figured out a better way using Airsonic’s built in transcoding support.

This post is a walkthrough of my Airsonic setup, including transcoding support, on NixOS.

Initial Setup

This has a few more moving parts than usual, because original flavour Airsonic has been discontinued in favour of Airsonic-Advanced, and NixOS hasn’t entirely caught up yet (and I haven’t gotten around to making a PR). Maybe, if you are reading this in THE FUTURE, some of this is unecessary, but for now here’s what you need to do to get AsA working in NixOS.

Airsonic-Advanced overlay

Step one is to actually overlay in the airsonic-advanced package to replace plain airsonic. Building maven-based stuff in nixos is a pain so I’m going to be lazy here and just use the published binary.

nixpkgs.overlays = [
  self: super: {
    airsonic = super.airsonic.overrideAttrs (_: rec {
      version = "11.0.0-SNAPSHOT.20220418221611";
      name = "airsonic-advanced-${version}";
      src = super.fetchurl {
        url = "https://github.com/airsonic-advanced/airsonic-advanced/releases/download/11.0.0-SNAPSHOT.20220418221611/airsonic.war";
        sha256 = "06mxx56c5i1d9ldcfznvib1c95066fc1dy4jpn3hska2grds5hgh";
      };
    });
  }];

Airsonic service

NixOS has configuration knobs for an Airsonic service already, and it’s compatible with AsA as long as you use the right JRE version.

services.airsonic = {
  enable = true;
  jre = pkgs.jdk17;
};

There are a bunch of other options you can set, like what interface and port to listen on. In particular you might want to look at the maxMemory setting; the default of 100MB is unlikely to be enough for any serious music collection.

Nginx

There is a services.airsonic.virtualHost option for automatically setting up nginx, but the configuration it generates is not suitable for AsA. So instead we will do it by hand.

nginx.virtualHosts."airsonic.example.net" = {
  forceSSL = true;
  enableACME = true;
  locations."/" = {
    proxyPass = "http://127.0.0.1:4040/";
    proxyWebsockets = true;
    extraConfig = ''
      proxy_redirect          http:// https://;
      proxy_read_timeout      600s;
      proxy_send_timeout      600s;
      proxy_buffering         off;
      proxy_request_buffering off;
      client_max_body_size    0;
    '';
  };
};

Of note here is that we have to turn on websocket proxying, since the AsA webclient relies on websockets. Disabling proxy_buffering is not strictly required, but (at least in my experience) it makes the client noticeably snappier.

At this point you should have a working Airsonic setup. Fire it up, visit it in the browser, import some music and make sure it works. Then read on.

Transcoding support

Airsonic transcoding support basically consists of three parts:

  • server configuration defining which files should be imported during scan
  • server configuration defining which commands to use to transcode which formats
  • symlinks in /var/lib/airsonic/transcode pointing to implementations of those commands

The default configuration uses ffmpeg to transcode a number of common audio and video formats, and xmp | lame to transcode tracker files. The goal here is to change things to use libopenmpt for tracker files (which is generally better than xmp), and add support for some console music formats like GYM and VGZ via libgme.

Server configuration

You will need to be logged in as admin on the airsonic server to make these changes.

Import formats

Under Settings/General/Music Files, add the extensions for all of the file formats you want to be able to play, including ones that airsonic doesn’t handle natively. Here’s what mine looks like (with added comments):

# Formats airsonic supports natively, or has a good default transcoding config for.
mp3 ogg oga aac m4a flac wav wma aif aiff ape mpc shn mka opus
# Tracker formats supported by libopenmpt. The default config transcodes these with xmp.
alm 669 mdl far xm mod fnk imf it liq wow mtm ptm rtm stm s3m ultdmf dbm med okt
emod sfx m15 mtn amf gdm stx gmc psm j2b umx amd rad hsc flx gtk mgt mtp
# Console formats supported by libgme.
ay gbs gym hes kss nsf nsfe sap spc vgm vgz

Transcoding config

Under Settings/Transcoding, you have the table of transcoding settings. Convert from is the list of extensions that it will use that transcoding configuration for; convert to is the output extension, typically mp3 for audio. Step 1 is the transcoding command (which should output the audio on stdout), and step 2, if set, is an additional command that the ouput of step 1 will be piped through.

When we’re done with this, we’ll have an ffmpeg that understands libopenmpt and libgme formats, so we can use ffmpeg for all of them. My command for libgme formats is ffmpeg -i %s -map 0:0 -b:a %bk -v 0 -f mp3 -, the same as the default transcoding command for other audio formats; the command for tracker files is ffmpeg -subsong all -i %s -map 0:0 -b:a %bk -v 0 -f mp3 -, which (for tracker files that contain multiple songs) will play all of them consecutively, rather than playing just the first one and then stopping. Make sure to enable the enable for new and existing players setting, too.

Note that airsonic comes with an existing configuration for tracker file transcoding, named mod > mp3. You’ll need to either overwrite that, or delete it and create it a new one. You may also need to enable it under Settings/Players/Active Transcoding — it didn’t default to on for me.

Transcoding commands

To make the commands available they need to be symlinked into ~airsonic/transcode. In addition to whatever commands we’re going to be using explicitly, there is an implicit dependency on ffprobe, which is used during library scans to collect file metadata. And unlike the transcoding, there is no way to configure this in airsonic itself, so if we need non-default arguments (and we do) we need to handle that outside airsonic.

There is a configuration knob for transcoders, but the default configuration uses a minimal ffmpeg binary and no ffprobe at all. Let’s change that.

let
  # Wrap ffprobe to add the -subsong option. This will tell it to select all subsongs
  # in tracker formats that support them, rather than only probing the first one. In
  # formats that do not support subsongs the option is safely ignored.
  ffprobe-subsong-wrapper = pkgs.writeShellScriptBin "ffprobe" ''
    exec ${pkgs.ffmpeg-full}/bin/ffprobe -subsong all "$@"
  '';
in {
  services.airsonic.transcoders = [
    "${pkgs.ffmpeg-full}/bin/ffmpeg"
    "${ffprobe-subsong-wrapper}/bin/ffprobe"
  ];
}

Note the use of a tiny shell script here to replace calls to ffprobe foo with ffprobe -subsong all foo. Without that, ffprobe will only probe the first subsong for tracker files while ffmpeg will transcode the whole thing, which will cause airsonic to cut the song off mid-play. (On formats without subsongs the flag will be safely ignored.)

libopenmpt and libgme support in ffmpeg

We aren’t quite done here, because despite the name, ffmpeg-full in nixpkgs is missing some less commonly used features. So the last thing we need to do is enable them:

nixpkgs.overlays = [
  self: super: {
  ffmpeg-full = super.ffmpeg-full.overrideAttrs (old: {
      configureFlags = old.configureFlags ++ [
        "--disable-libmodplug"  # Covers the same formats as openmpt, but not as well
        "--enable-libopenmpt"
        "--enable-libgme"
      ];
      buildInputs = old.buildInputs ++ [
        self.libopenmpt self.libgme
      ];
    });
  }];

And with that, all is ready. nixos-rebuild and it should be good to go. Toss some tracker files into your music library (modarchive.org has a good selection), rescan, and they should show up and be playable.

Known Issues

Some formats supported by libgme also support subsongs, and the libgme demuxer lets you select one via the track_index option. However, there’s no equivalent to libopenmpt’s subsong all setting to dump all of them in sequence. This could be worked around by using a separate transcoder script that uses ffprobe to get the subsong count and then loops over ffmpeg to output all the subsongs in sequence, but since none of the files I want to play with it actually contain subsongs, I haven’t bothered.

There’s a few things I would like to play that this setup doesn’t support, most notably PSX/PS2 game rips (PSF and PSF2 format). This is mostly because I don’t have any command-line tools handy that know how to decode them.