;;; emus.el --- Simple music player for Emacs. -*- lexical-binding:t -*- ;; Author: Tim Vaughan ;; Version: 1.0 ;; Keywords: multimedia ;; URL: https://thelambdalab.xyz/emus ;;; Commentary: ;; This is a simple package for playing audio from a local library ;; of audio files. ;;; Code: ;;; Customizations ;; (defgroup emus nil "Simple music player for Emacs." :group 'multimedia) (defcustom emus-directory "~/Music/" "Directory containing audio files for emus." :type '(string)) (defcustom emus-mpg123-program "mpg123" "Name of (and, optionally, path to) mpg123 binary." :type '(string)) (defface emus-artist '((t :inherit font-lock-keyword-face :background "#333")) "Face used for artist names in browser.") (defface emus-album '((t :inherit font-lock-function-name-face :background "#222")) "Face used for album names in browser.") (defface emus-track '((t :inherit font-lock-string-face)) "Face used for track titles in browser.") (defface emus-track-current '((t :inherit font-lock-string-face :inverse-video t)) "Face used for track titles in browser.") (defface emus-cursor '((t :inherit bold)) "Face used for current track cursor") ;;; Library ;; (defun emus-get-audio-files () "Get all mp3 files in main emus directory." (directory-files-recursively emus-directory ".*\\.mp3")) (defvar emus-records nil "Emus audio library.") (defun emus-make-record (filename tagstr) (let ((artist "") (album "") (title "")) (dolist (line (split-string tagstr "\n")) (let ((found-artist (elt (split-string line "@I ID3v2.artist:") 1)) (found-album (elt (split-string line "@I ID3v2.album:") 1)) (found-title (elt (split-string line "@I ID3v2.title:") 1))) (cond (found-artist (setq artist found-artist)) (found-album (setq album found-album)) (found-title (setq title found-title))))) (vector artist album title filename nil))) (defun emus-record-artist (record) (elt record 0)) (defun emus-record-album (record) (elt record 1)) (defun emus-record-title (record) (elt record 2)) (defun emus-record-file (record) (elt record 3)) (defun emus-record-browser-pos (record) (elt record 4)) (defun emus-set-record-browser-pos (record pos) (aset record 4 pos)) (defun emus-update-records () (interactive) (emus-suspend-cp) (setq emus-state 'stopped) (let ((proc (emus-get-process)) (tagstr "") (filenames (emus-get-audio-files))) (setq emus-records nil) (set-process-filter proc (lambda (proc string) (setq tagstr (concat tagstr string)) (when (string-suffix-p "@P 1\n" string) (add-to-list 'emus-records (emus-make-record (car filenames) tagstr)) (setq tagstr "") (setq filenames (cdr filenames)) (if filenames (emus-send-cmd "lp" (car filenames)) (set-process-filter proc nil) (setq emus-records (reverse emus-records)) (emus-sort-records) (emus-render-records) (emus-resume-cp))))) (emus-send-cmd "lp" (car filenames)))) (defun emus-sort-records () (sort emus-records (lambda (r1 r2) (let ((artist1 (emus-record-artist r1)) (artist2 (emus-record-artist r2))) (if (string= artist1 artist2) (let ((album1 (emus-record-album r1)) (album2 (emus-record-album r2))) (string< album1 album2)) (string< artist1 artist2)))))) ;;; mpg123 process ;; (defvar emus-proc-in-use nil) (defun emus-get-process () "Return current or new mpg123 process." (let* ((emus-process-raw (get-process "emus-process")) (emus-process (if emus-process-raw (if (process-live-p emus-process-raw) emus-process-raw (kill-process emus-process-raw) nil)))) (if emus-process emus-process (let ((proc (make-process :name "emus-process" ;; :buffer (get-buffer-create "*emus-process*") :command `(,emus-mpg123-program "-R")))) (process-send-string proc "silence\n") proc)))) (defun emus-send-cmd (cmd &rest args) (process-send-string (emus-get-process) (concat (seq-reduce (lambda (s1 s2) (concat s1 " " s2)) args cmd) "\n"))) (defun emus-send-and-process (respfun predfun cmd &rest args) (let ((respstr "")) (set-process-filter (emus-get-process) (lambda (proc string) (setq respstr (concat respstr string)) (when (funcall predfun respstr) (set-process-filter proc nil) (funcall respfun respstr)))) (apply #'emus-send-cmd cmd args))) ;;; Playback ;; (defvar emus-current-record nil) (defvar emus-state 'stopped) (defvar emus-continuous-playback t) (defun emus-suspend-cp () (setq emus-continuous-playback nil)) (defun emus-resume-cp () (setq emus-continuous-playback t) (set-process-filter (emus-get-process) (lambda (proc string) (and emus-continuous-playback (eq emus-state 'playing) (string-suffix-p "@P 0\n" string) (emus-play-next))))) (defun emus-play-record (record) "Set RECORD as current and start playing." (let ((old-record emus-current-record)) (emus-send-cmd "l" (emus-record-file record)) (setq emus-state 'playing) (setq emus-current-record record) (emus-update-record old-record) (emus-update-record record) (emus-resume-cp))) (defun emus-select-record (record) "Set RECORD as current, but do not start playing." (let ((old-record emus-current-record)) (setq emus-state 'stopped) (setq emus-current-record record) (emus-update-record old-record) (emus-update-record record) (emus-send-cmd "o") (emus-resume-cp))) (defun emus-stop () "Stop playback of the current record." (interactive) (setq emus-state 'stopped) (emus-update-record emus-current-record) (emus-send-cmd "s")) (defun emus-playpause () (interactive) (when emus-current-record (if (eq emus-state 'stopped) (emus-play-record emus-current-record) (emus-send-cmd "p") (pcase emus-state ((or 'paused 'stopped) (setq emus-state 'playing)) ('playing (setq emus-state 'paused))) (unless (eq emus-state 'paused))) (emus-update-record emus-current-record))) (defun emus-set-volume (pct) (emus-send-cmd "v" (number-to-string pct))) (defvar emus-current-volume 100) (defun emus-volume-delta (delta) (setq emus-current-volume (max 0 (min 100 (+ emus-current-volume delta)))) (emus-set-volume emus-current-volume)) (defun emus-volume-up () (interactive) (emus-volume-delta 10)) (defun emus-volume-down () (interactive) (emus-volume-delta -10)) (defun emus-play-nearby (offset) (let ((idx (seq-position emus-records emus-current-record))) (if idx (let ((next-record (elt emus-records (+ idx offset)))) (if next-record (if (eq emus-state 'playing) (emus-play-record next-record) (emus-select-record next-record)) (error "Track does not exist"))) (error "No track is currently selected.")))) (defun emus-play-next () (interactive) (emus-play-nearby 1)) (defun emus-play-prev () (interactive) (emus-play-nearby -1)) (defun emus-display-status () (interactive) (message (concat "Emus: Volume %d%%" (pcase emus-state ('stopped " [Stopped]") ('paused " [Paused]") ('playing " [Playing]") (_ "")) (if emus-current-record (format " - %.30s (%.20s)" (emus-record-title emus-current-record) (emus-record-artist emus-current-record)) "")) emus-current-volume)) ;;; Browser ;; (defun emus-insert-record (record &optional prev-record first) (let* ((artist (emus-record-artist record)) (album (emus-record-album record)) (title (emus-record-title record)) (help-str (format "mouse-1, RET: Play '%.30s' (%.20s)" title artist))) (when (or prev-record first) (unless (equal (emus-record-artist prev-record) artist) (insert-text-button (propertize artist 'face 'emus-artist) 'action #'emus-click-record 'follow-link t 'help-echo help-str 'emus-record record) (insert (propertize "\n" 'face 'emus-artist))) (unless (equal (emus-record-album prev-record) album) (insert-text-button (propertize (concat " " album) 'face 'emus-album) 'action #'emus-click-record 'follow-link t 'help-echo help-str 'emus-record record) (insert (propertize "\n" 'face 'emus-album)))) (emus-set-record-browser-pos record (point)) (let ((is-current (equal record emus-current-record))) (insert-text-button (concat (if is-current (propertize (pcase emus-state ('playing "->") ('paused "-)") ('stopped "-]")) 'face 'emus-cursor) (propertize " " 'face 'default)) (propertize (format " %s" title) 'face (if is-current 'emus-track-current 'emus-track))) 'action #'emus-click-record 'follow-link t 'help-echo help-str 'emus-record record) (insert (propertize "\n" 'face (if is-current 'emus-track-current 'emus-track)))))) (defun emus-update-record (record) (let ((record-pos (emus-record-browser-pos record))) (when (and (get-buffer "*emus*") (emus-record-browser-pos record)) (with-current-buffer "*emus*" (let ((inhibit-read-only t) (old-point (point))) (goto-char record-pos) (search-forward "\n") (delete-region record-pos (point)) (goto-char record-pos) (emus-insert-record record) (goto-char old-point)))))) (defun emus-render-records () (with-current-buffer "*emus*" (let ((inhibit-read-only t) (old-pos (point))) (erase-buffer) (goto-char (point-min)) (let ((prev-record nil)) (dolist (record emus-records) (emus-insert-record record prev-record (not prev-record)) (setq prev-record record))) (goto-char old-pos)))) (defun emus-click-record (button) (emus-play-record (button-get button 'emus-record)) (emus-display-status)) (defun emus-centre-current () (interactive) (when (get-buffer "*emus*") (switch-to-buffer "*emus*") (when emus-current-record (goto-char (emus-record-browser-pos emus-current-record)) (recenter)))) (defun emus-browse () "Switch to *emus* audio library browser." (interactive) (switch-to-buffer "*emus*") (emus-browser-mode) (emus-volume emus-current-volume) (if emus-records (emus-render-records) (emus-update-records))) (defun emus-playpause-status () (interactive) (emus-playpause) (emus-display-status)) (defun emus-stop-status () (interactive) (emus-stop) (emus-display-status)) (defun emus-volume-up-status () (interactive) (emus-volume-up) (emus-display-status)) (defun emus-volume-down-status () (interactive) (emus-volume-down) (emus-display-status)) (defun emus-update-records-status () (interactive) (emus-update-records) (emus-display-status)) (defun emus-play-next-status () (interactive) (emus-play-next) (emus-display-status)) (defun emus-play-prev-status () (interactive) (emus-play-prev) (emus-display-status)) (defun emus-centre-current-status () (interactive) (emus-centre-current) (emus-display-status)) (defvar emus-browser-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "SPC") 'emus-playpause-status) (define-key map (kbd "o") 'emus-stop-status) (define-key map (kbd "+") 'emus-volume-up-status) (define-key map (kbd "=") 'emus-volume-up-status) (define-key map (kbd "-") 'emus-volume-down-status) (define-key map (kbd "R") 'emus-update-records-status) (define-key map (kbd "n") 'emus-play-next-status) (define-key map (kbd "p") 'emus-play-prev-status) (define-key map (kbd "c") 'emus-centre-current-status) (when (fboundp 'evil-define-key*) (evil-define-key* 'motion map (kbd "SPC") 'emus-playpause-status (kbd "o") 'emus-stop-status (kbd "+") 'emus-volume-up-status (kbd "=") 'emus-volume-up-status (kbd "-") 'emus-volume-down-status (kbd "R") 'emus-update-records-status (kbd "n") 'emus-play-next-status (kbd "p") 'emus-play-prev-status (kbd "c") 'emus-centre-current-status)) map) "Keymap for emus.") (define-derived-mode emus-browser-mode special-mode "emus-browser" "Major mode for EMUS music player.") (when (fboundp 'evil-set-initial-state) (evil-set-initial-state 'emus-browser-mode 'motion)) ;;; Debugging ;;; emus.el ends here