Multi-zone audiobook player.

An audiobook playback appliance with one zone per room (independent MPD instances, a Go HTTP service, AngularJS UI, espeak greetings on boot). Built for a family member.

A small Linux appliance that played audiobooks in different rooms of a facility, each room on its own. Pause one, rewind another, browse a shared library on a tablet. I built it for a family member, because none of the off-the-shelf options did this well or cheaply enough.

Architecture

library/usr/share/mpd/music, shared by all zonesbrowserAngularJSgo service :6101/mpd/:cmd · /tracksMPD :66001MPD :66002MPD :66NNNzone 1zone 2zone NHTTPMPD/TCP, one connection per requestSFTP into the library; Rescan in the UI tells every MPD to update.12 zones provisioned at once by a Python script that templates per-zone mpd.conf and Upstart jobs.First sound from every speaker on boot is an espeak greeting: "Hello world. This is zone three."
Multi-zone MPD service. Library is shared (hot); each zone is its own MPD process and ALSA output, so pause/play/seek are independent.

MPD's multi-output mode mixes at the ALSA layer, so you can't pause one output without the others. So each zone is a separate MPD instance, with its own port, state file, sound card, playlist, clock and volume. Twelve by default, all sharing one music directory.

The Go service

The AngularJS UI talked to a small Go HTTP service, not to MPD. It used fhs/gompd for the MPD protocol and gorilla/mux for routing.

  • /mpd/{command}: status, library, current track, pause, resume, fast-forward, rewind.
  • /tracks/...: changes to the playlist.
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 and rwd jumps clamp at the start and end of the track. An audiobook chapter is sometimes only a minute and a half, and without the clamp the seek ran off the edge.

The Python configurator

Twelve zones meant twelve mpd.conf files, twelve Upstart scripts and twelve state directories. A Python script generated them from a config string with NNN placeholders:

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))

textToWav bakes an espeak greeting into each zone's music directory, so the first sound out of every speaker after provisioning is "Hello world. This is zone three." in a robot voice. If the right number comes out of the right speaker, the wiring is right.

Library

Books are folders on disk with ID3 tags for the metadata, Artist for the author and Album for the book, and adding one meant SFTPing the folder in, hitting Rescan in the UI, and letting every MPD update its database. The UI's filter pane narrows the library to one book, and Add Book queues the whole thing. That one button is most of what anybody ever touched.