[Top][All Lists]

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]


From: jcg . sturdy
Subject: lyric-mode.el
Date: Mon, 18 Aug 2008 05:02:46 -0700 (PDT)
User-agent: G2/1.0

The following code implements a major mode for editing lyric (.lrc)
files, which are used by various music players to display lyrics in
synch with the playing of a song.

There are commands for starting and stopping an ogg123 process, and
the timing output from that process is captured, allowing you to enter
synchronization tags by pressing a key in time with the start of each
line of the song.

If the synchronization tags are already there, the mode uses a
highlighting overlay to show the timing as the song plays, allowing
you to check the tags.  There are also commands for nudging tags
slightly earlier or later.

There are a couple of "todo"s still in the code, but it generally
works OK (at least for me!)


;;; lyric-mode.el --- Major mode for editing lyric (.lrc) files

;; Copyright (C) 2008  John Sturdy

;; Author: John Sturdy <address@hidden>
;; Keywords: multimedia, hypermedia

;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.

;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; 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 Street, Fifth
;; Boston, MA 02110-1301, USA.

;;; Commentary:

;; Edit lyric files, with playing of the music file at a controlled
;; speed and recording of the synchronization information.

;; This uses the external program `ogg123'.  Potentially, it could use
;; other, similar, programs for playing other types of music file.  It
;; assumes that the music file and the lyric file have the same
;; basename, e.g. another-gnu.ogg and another-gnu.lrc

;; If the file is already marked up with synchronization tags, an
;; overlay is displayed as the song is played.

;; Mostly, the mode is controlled by C-c C-whatever, but while the
;; song is playing, some other keys are redefined for easy capture of
;; the synchronization information.  The space bar inserts a
;; synchronization tag at point, and moves to the start of the next
;; line, and the return key moves to the start of the next line.
;; Lines matching `lyric-mode-skip-lines' (normally, blank lines) are
;; skipped by the space bar, so you get through marking a song with
;; just the space bar if there are no complications.

