Tags: programming, ruby, muzak
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
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
that uses on excellent
taglib-ruby to handle ID3v2 (and all
other common music tagging formats) via
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
Building a flexible plug-in API was one of my early goals, and one is now available.
Muzak::Instance has an
#event method, which takes a
a variable list
*args. This symbol (once validated) and arguments are then
dispatched to each instantiated plugin via a
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,
:player_deactivated). Here's a very simple example, which responds to the
:song_loaded event by invoking
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
Like every other "music player", muzak needed playlists.
To avoid excessive disk thrashing on load, muzak's playlists include full
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
used to "activate" a given playlist:
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
#songs_by are a little more readable (and
should be a little faster).
Underlying data structures (song, album, playlist) are now kept in isolated
I 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
The glue code between
mpv's JSON IPC and ruby interface (
#get_property, &c.) was completely rewritten to use threads and queues
to properly pump command/property responses and events to their respective
#deactivate! have been made much more reliable,
and should no longer be the direct cause of muzak crashes.
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
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:
An optional "deep" index option, which can be enabled to include song
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
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!