はじめに
昨年に私は、Emacs の起動時と終了時に「“プロ生ちゃん”のシステムボイス」音源を再生する emacs lisp を公開した。
【新版】Emacs の起動時と終了時に「“プロ生ちゃん”のシステムボイス」音源を再生する - blechmusikの日記
先月末になると、2014年版の「“プロ生ちゃん”のシステムボイス」音源が新たに公開された。今回の更新では様々な音源が多数追加されており、以前よりも利用しやすい音源集となっている。
- 上坂すみれが声を担当する“プロ生ちゃん”のシステムボイス第2弾が無償公開 - 窓の杜
- プロ生ちゃんこと暮井 慧(CV 上坂すみれ)の新ボイス公開のお知らせ! | プログラミング生放送
- 暮井 慧(プロ生ちゃん/CV:上坂すみれ)ボイス収録プロジェクト | CROSSクラウドファンディング
- 暮井 慧 ボイス(CV 上坂すみれ) | プログラミング生放送
そこで、2014年版のプロ生ちゃんシステムボイスに対応するよう、 emacs lisp を全面的に書き直してみた。なお、音源再生用のコマンドである autohotkey のスクリプトは、前回の内容と同様であるから、そのまま再掲する。
なお、ここでいう Emacs とは、ここでは Windows 環境で動作する NTEmacs を指す。
ファイルの置き場所
- 圧縮ファイルを展開した音源ファイル一式は、 ~/pronama-voice-2014-version/ 以下に、ディレクトリの構造を保ちながら置く
- 暮井 慧 ボイス(CV 上坂すみれ) | プログラミング生放送で配布されている、公式のワードリスト(.xslx)を、.csvに書き出し、~/pronama-voice-2014-version/ に置く
これより、ディレクトリの構造は以下の通りになる。wav ディレクトリには*.wavが存在している。
~/pronama-voice-2014-version/ |-- kei_voice_201308 | `-- kei1 | `-- wav |-- kei_voice_201404 | `-- kei2 | `-- wav `-- word_list.csv
AutoHotkeyを使って音源を再生する
AutoHotkey の soundplay コマンドを使うことで、指定したメディアファイルを再生する。Emacs 側から渡されるパスにスラッシュが含まれている可能性を想定して、スラッシュをバックスラッシュに書き換えることにした。
for i,v in get_command_argument() { filename := slash_to_backslash(v) SoundPlay, %Filename%, 1 } ExitApp return slash_to_backslash(string){ StringReplace, string, string, /, \ return string } get_command_argument(){ global local arg_list := [] Loop,100 { if ("" != Trim(%A_Index%)){ arg_list.insert(%A_Index%) } } return arg_list }
次に行うのは、 Emacs lisp を使って上記のAutoHotkeyスクリプトを実行し、音源を再生することだ。file-truename関数を使ってパスを展開することにしたから、これによってAutoHotkey のスクリプト側でシンボリック・リンクを張ったファイルについても問題なく取り扱える。
(defvar soundplay-command (file-truename "~/soundplay/soundplay.exe")) (defun play-sound (sound-file) (with-temp-buffer (start-process "sync" nil soundplay-command sound-file )))
だが、これでは Emacs の終了時に音源を再生し続けることができない。start-process の実行中にEmacsを終了させるとそこで音源の再生が中断してしまうのだ。この問題に対処するには start-process ではなく start-process-shell-command を使うことになる。
ついでに、file-truename関数を play-sound 内で利用しよう。
(defun play-sound (sound-file) (start-process-shell-command "async" nil soundplay-command (file-truename sound-file)))
これで音源を再生する準備が整った。
複数の音源ファイルをランダムに選択して、再生する
(defun randomly-select (l) (nth (random (length l)) l)) (defun play-sound-randomly (sound-files) (when sound-files (play-sound (randomly-select sound-files))))
モダンライブラリを使用する
(require 'f) (require 's) (require 'dash)
音声ファイルのパスを取得する
指定したディレクトリ以下から、.wavファイルのパスを収集する。
(setq *voice-directory* "~/pronama-voice-2014-version/") (defun get-kei-voice-filename-list (directory) (mapcar (lambda (x) (f-short x)) (f--files directory (equal (f-ext it) "wav") t))) (setq *kei-voice-filename-list* (get-kei-voice-filename-list *voice-directory*))
ワードリストの番号とテキストを取得する
ヘッダー行には、ワードリストの番号の部分に "no" という文字列が含まれているので、その行を取り除く。
(setq *voice-word-list* "~/pronama-voice-2014-version/word_list.csv") (defun get-kei-voice-ID-text-list (csv-file) (mapcar #'string->ID-text (read-lines csv-file))) (defun read-lines (file) (when (file-exists-p file) (with-temp-buffer (insert-file-contents file) (split-string (buffer-string) "\n" t) ))) (defun string->ID-text (string) (destructuring-bind (ID text &rest tag) (split-string string ",") (cons ID text))) (setq *kei-voice-ID-text* ;; ヘッダー行を削除する (--remove (string= (car it) "no") (get-kei-voice-ID-text-list *voice-word-list*)))
ワードリストの番号から、ファイル名、ファイルパス、テキストを取得する
;; ID -> filename, filepath, text ;; ID が A から始まるファイル名は kei_voice_*.wav ;; ID が B から始まるファイル名は kei2_voice_*.wav (defun ID->alphabet-number (ID) "ex.) B095 => (B 095), A095-1 => (A 095_1)" (cons (substring ID 0 1) (s-replace "-" "_" (substring ID 1)))) (defun ID->filename (ID) "ex.) B095 => kei2_voice_095.wav, A095-1 => kei_voice_095_1.wav" (destructuring-bind (a-or-b . number) (ID->alphabet-number ID) (format "kei%s_voice_%s.wav" (if (string= "A" a-or-b) "" "2") number))) (defun ID->filepath (ID) (when *kei-voice-filename-list* (loop for f in *kei-voice-filename-list* when (s-match (ID->filename ID) f) return f))) (defun ID->text (ID-string) (when *kei-voice-ID-text* (cdr (assoc ID-string *kei-voice-ID-text*))))
ワードリストのテキストから文字列を検索して、番号、ファイルパス、テキストを取得する
;; string -> ID, filepath, text (defun string->ID (string) (when *kei-voice-ID-text* (loop for (ID . text) in *kei-voice-ID-text* when (s-match string text) collect ID))) (defun string->filepath (string) (-map 'ID->filepath (string->ID string))) (defun string->text (string) (-map 'ID->text (string->ID string)))
完全版と断片版の、ワードリストの番号とファイルパスを取得する
;; filter with short or full version (defun filter-ID-short-version (l) (--filter (s-match "-" it) l)) (defun filter-ID-full-version (l) (--remove (s-match "-" it) l)) (defun filter-filepath-short-version (l) (--filter (s-match "_[0-9]\\." it) l)) (defun filter-filepath-full-version (l) (--remove (s-match "_[0-9]\\." it) l))
文字列のリストから、番号、テキスト、ファイルパス、断片版ではないファイルパスを取得する
(defun mapcar-flatten (fn l) (-flatten (mapcar fn l))) (defun strings->xs (fn l) (mapcar-flatten fn (-filter 'stringp l))) (defun strings->texts (l) (strings->xs #'string->text l)) (defun strings->IDs (l) (strings->xs #'string->ID l)) (defun strings->filepaths (l) (strings->xs #'string->filepath l)) (defun strings->filepaths-full-version (l) (filter-filepath-full-version (strings->filepaths l)))
0時~3時のように、特定の時間帯の判定処理用マクロを作成する
(defmacro <=&&<= (a b c) `(and (<= ,a ,b) (<= ,b ,c)))
特定の時間に対応する時報用テキストを再生するために、時報のテキストの一部を取得する
(defun get-text-of-time-signal (hour) (assoc hour '( (0 . "0時だ~") (1 . "1時~") (2 . "2時! えっ?") (3 . ")3時") (4 . ")4時…") (5 . ")5時~") (6 . "6時(") (7 . "7時! さぁ") (8 . "8時! ほら") (9 . "9時! はりきって") (10 . "10時!") (11 . "11時!") (12 . "12時!") (13 . "13時(") (14 . "14時!") (15 . ")15時…") (16 . "16時!") (17 . "17時!") (18 . "18時!") (19 . "19時!") (20 . "20時!") (21 . "21時!") (22 . "22時!") (23 . "23時!") )))
指定したテキストに対応する音源を再生する
(defun get-hour () (string-to-number (format-time-string "%H" (current-time)))) (defun play-voice-startup () (play-sound-randomly (strings->filepaths-full-version `("はじめるよ" "スタート" ,(get-text-of-time-signal (get-hour)) ,(when (<=&&<= 5 (get-hour) 9) "おはよう") ,(when (<=&&<= 11 (get-hour) 14) "こんにちは") ,(when (<=&&<= 20 (get-hour) 23) "こんばんは") ,(when (<=&&<= 0 (get-hour) 3) "夜はまだまだ") ,(when (<=&&<= 0 (get-hour) 3) "夜更かし") ,(when (<=&&<= 0 (get-hour) 3) "寝静まる") ,(when (<=&&<= 3 (get-hour) 5) "早起き") ,(when (<=&&<= 2 (get-hour) 4) "寝たほうが") )))) (defun play-voice-kill () (play-sound-randomly (strings->filepaths-full-version `("またきてね" "おわり、だよ" "はい、しゅーりょー" ,(when (or (< (get-hour) 3) (< 20 (get-hour))) "おやすみ") ,(when (<=&&<= 2 (get-hour) 4) "眠くなってきた") ))))
Emacs の起動時と終了時に、音源を再生する
これで設定が完了する。
(add-hook 'emacs-startup-hook 'play-voice-startup) (add-hook 'kill-emacs-hook (lambda () (when (file-exists-p soundplay-command) (play-voice-kill) (sleep-for 0.5) ) ))
helm を使い、音源の再生やテキストの取得を行う
最後に、helm を使って任意のテキストを探したり、音源を再生しよう。
(global-set-key (kbd "C-: C-j C-p") 'helm-pronama-voice-files) (defun helm-pronama-voice-files () (interactive) (let ((helm-candidate-number-limit (length *kei-voice-ID-text*))) (helm (list helm-c-source-pronama-voice-files)))) (setq helm-c-source-pronama-voice-files `((name . "pronama voice files") (candidates . ,(-map #'car *kei-voice-ID-text*)) (candidate-transformer . (lambda (candidates) (mapcar (function (lambda(arg) (cons (format "%-6s: %s" arg (ID->text arg)) arg))) candidates))) (action . (("play sound" . (lambda (ID) (play-sound (ID->filepath ID)))) ("insert filename" . (lambda (ID) (insert-string (ID->filename ID)))) ("insert text" . (lambda (ID) (insert-string (ID->text ID)))) ("insert ID" . insert-string) ))))
おわりに
このエントリーでは、Emacs の起動時と終了時に「“プロ生ちゃん”のシステムボイス」音源を再生する手順を説明した。昨年と同様に省略したが、任意のプログラムのコンパイルや実行、デバッグといった場面において、当該音源を活用しうる。また、今回の配布分の音源には、カットやコピー、ペーストといった、キーを複数個組み合わせて行う、一般的な処理の読み上げが収録されたので、利用しやすい音源集になったと評価できる。配布されている音源を活用して、よりよい開発環境を構築したいものだ。
emacs lisp のまとめ
ここまでのemacs lisp の設定を纏めるとこうなる。
(setq *voice-directory* "~/pronama-voice-2014-version/") (setq *voice-word-list* "~/pronama-voice-2014-version/word_list.csv") ;; ====================================================================== (require 'f) (require 's) (require 'dash) ;; ====================================================================== (defvar soundplay-command (file-truename "~/soundplay/soundplay.exe")) (defun play-sound (sound-file) (start-process-shell-command "async" nil soundplay-command (file-truename sound-file))) (defun randomly-select (l) (nth (random (length l)) l)) (defun play-sound-randomly (sound-files) (when sound-files (play-sound (randomly-select sound-files)))) ;; ====================================================================== (defun get-kei-voice-filename-list (directory) (mapcar (lambda (x) (f-short x)) (f--files directory (equal (f-ext it) "wav") t))) (defun get-kei-voice-ID-text-list (csv-file) (mapcar #'string->ID-text (read-lines csv-file))) (defun read-lines (file) (when (file-exists-p file) (with-temp-buffer (insert-file-contents file) (split-string (buffer-string) "\n" t) ))) (defun string->ID-text (string) (destructuring-bind (ID text &rest tag) (split-string string ",") (cons ID text))) (setq *kei-voice-filename-list* (get-kei-voice-filename-list *voice-directory*)) (setq *kei-voice-ID-text* ;; ヘッダー行を削除する (--remove (string= (car it) "no") (get-kei-voice-ID-text-list *voice-word-list*))) ;; ====================================================================== ;; ID -> filename, filepath, text ;; ID が A から始まるファイル名は kei_voice_*.wav ;; ID が B から始まるファイル名は kei2_voice_*.wav (defun ID->alphabet-number (ID) "ex.) B095 => (B 095), A095-1 => (A 095_1)" (cons (substring ID 0 1) (s-replace "-" "_" (substring ID 1)))) (defun ID->filename (ID) "ex.) B095 => kei2_voice_095.wav, A095-1 => kei_voice_095_1.wav" (destructuring-bind (a-or-b . number) (ID->alphabet-number ID) (format "kei%s_voice_%s.wav" (if (string= "A" a-or-b) "" "2") number))) (defun ID->filepath (ID) (when *kei-voice-filename-list* (loop for f in *kei-voice-filename-list* when (s-match (ID->filename ID) f) return f))) (defun ID->text (ID-string) (when *kei-voice-ID-text* (cdr (assoc ID-string *kei-voice-ID-text*)))) ;; ====================================================================== ;; string -> ID, filepath, text (defun string->ID (string) (when *kei-voice-ID-text* (loop for (ID . text) in *kei-voice-ID-text* when (s-match string text) collect ID))) (defun string->filepath (string) (-map 'ID->filepath (string->ID string))) (defun string->text (string) (-map 'ID->text (string->ID string))) ;; ====================================================================== ;; filter with short or full version (defun filter-ID-short-version (l) (--filter (s-match "-" it) l)) (defun filter-ID-full-version (l) (--remove (s-match "-" it) l)) (defun filter-filepath-short-version (l) (--filter (s-match "_[0-9]\\." it) l)) (defun filter-filepath-full-version (l) (--remove (s-match "_[0-9]\\." it) l)) ;; ====================================================================== (defun mapcar-flatten (fn l) (-flatten (mapcar fn l))) (defun strings->xs (fn l) (mapcar-flatten fn (-filter 'stringp l))) (defun strings->texts (l) (strings->xs #'string->text l)) (defun strings->IDs (l) (strings->xs #'string->ID l)) (defun strings->filepaths (l) (strings->xs #'string->filepath l)) (defun strings->filepaths-full-version (l) (filter-filepath-full-version (strings->filepaths l))) ;; ====================================================================== (defun get-hour () (string-to-number (format-time-string "%H" (current-time)))) ;; ====================================================================== (defmacro <=&&<= (a b c) `(and (<= ,a ,b) (<= ,b ,c))) (defun get-text-of-time-signal (hour) (assoc hour '( (0 . "0時だ~") (1 . "1時~") (2 . "2時! えっ?") (3 . ")3時") (4 . ")4時…") (5 . ")5時~") (6 . "6時(") (7 . "7時! さぁ") (8 . "8時! ほら") (9 . "9時! はりきって") (10 . "10時!") (11 . "11時!") (12 . "12時!") (13 . "13時(") (14 . "14時!") (15 . ")15時…") (16 . "16時!") (17 . "17時!") (18 . "18時!") (19 . "19時!") (20 . "20時!") (21 . "21時!") (22 . "22時!") (23 . "23時!") ))) (defun play-voice-startup () (play-sound-randomly (strings->filepaths-full-version `("はじめるよ" "スタート" ,(get-text-of-time-signal (get-hour)) ,(when (<=&&<= 5 (get-hour) 9) "おはよう") ,(when (<=&&<= 11 (get-hour) 14) "こんにちは") ,(when (<=&&<= 20 (get-hour) 23) "こんばんは") ,(when (<=&&<= 0 (get-hour) 3) "夜はまだまだ") ,(when (<=&&<= 0 (get-hour) 3) "夜更かし") ,(when (<=&&<= 0 (get-hour) 3) "寝静まる") ,(when (<=&&<= 3 (get-hour) 5) "早起き") ,(when (<=&&<= 2 (get-hour) 4) "寝たほうが") )))) (defun play-voice-kill () (play-sound-randomly (strings->filepaths-full-version `("またきてね" "おわり、だよ" "はい、しゅーりょー" ,(when (or (< (get-hour) 3) (< 20 (get-hour))) "おやすみ") ,(when (<=&&<= 2 (get-hour) 4) "眠くなってきた") )))) (add-hook 'emacs-startup-hook 'play-voice-startup) (add-hook 'kill-emacs-hook (lambda () (when (file-exists-p soundplay-command) (play-voice-kill) (sleep-for 0.5) ) )) ;; helm (global-set-key (kbd "C-: C-j C-p") 'helm-pronama-voice-files) (defun helm-pronama-voice-files () (interactive) (let ((helm-candidate-number-limit (length *kei-voice-ID-text*))) (helm (list helm-c-source-pronama-voice-files)))) (setq helm-c-source-pronama-voice-files `((name . "pronama voice files") (candidates . ,(-map #'car *kei-voice-ID-text*)) (candidate-transformer . (lambda (candidates) (mapcar (function (lambda(arg) (cons (format "%-6s: %s" arg (ID->text arg)) arg))) candidates))) (action . (("play sound" . (lambda (ID) (play-sound (ID->filepath ID)))) ("insert filename" . (lambda (ID) (insert-string (ID->filename ID)))) ("insert text" . (lambda (ID) (insert-string (ID->text ID)))) ("insert ID" . insert-string) ))))