Programming, philosophy, pedaling.

Muzak, Week 3

Dec 19, 2016

Tags: programming, ruby, muzak

Preword: This is the third weekly post on muzak's development. You can find the rest under the muzak tag.

Some quick statistics:

This week was a big refactoring week, which means that the user facing components haven't changed much week-over-week. I haven't even bothered to make a screenshot/recording of this week's work, since it's almost entirely internal.

Big(ger) changes

Flexible playlist support

Previously, a playlist could be loaded (and optionally created) with playlist-load <name> and modified with the playlist-add-* and playlist-del-* commands. This was simple to implement since only one playlist could be loaded at a time, but made it unnecessarily difficult to modify and enqueue multiple playlists during a single session.

To fix this, I removed playlist-load entirely and made all playlists load on startup by default. This requires that playlist-* commands now take a playlist name as a first argument.

Additionally, playlists are now automatically created once accessed for the first time:

# ~/.config/playlists/dad-rock.yml
muzak> playlist-add-album dad-rock Meet the Beatles
muzak> enqueue-playlist dad-rock

This should result in a much smoother playlist experience.

Static configuration

Muzak's configuration is now loaded into a big static Muzak::Const class at load time, instead of being created with each instance. This has two major benefits:

Just like instance commands, configuration keys are translated into (class) methods. For example, deep-index: true in muzak.yml becomes Muzak::Config.deep_index. There's also a Config#plugin?(pname) convenience method for testing whether a given plugin has been enabled.

User commands

Last week, I added ~/.config/muzak/plugins as a loading directory for user plugins. This week, I added ~/.config/muzak/commands as a directory for custom user commands.

Commands are different from plugins in a few different ways:

As an example of custom user commands, here are some that I've been using:


module Muzak
  module Cmd
    def favorite
      playlist_add_current "favorites"

    def unfavorite
      playlist_del_current "favorites"


module Muzak
  module Cmd
    def more_by_artist
      np = player.now_playing

      return unless np

      # obvious problem: we're comparing ID3 artist to filesystem artist
      songs = index.songs_by(np.artist)

      songs.each do |song|
        player.enqueue_song song

    def more_from_album
      np = player.now_playing

      return unless np

      # obvious problem: we're comparing ID3 album to filesystem album
      album = index.albums[np.album]

      return unless album

      album.songs.each do |song|
        player.enqueue_song song

Instance refactoring

Previously, Muzak::Instance initialization was a pretty messy affair. There were lots of calls into methods (doubling as commands) defined in Muzak::Cmd, plus lots of small details that should have been handled by the Muzak::Index, Muzak::Playlist, etc. classes individually.

Instance initialization now looks like this:

def initialize(opts = {})
  $debug = opts[:debug]
  $verbose = opts[:verbose]

  verbose "muzak is starting..."

  @index = Index.new(Config.music, deep: Config.deep_index)

  @player = Player::PLAYER_MAP[Config.player].new(self)

  @plugins = Plugin.load_plugins!

  @playlists = Playlist.load_playlists!

  enqueue_playlist Config.autoplay if Config.autoplay

Those global variables and player initialization are still a bit of an eyesore, but they'll go away eventually (probably via Config).

Small(er) changes

Faster index loading

Muzak now uses Marshal instead of YAML to serialize the Muzak::Index hash, improving both on-disk size and load speed significantly.

My (deep) index (>10,000 songs by >250 artists) went from nearly 7MB to 3.2MB, and now loads in around 150ms (compared to approximately 1 second with the YAML index). Depending on the performance hit, I may also enable a transparent compression option.

Bug squashing

Lots of small bugs were fixed, including a failure to index music files that ended with non-lowercase suffixes (e.g., song.MP3) and an incorrectly placed Thread#join on event threading.

Thanks for reading!

- William