Better faces, track selection without playing.
[emus.git] / emus.el
1 ;;; emus.el --- Simple music player for Emacs.  -*- lexical-binding:t -*-
2
3 ;; Author: Tim Vaughan <timv@ughan.xyz>
4 ;; Version: 1.0
5 ;; Keywords: multimedia
6 ;; URL: https://thelambdalab.xyz/emus
7
8 ;;; Commentary:
9
10 ;; This is a simple package for playing audio from a local library
11 ;; of audio files.
12
13 ;;; Code:
14
15 ;;; Customizations
16 ;;
17
18 (defgroup emus nil
19   "Simple music player for Emacs."
20   :group 'multimedia)
21
22 (defcustom emus-directory "~/Music/"
23   "Directory containing audio files for emus."
24   :type '(string))
25
26 (defcustom emus-mpg123-program "mpg123"
27   "Name of (and, optionally, path to) mpg123 binary."
28   :type '(string))
29
30 (defface emus-artist
31   '((t :inherit font-lock-keyword-face :background "#333"))
32   "Face used for artist names in browser.")
33
34 (defface emus-album
35   '((t :inherit font-lock-function-name-face :background "#222"))
36   "Face used for album names in browser.")
37
38 (defface emus-track
39   '((t :inherit font-lock-string-face))
40   "Face used for track titles in browser.")
41
42 (defface emus-track-current
43   '((t :inherit font-lock-string-face :inverse-video t))
44   "Face used for track titles in browser.")
45
46 (defface emus-cursor
47   '((t :inherit bold))
48   "Face used for current track cursor")
49
50 ;;; Library
51 ;;
52
53 (defun emus-get-audio-files ()
54   "Get all mp3 files in main emus directory."
55   (directory-files-recursively emus-directory ".*\\.mp3"))
56
57 (defvar emus-records nil
58   "Emus audio library.")
59
60 (defun emus-make-record (filename tagstr)
61   (let ((artist "")
62         (album "")
63         (title ""))
64     (dolist (line (split-string tagstr "\n"))
65       (let ((found-artist (elt (split-string line "@I ID3v2.artist:") 1))
66             (found-album (elt (split-string line "@I ID3v2.album:") 1))
67             (found-title (elt (split-string line "@I ID3v2.title:") 1)))
68         (cond
69          (found-artist (setq artist found-artist))
70          (found-album (setq album found-album))
71          (found-title (setq title found-title)))))
72     (vector artist album title filename nil)))
73
74 (defun emus-record-artist (record)
75   (elt record 0))
76
77 (defun emus-record-album (record)
78   (elt record 1))
79
80 (defun emus-record-title (record)
81   (elt record 2))
82
83 (defun emus-record-file (record)
84   (elt record 3))
85
86 (defun emus-record-browser-pos (record)
87   (elt record 4))
88
89 (defun emus-set-record-browser-pos (record pos)
90   (aset record 4 pos))
91
92 (defun emus-update-records ()
93   (interactive)
94   (emus-suspend-cp)
95   (setq emus-state 'stopped)
96   (let ((proc (emus-get-process))
97         (tagstr "")
98         (filenames (emus-get-audio-files)))
99     (setq emus-records nil)
100     (set-process-filter proc (lambda (proc string)
101                                (setq tagstr (concat tagstr string))
102                                (when (string-suffix-p "@P 1\n" string)
103                                  (add-to-list 'emus-records
104                                               (emus-make-record (car filenames)
105                                                                 tagstr))
106                                  (setq tagstr "")
107                                  (setq filenames (cdr filenames))
108                                  (if filenames
109                                      (emus-send-cmd "lp" (car filenames))
110                                    (set-process-filter proc nil)
111                                    (setq emus-records (reverse emus-records))
112                                    (emus-sort-records)
113                                    (emus-render-records)
114                                    (emus-resume-cp)))))
115     (emus-send-cmd "lp" (car filenames))))
116
117 (defun emus-sort-records ()
118   (sort emus-records
119         (lambda (r1 r2)
120           (let ((artist1 (emus-record-artist r1))
121                 (artist2 (emus-record-artist r2)))
122             (if (string= artist1 artist2)
123                 (let ((album1 (emus-record-album r1))
124                       (album2 (emus-record-album r2)))
125                   (string< album1 album2))
126               (string< artist1 artist2))))))        
127
128 ;;; mpg123 process
129 ;;
130
131 (defvar emus-proc-in-use nil)
132
133 (defun emus-get-process ()
134   "Return current or new mpg123 process."
135   (let* ((emus-process-raw (get-process "emus-process"))
136          (emus-process (if emus-process-raw
137                            (if (process-live-p emus-process-raw)
138                                emus-process-raw
139                              (kill-process emus-process-raw)
140                              nil))))
141     (if emus-process
142         emus-process
143       (let ((proc
144              (make-process :name "emus-process"
145                            ;; :buffer (get-buffer-create "*emus-process*")
146                            :command `(,emus-mpg123-program "-R"))))
147         (process-send-string proc "silence\n")
148         proc))))
149                     
150
151 (defun emus-send-cmd (cmd &rest args)
152   (process-send-string (emus-get-process)
153                        (concat
154                         (seq-reduce (lambda (s1 s2) (concat s1 " " s2)) args cmd)
155                         "\n")))
156
157 (defun emus-send-and-process (respfun predfun cmd &rest args)
158   (let ((respstr ""))
159     (set-process-filter (emus-get-process)
160                         (lambda (proc string)
161                           (setq respstr (concat respstr string))
162                           (when (funcall predfun respstr)
163                             (set-process-filter proc nil)
164                             (funcall respfun respstr))))
165     (apply #'emus-send-cmd cmd args)))
166
167
168 ;;; Playback
169 ;;
170
171 (defvar emus-current-record nil)
172 (defvar emus-state 'stopped)
173 (defvar emus-continuous-playback t)
174
175 (defun emus-suspend-cp ()
176   (setq emus-continuous-playback nil))
177
178 (defun emus-resume-cp ()
179   (setq emus-continuous-playback t)
180   (set-process-filter (emus-get-process)
181                       (lambda (proc string)
182                         (and emus-continuous-playback
183                              (eq emus-state 'playing)
184                              (string-suffix-p "@P 0\n" string)
185                              (emus-play-next)))))
186
187 (defun emus-play-record (record)
188   "Set RECORD as current and start playing."
189   (let ((old-record emus-current-record))
190     (emus-send-cmd "l" (emus-record-file record))
191     (setq emus-state 'playing)
192     (setq emus-current-record record)
193     (emus-update-record old-record)
194     (emus-update-record record)
195     (emus-resume-cp)))
196
197 (defun emus-select-record (record)
198   "Set RECORD as current, but do not start playing."
199   (let ((old-record emus-current-record))
200     (setq emus-state 'stopped)
201     (setq emus-current-record record)
202     (emus-update-record old-record)
203     (emus-update-record record)
204     (emus-send-cmd "o")
205     (emus-resume-cp)))
206
207 (defun emus-stop ()
208   "Stop playback of the current record."
209   (interactive)
210   (setq emus-state 'stopped)
211   (emus-update-record emus-current-record)
212   (emus-send-cmd "s"))
213
214 (defun emus-playpause ()
215   (interactive)
216   (when emus-current-record
217     (if (eq emus-state 'stopped)
218         (emus-play-record emus-current-record)
219       (emus-send-cmd "p")
220       (pcase emus-state
221         ((or 'paused 'stopped) (setq emus-state 'playing))
222         ('playing (setq emus-state 'paused)))
223       (unless (eq emus-state 'paused)))
224     (emus-update-record emus-current-record)))
225
226 (defun emus-set-volume (pct)
227   (emus-send-cmd "v" (number-to-string pct)))
228
229 (defvar emus-current-volume 100)
230
231 (defun emus-volume-delta (delta)
232   (setq emus-current-volume (max 0 (min 100 (+ emus-current-volume delta))))
233   (emus-set-volume emus-current-volume))
234
235 (defun emus-volume-up ()
236   (interactive)
237   (emus-volume-delta 10))
238
239 (defun emus-volume-down ()
240   (interactive)
241   (emus-volume-delta -10))
242
243 (defun emus-play-nearby (offset)
244   (let ((idx (seq-position emus-records emus-current-record)))
245     (if idx
246         (let ((next-record (elt emus-records (+ idx offset))))
247           (if next-record
248               (if (eq emus-state 'playing)
249                   (emus-play-record next-record)
250                 (emus-select-record next-record))
251             (error "Track does not exist")))
252       (error "No track is currently selected."))))
253
254 (defun emus-play-next ()
255   (interactive)
256   (emus-play-nearby 1))
257
258 (defun emus-play-prev ()
259   (interactive)
260   (emus-play-nearby -1))
261
262 (defun emus-display-status ()
263   (interactive)
264   (message
265    (concat "Emus: Volume %d%%"
266            (pcase emus-state
267              ('stopped " [Stopped]")
268              ('paused " [Paused]")
269              ('playing " [Playing]")
270              (_ ""))
271            (if emus-current-record
272                (format " - %.30s (%.20s)"
273                        (emus-record-title emus-current-record)
274                        (emus-record-artist emus-current-record))
275              ""))
276    emus-current-volume))
277
278
279 ;;; Browser
280 ;;
281
282 (defun emus-insert-record (record &optional prev-record first)
283   (let* ((artist (emus-record-artist record))
284          (album (emus-record-album record))
285          (title (emus-record-title record))
286          (help-str (format "mouse-1, RET: Play '%.30s' (%.20s)" title artist)))
287     (when (or prev-record first)
288       (unless (equal (emus-record-artist prev-record) artist)
289         (insert-text-button
290          (propertize artist 'face 'emus-artist)
291          'action #'emus-click-record
292          'follow-link t
293          'help-echo help-str
294          'emus-record record)
295         (insert (propertize "\n" 'face 'emus-artist)))
296       (unless (equal (emus-record-album prev-record) album)
297         (insert-text-button
298          (propertize (concat "  " album) 'face 'emus-album)
299          'action #'emus-click-record
300          'follow-link t
301          'help-echo help-str
302          'emus-record record)
303         (insert (propertize "\n" 'face 'emus-album))))
304     (emus-set-record-browser-pos record (point))
305     (let ((is-current (equal record emus-current-record)))
306       (insert-text-button
307        (concat
308         (if is-current
309             (propertize
310              (pcase emus-state
311                ('playing "->")
312                ('paused "-)")
313                ('stopped "-]"))
314              'face 'emus-cursor)
315           (propertize "  " 'face 'default))
316         (propertize (format "   %s" title)
317                     'face (if is-current
318                               'emus-track-current
319                             'emus-track)))
320        'action #'emus-click-record
321        'follow-link t
322        'help-echo help-str
323        'emus-record record)
324       (insert (propertize "\n"
325                           'face (if is-current
326                                     'emus-track-current
327                                   'emus-track))))))
328
329 (defun emus-update-record (record)
330   (let ((record-pos (emus-record-browser-pos record)))
331     (when (and (get-buffer "*emus*")
332                (emus-record-browser-pos record))
333       (with-current-buffer "*emus*"
334         (let ((inhibit-read-only t)
335               (old-point (point)))
336             (goto-char record-pos)
337             (search-forward "\n")
338             (delete-region record-pos (point))
339             (goto-char record-pos)
340             (emus-insert-record record)
341             (goto-char old-point))))))
342
343 (defun emus-render-records ()
344   (with-current-buffer "*emus*"
345     (let ((inhibit-read-only t)
346           (old-pos (point)))
347       (erase-buffer)
348       (goto-char (point-min))
349       (let ((prev-record nil))
350         (dolist (record emus-records)
351           (emus-insert-record record prev-record (not prev-record))
352           (setq prev-record record)))
353       (goto-char old-pos))))
354
355 (defun emus-click-record (button)
356   (emus-play-record (button-get button 'emus-record)))
357
358 (defun emus-centre-current ()
359   (interactive)
360   (when (get-buffer "*emus*")
361     (switch-to-buffer "*emus*")
362     (when emus-current-record
363       (goto-char (emus-record-browser-pos emus-current-record))
364       (recenter))))
365
366 (defun emus-browse ()
367   "Switch to *emus* audio library browser."
368   (interactive)
369   (switch-to-buffer "*emus*")
370   (emus-browser-mode)
371   (emus-volume emus-current-volume)
372   (if emus-records
373       (emus-render-records)
374     (emus-update-records)))
375
376 (defvar emus-browser-mode-map
377   (let ((map (make-sparse-keymap)))
378     (define-key map (kbd "SPC") 'emus-playpause)
379     (define-key map (kbd "o") 'emus-stop)
380     (define-key map (kbd "+") 'emus-volume-up)
381     (define-key map (kbd "=") 'emus-volume-up)
382     (define-key map (kbd "-") 'emus-volume-down)
383     (define-key map (kbd "R") 'emus-update-records)
384     (define-key map (kbd "n") 'emus-play-next)
385     (define-key map (kbd "p") 'emus-play-prev)
386     (define-key map (kbd "c") 'emus-centre-current)
387     (when (fboundp 'evil-define-key*)
388       (evil-define-key* 'motion map
389         (kbd "SPC") 'emus-playpause
390         (kbd "o") 'emus-stop
391         (kbd "+") 'emus-volume-up
392         (kbd "=") 'emus-volume-up
393         (kbd "-") 'emus-volume-down
394         (kbd "R") 'emus-update-records
395         (kbd "n") 'emus-play-next
396         (kbd "p") 'emus-play-prev
397         (kbd "c") 'emus-centre-current))
398     map)
399   "Keymap for emus.")
400
401 (define-derived-mode emus-browser-mode special-mode "emus-browser"
402   "Major mode for EMUS music player.")
403
404 (when (fboundp 'evil-set-initial-state)
405   (evil-set-initial-state 'emus-browser-mode 'motion))
406
407 ;;; Debugging
408
409 ;;; emus.el ends here