"I'm not proud of being a congenital pain in the ass. But I will take money for it."

Building Dad's Jukebox

Sun 06 December 2020 | -- (permalink)

This is a tech story, but the starting point is personal.


I lost my dad a couple of years ago. He was an electronics engineer and a music lover, and Mom had no use for his old desk computer after he was gone, so after extracting his photo albums, I found myself wondering what useful thing I could do with an obsolete computer. At which point I noticed two interesting things:

  1. Although now somewhat elderly, it had been a pretty decent workstation PC for its day, and the motherboard included both fairly a decent audio chip and a TOSLINK (optical S/PDIF) jack;

  2. In spite of its age, this PC is also really quiet, because Dad was persnickity about things like that, and paid the few extra bucks for the fancy case with sound dampening, quiet fans, and so forth.

As it happens, I had an old home theater amplifier of about the same vintage gathering dust, no longer useful for home theater because of a blown center speaker channel, but otherwise still a decent amplifier, and lo, it also had a TOSLINK jack.

As it also happens, I already had the wreckage of what had even longer ago been a reasonable stereo in my home office. Hadn't used it much lately because the CD player had died (again), and while I'd long since kitted it out with a nice after-market Bluetooth receiver, the audio quality of music played on my laptop just wasn't that good.

The TOSLINK jacks caught my interest because they suggested that, with a bit of work, I could build a purely digital path all the way into a real amplifier, with the optical cable provided some isolation between the amplifier and all the noisy circuitry in the computer.

At this point it became obvious that I needed to turn Dad's PC into an audio server that would have amused him. The only other rule for this project was that it also had to amuse me.

OS and other low-level stuff

I long since stopped running single-disk server machines, so the first task was to build a basic RAID1 Linux system. This required a certain amount of fiddling in the Debian Installer to build the desired disk configuration, but wasn't all that hard. Basics:

  • Debian 10 (Buster)

  • Two (identical) hard drives (/dev/sda and /dev/sdb);

  • Each drive partitioned with a couple of GB for swap (/dev/sda1 and /dev/sdb1), everything else in a RAID partition (/dev/sda2 and /dev/sdb2);

  • Software RAID1 (/dev/md0) configured on top of the RAID partitions;

  • RAID1 device partitioned into an ext4 partition (/dev/md0p1, mounted on /boot) and an LVM partition (/dev/md0p2);

  • LVM with one volume group and one logical volume formatted as an ext4 filesystem, mounted as root.

Other than the minor tedium of configuring all of this in the installer, the one hitch came when I discovered that this configuration on this motherboard didn't work with GPT partitioning. I haven't used "DOS" partitioning in a decade, so it feels kind of nasty, but what the heck, it works in this case, do it and move on.

Initial package selection during installation was a fairly basic server configuration, no GUI, just ssh.

Getting TOSLINK (S/PDIF) working

Getting the TOSLINK connection working turned out to be easy, Just install ALSA and fiddle with one configuration file. Out of the box, ALSA wants to use the analog audio connectors, one has to write a short /etc/asound.conf file to tell it otherwise:

