Nov 29, 2016 Tags: muzak, programming, ruby
Like a lot of people, I enjoy listening to music as I work. It keeps me focused on the task at hand, and allows me to slow down my thinking pace. Without it, I tend to give too much thought to individual components of whatever larger task I’m working on.
Unlike a lot of people (or, at the very least, an increasingly large minority of programmers), I don’t like using a streaming service (Spotify, Pandora, &c.) to listen to music. There are a number of reasons why I prefer to avoid such services (you can find one in my rant about CAN-SPAM compliance), but it essentially comes down to three observations:
Paid streaming services don’t always treat artists (or listeners) well. See Jack Stratton of Vulfpeck’s excellent post on (one of the facets) of this.
Streaming services won’t (and can’t) provide all the music I want to listen to. Neither Spotify nor Pandora has Nasty Facts’ 3-track EP, and I couldn’t reasonably expect them to – it’s an (amazing) obscure 35-year old proto-power-pop EP performed by teenagers. It’s on YouTube, but that’s not an ideal solution either, for the next reason.
Even when music is available on “free” services like YouTube, the lack of customization and flexibility is non-ideal. YouTube’s playlist feature is inflexible and unreliable (anecdote: my roommate is constantly pruning dead songs from his playlists), and everything you share with it will inevitably be added to your greater Google profile for advertising purposes. I’m not an audiophile, but being unable to adjust audio quality except for in terms of the video’s resolution is also a problem point.
For a long time, my solution to all of this was to keep my (very large) music library on a server running Subsonic and, when that became proprietary, Libresonic.
This setup allowed me to access all of my music on all my devices (including my phone), at my preferred quality without having to duplicate and sync over 150GB across N devices. It also allowed me to share access with my friends in a controlled fashion, allowing them to upload their own music and allowing me to set bitrate limits when streaming traffic began to negatively affect my home network. For all of these benefits, Subsonic/Libresonic also had plenty of downsides:
By default, Subsonic/Libresonic’s web player used a terrible little flash
applet (in 2016!) for playback and a mosaic of <iframe>
s to display different
components of the interface. It worked, but didn’t allow for gapless playback
and resulted in an overall less-than-perfect UI. Newer (proprietary)
versions of Subsonic seem to have added an HTML5 player, but the <iframe>
s
(and the generally sorry state of web audio) remain.
At their core, Subsonic and Libresonic are big Java blobs running on Tomcat
(an even bigger Java blob). When things went wrong (which wasn’t regularly,
but did happen), debugging was often a frustrating experience of stitching
together multiple logs. In the end, I usually “fixed” things by simply deleting
the track/album caches or re-downloading subsonic.war
and replacing the
defective one.
Although I never found an RCE vulnerability or anything as serious as that, Subsonic’s size and complexity make it an appealing target for attackers. Over the course of 2 years of casual use, I found bugs that made it possible to download music without authentication and generally sketchy security practices (HTTP password exchange in the API, hard-coded HTTPS cert, etc). Many of these haven’t been fixed (or completely fixed), despite being both privately and publicly reported.
When the server that was hosting my Libresonic instance finally kicked the bucket (it was over a decade old, with repurposed disks), I decided to look into replacing it with something simpler. I came up with the following design goals:
Music indexing and streaming should be as protocol-agnostic as possible.
In practice, this means indexing a filesystem tree (with remote media sources
mounted via NFS/CIFS, FUSE, sshfs
, or a something similar).
The player should be decoupled from the indexing logic. In practice, this means that the player should be an entirely separate program controlled via IPC and only exposed to the user with a limited subset of functionality (actions like play, pause, &c). To access the richer functionality of the player, the user should just interact directly with it.
The (default) interface should be interactive, with a batch-mode available
for scripting. In practice, this means using something like readline
to
build a shell-like interface and using ruby
’s metaprogramming flexibility
to make command definition easy.
The user should be able to create their own interface(s) easily through an API.
With these in mind, I created muzak:
Just as the genre isn’t “real” music, muzak isn’t a “real” music player. I’ve been calling it a “metamusic player,” which is just a fancy way of saying “index manager that also controls your player of choice via IPC and an interactive prompt.”
In action (ffmpeg
mangled the audio recording, actual playback is fine):
As evidenced, muzak is still very much a work-in-progress. The demonstrated
interface (muzak.rb
) is just one potential frontend – it’s really just
some boilerplate and a readline
loop:
1
2
3
4
5
6
7
8
9
10
11
12
13
opts = {
debug: ARGV.include?("--debug") || ARGV.include?("-d"),
verbose: ARGV.include?("--verbose") || ARGV.include?("-v"),
batch: ARGV.include?("--batch") || ARGV.include?("-b")
}
muzak = Muzak::Instance.new(opts)
while line = Readline.readline("muzak> ", true)
cmd_argv = Shellwords.split(line)
next if cmd_argv.empty?
muzak.send Muzak::Cmd.resolve_command(cmd_argv.shift), *cmd_argv
end
As suggested by Muzak::Cmd.resolve_command
and muzak.send
, interactive
commands are just methods defined under Muzak::Instance
(by convention,
included through Muzak::Cmd
):
1
2
3
4
5
6
7
8
def self.resolve_command(cmd)
cmd.tr "-", "_"
end
def help(*args)
commands = Muzak::Cmd.humanize_commands!.join(", ")
info "available commands: #{commands}"
end
This makes extending muzak’s “API” extremely easy, as all new commands are just ruby methods that take a variable number of arguments.
There’s quite a bit more work to be done, but here are some immediate steps I plan to take:
Adding a plug-in system (last.fm support, lyrics?) as well as support for
more media players (Muzak::Player::MPV
is the only one currently implemented).
Experimenting with a graphical UI, possibly one as simple as feeding from the pseudo-shell with some beautification.
Fleshing-out batch-mode, to allow people to write noninteractive muzak “scripts”.
A transcoding layer to make muzak easier on network-mounted drives.
As it currently stands, muzak is just a side project. I’m not quite sure where it’ll go, but I hope that it’ll eventually become flexible enough to replace my dependence on Subsonic/Libresonic and other opaque streaming software.
The source code can be found here. Please try it out, and thanks for reading!
- William