X-Git-Url: https://thelambdalab.xyz/gitweb/index.cgi?a=blobdiff_plain;f=emus.el;h=75b92e0885d016531161cb0abf98081b8f0ad698;hb=2038af38874f89d1c3598adf559bc631645b8552;hp=315ca6e289733a3d55fde413c653ff41830362dc;hpb=a7ea35404e6d84b1f035bee2eaaa2569a0af5cc3;p=emus.git diff --git a/emus.el b/emus.el index 315ca6e..75b92e0 100644 --- a/emus.el +++ b/emus.el @@ -1,17 +1,46 @@ -;;; emus.el --- Simple music player for Emacs. -*- lexical-binding:t -*- +;;; emus.el --- Simple mp3 player -*- lexical-binding:t -*- + +;; Copyright (C) 2019 Tim Vaughan ;; Author: Tim Vaughan +;; Created: 8 December 2019 ;; Version: 1.0 ;; Keywords: multimedia -;; URL: https://thelambdalab.xyz/emus +;; Homepage: http://thelambdalab.xy/emus +;; Package-Requires: ((emacs "26")) + +;; This file is not part of GNU Emacs. + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this file. If not, see . ;;; Commentary: -;; This is a simple package for playing audio from a local library -;; of audio files. +;; This is a simple package for playing audio from a local directory +;; tree of mp3 files. It uses the program mpg123 as its back-end. +;; Currently the library is loaded completely every time emus starts. ;;; Code: +(provide 'emus) + + +;;; Dependencies +;; + +(require 'seq) + + ;;; Customizations ;; @@ -28,25 +57,59 @@ :type '(string)) (defface emus-artist - '((t :inherit font-lock-keyword-face :background "#333")) + '((t :inherit font-lock-string-face :background "#333")) "Face used for artist names in browser.") (defface emus-album - '((t :inherit font-lock-function-name-face :background "#222")) + '((t :inherit font-lock-constant-face :background "#222")) "Face used for album names in browser.") (defface emus-track - '((t :inherit font-lock-string-face)) + '((t :inherit font-lock-keyword-face)) "Face used for track titles in browser.") (defface emus-track-current - '((t :inherit font-lock-string-face :inverse-video t)) + '((t :inherit font-lock-keyword-face :inverse-video t)) "Face used for track titles in browser.") (defface emus-cursor '((t :inherit bold)) "Face used for current track cursor") +;;; 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")))) + (set-process-query-on-exit-flag proc nil) + (process-send-string proc "silence\n") + proc)))) + +(defun emus--send-cmd-raw (cmd &rest args) + (process-send-string (emus-get-process) + (concat + (seq-reduce (lambda (s1 s2) (concat s1 " " s2)) args cmd) + "\n"))) + +(defun emus-send-cmd (cmd &rest args) + (unless emus--proc-in-use + (apply #'emus--send-cmd-raw cmd args))) + + ;;; Library ;; @@ -54,10 +117,60 @@ "Get all mp3 files in main emus directory." (directory-files-recursively emus-directory ".*\\.mp3")) -(defvar emus-records nil +(defvar emus-tracks nil "Emus audio library.") -(defun emus-make-record (filename tagstr) +(defun emus-make-track (artist album title filename &optional pos) + (vector artist album title filename pos)) + +(defun emus-track-artist (track) + (elt track 0)) + +(defun emus-track-album (track) + (elt track 1)) + +(defun emus-track-title (track) + (elt track 2)) + +(defun emus-track-file (track) + (elt track 3)) + +(defun emus-track-browser-pos (track) + (elt track 4)) + +(defun emus-set-track-browser-pos (track pos) + (aset track 4 pos)) + +(defun emus--load-library (then) + (unless emus--proc-in-use + (setq emus--proc-in-use t) + (emus--suspend-cp) + (setq emus-state 'stopped) + (let ((proc (emus-get-process)) + (tagstr "") + (filenames (emus-get-audio-files))) + (setq emus-tracks 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-tracks + (emus--make-track-from-tagstr (car filenames) + tagstr)) + (setq tagstr "") + (setq filenames (cdr filenames)) + (if filenames + (emus--send-cmd-raw "lp" (car filenames)) + (set-process-filter proc nil) + (setq emus-tracks (reverse emus-tracks)) + (emus--sort-tracks) + (unless emus-current-track + (setq emus-current-track (car emus-tracks))) + (funcall then) + (emus--resume-cp) + (setq emus--proc-in-use nil))))) + (emus--send-cmd-raw "lp" (car filenames))))) + +(defun emus--make-track-from-tagstr (filename tagstr) (let ((artist "") (album "") (title "")) @@ -69,113 +182,37 @@ (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)) + (emus-make-track artist album title filename nil))) -(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 +(defun emus--sort-tracks () + (sort emus-tracks (lambda (r1 r2) - (let ((artist1 (emus-record-artist r1)) - (artist2 (emus-record-artist r2))) + (let ((artist1 (emus-track-artist r1)) + (artist2 (emus-track-artist r2))) (if (string= artist1 artist2) - (let ((album1 (emus-record-album r1)) - (album2 (emus-record-album r2))) + (let ((album1 (emus-track-album r1)) + (album2 (emus-track-album r2))) (string< album1 album2)) - (string< artist1 artist2)))))) + (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))) +(defmacro emus--with-library (&rest args) + `(if emus-tracks + (progn ,@args) + (emus--load-library + (lambda () ,@args)))) ;;; Playback ;; -(defvar emus-current-record nil) +(defvar emus-current-track nil) (defvar emus-state 'stopped) (defvar emus-continuous-playback t) -(defun emus-suspend-cp () +(defun emus--suspend-cp () (setq emus-continuous-playback nil)) -(defun emus-resume-cp () +(defun emus--resume-cp () (setq emus-continuous-playback t) (set-process-filter (emus-get-process) (lambda (proc string) @@ -184,125 +221,177 @@ (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-play-track (track) + "Set TRACK as current and start playing." + (emus--with-library + (let ((old-track emus-current-track)) + (emus-send-cmd "l" (emus-track-file track)) + (setq emus-state 'playing) + (setq emus-current-track track) + (emus--update-track old-track) + (emus--update-track track) + (emus--resume-cp)))) + +(defun emus-select-track (track) + "Set TRACK as current, but do not start playing." + (emus--with-library + (let ((old-track emus-current-track)) + (setq emus-state 'stopped) + (setq emus-current-track track) + (emus--update-track old-track) + (emus--update-track track) + (emus-send-cmd "o") + (emus--resume-cp)))) (defun emus-stop () - "Stop playback of the current record." + "Stop playback of the current track." (interactive) - (setq emus-state 'stopped) - (emus-update-record emus-current-record) - (emus-send-cmd "s")) + (emus--with-library + (setq emus-state 'stopped) + (emus--update-track emus-current-track) + (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))) + (emus--with-library + (when emus-current-track + (if (eq emus-state 'stopped) + (emus-play-track emus-current-track) + (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-track emus-current-track)))) (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-set-volume (pct) + (emus--with-library + (setq emus-current-volume pct) + (emus-send-cmd "v" (number-to-string pct)))) + +(defun emus-volume-increase-by (delta) + (emus-set-volume (max 0 (min 100 (+ emus-current-volume delta))))) (defun emus-volume-up () (interactive) - (emus-volume-delta 10)) + (emus-volume-increase-by 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.")))) + (emus-volume-increase-by -10)) + +(defun emus--play-adjacent-track (&optional prev) + (emus--with-library + (let ((idx (seq-position emus-tracks emus-current-track)) + (offset (if prev -1 +1))) + (if idx + (let ((next-track (elt emus-tracks (+ idx offset)))) + (if next-track + (if (eq emus-state 'playing) + (emus-play-track next-track) + (emus-select-track next-track)) + (error "Track does not exist"))) + (error "No track selected"))))) + +(defun emus--play-adjacent-album (&optional prev) + (emus--with-library + (let ((idx (seq-position emus-tracks emus-current-track))) + (if idx + (let* ((search-list (if prev + (reverse (seq-subseq emus-tracks 0 idx)) + (seq-subseq emus-tracks (+ idx 1)))) + (current-album (emus-track-album emus-current-track)) + (next-track (seq-some (lambda (r) + (if (string= (emus-track-album r) + current-album) + nil + r)) + search-list))) + (if next-track + (if (eq emus-state 'playing) + (emus-play-track next-track) + (emus-select-track next-track)) + (error "Track does not exist"))) + (error "No track selected"))))) (defun emus-play-next () (interactive) - (emus-play-nearby 1)) + (emus--play-adjacent-track)) (defun emus-play-prev () (interactive) - (emus-play-nearby -1)) + (emus--play-adjacent-track t)) + +(defun emus-play-next-album () + (interactive) + (emus--play-adjacent-album)) + +(defun emus-play-prev-album () + (interactive) + (emus--play-adjacent-album t)) + +(defun emus-jump (seconds) + "Jump forward in current track by SECONDS seconds." + (emus--with-library + (emus-send-cmd "jump" (format "%+ds" seconds)))) + +(defun emus-jump-10s-forward () + "Jump 10 seconds forward in current track." + (interactive) + (emus-jump 10)) + +(defun emus-jump-10s-backward () + "Jump 10 seconds backward in current track." + (interactive) + (emus-jump -10)) (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)) + (emus--with-library + (message + (concat "Emus: Volume %d%%" + (pcase emus-state + ('stopped " [Stopped]") + ('paused " [Paused]") + ('playing " [Playing]") + (_ "")) + (if emus-current-track + (format " - %.30s (%.20s)" + (emus-track-title emus-current-track) + (emus-track-artist emus-current-track)) + "")) + 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)) +(defun emus--insert-track (track &optional prev-track first) + (let* ((artist (emus-track-artist track)) + (album (emus-track-album track)) + (title (emus-track-title track)) (help-str (format "mouse-1, RET: Play '%.30s' (%.20s)" title artist))) - (when (or prev-record first) - (unless (equal (emus-record-artist prev-record) artist) + (when (or prev-track first) + (unless (equal (emus-track-artist prev-track) artist) (insert-text-button (propertize artist 'face 'emus-artist) - 'action #'emus-click-record + 'action #'emus--click-track 'follow-link t 'help-echo help-str - 'emus-record record) + 'emus-track track) (insert (propertize "\n" 'face 'emus-artist))) - (unless (equal (emus-record-album prev-record) album) + (unless (equal (emus-track-album prev-track) album) (insert-text-button (propertize (concat " " album) 'face 'emus-album) - 'action #'emus-click-record + 'action #'emus--click-track 'follow-link t 'help-echo help-str - 'emus-record record) + 'emus-track track) (insert (propertize "\n" 'face 'emus-album)))) - (emus-set-record-browser-pos record (point)) - (let ((is-current (equal record emus-current-record))) + (emus-set-track-browser-pos track (point)) + (let ((is-current (equal track emus-current-track))) (insert-text-button (concat (if is-current @@ -317,93 +406,124 @@ 'face (if is-current 'emus-track-current 'emus-track))) - 'action #'emus-click-record + 'action #'emus--click-track 'follow-link t 'help-echo help-str - 'emus-record record) + 'emus-track track) (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))) +(defun emus--update-track (track) + (let ((track-pos (emus-track-browser-pos track))) (when (and (get-buffer "*emus*") - (emus-record-browser-pos record)) + (emus-track-browser-pos track)) (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 () + (goto-char track-pos) + (search-forward "\n") + (delete-region track-pos (point)) + (goto-char track-pos) + (emus--insert-track track) + (goto-char old-point)))))) + +(defun emus--render-tracks () (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))) + (let ((prev-track nil)) + (dolist (track emus-tracks) + (emus--insert-track track prev-track (not prev-track)) + (setq prev-track track))) (goto-char old-pos)))) -(defun emus-click-record (button) - (emus-play-record (button-get button 'emus-record))) +(defun emus--click-track (button) + (emus-play-track (button-get button 'emus-track)) + (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)) + (when emus-current-track + (goto-char (emus-track-browser-pos emus-current-track)) (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))) + (emus--with-library + (switch-to-buffer "*emus*") + (emus-browser-mode) + (emus--render-tracks) + (emus-centre-current))) + +(defun emus-refresh () + (interactive) + (emus-stop) + (setq emus-tracks nil) + (emus-browse)) + +(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-play-next-status () (interactive) (emus-play-next) (emus-display-status)) +(defun emus-play-prev-status () (interactive) (emus-play-prev) (emus-display-status)) +(defun emus-play-next-album-status () (interactive) (emus-play-next-album) (emus-display-status)) +(defun emus-play-prev-album-status () (interactive) (emus-play-prev-album) (emus-display-status)) +(defun emus-jump-10s-forward-status () (interactive) (emus-jump-10s-forward) (emus-display-status)) +(defun emus-jump-10s-backward-status () (interactive) (emus-jump-10s-backward) (emus-display-status)) +(defun emus-centre-current-status () (interactive) (emus-centre-current) (emus-display-status)) + +(defun emus-refresh-status () + (interactive) + (emus-stop) + (setq emus-tracks nil) + (emus--with-library + (emus-browse) + (emus-display-status))) (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) + (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-refresh-status) + (define-key map (kbd "n") 'emus-play-next-status) + (define-key map (kbd "p") 'emus-play-prev-status) + (define-key map (kbd "N") 'emus-play-next-album-status) + (define-key map (kbd "P") 'emus-play-prev-album-status) + (define-key map (kbd ",") 'emus-jump-10s-backward-status) + (define-key map (kbd ".") 'emus-jump-10s-forward-status) + (define-key map (kbd "c") 'emus-centre-current-status) (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)) + (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-refresh-status + (kbd "n") 'emus-play-next-status + (kbd "p") 'emus-play-prev-status + (kbd "N") 'emus-play-next-album-status + (kbd "P") 'emus-play-prev-album-status + (kbd ",") 'emus-jump-10s-backward-status + (kbd ".") 'emus-jump-10s-forward-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.") + "Major mode for EMUS music player file browser.") (when (fboundp 'evil-set-initial-state) (evil-set-initial-state 'emus-browser-mode 'motion)) -;;; Debugging - ;;; emus.el ends here