pcm.!default {
   type plug
   slave {
       pcm "spdif"
       rate 48000
       format S16_LE

More information on configuring S/PDIF

This step required purchasing a TOSLINK cable. Hat tip here to Best Buy for their well-thought-out mid-pandemic curbside pickup service.

An unexpected digression

At this point I had the most basic portions of a working system, except that, as it turns out, I didn't: the speakers in my office stereo had died since the last time I'd had a working CD player. This took a while to debug, but in retrospect the problem was obvious: these are hulking old three-way speakers I bought used from a friend of a friend in 1985 (the guy was moving cross country, folding money packs better than a pair of loudspeakers). They held up pretty well for decades, but the foam surrounds on the woofers had finally crumbled into chips of dessicated neoprene which tumbled out onto the floor as soon as I thought to remove the cloth grills and look for physical damage to the speaker cones.

As it happens, Dad had liked these speakers. They're nothing fancy, no-name brand and bought used at that, but they were a decent value and it seemed a shame to toss them. Well, it turns out that there's an entire small industry dealing with after-market repairs for this problem. After some hunting around, reading ads and reviews both positive and negative, and thinking about the logistics of hauling a set of cabinet speakers from one repair shop to another during a pandemic, I ended up ordering a "refoaming kit" from Simply Speakers.

There are several mail order companies in this market, I picked Simply Speakers for three reasons:

  1. They had really good instructional videos on the repair process;

  2. While nobody had replacement surrounds sized specifically for my no-name speakers, theirs looked to be a slightly better fit than the other company that ended up on my short list;

  3. Their instructional videos mentioned that their kits include a specially formulated adhesive for gluing the surround onto the cone and the speaker housing: ordinarily I would dismiss this as hype, but they went into some detail about how the properties of their adhesive simplified the process of centering the cone properly.

To cut to the chase: the kit arrived promptly, the repair went well, everything performed as advertised, and the speakers sound fine. Good company, good product.

The Quest for Content

OK, back to the main plot. A jukebox isn't very useful without some music to play. I'm old fashioned enough that most of my music collection is CDs (and LPs, but ignore that for now). Given the history of CD players failing on me, I wanted a format that would free me from any day to day need for the CDs themselves. At which point this part pretty much writes itself: I wanted to rip my CD collection into FLAC format, one file per CD, with .cue file track lists.

Fortunately, this is something that abcde can do out of the box. Furthermore, abcde is designed to be able to use the result of this operation as an input source for other tasks such as generating MP3 (OGG, OPUS, whatever) files for use in my car or on my phone. Ripping a large CD collection is still a bit tedious, but one can automate much of it. So I ended up writing a little Python script to sit on the CDROM drive and start abcde for me automatically. See /usr/include/linux/cdrom.h and https://superuser.com/questions/630588/how-to-detect-whether-there-is-a-cd-rom-in-the-drive for more details.

#!/usr/bin/env python3

import os, time, fcntl, subprocess

CDROM_DRIVE_STATUS      = 0x5326
CDROM_DISC_STATUS       = 0x5327
CDS_DISC_OK             = 4
CDS_AUDIO               = 100

fd = os.open("/dev/cdrom", os.O_RDONLY | os.O_NONBLOCK)

while True:
    if fcntl.ioctl(fd, CDROM_DRIVE_STATUS) != CDS_DISC_OK or fcntl.ioctl(fd, CDROM_DISC_STATUS) != CDS_AUDIO:
    dn = str(time.time())
    abcde = subprocess.run(["abcde", "-1", "-o", "flac", "-x", "-G", "-a", "default,cue"], cwd = dn)
    print("abcde exited with status {}\n".format(abcde.returncode))

The stuff with dn might require a bit of explanation. By default, abcde will drop its output into the current directory, which is fine, except when dealing with multi-disc albums, in which case the Musicbrainz entry will probably assign the same title to all the discs, so if you rip both discs of a two disc set, the second one will overwrite the first one. Feh. Hence this timestamp hack, which puts each ripped disc into a separate subdirectory. This requires a bit of manual cleanup afterwards, but doesn't lose anything. Feel free to remove this silliness if you don't need it.

Picking a player

Given the above choices of ALSA and FLAC, the decision on what player software to install ended up making itself too, although it took a little while for this to become obvious. After some experimentation, it became clear that I wanted XMMS2, for several reasons:

  • It has a client/server architecture, giving clean separation between the player itself (xmms2d) and zero or more user interface programs active at any given time;

  • One of the user interfaces is a command line tool;

  • Another of the user interfaces is a Python library;

  • The plug-in architecture supports ALSA, FLAC, and CUE out of the box.

It turns out that XMMS2 is perfectly happy to treat the .cue files generated by abcde as playlists, so I get usable playlists as a side effect of the ripping process. One can of course also construct arbitrary playlists.

What's missing?

At this point I had a somewhat clunky bare bones music system, so I started using it while ripping CDs and as background music while working. It quickly became apparent that, while there are a bunch of improvements I could make to the basic system, one particular missing feature was at the top of the list: there was no simple way of starting or pausing playback without messing about with a computer. Having to type or click a trackball to select a new playlist isn't a big deal, but having to wake up a computer just to pause the music when the phone rings? Ick.

In other words, this jukebox needed a remote control. Well, I already had one, in fact I have more remote controls than I have devices because I don't always remember to bin old remotes when the equipment they used to control dies. So I just needed an IR interface.

Meet the Iguana

After a bit of poking around on the LIRC web site, I picked the IguanaWorks "Socket Receive" USB IR transceiver.

This particular model is built for use with an external IR receiver, as well as an external transmitter ("blaster"), so that I could hide the USB dongle itself in the back of the computer and have only the receiver and transmitter probes sticking out. Nice hardware, I'm pretty happy with it.

The one thing I really wish I had known going in is to ignore LIRC and go straight for the in-kernel support (the so-called "rc-core" project). Unfortunately, the documentation for this stuff is abysmal (and mostly out of date), so I ended up having to follow the LIRC trail for a while before the documentation on how to avoid LIRC made any sense. In the hope of saving others from having to repeat this:

  • The two userland programs you really need are ir-keytable and ir-ctl, on Debian these are in the ir-keytable and v4l-utils packages, respectively;

  • You can use ir-keytable to figure out what codes your remote sends, and to tell Linux what keycodes you want to receive for any particular button on your remote;

  • You can roll your own program to process the keycodes received from your remote if you like, but you can also use a tool like inputexec to run a different /bin/sh command for every keycode;

  • Once you know what codes your remote sends, you can use ir-ctl to send those same codes if you want your computer to be able to control things like the power, volume, and input source on your amplifier (or a stack of other gear, for that matter).


The IguanaWorks driver is included in the stock Linux kernel that ships with Debian Buster, and basically just works out of the box once one knows which software to run with it.

So after some time spent off in the weeds due to LIRC, I ended up with something pretty simple using ir-keytable and inputexec to run xmms2 commands when I press the appropriate buttons on the remote, and as a bonus prize the computer can now control the amplifier.

Web interface

Of course, no appliance these days can be complete unless it has a web interface. Well, no, not really, but in this case a web interface would be useful. So I've started whacking together something simple using CherrPy, Jinja2, and the XMMS2 Python API. It'll be awesome when it's finished, and even if it's not, at least I won't be stuck with somebody else's dumb user interface mistakes, I can make my own!

Bluetooth, wait, what?

Having gone to all this trouble to get an S/PDIF interface working, why would we want to add Bluetooth to the mix? Well, think about how the power button on a remote control works: it's a toggle. So one can issue a command to change the power state of the amplifier, but one can't issue a command to "turn the amplifier on" or "turn the amplifier off" without knowing what power state it was in to begin with. Bluetooth is a fairly silly way to detect whether the amplifier is powered, but it's also really cheap, and might be useful for other purposes eventually.

Well, the good news is that it is cheap: a (supposedly) Linux-compatible USB Bluetooth transceiver costs under 10 USD these days. The bad news is that the market has been flooded with cheap knock-offs of the Cambridge Silicon Radio USB Bluetooth dongle, each of them slightly broken in different ways, and the Linux drivers haven't been able to keep up. So the first cheap dongle I bought works fine up until the point where I ask it to scan for neighboring Bluetooth devices, at which point...crickets. Sigh.

There's a really long thread on this problem, which apparently has been going on for more than five years now. In theory, if I'm reading the bread crumbs correctly, the problem may just magically go away once I upgrade to Debian 11 (Bullseye) (not yet released), unless of course it doesn't.

So after putting this on the shelf for a while, I eventually tried again with a different cheap USB tranceiver, and that one worked. Brand name on the working one is "Plugable", and the packaging actually claims to support Linux (the first one also claimed this, but only in the online advertisement, not on the packaging, hmmm...). Supposedly the Plugable adapter uses the Broadcom BCM20702 chipset.

Once I got a working adapter, i still had to figure out how to use it for this purpose. Unsurprising, there's not a lot of documentation on this particular application. Setup went something like this:

sudo apt-get install bluetooth bluez bluez-tools python-bluez python3-bluez
sudo bluetoothctl power on
sudo bluetoothctl scan on
sudo bluetoothctl pair aa:bb:cc:dd:ee:ff

Where aa:bb:cc:dd:ee:ff is the MAC address shown by the scan. In order to get it to show up in the scan, I had to press the "pair now" button on the amplifier's Bluetooth adapter, but I would have needed to do that to pair anyway.

Once having paired with the amplifier, I could use the Python API to poke at the amplifier. I haven't dug very far into this, but for my simple purpose I only needed something equivalent to "ping", this seems to work:

def is_amplifier_turned_on(macaddr = "aa:bb:cc:dd:ee:ff"):
    return bluetooth.bluez.lookup_name(macaddr) is not None

What about that shelf of LPs?

Yes, I still have a working turntable and a shelf of LPs. Decades ago I made it a policy simply to buy the CD re-release of anything I still want that's available on CD, but some of the older and more obscure stuff has never been re-released and probably never will be, so when the turntable or LP dies, it's gone. If only I had a digital audio system. Wait....

There are various solutions in this problem space, but the one I've used in the past is gramofile. One of the CDs I already ripped as part of this project was in fact the output of a gramofile conversion from LP (an obscure concert album given to me by a friend who had played lead guitar on that album). Sound quality will never be as good as something done with proper equipment from clean masters, but that's not really the point in edge cases like this. Feeding gramofile output into FLAC seems a bit over the top, but noise is noise, and the less of it the better, so if I'm going to bother digitalizing LPs at all I'll probably want to use the same FLAC + CUE format I'm already using for CDs. gramofile doesn't emit .cue files per se, but it does write a .tracks file that's (probably) isomorphic to a .cue file, so it should be relatively straightforward to write a converter (famous last words).

Haven't even started on this one yet. Probably ought to get to it while I can still get parts for the turntable.

Perfection not yet achieved, or, back to channeling Dad

There's still one thing very wrong here: there are too many moving parts, literally. In particular, this rig still has old style disk drives (known in the trade as "spinning rust"). This is of course anathema to the quest for the machine Dad really would have wanted. Fortunately, it's now possible to do something about that too.

Solid state disks are still somewhat expensive, but there are some nifty special-purpose devices available these days. In particular, I'm looking at the SilverStone SDP11 M.2 SATA adaptor, which occupies one old style drive slot and takes up to four M.2 SSDs, which are relatively cheap as well as occupying a lot less space in the landfill when they finally give up the ghost. Price for this adaptor with a pair of M.2s is a bit more than the equivalent storage volume of spinning rust, but not by all that much. I didn't worry about this going in because I didn't want to fork out for fancy disks during the proof-of-concept phase, but long term this kind of SSD rig might be the way to go.

One could expand this rig into a general purpose household file server if one wanted to amortize the cost of the disks over a wider set of tasks. Disks are a bit like tires, they wear out eventually no matter what one does, one buys a new set as needed. With everybody moving to laptops and phones, there's not that much spinning rust left in the house, this might be a path to phasing out the last of it.

That's all (for now), folks

Thanks for reading this far. Hope some of it was useful, or at least amusing. At some point I might get around to putting more detailed instructions (lists of packages to install, more configuration snippets, etc) here or in an accompanying git repository, but right now it's a Sunday afternoon, the driveway is clear of snow, and I need to make an important decision: whether the next album up should be Tom Rush or the Grateful Dead.