+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; 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)
+
+
+;;; Global constants
+;;
+
+(defconst emus-version "1.0.0"
+ "Current version of emus.")
+
+
+;;; 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-tracks nil
+ "Emus audio library.")
+
+(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)
+ (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 "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-render-tracks)
+ (emus--resume-cp)))))
+ (emus-send-cmd "lp" (car filenames))))
+
+(defun emus--make-track-from-tagstr (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)))))
+ (emus-make-track artist album title filename nil)))
+
+(defun emus--sort-tracks ()
+ (sort emus-tracks
+ (lambda (r1 r2)
+ (let ((artist1 (emus-track-artist r1))
+ (artist2 (emus-track-artist r2)))
+ (if (string= artist1 artist2)
+ (let ((album1 (emus-track-album r1))
+ (album2 (emus-track-album r2)))
+ (string< album1 album2))
+ (string< artist1 artist2))))))
+
+(defmacro emus--with-library (&rest args)
+ `(if emus-tracks
+ (progn ,@args)
+ (emus--load-library
+ (lambda () ,@args))))
+
+;;; 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 (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-track nil)
+(defvar emus-state 'stopped)
+(defvar emus-continuous-playback t)