From bb65333ef00df02dbf6bd83294b4df49e64ea325 Mon Sep 17 00:00:00 2001 From: forcer Date: Sun, 11 Sep 2005 20:05:00 +0000 Subject: Initial commit (CVS 2005-09-11) darcs-hash:20050911200506-2189f-48a136015e33465c3cf09940ce935ec2203df463.gz --- emms.el | 676 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 676 insertions(+) create mode 100644 emms.el (limited to 'emms.el') diff --git a/emms.el b/emms.el new file mode 100644 index 0000000..a010ce5 --- /dev/null +++ b/emms.el @@ -0,0 +1,676 @@ +;;; emms.el --- The Emacs Multimedia System + +;; Copyright (C) 2003, 2004, 2005 Jorgen Schäfer + +;; Author: Jorgen Schäfer +;; Keywords: emms, mp3, mpeg, multimedia + +;; This file 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 2, or (at your option) +;; any later version. +;; +;; GNU Emacs 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 GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, +;; Boston, MA 02110-1301 USA + +;;; Commentary: + +;; This is the very core of EMMS. It provides ways to play a track +;; using `emms-start', to go through the playlist using the commands +;; `emms-next' and `emms-previous', to stop the playback using +;; `emms-stop', and to see what's currently playing using `emms-show'. + +;; But in itself, this core is useless, because it doesn't know how to +;; play any tracks --- you need players for this. In fact, it doesn't +;; even know how to find any tracks to consider playing --- for this, +;; you need sources. + +;; A sample configuration is offered in emms-default.el, so you might +;; just want to use that file. + +;;; Code: + +;; $Id: emms.el,v 1.63 2005/08/18 13:52:23 forcer Exp $ +(defvar emms-version "1.3 $Revision: 1.63 $" + "EMMS version string.") + +(defmacro emms-define-obsolete-variable-alias + (obsolete-name current-name &optional when docstring) + "Make OBSOLETE-NAME an obsolete variable alias for CURRENT-NAME. +See `define-obsolete-variable-alias' in Emacs 22.1 and above." + `(progn + (when (fboundp 'defvaralias) + (defvaralias ,obsolete-name ,current-name ,docstring)) + (make-obsolete-variable ,obsolete-name ,current-name ,when))) + +(defmacro emms-define-obsolete-function-alias + (obsolete-name current-name &optional when docstring) + "Make OBSOLETE-NAME an obsolete function alias for CURRENT-NAME. +See `define-obsolete-function-alias' in Emacs 22.1 and above." + `(progn + (defalias ,obsolete-name ,current-name ,docstring) + (make-obsolete ,obsolete-name ,current-name ,when))) + + +;;; User Customization + +(defgroup emms nil + "*The Emacs Multimedia System." + :prefix "emms-" + :group 'multimedia + :group 'applications) + +(defgroup emms-player nil + "*Track players for EMMS." + :prefix "emms-player-" + :group 'emms) + +(defgroup emms-source nil + "*Track sources for EMMS." + :prefix "emms-source-" + :group 'emms) + +(defcustom emms-player-list nil + "*List of players that EMMS can use. You need to set this!" + :group 'emms + :type '(repeat (symbol :tag "Player"))) + +(defcustom emms-show-format "Currently playing: %s" + "*The format to use for `emms-show'. +Any \"%s\" is replaced by what `emms-track-description-function' returns +for the currently playing track." + :group 'emms + :type 'string) + +(defcustom emms-repeat-playlist nil + "*Non-nil if the EMMS playlist should automatically repeat. +If nil, playback will stop when the last track finishes playing. +If non-nil, EMMS will wrap back to the first track when that happens." + :group 'emms + :type 'boolean) + +(defcustom emms-repeat-track nil + "Non-nil, playback will repeat current track. If nil, EMMS will play +track by track normally." + :group 'emms + :type 'boolean) + +(defcustom emms-track-description-function 'emms-track-description + "*Function for describing an EMMS track in a user-friendly way." + :group 'emms + :type 'function) + +(defcustom emms-player-delay 0 + "The delay to pause after a player finished. +This is a floating-point number of seconds. +This is necessary for some platforms where it takes a bit to free +the audio device after a player has finished. If EMMS is skipping +songs, increase this number." + :type 'number + :group 'emms) + +(defcustom emms-sort-lessp-function 'emms-sort-track-name-less-p + "*Function for comparing two EMMS tracks. +The function should return non-nil if and only if the first track +sorts before the second (see `sort')." + :group 'emms + :type 'function) + +(defcustom emms-playlist-changed-hook nil + "*Hook run after the EMMS playlist changes." + :group 'emms + :type 'hook) + +(emms-define-obsolete-variable-alias + 'emms-playlist-current-changed-hook + 'emms-playlist-current-track-changed-hook) + +(defcustom emms-playlist-current-track-changed-hook nil + "*Hook run after another track is selected in the EMMS playlist." + :group 'emms + :type 'hook) + +(defcustom emms-track-initialize-functions nil + "*List of functions to call for each new EMMS track. +This can be used to initialize tracks with various info." + :group 'emms + :type 'hook) + +(defcustom emms-player-started-hook nil + "*Hook run when an EMMS player starts playing." + :group 'emms + :type 'hook + :options '(emms-show)) + +(defcustom emms-player-stopped-hook nil + "*Hook run when an EMMS player is stopped by the user. +See `emms-player-finished-hook'." + :group 'emms + :type 'hook) + +(defcustom emms-player-finished-hook '(emms-next-noerror) + "*Hook run when an EMMS player finishes playing a track. +Please pay attention to the differences between +`emms-player-finished-hook' and `emms-player-stopped-hook'. +The former is called only when the player is stopped interactively; +the latter, only when the player actually finishes playing a track." + :group 'emms + :type 'hook + :options '(emms-next-noerror)) + +(defvar emms-player-playing-p nil + "The currently playing EMMS player, or nil.") + +(defvar emms-playlist [] + "The current EMMS playlist: a vector of tracks.") +(defvar emms-playlist-current nil + "The zero-based playlist index of the current EMMS track. +If there is no playlist, this will be set to nil.") + +(defcustom emms-playlist-sort-added-tracks-p nil + "*If non-nil, sort tracks before adding them to the EMMS playlist." + :group 'emms + :type 'boolean) + +(emms-define-obsolete-variable-alias + 'emms-sort-on-file-add + 'emms-playlist-sort-added-tracks-p) + + +;;; User Interface + +(defun emms-start () + "Start playing the current track in the EMMS playlist." + (interactive) + (unless emms-player-playing-p + (emms-player-start (emms-playlist-current-track)))) + +(defun emms-stop () + "Stop any current EMMS playback." + (interactive) + (when emms-player-playing-p + (emms-player-stop))) + +(defun emms-next () + "Start playing the next track in the EMMS playlist. +This might behave funny if called from `emms-player-finished-hook', +so use `emms-next-noerror' in that case." + (interactive) + (when emms-player-playing-p + (emms-stop)) + (if (emms-playlist-next) + (emms-start) + (error "No next track in playlist"))) + +(defun emms-next-noerror () + "Start playing the next track in the EMMS playlist. +Unlike `emms-next', this function doesn't signal an error when called +at the end of the playlist. +This function should only be called when no player is playing. +This is a good function to put in `emms-player-finished-hook'." + (interactive) + (when emms-player-playing-p + (error "A track is already being played")) + (cond (emms-repeat-track + (emms-start)) + ((emms-playlist-next) + (emms-start)) + (emms-repeat-playlist + (setq emms-playlist-current 0) + (emms-start)) + (t + (message "No next track in playlist")))) + +(defun emms-previous () + "Start playing the previous track in the EMMS playlist." + (interactive) + (when emms-player-playing-p + (emms-stop)) + (if (emms-playlist-previous) + (emms-start) + (error "No previous track in playlist"))) + +(defun emms-show (&optional insertp) + "Describe the current EMMS track in the minibuffer. +If INSERTP is non-nil, insert the description into the current buffer instead. +This function uses `emms-show-format' to format the current track." + (interactive "P") + (let ((string (format emms-show-format (emms-playlist-current)))) + (if insertp + (insert string) + (message "%s" string)))) + +(defun emms-shuffle () + "Shuffle the EMMS playlist." + (interactive) + (emms-playlist-shuffle)) + +(defun emms-sort () + "Sort the EMMS playlist." + (interactive) + (emms-playlist-sort)) + +(defun emms-toggle-repeat-playlist () + "Toggle whether emms repeats the playlist after it is done. +See `emms-repeat-playlist'." + (interactive) + (setq emms-repeat-playlist (not emms-repeat-playlist)) + (if emms-repeat-playlist + (message "Will repeat the playlist after it is done.") + (message "Will stop after the playlist is over."))) + +(defun emms-toggle-repeat-track () + "Toggle whether emms repeats the current track. +See `emms-repeat-track'." + (interactive) + (setq emms-repeat-track (not emms-repeat-track)) + (if emms-repeat-track + (message "Will repeat the current track.") + (message "Will advance to the next track after this one."))) + +(defun emms-sort-track-name-less-p (a b) + "Return non-nil if the track name of A sorts before B." + (string< (emms-track-name a) + (emms-track-name b))) + + +;;; Tracks + +;; This is a simple datatype to store track information. +;; Each track consists of a type (a symbol) and a name (a string). +;; In addition, each track has an associated dictionary of information. + +(defun emms-track (type name) + "Create an EMMS track with type TYPE and name NAME." + (let ((track (emms-dictionary '*track*))) + (emms-track-set track 'type type) + (emms-track-set track 'name name) + (run-hook-with-args 'emms-track-initialize-functions track) + track)) + +(defun emms-track-type (track) + "Return the type of TRACK." + (emms-track-get track 'type)) + +(defun emms-track-name (track) + "Return the name of TRACK." + (emms-track-get track 'name)) + +(defun emms-track-get (track name &optional default) + "Return the value of NAME for TRACK. +If there is no value, return DEFAULT (or nil, if not given)." + (emms-dictionary-get track name default)) + +(defun emms-track-set (track name value) + "Set the value of NAME for TRACK to VALUE." + (emms-dictionary-set track name value)) + +(defun emms-track-description (track) + "Simple function to give a user-readable description of a track. +If it's a file track, just return the file name. +Otherwise, return the type and the name with a colon in between." + (if (eq 'file (emms-track-type track)) + (emms-track-name track) + (concat (symbol-name (emms-track-type track)) + ":" + (emms-track-name track)))) + + +;;; The Playlist + +;; This is a simple vector storing the current playlist. You should avoid +;; accessing the vector directly, and use the functions provided here instead. +;; If you can't avoid accessing the vector directly, be careful to call the +;; right hooks at the right times. + +(defun emms-playlist-current () + "Return a description of the currently playing EMMS track. +This function uses `emms-track-description-function'." + (funcall emms-track-description-function + (emms-playlist-current-track))) + +(defun emms-playlist-current-track () + "Return the currently playing EMMS track." + (when emms-playlist-current + (emms-playlist-get-track emms-playlist-current))) + +(defun emms-playlist-get-track-description (track) + "Return a description of TRACK. +This uses `emms-track-description-function'." + (funcall emms-track-description-function track)) + +(defun emms-playlist-get (n) + "Return a description of the Nth entry of the current EMMS playlist. +This uses `emms-track-description-function'" + (funcall emms-track-description-function + (emms-playlist-get-track n))) + +(defun emms-playlist-get-track (n) + "Return the Nth track of the current EMMS playlist." + (aref emms-playlist n)) + +(defun emms-playlist-set-playlist (new) + "Set the current EMMS playlist to NEW. +This runs `emms-playlist-changed-hook'." + (setq emms-playlist new) + (cond + ((= 0 (length new)) + (setq emms-playlist-current nil)) + ((null emms-playlist-current) + (setq emms-playlist-current 0)) + ((>= emms-playlist-current (length emms-playlist)) + (setq emms-playlist-current (- (length emms-playlist) 1)))) + (run-hooks 'emms-playlist-changed-hook)) + +(defun emms-playlist-get-playlist () + "Return the current EMMS playlist. +Avoid changing the structure returned by this function." + emms-playlist) + +(defun emms-playlist-set-current (n) + "Set the current track in the EMMS playlist to N (a number). +This runs `emms-playlist-current-track-changed-hook'." + (setq emms-playlist-current n) + (run-hooks 'emms-playlist-current-track-changed-hook)) + +(defun emms-playlist-get-current () + "Return the index number of the current EMMS track. +If the playlist is empty, returns nil." + emms-playlist-current) + +(defun emms-playlist-next () + "Advance to the next entry in the EMMS playlist. +Return nil if there was no next track, or non-nil otherwise." + (let ((cur (emms-playlist-get-current))) + (when (and cur + (< cur (- (length (emms-playlist-get-playlist)) 1))) + (emms-playlist-set-current (+ 1 cur)) + t))) + +(defun emms-playlist-previous () + "Back up to the previous entry in the EMMS playlist. +Return nil if there was no previous track, or non-nil otherwise." + (let ((cur (emms-playlist-get-current))) + (when (and cur + (> cur 0)) + (emms-playlist-set-current (- cur 1)) + t))) + +(defun emms-playlist-add (seq &optional idx) + "Add each track of the sequence SEQ to the current playlist. +Insert at IDX, which defaults to the end." + (let ((idx (or idx (length emms-playlist)))) + (emms-playlist-set-playlist + (vconcat (substring emms-playlist 0 idx) + (if emms-playlist-sort-added-tracks-p + (emms-playlist-sort-vector seq) + seq) + (substring emms-playlist idx))))) + +(defun emms-playlist-remove (idx) + "Remove track at IDX from the EMMS playlist." + (emms-playlist-set-playlist + (vconcat (substring emms-playlist 0 idx) + (substring emms-playlist (1+ idx))))) + +(defun emms-playlist-search-vector (track vector) + "Return the index of TRACK in VECTOR, or nil if not found. +Comparison is done with `eq'." + (catch 'loop + (let ((i 0)) + (while (< i (length vector)) + (if (eq track + (elt vector i)) + (throw 'loop i) + (setq i (1+ i))))))) + +(defun emms-playlist-shuffle () + "Shuffle the current EMMS playlist. +If a track is currently being played, it will end up at the front +of the playlist after shuffling." + (if (not emms-player-playing-p) + (emms-playlist-set-playlist + (emms-playlist-shuffle-vector + (emms-playlist-get-playlist))) + (let* ((current-track (emms-playlist-current-track)) + (playlist (emms-playlist-shuffle-vector + (emms-playlist-get-playlist))) + (new-index (emms-playlist-search-vector current-track playlist)) + (first (elt playlist 0))) + (aset playlist 0 (elt playlist new-index)) + (aset playlist new-index first) + (emms-playlist-set-playlist playlist) + (emms-playlist-set-current 0)))) + +(defun emms-playlist-sort () + "Sort the current EMMS playlist. +Comparison is done with `emms-sort-lessp-function'. +If a song is currently being played, it will remain the current track +after sorting, though its index may change as appropriate." + (if (not emms-player-playing-p) + (emms-playlist-set-playlist + (emms-playlist-sort-vector + (emms-playlist-get-playlist))) + (let* ((current-track (emms-playlist-current-track)) + (playlist (emms-playlist-sort-vector + (emms-playlist-get-playlist))) + (new-index (emms-playlist-search-vector current-track playlist))) + (emms-playlist-set-playlist playlist) + (emms-playlist-set-current new-index)))) + +(defun emms-playlist-shuffle-vector (vector) + "Shuffle VECTOR." + (let ((i (- (length vector) 1))) + (while (>= i 0) + (let* ((r (random (1+ i))) + (old (aref vector r))) + (aset vector r (aref vector i)) + (aset vector i old)) + (setq i (- i 1)))) + vector) + +(defun emms-playlist-sort-vector (vector) + "Sort VECTOR according to `emms-sort-lessp-function'." + (vconcat (sort (append vector nil) + emms-sort-lessp-function))) + + +;;; User-defined playlists. +(defmacro define-emms-playlist (name shufflep tracklist) + "Define a `emms-play-X' and `emms-add-X' function for TRACKLIST." + `(define-emms-source ,name () + "An EMMS source for a tracklist." + (interactive) + (let* ((new (apply #'append + (mapcar (lambda (source) + (apply (car source) + (cdr source))) + ,tracklist)))) + ,(if shufflep + '(append (emms-playlist-shuffle-vector (vconcat new)) nil) + 'new)))) + + +;;; Sources + +;; A source is just a function that returns a list of tracks. +;; The define-emms-source macro also defines functions emms-play-SOURCE +;; and emms-add-SOURCE. The former will replace the current playlist, +;; while the latter will add to the end. + +(defmacro define-emms-source (name arglist &rest body) + "Define a new EMMS source called NAME. +This macro defines three functions: `emms-source-NAME', `emms-play-NAME' +and `emms-add-NAME'. BODY should evaluate do a list of tracks to be played, +which is exactly what `emms-source-NAME' will return. +The other two functions will be simple wrappers around `emms-source-NAME'; +any `interactive' form that you specify in BODY will end up in these. +See emms-source-file.el for some examples." + (let ((source-name (intern (format "emms-source-%s" name))) + (source-play (intern (format "emms-play-%s" name))) + (source-add (intern (format "emms-add-%s" name))) + (docstring "A source of tracks for EMMS.") + (interactive nil) + (call-args (delete '&rest + (delete '&optional + arglist)))) + (when (stringp (car body)) + (setq docstring (car body) + body (cdr body))) + (when (eq 'interactive (caar body)) + (setq interactive (car body) + body (cdr body))) + `(progn + (defun ,source-name ,arglist + ,docstring + ,@body) + (defun ,source-play ,arglist + ,docstring + ,interactive + (emms-source-play (,source-name ,@call-args))) + (defun ,source-add ,arglist + ,docstring + ,interactive + (emms-source-add (,source-name ,@call-args)))))) + +(defun emms-source-play (lis) + "Play the tracks in LIS, after first clearing the EMMS playlist." + (let ((new + (if emms-playlist-sort-added-tracks-p + (emms-playlist-sort-vector (vconcat lis)) + (vconcat lis)))) + (when (zerop (length new)) + (error "No tracks found")) + (emms-stop) + (emms-playlist-set-playlist new) + (emms-playlist-set-current 0) + (emms-start))) + +(defun emms-source-add (lis) + "Add the tracks in LIS to the end of the EMMS playlist." + (emms-playlist-add lis)) + + +;;; Players + +;; A player is a data structure created by `emms-player'. +;; See the docstring of that function for more information. + +(defvar emms-player-stopped-p nil + "Non-nil if the last EMMS player was stopped by the user.") + +(defun emms-player (start stop playablep) + "Create a new EMMS player. +The start function will be START, and the stop function STOP. +PLAYABLEP should return non-nil for tracks that this player can play. + +When trying to play a track, EMMS walks `emms-player-list'. +For each player,it calls the PLAYABLEP function. +The player corresponding to the first PLAYABLEP function that returns +non-nil is used to play the track. +To actually play the track, EMMS calls the START function, +passing the chosen track as a parameter. + +If the user tells EMMS to stop playing, the STOP function is called. +Once the player has finished playing, it should call `emms-player-stopped' +to let EMMS know." + (let ((p (emms-dictionary '*player*))) + (emms-player-set p 'start start) + (emms-player-set p 'stop stop) + (emms-player-set p 'playablep playablep) + p)) + +(defun emms-player-get (player name &optional inexistent) + "Return the value of entry NAME in PLAYER." + (let ((p (if (symbolp player) + (symbol-value player) + player))) + (emms-dictionary-get p name inexistent))) + +(defun emms-player-set (player name value) + "Set the value of entry NAME in PLAYER to VALUE." + (let ((p (if (symbolp player) + (symbol-value player) + player))) + (emms-dictionary-set p name value))) + +(defun emms-player-for (track) + "Return an EMMS player capable of playing TRACK. +This will be the first player whose PLAYABLEP function returns non-nil, +or nil if no such player exists." + (let ((lis emms-player-list)) + (while (and lis + (not (funcall (emms-player-get (car lis) 'playablep) + track))) + (setq lis (cdr lis))) + (if lis + (car lis) + nil))) + +(defun emms-player-start (track) + "Start playing TRACK." + (if emms-player-playing-p + (error "A player is already playing") + (let ((player (emms-player-for track))) + (if (not player) + (error "Don't know how to play track: %s" track) + (funcall (emms-player-get player 'start) + track) + (setq emms-player-playing-p player) + (run-hooks 'emms-player-started-hook))))) + +(defun emms-player-stop () + "Stop the current EMMS player." + (when emms-player-playing-p + (let ((emms-player-stopped-p t)) + (funcall (emms-player-get emms-player-playing-p 'stop))) + (setq emms-player-playing-p nil))) + +(defun emms-player-stopped () + "Declare that the current EMMS player is finished. +This should only be done by the current player itself." + (setq emms-player-playing-p nil) + (if emms-player-stopped-p + (run-hooks 'emms-player-stopped-hook) + (sleep-for emms-player-delay) + (run-hooks 'emms-player-finished-hook))) + + +;;; Dictionaries + +;; This is a simple helper data structure, used by both players +;; and tracks. + +(defun emms-dictionary (name) + "Create a new dictionary of type NAME." + (list name)) + +(defun emms-dictionary-type (dict) + "Return the type of the dictionary DICT." + (car dict)) + +(defun emms-dictionary-get (dict name &optional default) + "Return the value of NAME in DICT." + (let ((item (assq name (cdr dict)))) + (if item + (cdr item) + default))) + +(defun emms-dictionary-set (dict name value) + "Set the value of NAME in DICT to VALUE." + (let ((item (assq name (cdr dict)))) + (if item + (setcdr item value) + (setcdr dict (append (cdr dict) + (list (cons name value)))))) + dict) + +(provide 'emms) +;;; emms.el ends here -- cgit v1.2.3