Dec 5, 2016 Tags: muzak, programming, ruby
This post is at least a year old.
Preword: Following up on the introductory post for muzak, I’m going to make an effort to post weekly summaries of my work (until muzak is stable or I become too busy, whichever comes first).
I’ve been working on muzak for about a week now, which has translated to the following stats (as I am writing this):
As of today, this is what a typical muzak session looks like:
(not pictured: notifyd and Last.fm support)
The earliest working versions of muzak used only the pathnames of files
in the music hierarchy. This worked (and continues to work) well enough when
enqueuing artists/albums, but doesn’t give the kind of reliable rich output that
commands like now-playing
should:
1
2
3
muzak> enqueue-album Coloring Book
muzak> now-playing
[info] All We Got (feat. Kanye West Chicago Children's Choir) by Chance The Rapper on Coloring Book
To handle the need for rich metadata, I added a simple Muzak::Song
class
that uses on excellent
taglib-ruby to handle ID3v2 (and all
other common music tagging formats) via taglib
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Song
def initialize(path)
@path = path
TagLib::FileRef.open(path) do |ref|
break if ref.null?
@title = ref.tag.title || File.basename(path, File.extname(path))
@artist = ref.tag.artist
@album = ref.tag.album
@year = ref.tag.year
@track = ref.tag.track
@genre = ref.tag.genre
@comment = ref.tag.comment
@length = ref.audio_properties.length
end
end
end
This is complemented by a Muzak::Album
class, which wraps album directories
and encapsulates their songs as an Array<Muzak::Song>
.
Building a flexible plug-in API was one of my early goals, and one is now available.
Every Muzak::Instance
has an #event
method, which takes a Symbol
and
a variable list *args
. This symbol (once validated) and arguments are then
dispatched to each instantiated plugin via a Thread
:
1
2
3
4
5
6
7
8
9
def event(type, *args)
return unless PLUGIN_EVENTS.include?(type)
@plugins.each do |plugin|
Thread.new do
plugin.send(type, *args)
end.join
end
end
Plugins are defined under Muzak::Plugin
, inherit from
Muzak::Plugin::StubPlugin
, and simply receive the methods corresponding
to event symbols (currently, :song_loaded
, :player_activated
, and
:player_deactivated
). Here’s a very simple example, which responds to the
:song_loaded
event by invoking notify-send
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module Muzak
module Plugin
class Notify < StubPlugin
def song_loaded(song)
notify song.full_title
end
private
def notify(msg)
pid = Process.spawn("notify-send", "muzak", msg)
Process.detach(pid)
end
end
end
end
(StubPlugin
handles non-overridden event methods.)
Other examples include a scrobbler and cava support.
Actually getting the :song_loaded
event is described below, under “Better mpv
support”.
Like every other “music player”, muzak needed playlists.
To avoid excessive disk thrashing on load, muzak’s playlists include full
serializations of Muzak::Song
objects (unlike Muzak::Index
, which only
contains pathnames). These are stored, under
~/.config/muzak/playlists/<name>.yml
by default.
Controlling playlists is fairly straightforward, with load-playlist
being
used to “activate” a given playlist:
1
2
3
4
5
6
muzak> load-playlist favorites # loads ~/.config/muzak/playlists/favorites.yml
muzak> enqueue-album Coloring Book # start playing an album
muzak> playlist-add-current # add the currently playing song to 'favorites'
muzak> playlist-del-current # delete the currently playing song from 'favorites'
muzak> playlist-add-album Acid Rap # add an entire album to 'favorites'
muzak> playlist-shuffle # shuffle the entire playlist (permanent!)
Muzak::Index
is still pretty ugly, but has been cleaned up slightly. In
particular, #albums_by
and #songs_by
are a little more readable (and
should be a little faster).
Underlying data structures (song, album, playlist) are now kept in isolated
classes (Muzak::Song
, Muzak::Album
, Muzak::Playlist
).
mpv
supportI put this under smaller changes since mpv
support was already part of the
first working versions, but the changes to Muzak::Player::MPV
are actually
pretty significant:
The glue code between mpv
’s JSON IPC and ruby interface (#command
,
#get_property
, &c.) was completely rewritten to use threads and queues
to properly pump command/property responses and events to their respective
destinations.
#activate!
and #deactivate!
have been made much more reliable,
and should no longer be the direct cause of muzak crashes.
The running mpv
instance has been made completely dumb (no OSC, keyboard,
or mouse support), only taking commands via its JSON IPC socket. This is to
avoid state changes in the player that don’t get necessarily sent back as
JSON events.
I’m pretty satisfied with my progress over the past week, and I hope to keep the pace up over the next 7 days (final exam schedule permitting). In particular, here are some additions I plan to get around to:
A “graphical” interface, possibly something as simple as
dmenu
or
lighthouse
controlling
a daemonized Muzak::Instance
.
An optional “deep” index option, which can be enabled to include song
metadata in index.yml
. This will be non-default and complete optional, but
might be useful to users who want to minimize discrete reads to the music tree
(for example, a network share).
Formal specification of muzak’s expected player interface by
Muzak::Player::StubPlayer
or something similar. Currently, this
interface is informally specified by the methods that Muzak::Player::MPV
exposes to Muzak::Instance
.
In the spirit of the previous bullet, support for another media player.
I currently only use mpv
, but adding first-class support for another
common playback program would validate muzak’s design and give future
users/developers extra reference when hacking on the core.
Thanks for reading!
- William