summaryrefslogtreecommitdiff
path: root/elisp
diff options
context:
space:
mode:
authorMilan Zamazal <pdm@brailcom.org>2007-02-12 12:20:16 +0100
committerHan-Wen Nienhuys <hanwen@xs4all.nl>2007-02-12 18:00:07 +0100
commitd0c106f0391e64451d41db3ed11d1aa27afebbbb (patch)
treea43630ea815927711b39cb63a5688b7d6aaf2224 /elisp
parent717f0dbcab1ca4dc274ec356d735736032401d0e (diff)
Singing support
Diffstat (limited to 'elisp')
-rw-r--r--elisp/lilypond-song.el558
1 files changed, 558 insertions, 0 deletions
diff --git a/elisp/lilypond-song.el b/elisp/lilypond-song.el
new file mode 100644
index 0000000000..9886dac685
--- /dev/null
+++ b/elisp/lilypond-song.el
@@ -0,0 +1,558 @@
+;;; lilypond-song.el --- Emacs support for LilyPond singing
+
+;; Copyright (C) 2006 Brailcom, o.p.s.
+
+;; Author: Milan Zamazal <pdm@brailcom.org>
+
+;; COPYRIGHT NOTICE
+
+;; 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 2 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 program; if not, write to the Free Software
+;; Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+
+;;; Commentary:
+
+;; This file adds Emacs support for singing lyrics of LilyPond files.
+;; It extends lilypond-mode with the following commands (see their
+;; documentation for more information):
+;;
+;; - M-x LilyPond-command-sing (C-c C-a)
+;; - M-x LilyPond-command-sing-and-play (C-c C-q)
+;; - M-x LilyPond-command-sing-last (C-c C-z)
+;;
+;; Note these commands are not available from the standard LilyPond mode
+;; command menus.
+
+;;; Code:
+
+
+(require 'cl)
+(require 'lilypond-mode)
+
+(ignore-errors (require 'ecasound))
+
+
+;;; User options
+
+
+(defcustom LilyPond-synthesize-command "lilysong"
+ "Command used to sing LilyPond files."
+ :group 'LilyPond
+ :type 'string)
+
+(defcustom LilyPond-play-command (or (executable-find "ecaplay") "play")
+ "Command used to play WAV files."
+ :group 'LilyPond
+ :type 'string)
+
+;; In case you would like to use fluidsynth (not recommended as fluidsynth
+;; can perform wave file synthesis only in real time), you can use the
+;; following setting:
+;; (setq LilyPond-midi->wav-command "fluidsynth -nil -a file soundfont.sf2 '%s' && sox -t raw -s -r 44100 -w -c 2 fluidsynth.raw '%t'")
+(defcustom LilyPond-midi->wav-command "timidity -Ow %m -s %r -o '%t' '%s'"
+ "Command used to make a WAV file from a MIDI file.
+%s in the string is replaced with the source MIDI file name,
+%t is replaced with the target WAV file name.
+%r is replaced with rate.
+%m is replaced with lilymidi call."
+ :group 'LilyPond
+ :type 'string)
+
+(defcustom LilyPond-voice-rates
+ '((".*czech.*" . 44100)
+ (".*\\<fi\\(\\>\\|nnish\\).*" . 22050)
+ (".*" . 16000))
+ "Alist of regexps matching voices and the corresponding voice rates.
+It may be necessary to define proper voice rates here in order to
+avoid ecasound resampling problems."
+ :group 'LilyPond
+ :type '(alist :key-type regexp :value-type integer))
+
+(defcustom LilyPond-use-ecasound (and (featurep 'ecasound)
+ (executable-find "ecasound")
+ t)
+ "If non-nil, use ecasound for mixing and playing songs."
+ :group 'LilyPond
+ :type 'boolean)
+
+(defcustom LilyPond-voice-track-regexp "voice"
+ "Perl regexp matching names of MIDI tracks to be ignored on sing&play."
+ :group 'LilyPond
+ :type 'string)
+
+(defcustom LilyPond-lilymidi-command "\"`lilymidi --prefix-tracks -Q --filter-tracks '%s' '%f'`\""
+ "Command to insert into LilyPond-midi->wav-command calls.
+%f is replaced with the corresponding MIDI file name.
+%s is replaced with `LilyPond-voice-track-regexp'."
+ :group 'LilyPond
+ :type 'string)
+
+
+;;; Lyrics language handling
+
+
+(defvar lilysong-language nil)
+(make-variable-buffer-local 'lilysong-language)
+
+(defvar lilysong-last-language nil)
+(make-variable-buffer-local 'lilysong-last-language)
+
+(defvar lilysong-languages '("cs" "en"))
+
+(defvar lilysong-voices nil)
+
+(defun lilysong-voices ()
+ (or lilysong-voices
+ (with-temp-buffer
+ (call-process "lilysong" nil t nil "--list-voices")
+ (call-process "lilysong" nil t nil "--list-languages")
+ (goto-char (point-min))
+ (while (not (eobp))
+ (push (buffer-substring-no-properties
+ (line-beginning-position) (line-end-position))
+ lilysong-voices)
+ (forward-line))
+ lilysong-voices)))
+
+(defun lilysong-change-language ()
+ "Change synthesis language or voice of the current document."
+ (interactive)
+ (setq lilysong-language
+ (completing-read "Lyrics language or voice: "
+ (mapcar 'list (lilysong-voices)))))
+
+(defun lilysong-update-language ()
+ (unless lilysong-language
+ (lilysong-change-language)))
+
+
+;;; Looking for \festival* and \midi commands
+
+
+(defun lilysong-document-files ()
+ (let ((resulting-files ())
+ (stack (list (LilyPond-get-master-file))))
+ (while (not (null stack))
+ (let ((file (expand-file-name (pop stack))))
+ (when (and (file-exists-p file)
+ (not (member file resulting-files)))
+ (push file resulting-files)
+ (save-excursion
+ (save-restriction
+ (set-buffer (find-file-noselect file nil))
+ (widen)
+ (goto-char (point-min))
+ (while (re-search-forward "^[^%\n]*\\\\include +\"\\([^\"]+\\)\"" nil t)
+ (push (match-string 1) stack)))))))
+ (nreverse resulting-files)))
+
+(defvar lilysong-festival-command-regexp
+ "^[^%\n]*\\\\festival\\(syl\\)? +#\"\\([^\"]+\\)\"")
+
+(defun lilysong-find-song (direction)
+ "Find XML file name of the nearest Festival command in the given DIRECTION.
+DIRECTION is one of the symbols `forward' or `backward'.
+If no Festival command is found in the current buffer, return nil.
+The point is left at the position where the command occurrence was found."
+ (save-match-data
+ (when (funcall (if (eq direction 'backward)
+ 're-search-backward
+ 're-search-forward)
+ lilysong-festival-command-regexp nil t)
+ (match-string-no-properties 2))))
+
+(defun lilysong-current-song ()
+ "Return the XML file name corresponding to the song around current point.
+If there is none, return nil."
+ (save-excursion
+ (or (progn (end-of-line) (lilysong-find-song 'backward))
+ (progn (beginning-of-line) (lilysong-find-song 'forward)))))
+
+(defun lilysong-all-songs (&optional limit-to-region)
+ "Return list of XML file names of the song commands in the current buffer.
+If there are none, return an empty list.
+If LIMIT-TO-REGION is non-nil, look for the commands in the current region
+only."
+ (let ((result '())
+ (current nil))
+ (save-excursion
+ (save-restriction
+ (when limit-to-region
+ (narrow-to-region (or (mark) (point)) (point)))
+ (goto-char (point-min))
+ (while (setq current (lilysong-find-song 'forward))
+ (push current result))))
+ (nreverse result)))
+
+(defun lilysong-walk-files (collector)
+ (save-excursion
+ (mapcar (lambda (f)
+ (set-buffer (find-file-noselect f))
+ (funcall collector))
+ (lilysong-document-files))))
+
+(defun lilysong-all-songs* ()
+ "Return list of XML file names of the song commands in the current document."
+ (remove-duplicates (apply #'append (lilysong-walk-files #'lilysong-all-songs))
+ :test #'equal))
+
+(defvar lilysong-song-history nil)
+(make-variable-buffer-local 'lilysong-song-history)
+
+(defvar lilysong-last-song-list nil)
+(make-variable-buffer-local 'lilysong-last-song-list)
+
+(defvar lilysong-last-command-args nil)
+(make-variable-buffer-local 'lilysong-last-command-args)
+
+(defun lilysong-song-list (multi)
+ (cond
+ ((eq multi 'all)
+ (lilysong-all-songs*))
+ (multi
+ (lilysong-select-songs))
+ (t
+ (lilysong-select-single-song))))
+
+(defun lilysong-select-single-song ()
+ (let ((song (lilysong-current-song)))
+ (if song
+ (list song)
+ (error "No song found"))))
+
+(defun lilysong-select-songs ()
+ (let* ((all-songs (lilysong-all-songs*))
+ (available-songs all-songs)
+ (initial-songs (if (or (not lilysong-last-song-list)
+ (eq LilyPond-command-current
+ 'LilyPond-command-region))
+ (lilysong-all-songs t)
+ lilysong-last-song-list))
+ (last-input (completing-read
+ (format "Sing file%s: "
+ (if initial-songs
+ (format " (default `%s')"
+ (mapconcat 'identity initial-songs
+ ", "))
+ ""))
+ (mapcar 'list all-songs)
+ nil t nil
+ 'lilysong-song-history)))
+ (if (equal last-input "")
+ initial-songs
+ (let ((song-list '())
+ default-input)
+ (while (not (equal last-input ""))
+ (push last-input song-list)
+ (setq default-input (second (member last-input available-songs)))
+ (setq available-songs (remove last-input available-songs))
+ (setq last-input (completing-read "Sing file: "
+ (mapcar #'list available-songs)
+ nil t default-input
+ 'lilysong-song-history)))
+ (setq lilysong-last-song-list (nreverse song-list))))))
+
+(defun lilysong-count-midi-words ()
+ (count-rexp (point-min) (point-max) "^[^%]*\\\\midi"))
+
+(defun lilysong-midi-list (multi)
+ (if multi
+ (let ((basename (file-name-sans-extension (buffer-file-name)))
+ (count (apply #'+ (save-match-data
+ (lilysong-walk-files #'lilysong-count-midi-words))))
+ (midi-files '()))
+ (while (> count 0)
+ (setq count (1- count))
+ (if (= count 0)
+ (push (concat basename ".midi") midi-files)
+ (push (format "%s-%d.midi" basename count) midi-files)))
+ midi-files)
+ (list (LilyPond-string-current-midi))))
+
+
+;;; Compilation
+
+
+(defun lilysong-file->wav (filename &optional extension)
+ (format "%s.%s" (save-match-data
+ (if (string-match "\\.midi$" filename)
+ filename
+ (file-name-sans-extension filename)))
+ (or extension "wav")))
+
+(defun lilysong-file->ewf (filename)
+ (lilysong-file->wav filename "ewf"))
+
+(defstruct lilysong-compilation-data
+ command
+ makefile
+ buffer
+ songs
+ midi
+ in-parallel)
+(defvar lilysong-compilation-data nil)
+(defun lilysong-sing (songs &optional midi-files in-parallel)
+ (setq lilysong-last-command-args (list songs midi-files in-parallel))
+ (lilysong-update-language)
+ (add-to-list 'compilation-finish-functions 'lilysong-after-compilation)
+ (setq songs (mapcar #'expand-file-name songs))
+ (let* ((makefile (lilysong-makefile (current-buffer) songs midi-files))
+ (command (format "make -f %s" makefile)))
+ (setq lilysong-compilation-data
+ (make-lilysong-compilation-data
+ :command command
+ :makefile makefile
+ :buffer (current-buffer)
+ :songs songs
+ :midi midi-files
+ :in-parallel in-parallel))
+ (save-some-buffers (not compilation-ask-about-save))
+ (unless (equal lilysong-language lilysong-last-language)
+ (mapc #'(lambda (f) (when (file-exists-p f) (delete-file f)))
+ (append songs (mapcar 'lilysong-file->wav midi-files))))
+ (if (lilysong-up-to-date-p makefile)
+ (lilysong-process-generated-files lilysong-compilation-data)
+ (compile command))))
+
+(defun lilysong-up-to-date-p (makefile)
+ (equal (call-process "make" nil nil nil "-f" makefile "-q") 0))
+
+(defun lilysong-makefile (buffer songs midi-files)
+ (let ((temp-file (make-temp-file "Makefile.lilysong-el"))
+ (language lilysong-language))
+ (with-temp-file temp-file
+ (let ((source-files (save-excursion
+ (set-buffer buffer)
+ (lilysong-document-files)))
+ (master-file (save-excursion
+ (set-buffer buffer)
+ (LilyPond-get-master-file)))
+ (lilyfiles (append songs midi-files)))
+ (insert "all:")
+ (dolist (f (mapcar 'lilysong-file->wav (append songs midi-files)))
+ (insert " " f))
+ (insert "\n")
+ (when lilyfiles
+ (dolist (f songs)
+ (insert f " "))
+ (when midi-files
+ (dolist (f midi-files)
+ (insert f " ")))
+ (insert ": " master-file "\n")
+ (insert "\t" LilyPond-lilypond-command " " master-file "\n")
+ (dolist (f songs)
+ (insert (lilysong-file->wav f) ": " f "\n")
+ (insert "\t" LilyPond-synthesize-command " $< " (or language "") "\n"))
+ ;; We can't use midi files in ecasound directly, because setpos
+ ;; doesn't work on them.
+ (let ((lilymidi LilyPond-lilymidi-command)
+ (voice-rate (format "%d" (or (cdr (assoc-if (lambda (key) (string-match key language))
+ LilyPond-voice-rates))
+ 16000))))
+ (when (string-match "%s" lilymidi)
+ (setq lilymidi (replace-match LilyPond-voice-track-regexp nil nil lilymidi)))
+ (dolist (f midi-files)
+ (insert (lilysong-file->wav f) ": " f "\n")
+ (let ((command LilyPond-midi->wav-command)
+ (lilymidi* lilymidi))
+ (when (string-match "%s" command)
+ (setq command (replace-match f nil nil command)))
+ (when (string-match "%t" command)
+ (setq command (replace-match (lilysong-file->wav f) nil nil command)))
+ (when (string-match "%r" command)
+ (setq command (replace-match voice-rate nil nil command)))
+ (when (string-match "%f" lilymidi*)
+ (setq lilymidi (replace-match f nil nil lilymidi*)))
+ (when (string-match "%m" command)
+ (setq command (replace-match lilymidi nil nil command)))
+ (insert "\t" command "\n")))
+ ))))
+ temp-file))
+
+(defun lilysong-after-compilation (buffer message)
+ (let ((data lilysong-compilation-data))
+ (when (and data
+ (equal compile-command
+ (lilysong-compilation-data-command data)))
+ (unwind-protect
+ (when (lilysong-up-to-date-p (lilysong-compilation-data-makefile data))
+ (lilysong-process-generated-files data))
+ (delete-file (lilysong-compilation-data-makefile data))))))
+
+(defun lilysong-process-generated-files (data)
+ (with-current-buffer (lilysong-compilation-data-buffer data)
+ (setq lilysong-last-language lilysong-language))
+ (lilysong-play-files (lilysong-compilation-data-in-parallel data)
+ (lilysong-compilation-data-songs data)
+ (lilysong-compilation-data-midi data)))
+
+
+;;; Playing files
+
+
+(defun lilysong-play-files (in-parallel songs midi-files)
+ (funcall (if LilyPond-use-ecasound
+ 'lilysong-play-with-ecasound
+ 'lilysong-play-with-play)
+ in-parallel songs midi-files))
+
+(defun lilysong-call-play (files)
+ (apply 'start-process "lilysong-el" nil LilyPond-play-command files))
+
+(defun lilysong-play-with-play (in-parallel songs midi-files)
+ (let ((files (mapcar 'lilysong-file->wav (append songs midi-files))))
+ (if in-parallel
+ (dolist (f files)
+ (lilysong-call-play (list f)))
+ (lilysong-call-play files))))
+
+(defun lilysong-make-ewf-files (files)
+ (let ((offset 0.0))
+ (dolist (f files)
+ (let* ((wav-file (lilysong-file->wav f))
+ (length (with-temp-buffer
+ (call-process "ecalength" nil t nil "-s" wav-file)
+ (goto-char (point-max))
+ (forward-line -1)
+ (read (current-buffer)))))
+ (with-temp-file (lilysong-file->ewf f)
+ (insert "source = " wav-file "\n")
+ (insert (format "offset = %s\n" offset))
+ (insert "start-position = 0.0\n")
+ (insert (format "length = %s\n" length))
+ (insert "looping = false\n"))
+ (setq offset (+ offset length))))))
+
+(when (and (featurep 'ecasound)
+ (not (fboundp 'eci-cs-set-param)))
+ (defeci cs-set-param ((parameter "sChainsetup option: " "%s"))))
+
+(defun lilysong-play-with-ecasound (in-parallel songs midi-files)
+ (ecasound)
+ (eci-cs-add "lilysong-el")
+ (eci-cs-select "lilysong-el")
+ (eci-cs-remove)
+ (eci-cs-add "lilysong-el")
+ (eci-cs-select "lilysong-el")
+ (eci-cs-set-param "-z:mixmode,sum")
+ (unless in-parallel
+ (lilysong-make-ewf-files songs)
+ ;; MIDI files should actually start with each of the songs
+ (mapc 'lilysong-make-ewf-files (mapcar 'list midi-files)))
+ (let* ((file->wav (if in-parallel 'lilysong-file->wav 'lilysong-file->ewf))
+ (files (mapcar file->wav (append songs midi-files))))
+ (dolist (f files)
+ (eci-c-add f)
+ (eci-c-select f)
+ (eci-ai-add f))
+ (eci-c-select-all)
+ (eci-ao-add-default)
+ (let* ((n (length songs))
+ (right (if (<= n 1) 50 0))
+ (step (if (<= n 1) 0 (/ 100.0 (1- n)))))
+ (dolist (f songs)
+ (let ((chain (funcall file->wav f)))
+ (eci-c-select chain)
+ (eci-cop-add "-erc:1,2")
+ (eci-cop-add (format "-epp:%f" (min right 100)))
+ (incf right step))))
+ (eci-start)))
+
+
+;;; User commands
+
+
+(defun lilysong-arg->multi (arg)
+ (cond
+ ((not arg)
+ nil)
+ ((or
+ (numberp arg)
+ (equal arg '(4)))
+ t)
+ (t
+ 'all)))
+
+(defun lilysong-command (arg play-midi?)
+ (let* ((multi (lilysong-arg->multi arg))
+ (song-list (lilysong-song-list multi))
+ (midi-list (if play-midi? (lilysong-midi-list multi))))
+ (message "Singing %s" (mapconcat 'identity song-list ", "))
+ (lilysong-sing song-list midi-list (if play-midi? t (listp arg)))))
+
+(defun LilyPond-command-sing (&optional arg)
+ "Sing lyrics of the current LilyPond buffer.
+Without any prefix argument, sing current \\festival* command.
+With the universal prefix argument, ask which parts to sing.
+With a double universal prefix argument, sing all the parts.
+With a numeric prefix argument, ask which parts to sing and sing them
+sequentially rather than in parallel."
+ (interactive "P")
+ (lilysong-command arg nil))
+
+(defun LilyPond-command-sing-and-play (&optional arg)
+ "Sing lyrics and play midi of the current LilyPond buffer.
+Without any prefix argument, sing and play current \\festival* and \\midi
+commands.
+With the universal prefix argument, ask which parts to sing and play.
+With a double universal prefix argument, sing and play all the parts."
+ (interactive "P")
+ (lilysong-command arg t))
+
+(defun LilyPond-command-sing-last ()
+ "Repeat last LilyPond singing command."
+ (interactive)
+ (if lilysong-last-command-args
+ (apply 'lilysong-sing lilysong-last-command-args)
+ (error "No previous singing command")))
+
+(defun LilyPond-command-clean ()
+ "Remove generated *.xml and *.wav files used for singing."
+ (interactive)
+ (flet ((delete-file* (file)
+ (when (file-exists-p file)
+ (delete-file file))))
+ (dolist (xml-file (lilysong-song-list 'all))
+ (delete-file* xml-file)
+ (delete-file* (lilysong-file->wav xml-file)))
+ (mapc 'delete-file* (mapcar 'lilysong-file->wav (lilysong-midi-list 'all)))))
+
+(define-key LilyPond-mode-map "\C-c\C-a" 'LilyPond-command-sing)
+(define-key LilyPond-mode-map "\C-c\C-q" 'LilyPond-command-sing-and-play)
+(define-key LilyPond-mode-map "\C-c\C-x" 'LilyPond-command-clean)
+(define-key LilyPond-mode-map "\C-c\C-z" 'LilyPond-command-sing-last)
+
+(easy-menu-add-item LilyPond-command-menu nil
+ ["Sing Current" LilyPond-command-sing t])
+(easy-menu-add-item LilyPond-command-menu nil
+ ["Sing Selected" (LilyPond-command-sing '(4)) t])
+(easy-menu-add-item LilyPond-command-menu nil
+ ["Sing All" (LilyPond-command-sing '(16)) t])
+(easy-menu-add-item LilyPond-command-menu nil
+ ["Sing Selected Sequentially" (LilyPond-command-sing 1) t])
+(easy-menu-add-item LilyPond-command-menu nil
+ ["Sing and Play Current" LilyPond-command-sing-and-play t])
+(easy-menu-add-item LilyPond-command-menu nil
+ ["Sing and Play Selected" (LilyPond-command-sing-and-play '(4)) t])
+(easy-menu-add-item LilyPond-command-menu nil
+ ["Sing and Play All" (LilyPond-command-sing-and-play '(16)) t])
+(easy-menu-add-item LilyPond-command-menu nil
+ ["Sing Last" LilyPond-command-sing-last t])
+
+
+;;; Announce
+
+(provide 'lilypond-song)
+
+
+;;; lilypond-song.el ends here