;; When point is on a tag, you can nudge the tag time backward or
;; forward by half a second using `<' and '>'.

;;; History:

;; Originally written 2008-08-16 by John C. G. Sturdy.

;;; Code:

(defgroup lyric-mode nil
  "Customization for lyric mode.")

(defcustom lyric-mode-skip-lines "^\\s-*$"
  "Regexp for lines to skip automatically.
This is normally set to skip blank lines.
You could also write one to skip titles, verse numbers, etc."
  :group 'lyric-mode
  :type 'regexp)

(defcustom lyric-tag-delta 0.5
  "Number of seconds by which to adjust tag times."
  :group 'lyric-mode
  :type 'number)

(defcustom lyric-ogg-command "ogg123"
  "The command to play an ogg file."
  :group 'lyric-mode
  :type 'string)

(defcustom lyric-ogg-command "mpg123"
  "The command to play an mp3 file.
This won't work yet; more work is needed to support anything but ogg
  :group 'lyric-mode
  :type 'string)

(defvar lyric-mode-slowdown 1
  "Slowdown for recording synchronization.")

(defvar lyric-mode-music-file nil
"The music file corresponding to this buffer.")

(make-variable-buffer-local 'lyric-mode-music-file)

(defvar lyric-mode-player nil
  "The shell command to use to play the music for this buffer.")

(make-variable-buffer-local 'lyric-mode-player)

(defvar lyric-mode-player-process nil
  "Player process for this buffer.")

(make-variable-buffer-local 'lyric-mode-player-process)

(defvar lyric-mode-latest-time-string "0:0"
  "The latest time observed back from the process, as a string.")

(make-variable-buffer-local 'lyric-mode-latest-time-string)

(defvar lyric-mode-latest-time-seconds 0
  "The latest time observed back from the process, as seconds.")

(make-variable-buffer-local 'lyric-mode-latest-time-seconds)

(defvar lyric-mode-stop-string nil
  "A string telling the player when to stop.
Normally nil, to let the player run on.")

(make-variable-buffer-local 'lyric-mode-stop-string)

(defvar lyric-mode-time-overlay nil
  "An overlay for showing where in the song the player is.")

(make-variable-buffer-local 'lyric-mode-time-overlay)

(defvar lyric-mode-sync-regexp "\\[\\(\\([0-9]+\\):\\([0-9.]+\\)\\)\
  "Regexp to recognize a lyric synchronization tag.")

(defvar lyric-mode-ogg123-time-regexp "Time: \\(\\([0-9]+\\):\\([0-9.]+
  "Regexp to get the time from ogg123.")

(defun lyric-mode-next-synchronization-tag (&optional from)
  "Move to the next synchronization tag, and return it in seconds.
Optional FROM says where to look from."
  (when from (goto-char from))
  (if (re-search-forward lyric-mode-sync-regexp (point-max) t)
        (goto-char (match-beginning 0))
        (+ (* 60 (string-to-number (match-string-no-properties 2)))
           (string-to-number (match-string-no-properties 3))))

(defun lyric-mode-next-synchronization-tag-position (&optional from)
  "Return the position of the next synchronization tag.
Optional FROM says where to look from."
    (when from (goto-char from))
    (if (re-search-forward lyric-mode-sync-regexp (point-max) t)
        (match-beginning 0)

(defun lyric-mode-find-time (time &optional hint)
  "Find the start of the tag most nearly before TIME.
Optional HINT is a suggestion for where to start looking."
  ;; todo: something is going wrong for the last two lines of the song
  (unless hint (setq hint (point-min)))
  (let ((wrapped nil)
        (here hint)
    (catch 'found
      (while t
         ((null (setq there (next-single-property-change here 'lyric-time)))
          (when wrapped (throw 'found nil)) ; just to be sure
          (setq there (point-min)
                wrapped t))
         ((and (setq scratch-time (get-text-property there 'lyric-time))
               (> scratch-time time))
          (throw 'found here))
         ((and wrapped
               (>= there hint))
          (throw 'found nil)))
        (setq here there)))))

(defun lyric-mode-prepare-time-data (&optional from)
  "Prepare time data in the buffer.
If FROM is given, start there, otherwise from the beginning."
  (unless from (setq from (point-min)))
    (goto-char from)
    (let* ((modified (buffer-modified-p))
           (this-time (lyric-mode-next-synchronization-tag from))
           (this-place (point))
      ;; lyric-mode-next-synchronization-tag sets the match data as a
side effect
      (put-text-property (match-beginning 0) (match-end 0)
      (while (setq next-time (lyric-mode-next-synchronization-tag
                              (1+ this-place)))
        (setq next-place (point))
        (put-text-property (match-beginning 0) (match-end 0)
        (put-text-property this-place next-place
                           'lyric-time this-time)
        (setq this-time next-time
              this-place next-place))
      (restore-buffer-modified-p modified))))

(defun lyric-mode-move-overlay ()
  "Move the time overlay to the current time in playing the song."
  (condition-case evar
      (let* ((start (lyric-mode-find-time lyric-mode-latest-time-
             (next-start (lyric-mode-next-synchronization-tag-position (1+
             (next-skip (save-excursion (goto-char start) (re-search-forward
lyric-mode-skip-lines (point-max) t)))
             (end (if (and next-start next-skip)
                      (min next-start next-skip)
        (if (overlayp lyric-mode-time-overlay)
            (move-overlay lyric-mode-time-overlay start end)
          (setq lyric-mode-time-overlay (make-overlay start end))
          (overlay-put lyric-mode-time-overlay 'face (cons 'background-color
    (error nil)))

(defun lyric-mode-filter (process text)
  "Handle output of player PROCESS which is TEXT."
  (when (string-match lyric-mode-ogg123-time-regexp text)
    (setq lyric-mode-latest-time-string (match-string 1 text)
          (+ (* 60
                (string-to-number (match-string 2 text)))
             (string-to-number (match-string 3 text))))

(defun lyric-mode-make-player-args (player file)
  "Make the player args list for PLAYER to play FILE."
   ((string= lyric-mode-player "ogg123")
     (if lyric-mode-stop-string
         (list "-K" lyric-mode-stop-string)
      "-k" lyric-mode-latest-time-string
      "-y" (number-to-string lyric-mode-slowdown)
   ((string= lyric-mode-player "mpg123")
    (error "Cannot do this with this player"))))

(defun lyric-mode-resume-playing ()
  "Resume playing of the associated music file."
  (lyric-mode-stop-playing)             ; stop any previous player
  (let* ((player-args (lyric-mode-make-player-args lyric-mode-player
    ;; (message "Player args are %S" player-args)
    (setq lyric-mode-player-process
          (apply 'start-process (format "*Player for %s*" lyric-mode-music-
  (set-process-filter lyric-mode-player-process
                      (symbol-function 'lyric-mode-filter)))

(defun lyric-mode-full-speed ()
  "Set the speed to full, and continue."
  (setq lyric-mode-slowdown 1)

(defun lyric-mode-half-speed ()
  "Set the speed to half, and continue."
  (setq lyric-mode-slowdown 2)

(defun lyric-mode-third-speed ()
  "Set the speed to one-third, and continue."
  (setq lyric-mode-slowdown 3)

(defun lyric-mode-quarter-speed ()
  "Set the speed to one-quarter, and continue."
  (setq lyric-mode-slowdown 4)

(defun lyric-mode-start-playing ()
  "Start playing of the associated music file, from the beginning."
  (setq lyric-mode-latest-time-string "0:0")
  (message (substitute-command-keys "\\[lyric-mode-stop-playing] to
stop playing; \\[lyric-mode-space-or-tag] to mark the time; \\[lyric-
mode-newline-or-next-line] to move down")))

(defun lyric-mode-go ()
  "Resume playing, starting at the marker above point, if possible."
    (when (re-search-backward lyric-mode-sync-regexp (point-min) t)
      (setq lyric-mode-latest-time-string (match-string-no-properties

(defun lyric-mode-play-line ()
  "Play between the two markers around point."
    (when (re-search-backward lyric-mode-sync-regexp (point-min) t)
      (setq lyric-mode-latest-time-string (match-string-no-properties
    (forward-char 1)                   ; don't find the same one again
    (let ((lyric-mode-stop-string
           (if (re-search-forward lyric-mode-sync-regexp (point-max) t)
               (match-string-no-properties 1)

(defun lyric-mode-stop-playing ()
  "Toggle playing of the associated music file."
  (when (and (processp lyric-mode-player-process)
             (eq (process-status lyric-mode-player-process) 'run))
    (kill-process lyric-mode-player-process))
  (setq lyric-mode-player-process nil)
  (message (substitute-command-keys "\\[lyric-mode-resume-playing] to
resume playing")))

(defun lyric-mode-toggle-playing ()
  "Toggle playing of the associated music file."
  (if (and (processp lyric-mode-player-process)
           (eq (process-status lyric-mode-player-process) 'run))

(defun lyric-mode-insert-synchronization-tag ()
  "Insert a synchronization tag at point."
  (let ((start (point)))
    (insert "[" lyric-mode-latest-time-string "]")
    (lyric-mode-prepare-time-data start)))

(defun lyric-move-tag-at-point (delta)
  "Add DELTA to the time of the tag around point."
  (goto-char (previous-single-property-change
              (point) 'keymap))
  (if (looking-at lyric-mode-sync-regexp)
      (let* ((old-time (+ (* 60 (string-to-number (match-string-no-
properties 2)))
                          (string-to-number (match-string-no-properties 3))))
             (new-time (+ old-time delta))
             (minutes (floor (/ new-time 60.0)))
             (seconds (floor (- new-time (* minutes 60))))
             (centiseconds (* 100 (- new-time (* minutes 60) seconds)))
             (new-time-string (format "[%02d:%02d.%d]" minutes seconds
        (replace-match (propertize new-time-string
                       t t)
        (goto-char (match-end 0)))
    (error "Not on a synchronization tag")))

(defun lyric-tag-move-earlier ()
  "Move the tag at point half a second earlier."
    (lyric-move-tag-at-point (- lyric-tag-delta))

(defun lyric-tag-move-later ()
  "Move the tag at point half a second later."
    (lyric-move-tag-at-point lyric-tag-delta)

(defun lyric-mode-space-or-tag ()
  "If currently playing, insert a tag, otherwise insert space."
  (if lyric-mode-player-process
        (beginning-of-line 2)
        (when lyric-mode-skip-lines
          (while (and (not (eobp))
                      (looking-at lyric-mode-skip-lines))
            (beginning-of-line 2))))
    (insert " ")))

(defun lyric-mode-newline-or-next-line ()
  "If currently playing, move to the next line, else add newline."
  (if lyric-mode-player-process
      (beginning-of-line 2)

(defvar lyric-mode-map
  (let ((map (make-sparse-keymap "Lyric mode")))
    (define-key map "\C-c\C-p" 'lyric-mode-start-playing)
    (define-key map "\C-c\C-g" 'lyric-mode-go)
    (define-key map "\C-c\C-r" 'lyric-mode-resume-playing)
    (define-key map "\C-c\C-s" 'lyric-mode-stop-playing)
    (define-key map "\C-c\C-c" 'lyric-mode-toggle-playing)
    (define-key map "\C-c\C-l" 'lyric-mode-play-line)
    (define-key map "\C-c1" 'lyric-mode-full-speed)
    (define-key map "\C-c2" 'lyric-mode-half-speed)
    (define-key map "\C-c3" 'lyric-mode-third-speed)
    (define-key map "\C-c4" 'lyric-mode-quarter-speed)
    (define-key map " " 'lyric-mode-space-or-tag)
    (define-key map "\r" 'lyric-mode-newline-or-next-line)
  "Keymap for `lyric-mode'.")

