Smart Media Selector For The Audio Desktop
1. Overview
I have over 60MB of audio content on my laptop spread across 755 subdirecories in over 9100 files. I also have many Internet stream shortcuts that I listen to on a regular basis.
This blog article outlines the media selector implementation in Emacspeak and shows how a small amount of Lisp code built atop Emacs' built-in affordances of completion provides a light-weight yet efficient interface. Notice that the implementation does not involve fancy things like SQL databases, MP3 tags that one needs to update etc.; the solution relies on the speed of today's laptops, especially given the speed of disk access.
2. User Experience
As I type this up, the set of requirements as expressed in English is far more verbose (and likely more complicated) than its expression in Lisp!
2.1. Pre-requisites for content selection and playback
- Launch either MPV (via package
empv.el
) ormplayer
via Emacspeak'semacspeak-mplayer
with a few keystrokes. - Media selection uses
ido
withfuzzy
matching. - Choices are filtered incrementally for efficient eyes-free interaction; see the relevant blog article on Search, Input, Filter, Target for additional background.
- Content can be filtered using the directory structure, where directories conceptually equate to music albums, audio books or othre logical content groups.Once selected, a directory and its contents are played as a conceptual play-list.
- Searching and filtering can also occur across the list of all 9,100+ media files spread across 700+ directories.
- Starting point of the SIFT process should be influenced by one's current context, e.g., default-directory.
- Each step of this process should have reasonable fallbacks.
3. Mapping Design To Implementation
- Directory where we start AKA context is selected by function emacspeak-media-guess-directory.
- If default directory matches emacspeak-media-directory-regexp,use it.
- If default directory contains media files, then use it.
- If default directory contains directory emacspeak-media — then use it.
- Otherwise use emacspeak-media-shortcuts as the fallback.
- Once we have selected the context, function
emacspeak-media-read-resourceuses
ido
style interaction with fuzzy-matching to pick the file to play. - That function uses Emacs' built-in
directory-files-recursively
to build thecollection
to hand-off tocompleting-read
; It uses an Emacspeak provided function ems–subdirs-recursively to build up the list of 755+ sub-directories that live under $XDGMUSICDIR.
4. Resulting Experience
- I can pick the media to play with a few keystrokes.
- I use Emacs'
repeat-mode
to advantage whereby I can quickly change volume etc once content is playing before going back to work. - There's no media-player UI to get in my way while working, but I can stop playing media with a single keystroke.
- Most importantly, I dont have to tag media, maintain databases or do other busy work to be able to launch the media that I want!
5. The Lisp Code
The hyperlinks to the Emacspeak code-base are the source of truth. I'll include a snapshot of the functions mentioned above for completeness.
5.1. Guess Context
(defun emacspeak-media-guess-directory () "Guess media directory. 1. If default directory matches emacspeak-media-directory-regexp,use it. 2. If default directory contains media files, then use it. 3. If default directory contains directory emacspeak-media --- then use it. 4. Otherwise use emacspeak-media-shortcuts as the fallback." (cl-declare (special emacspeak-media-directory-regexp emacspeak-media emacspeak-m-player-hotkey-p)) (let ((case-fold-search t)) (cond ((or (eq major-mode 'dired-mode) (eq major-mode 'locate-mode)) nil) (emacspeak-m-player-hotkey-p emacspeak-media-shortcuts) ((or ; dir contains media: (string-match emacspeak-media-directory-regexp default-directory) (directory-files default-directory nil emacspeak-media-extensions)) default-directory) ((file-in-directory-p emacspeak-media default-directory) emacspeak-media) (t emacspeak-media-shortcuts))))
5.2. Read Resource
(defun emacspeak-media-read-resource (&optional prefix) "Read resource from minibuffer. If a dynamic playlist exists, just use it." (cl-declare (special emacspeak-media-dynamic-playlist emacspeak-m-player-hotkey-p)) (cond (emacspeak-media-dynamic-playlist nil) ; do nothing if dynamic playlist (emacspeak-m-player-hotkey-p (emacspeak-media-local-resource prefix)) (t ; not hotkey, not dynamic playlist (let* ((completion-ignore-case t) (read-file-name-completion-ignore-case t) (filename (when (memq major-mode '(dired-mode locate-mode)) (dired-get-filename 'local 'no-error))) (dir (emacspeak-media-guess-directory)) (collection (or filename ; short-circuit expensive call (if prefix (ems--subdirs-recursively dir) ;list dirs (directory-files-recursively dir emacspeak-media-extensions))))) (or filename (completing-read "Media: " collection))))))
5.3. Helper: Recursive List Of Sub-directories
;;; Helpers: subdirs (defconst ems--subdirs-filter (eval-when-compile (concat (regexp-opt '("/.." "/." "/.git")) "$")) "Pattern to filter out dirs during traversal.") (defsubst ems--subdirs (d) "Return list of subdirs in directory d" (cl-remove-if-not #'file-directory-p (cddr (directory-files d 'full)))) (defun ems--subdirs-recursively (d) "Recursive list of subdirs" (cl-declare (special ems--subdirs-filter)) (let ((result (list d)) (subdirs (ems--subdirs d))) (cond ((string-match ems--subdirs-filter d) nil) ; pass (t (cl-loop for dir in subdirs if (not (string-match ems--subdirs-filter dir)) do (setq result (nconc result (ems--subdirs-recursively dir)))))) result))