aboutsummaryrefslogtreecommitdiff
path: root/emms-lyrics.el
diff options
context:
space:
mode:
Diffstat (limited to 'emms-lyrics.el')
-rw-r--r--emms-lyrics.el585
1 files changed, 585 insertions, 0 deletions
diff --git a/emms-lyrics.el b/emms-lyrics.el
new file mode 100644
index 0000000..95b8e29
--- /dev/null
+++ b/emms-lyrics.el
@@ -0,0 +1,585 @@
+;;; emms-lyrics.el --- Display lyrics synchronically
+
+;; Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2013 Free Software Foundation, Inc.
+
+;; Author: William Xu <william.xwl@gmail.com>
+;; Keywords: emms music lyrics
+
+;; This file is part of EMMS.
+
+;; EMMS 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, or (at your option)
+;; any later version.
+;;
+;; EMMS 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 EMMS; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
+
+;;; Commentary:
+
+;; This package enables you to play music files and display lyrics
+;; synchronically! :-) Plus, it provides a `emms-lyrics-mode' for
+;; making lyric files.
+
+;; Put this file into your load-path and the following into your
+;; ~/.emacs:
+;; (require 'emms-lyrics)
+;;
+;; Then either `M-x emms-lyrics-enable' or add (emms-lyrics 1) in
+;; your .emacs to enable.
+
+;;; TODO:
+
+;; 1. Maybe the lyric setup should run before `emms-start'.
+;; 2. Give a user a chance to choose when finding out multiple lyrics.
+;; 3. Search .lrc format lyrics from internet ?
+
+;;; Code:
+
+(require 'emms)
+(require 'emms-player-simple)
+(require 'emms-source-file)
+(require 'time-date)
+(require 'emms-url)
+(require 'emms-compat)
+
+;;; User Customization
+
+(defgroup emms-lyrics nil
+ "Lyrics module for EMMS."
+ :group 'emms)
+
+(defcustom emms-lyrics-display-on-modeline t
+ "If non-nil, display lyrics on mode line."
+ :type 'boolean
+ :group 'emms-lyrics)
+
+(defcustom emms-lyrics-display-on-minibuffer nil
+ "If non-nil, display lyrics on minibuffer."
+ :type 'boolean
+ :group 'emms-lyrics)
+
+(defcustom emms-lyrics-display-buffer nil
+ "Non-nil will create deciated `emms-lyrics-buffer' to display lyrics."
+ :type 'boolean
+ :group 'emms-lyrics)
+
+(defcustom emms-lyrics-dir "~/music/lyrics"
+ "Local lyrics repository.
+`emms-lyrics-find-lyric' will look for lyrics in current directory(i.e.,
+same as the music file) and this directory."
+ :type 'string
+ :group 'emms-lyrics)
+
+(defcustom emms-lyrics-display-format " %s "
+ "Format for displaying lyrics."
+ :type 'string
+ :group 'emms-lyrics)
+
+(defcustom emms-lyrics-coding-system nil
+ "Coding system for reading lyrics files.
+
+If all your lyrics use the same coding system, you can set this
+variable to that value; else you'd better leave it to nil, and
+rely on `prefer-coding-system', `file-coding-system-alist' or
+\(info \"(emacs)File Variables\"), sorted by priority
+increasingly."
+ :type 'coding-system
+ :group 'emms-lyrics)
+
+(defcustom emms-lyrics-mode-hook nil
+ "Normal hook run after entering Emms Lyric mode."
+ :type 'hook
+ :group 'emms-lyrics)
+
+(defcustom emms-lyrics-find-lyric-function 'emms-lyrics-find-lyric
+ "Function for finding lyric files."
+ :type 'symbol
+ :group 'emms-lyrics)
+
+(defcustom emms-lyrics-scroll-p t
+ "Non-nil value will enable lyrics scrolling on mode line.
+
+Note: Even if this is set to t, it also depends on
+`emms-lyrics-display-on-modeline' to be t."
+ :type 'boolean
+ :group 'emms-lyrics)
+
+(defcustom emms-lyrics-scroll-timer-interval 0.4
+ "Interval between scroller timers. The shorter, the faster."
+ :type 'number
+ :group 'emms-lyrics)
+
+
+;;; User Interfaces
+
+(defvar emms-lyrics-display-p t
+ "If non-nil, will diplay lyrics.")
+
+(defvar emms-lyrics-mode-line-string ""
+ "Current lyric.")
+
+(defvar emms-lyrics-buffer nil
+ "Buffer to show lyrics.")
+
+(defvar emms-lyrics-chinese-url "http://mp3.baidu.com/m?f=ms&rn=10&tn=baidump3lyric&ct=150994944&word=%s&lm=-1"
+ "URL used to find Chinese lyrics.
+Should contain one %s which is replaced with the filename.")
+
+(defvar emms-lyrics-latin-url "http://lyrics.wikia.com/%s%s"
+ "URL used to find Latin lyrics.
+Should contain two %s-expressions. The first is replaced with
+the artist and second with the title.")
+
+;;;###autoload
+(defun emms-lyrics-enable ()
+ "Enable displaying emms lyrics."
+ (interactive)
+ (emms-lyrics 1)
+ (message "emms lyrics enabled."))
+
+;;;###autoload
+(defun emms-lyrics-disable ()
+ "Disable displaying emms lyrics."
+ (interactive)
+ (emms-lyrics -1)
+ (message "EMMS lyrics disabled"))
+
+;;;###autoload
+(defun emms-lyrics-toggle ()
+ "Toggle displaying emms lyrics."
+ (interactive)
+ (if emms-lyrics-display-p
+ (emms-lyrics-disable)
+ (emms-lyrics-enable)))
+
+(defun emms-lyrics-toggle-display-on-minibuffer ()
+ "Toggle display lyrics on minibbufer."
+ (interactive)
+ (if emms-lyrics-display-on-minibuffer
+ (progn
+ (setq emms-lyrics-display-on-minibuffer nil)
+ (message "Disable lyrics on minibufer"))
+ (setq emms-lyrics-display-on-minibuffer t)
+ (message "Enable lyrics on minibufer")))
+
+(defun emms-lyrics-toggle-display-on-modeline ()
+ "Toggle display lyrics on mode line."
+ (interactive)
+ (if emms-lyrics-display-on-modeline
+ (progn
+ (setq emms-lyrics-display-on-modeline nil
+ emms-lyrics-mode-line-string "")
+ (message "Disable lyrics on mode line"))
+ (setq emms-lyrics-display-on-modeline t)
+ (message "Enable lyrics on mode line")))
+
+(defun emms-lyrics-toggle-display-buffer ()
+ "Toggle showing/hiding `emms-lyrics-buffer'."
+ (interactive)
+ (let ((w (get-buffer-window emms-lyrics-buffer)))
+ (if w
+ (delete-window w)
+ (save-selected-window
+ (pop-to-buffer emms-lyrics-buffer)
+ (set-window-dedicated-p w t)))))
+
+(defun emms-lyrics (arg)
+ "Turn on emms lyrics display if ARG is positive, off otherwise."
+ (interactive "p")
+ (if (and arg (> arg 0))
+ (progn
+ (setq emms-lyrics-display-p t)
+ (add-hook 'emms-player-started-hook 'emms-lyrics-start)
+ (add-hook 'emms-player-stopped-hook 'emms-lyrics-stop)
+ (add-hook 'emms-player-finished-hook 'emms-lyrics-stop)
+ (add-hook 'emms-player-paused-hook 'emms-lyrics-pause)
+ (add-hook 'emms-player-seeked-functions 'emms-lyrics-seek)
+ (add-hook 'emms-player-time-set-functions 'emms-lyrics-sync))
+ (emms-lyrics-stop)
+ (setq emms-lyrics-display-p nil)
+ (emms-lyrics-restore-mode-line)
+ (remove-hook 'emms-player-started-hook 'emms-lyrics-start)
+ (remove-hook 'emms-player-stopped-hook 'emms-lyrics-stop)
+ (remove-hook 'emms-player-finished-hook 'emms-lyrics-stop)
+ (remove-hook 'emms-player-paused-hook 'emms-lyrics-pause)
+ (remove-hook 'emms-player-seeked-functions 'emms-lyrics-seek)
+ (remove-hook 'emms-player-time-set-functions 'emms-lyrics-sync)))
+
+(defun emms-lyrics-visit-lyric ()
+ "Visit playing track's lyric file.
+If we can't find it from local disk, then search it from internet."
+ (interactive)
+ (let* ((track (emms-playlist-current-selected-track))
+ (name (emms-track-get track 'name))
+ (lrc (funcall emms-lyrics-find-lyric-function
+ (emms-replace-regexp-in-string
+ (concat "\\." (file-name-extension name) "\\'")
+ ".lrc"
+ (file-name-nondirectory name)))))
+ (if (and lrc (file-exists-p lrc) (not (string= lrc "")))
+ (find-file lrc)
+ (message "Lyric file does not exist on file-system. Searching online...")
+ (let* ((title (or (emms-track-get track 'info-title)
+ (file-name-sans-extension
+ (file-name-nondirectory name))))
+ (artist (when (emms-track-get track 'info-title)
+ (emms-track-get track 'info-artist)))
+ (url
+ (cond ((string-match "\\cc" title) ; Chinese lyrics.
+ ;; Since tag info might be encoded using various coding
+ ;; systems, we'd better fall back on filename.
+ (format emms-lyrics-chinese-url
+ (emms-url-quote-plus
+ (encode-coding-string name 'gb2312))))
+ (t ; English lyrics.g
+ (format emms-lyrics-latin-url
+ (if artist (concat (emms-url-quote-underscore artist) ":") "")
+ (emms-url-quote-underscore title))))))
+ (if (fboundp 'eww)
+ (progn (require 'eww)
+ (let ((readable-hook (when (fboundp 'eww-readable)
+ (add-hook 'eww-after-render-hook 'eww-readable))))
+ (eww url)
+ (when readable-hook
+ (remove-hook 'eww-after-render-hook 'eww-readable))))
+ (browse-url url))
+ (message "Lyric file does not exist on file-system. Searching online...")))))
+
+
+;;; EMMS Lyrics
+
+(defvar emms-lyrics-alist nil
+ "a list of the form: '((time0 . lyric0) (time1 . lyric1)...)). In
+short, at time-i, display lyric-i.")
+
+(defvar emms-lyrics-timers nil
+ "timers for displaying lyric.")
+
+(defvar emms-lyrics-start-time nil
+ "emms lyric start time.")
+
+(defvar emms-lyrics-pause-time nil
+ "emms lyric pause time.")
+
+(defvar emms-lyrics-elapsed-time 0
+ "How long time has emms lyric played.")
+
+(defvar emms-lyrics-scroll-timers nil
+ "Lyrics scroller timers.")
+
+(defun emms-lyrics-read-file (file &optional catchup)
+ "Read a lyric file(LRC format).
+Optional CATCHUP is for recognizing `emms-lyrics-catchup'.
+FILE should end up with \".lrc\", its content looks like one of the
+following:
+
+ [1:39]I love you, Emacs!
+ [00:39]I love you, Emacs!
+ [00:39.67]I love you, Emacs!
+
+FILE should be under the same directory as the music file, or under
+`emms-lyrics-dir'."
+ (or catchup
+ (setq file (funcall emms-lyrics-find-lyric-function file)))
+ (when (and file (file-exists-p file))
+ (with-temp-buffer
+ (let ((coding-system-for-read emms-lyrics-coding-system))
+ (emms-insert-file-contents file)
+ (while (search-forward-regexp "\\[[0-9:.]+\\].*" nil t)
+ (let ((lyric-string (match-string 0))
+ (time 0)
+ (lyric ""))
+ (setq lyric
+ (emms-replace-regexp-in-string ".*\\]" "" lyric-string))
+ (while (string-match "\\[[0-9:.]+\\]" lyric-string)
+ (let* ((time-string (match-string 0 lyric-string))
+ (semi-pos (string-match ":" time-string)))
+ (setq time
+ (+ (* (string-to-number
+ (substring time-string 1 semi-pos))
+ 60)
+ (string-to-number
+ (substring time-string
+ (1+ semi-pos)
+ (1- (length time-string))))))
+ (setq lyric-string
+ (substring lyric-string (length time-string)))
+ (setq emms-lyrics-alist
+ (append emms-lyrics-alist `((,time . ,lyric))))
+ (setq time 0)))))
+ (setq emms-lyrics-alist
+ (sort emms-lyrics-alist (lambda (a b) (< (car a) (car b))))))
+ t)))
+
+(defun emms-lyrics-create-buffer ()
+ "Create `emms-lyrics-buffer' dedicated to lyrics. "
+ ;; leading white space in buffer name to hide the buffer
+ (setq emms-lyrics-buffer (get-buffer-create " *EMMS Lyrics*"))
+ (set-buffer emms-lyrics-buffer)
+ (setq buffer-read-only nil
+ cursor-type nil)
+ (erase-buffer)
+ (mapc (lambda (time-lyric) (insert (cdr time-lyric) "\n"))
+ emms-lyrics-alist)
+ (goto-char (point-min))
+ (emms-activate-highlighting-mode)
+ (setq buffer-read-only t))
+
+(defun emms-lyrics-start ()
+ "Start displaying lryics."
+ (setq emms-lyrics-start-time (current-time)
+ emms-lyrics-pause-time nil
+ emms-lyrics-elapsed-time 0)
+ (when (let ((file
+ (emms-track-get
+ (emms-playlist-current-selected-track)
+ 'name)))
+ (emms-lyrics-read-file
+ (emms-replace-regexp-in-string
+ (concat "\\." (file-name-extension file) "\\'")
+ ".lrc"
+ (file-name-nondirectory file))))
+ (when emms-lyrics-display-buffer
+ (emms-lyrics-create-buffer))
+ (emms-lyrics-set-timer)))
+
+(defun emms-lyrics-catchup (lrc)
+ "Catchup with later downloaded LRC file(full path).
+If you write some lyrics crawler, which is running asynchronically,
+then this function would be useful to call when the crawler finishes its
+job."
+ (let ((old-start emms-lyrics-start-time))
+ (setq emms-lyrics-start-time (current-time)
+ emms-lyrics-pause-time nil
+ emms-lyrics-elapsed-time 0)
+ (emms-lyrics-read-file lrc t)
+ (emms-lyrics-set-timer)
+ (emms-lyrics-seek (float-time (time-since old-start)))))
+
+(defun emms-lyrics-stop ()
+ "Stop displaying lyrics."
+ (interactive)
+ (when emms-lyrics-alist
+ (mapc #'emms-cancel-timer emms-lyrics-timers)
+ (if (or (not emms-player-paused-p)
+ emms-player-stopped-p)
+ (setq emms-lyrics-alist nil
+ emms-lyrics-timers nil
+ emms-lyrics-mode-line-string ""))))
+
+(defun emms-lyrics-pause ()
+ "Pause displaying lyrics."
+ (if emms-player-paused-p
+ (setq emms-lyrics-pause-time (current-time))
+ (when emms-lyrics-pause-time
+ (setq emms-lyrics-elapsed-time
+ (+ (float-time
+ (time-subtract emms-lyrics-pause-time
+ emms-lyrics-start-time))
+ emms-lyrics-elapsed-time)))
+ (setq emms-lyrics-start-time (current-time)))
+ (when emms-lyrics-alist
+ (if emms-player-paused-p
+ (emms-lyrics-stop)
+ (emms-lyrics-set-timer))))
+
+(defun emms-lyrics-seek (sec)
+ "Seek forward or backward SEC seconds lyrics."
+ (setq emms-lyrics-elapsed-time
+ (+ emms-lyrics-elapsed-time
+ (float-time (time-since emms-lyrics-start-time))
+ sec))
+ (when (< emms-lyrics-elapsed-time 0) ; back to start point
+ (setq emms-lyrics-elapsed-time 0))
+ (setq emms-lyrics-start-time (current-time))
+ (when emms-lyrics-alist
+ (let ((paused-orig emms-player-paused-p))
+ (setq emms-player-paused-p t)
+ (emms-lyrics-stop)
+ (setq emms-player-paused-p paused-orig))
+ (emms-lyrics-set-timer)))
+
+(defun emms-lyrics-sync (sec)
+ "Synchronize the lyric display at SEC seconds."
+ (setq emms-lyrics-start-time (current-time)
+ emms-lyrics-elapsed-time 0)
+ (emms-lyrics-seek sec))
+
+(defun emms-lyrics-set-timer ()
+ "Set timers for displaying lyrics."
+ (setq emms-lyrics-timers '())
+ (let ((lyrics-alist emms-lyrics-alist)
+ (line 0))
+ (while lyrics-alist
+ (let ((time (- (caar lyrics-alist) emms-lyrics-elapsed-time))
+ (lyric (cdar lyrics-alist))
+ (next-time (and (cdr lyrics-alist)
+ (- (car (cadr lyrics-alist))
+ emms-lyrics-elapsed-time)))
+ (next-lyric (and (cdr lyrics-alist)
+ (cdr (cadr lyrics-alist)))))
+ (setq line (1+ line))
+ (when (> time 0)
+ (setq emms-lyrics-timers
+ (append emms-lyrics-timers
+ (list
+ (run-at-time (format "%d sec" time)
+ nil
+ 'emms-lyrics-display-handler
+ lyric
+ next-lyric
+ line
+ (and next-time (- next-time time)))))))
+ (setq lyrics-alist (cdr lyrics-alist))))))
+
+(defun emms-lyrics-mode-line ()
+ "Add lyric to the mode line."
+ (or global-mode-string (setq global-mode-string '("")))
+ (unless (member 'emms-lyrics-mode-line-string
+ global-mode-string)
+ (setq global-mode-string
+ (append global-mode-string
+ '(emms-lyrics-mode-line-string)))))
+
+(defun emms-lyrics-restore-mode-line ()
+ "Restore the mode line."
+ (setq global-mode-string
+ (remove 'emms-lyrics-mode-line-string global-mode-string))
+ (force-mode-line-update))
+
+(defun emms-lyrics-display-handler (lyric next-lyric line diff)
+ "DIFF is the timestamp differences between current LYRIC and
+NEXT-LYRIC; LINE corresponds line number for LYRIC in `emms-lyrics-buffer'."
+ (emms-lyrics-display (format emms-lyrics-display-format lyric) line)
+ (when (and emms-lyrics-display-on-modeline emms-lyrics-scroll-p)
+ (emms-lyrics-scroll lyric next-lyric diff)))
+
+(defun emms-lyrics-display (lyric line)
+ "Display LYRIC now.
+See `emms-lyrics-display-on-modeline' and
+`emms-lyrics-display-on-minibuffer' on how to config where to
+display."
+ (when emms-lyrics-alist
+ (when emms-lyrics-display-on-modeline
+ (emms-lyrics-mode-line)
+ (setq emms-lyrics-mode-line-string lyric)
+ ;; (setq emms-lyrics-mode-line-string ; make it fit scroller width
+ ;; (concat emms-lyrics-mode-line-string
+ ;; (make-string
+ ;; (abs (- emms-lyrics-scroll-width (length lyric)))
+ ;; (string-to-char " "))))
+ (force-mode-line-update))
+
+ (when emms-lyrics-display-on-minibuffer
+ (unless (minibuffer-window-active-p (selected-window))
+ (message lyric)))
+
+ (when emms-lyrics-display-buffer
+ (with-current-buffer emms-lyrics-buffer
+ (when line
+ (goto-char (point-min))
+ (forward-line (1- line))
+ (emms-line-highlight))))))
+
+(defun emms-lyrics-find-lyric (file)
+ "Return full path of found lrc FILE, or nil if not found.
+Use `emms-source-file-directory-tree-function' to find lrc FILE under
+current directory and `emms-lyrics-dir'.
+e.g., (emms-lyrics-find-lyric \"abc.lrc\")"
+ (let* ((track (emms-playlist-current-selected-track))
+ (lyric-under-curr-dir
+ (concat (file-name-directory (emms-track-get track 'name))
+ file)))
+ (or (and (eq (emms-track-type track) 'file)
+ (file-exists-p lyric-under-curr-dir)
+ lyric-under-curr-dir)
+ (car (funcall emms-source-file-directory-tree-function
+ emms-lyrics-dir
+ file)))))
+
+;; (setq emms-lyrics-scroll-width 20)
+
+(defun emms-lyrics-scroll (lyric next-lyric diff)
+ "Scroll LYRIC to left smoothly in DIFF seconds.
+DIFF is the timestamp differences between current LYRIC and
+NEXT-LYRIC."
+ (setq diff (floor diff))
+ (setq emms-lyrics-scroll-timers '())
+ (let ((scrolled-lyric (concat lyric " " next-lyric))
+ (time 0)
+ (pos 0))
+ (catch 'return
+ (while (< time diff)
+ (setq emms-lyrics-scroll-timers
+ (append emms-lyrics-scroll-timers
+ (list
+ (run-at-time time
+ nil
+ 'emms-lyrics-display
+ (if (>= (length lyric) pos)
+ (substring scrolled-lyric pos)
+ (throw 'return t))
+ nil))))
+ (setq time (+ time emms-lyrics-scroll-timer-interval))
+ (setq pos (1+ pos))))))
+
+
+;;; emms-lyrics-mode
+
+(defvar emms-lyrics-mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map "p" 'emms-lyrics-previous-line)
+ (define-key map "n" 'emms-lyrics-next-line)
+ (define-key map "i" 'emms-lyrics-insert-time)
+ map)
+ "Keymap for `emms-lyrics-mode'.")
+
+(defun emms-lyrics-rem* (x y)
+ "The remainder of X divided by Y, with the same sign as X."
+ (let* ((q (floor x y))
+ (rem (- x (* y q))))
+ (if (= rem 0)
+ 0
+ (if (eq (>= x 0) (>= y 0))
+ rem
+ (- rem y)))))
+
+(defun emms-lyrics-insert-time ()
+ "Insert lyric time in the form: [01:23.21], then goto the
+beginning of next line."
+ (interactive)
+ (let* ((total (+ (float-time
+ (time-subtract (current-time)
+ emms-lyrics-start-time))
+ emms-lyrics-elapsed-time))
+ (min (/ (* (floor (/ total 60)) 100) 100))
+ (sec (/ (floor (* (emms-lyrics-rem* total 60) 100)) 100.0)))
+ (insert (emms-replace-regexp-in-string
+ " " "0" (format "[%2d:%2d]" min sec))))
+ (emms-lyrics-next-line))
+
+(defun emms-lyrics-next-line ()
+ "Goto the beginning of next line."
+ (interactive)
+ (forward-line 1))
+
+(defun emms-lyrics-previous-line ()
+ "Goto the beginning of previous line."
+ (interactive)
+ (forward-line -1))
+
+(define-derived-mode emms-lyrics-mode nil "Emms Lyric"
+ "Major mode for creating lyric files.
+\\{emms-lyrics-mode-map}"
+ (run-hooks 'emms-lyrics-mode-hook))
+
+(provide 'emms-lyrics)
+
+;;; emms-lyrics.el ends here