(defvar lyric-mode-tag-keymap
  (let ((map (make-sparse-keymap "Lyric mode")))
    (define-key map "<" 'lyric-tag-move-earlier)
    (define-key map ">" 'lyric-tag-move-later)
  "Keymap for tag text in lyric-mode.")

(defun lyric-mode ()
  "Major mode for editing lyric files.
Commands are provided to start and stop a music player, and to insert
timestamp tags.


Use \\[lyric-mode-start-playing] to start playing from the beginning.

Use the variable `lyric-mode-slowdown' to set the speed.

\\[lyric-mode-stop-playing] stops playing, and \\[lyric-mode-resume-
playing] resumes.

\\[lyric-mode-go] starts playing at the tag nearest above point.

When not playing, the space and return keys insert space and
newline; when playing, they insert a tag and move to the next
line, respectively, letting you move rapidly through a
ready-typed text to add a tag to each line.

Full commands are:

While on a tag, you can adjust its time by `lyric-tag-delta' using
some extra commands:
  (let* ((file-name-base (file-name-sans-extension (buffer-file-
         (ogg (concat file-name-base ".ogg"))
         (mp3 (concat file-name-base ".mp3")))
    (setq major-mode 'lyric-mode
          mode-name "Lyric"
          font-lock-defaults '(("\\[[0-9:.]+\\]") t))
    (unless (eq (car (cdr (cdr mode-line-modes))) 'lyric-mode-latest-
      ;; todo: make this buffer-local
      (rplacd (cdr mode-line-modes)
              (cons 'lyric-mode-latest-time-string
                    (cons " " (cdr (cdr mode-line-modes))))))
    (use-local-map lyric-mode-map)
     ((file-exists-p ogg)
      (setq lyric-mode-music-file ogg
            lyric-mode-player lyric-ogg-command))
     ((file-exists-p ogg)
      (setq lyric-mode-music-file mp3
            lyric-mode-player lyric-mp3-command)))
    (message (substitute-command-keys "\\[lyric-mode-start-playing] to
start playing"))))

(add-to-list 'auto-mode-alist (cons "\\.lrc$" 'lyric-mode))

(provide 'lyric-mode)

;;; lyric-mode.el ends here

reply via email to

[Prev in Thread] Current Thread [Next in Thread]