X-Git-Url: https://thelambdalab.xyz/gitweb/index.cgi?a=blobdiff_plain;f=emus.el;h=315ca6e289733a3d55fde413c653ff41830362dc;hb=a7ea35404e6d84b1f035bee2eaaa2569a0af5cc3;hp=c66aed318e37b09b68a9e79f10be7a7285eb9906;hpb=eb93f79afd534728fb10653f030dae8b33da817a;p=emus.git diff --git a/emus.el b/emus.el index c66aed3..315ca6e 100644 --- a/emus.el +++ b/emus.el @@ -1,9 +1,9 @@ -;;; emus.el --- Simple music player for Emacs +;;; emus.el --- Simple music player for Emacs. -*- lexical-binding:t -*- -;; Author: T. G. Vaughan +;; Author: Tim Vaughan ;; Version: 1.0 ;; Keywords: multimedia -;; URL: http://github.com/tgvaughan/emus +;; URL: https://thelambdalab.xyz/emus ;;; Commentary: @@ -12,36 +12,398 @@ ;;; Code: +;;; Customizations +;; + (defgroup emus nil - "Simple music player for Emacs inspired by CMUS." + "Simple music player for Emacs." :group 'multimedia) (defcustom emus-directory "~/Music/" "Directory containing audio files for emus." - :type 'string - :group 'emus) + :type '(string)) -(defcustom emus-mpg123-excecutable "mpg123" +(defcustom emus-mpg123-program "mpg123" "Name of (and, optionally, path to) mpg123 binary." - :type 'string - :group 'emus) + :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")) -(defun emus () - "Switch to *emus* audio library buffer." +(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))) + +(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-mode)) + (emus-browser-mode) + (emus-volume emus-current-volume) + (if emus-records + (emus-render-records) + (emus-update-records))) + +(defvar emus-browser-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "SPC") 'emus-playpause) + (define-key map (kbd "o") 'emus-stop) + (define-key map (kbd "+") 'emus-volume-up) + (define-key map (kbd "=") 'emus-volume-up) + (define-key map (kbd "-") 'emus-volume-down) + (define-key map (kbd "R") 'emus-update-records) + (define-key map (kbd "n") 'emus-play-next) + (define-key map (kbd "p") 'emus-play-prev) + (define-key map (kbd "c") 'emus-centre-current) + (when (fboundp 'evil-define-key*) + (evil-define-key* 'motion map + (kbd "SPC") 'emus-playpause + (kbd "o") 'emus-stop + (kbd "+") 'emus-volume-up + (kbd "=") 'emus-volume-up + (kbd "-") 'emus-volume-down + (kbd "R") 'emus-update-records + (kbd "n") 'emus-play-next + (kbd "p") 'emus-play-prev + (kbd "c") 'emus-centre-current)) + map) + "Keymap for emus.") -(define-derived-mode emus-mode special-mode "Emus" - "Major mode for EMUS music player." +(define-derived-mode emus-browser-mode special-mode "emus-browser" + "Major mode for EMUS music player.") - (setq-local default-directory emus-directory) +(when (fboundp 'evil-set-initial-state) + (evil-set-initial-state 'emus-browser-mode 'motion)) - (let ((player (make-process :name "mpg123" :command '(emus-mpg123-excecutable "-R")))) - (process-send-string player "load The Midnight - Endless Summer - 01 Endless Summer.mp3\n"))) +;;; Debugging ;;; emus.el ends here