ENOSUCHBLOG

Programming, philosophy, pedaling.


Muzak, Week 1

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:

muzak session (not pictured: notifyd and Last.fm support)

Big Changes

ID3 tag 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>.

Plug-in API

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”.

Playlists

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

Smaller Changes

Index improvement

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

Encapsulation

Underlying data structures (song, album, playlist) are now kept in isolated classes (Muzak::Song, Muzak::Album, Muzak::Playlist).

Better mpv support

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 pretty significant:

Next Week

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:

Thanks for reading!

- William