A small Linux appliance that played audiobooks in different rooms of a facility, independently — pause one, rewind another, browse a shared library on a tablet, and have each zone keep its own clock. Built for a family member with a specific need that none of the off-the-shelf options solved well, or cheaply, enough.
Architecture
The unusual choice is that each zone is its own MPD instance, not a single MPD with multiple outputs. MPD's multi-output mode mixes at the ALSA layer; you can't pause one zone without affecting others. By giving each zone its own MPD process — different port, different state file, different sound card — you get true independence. Each MPD has its own playlist, its own clock, its own volume. Pause one, the others keep going. Twelve of them, by default, all sharing one music directory.
The Go service
The web UI didn't talk to MPD directly. A small Go HTTP service did, with a
JSON API: /mpd/{command} for read-mostly operations (status, library,
current track, pause, resume, fast-forward, rewind), and /tracks/... for
mutating the playlist. It used fhs/gompd for
the MPD wire protocol and gorilla/mux for routing.
case param == "longstatus":
var longstatus string
status, _ := conn.Status()
track, _ := conn.CurrentSong()
switch {
case status["state"] == "play":
longstatus = "Playing - " + track["Album"] + " by " + track["Artist"]
case status["state"] == "pause":
longstatus = "Paused - " + track["Album"] + " by " + track["Artist"]
default:
longstatus = "Stopped"
}
return longstatus, nil
The 30-second fwd/rwd jumps clamp at the start and end of the track, which
sounds trivial — until an audiobook chapter is sometimes only a minute and a
half and the seek bar trips over the edge.
The Python configurator
Twelve zones meant twelve mpd.conf files, twelve Upstart init scripts, twelve
state directories. That's not something I wanted to bootstrap again. A Python script
generated everything from a NNN-templated config string:
for x in range(1, 13):
xstr = str(x).zfill(2)
# Generate a per-zone "Hello world. This is zone N" greeting, one-time
textToWav('Hello world. This is zone ' + str(x),
'/usr/share/mpd/music/zone' + xstr)
directory = "/usr/share/mpd/zone" + xstr
# ... per-zone mpd.conf and Upstart script written from templates
mpdconf.write(confstring.replace("NNN", xstr))
That textToWav call was fun. Each zone gets a one-time
espeak-synthesized greeting baked into its music directory: "Hello world.
This is zone three." The first sound out of every speaker after a fresh
provisioning is its own zone number, in a stilted robot voice. It's the
cheapest possible installation sanity check - if the right number comes out of
the right speaker, your wiring is correct.
Library management
Books lived as folders on disk; ID3 tags carry the metadata (Artist = author, Album = book). Adding content was just SFTP — drop a book's folder in, hit Rescan in the UI, every MPD updated its database. The filter pane in the AngularJS UI let you narrow the library to a single book and Add Book the entire thing to the playlist with one click. That last bit is mostly what the appliance is, in practice.