2011年4月3日日曜日

Emacs でキータイプのたびに音を鳴らしてみる on Windows7

最近、「Ommwriter」というテキストエディタ(?)の存在を知った。

デモ動画を見て真っ先に「いいなあ」と思ったのは、キーをタイプするたびに気持ちいい音が出るところ。これならEmacs でもできるだろうと思って試してみた。

環境は次のとおり。

OS
Windows 7 Home Premium
Emacs
GNU Emacs 23.3.1 (i386-mingw-nt6.1.7600)

Emacsから外部プロセスを使って音を出す

Emacsで任意の音声ファイルを再生することはできなそうなので、外部のコマンドを利用して鳴らす方針にする。とはいえ、Windows環境だからコマンドラインから音声を再生するコマンドがあるのかがわからない。Linux環境であればコマンドもいろいろありそうだが…。

しばらくWebサイトを漂流した結果、QuickView 2.52、MPXPLAY 1.50、(Free)CDP 1.1/2.1、OpenCP 2.60、XTC-PLAY 0.97c などがあるとわかった。試した結果、"Mpxplay" が動作したのでこれにした。

Mpxplayのインストール

Welcome to the PDSoft Homepage から、"Mpxplay v1.57 for Win32"をダウンロードして、C:/Program Files (x86)/MPXP157W/に置く。

音源

次に、それっぽい音源を用意しなければならない。

クリック音みたいな短い音で自由に使える素材がないかなあと探した結果、このサイト「全ての効果音・SE1|フリー音素材01SoundEarth」がみつかった。ここから「ボタン音・ワンショット系」のmp3をとりあえず10個(b_001.mp3 から b_010.mp3 まで)ダウンロードして、~/.emacs.d/mp3/ に配置。

以上で必要なものがそろったので、次はEmacsを使っての実験。以下のようなコードを*scratch*バッファで実行してみて音が出ればOK。

(let ((sndfile
       (concat (getenv "HOME") "/.emacs.d/mp3/" "b_001.mp3")))
  (start-process
   "my-test-process"
   nil
   "C:/Program Files (x86)/MPXP157W/MPXPLAY" "-f0" "-xel" sndfile))

ここで、start-procesが非同期に外部コマンドを実行するための組み込み関数。また、外部コマンドであるMPXPLAYに指定しているオプションの意味は、以下のとおり(Mpxplayに付属のREADME.TXTより抜粋)。

-f0 : no screen output (some warning/error messages are not displayed in this mode (ie: serial-control and LCD errors))
-xel : exit at end of (play)list (has effect in directory browser and in jukebox queue too)

Mpxplayのプロセスが増える問題(.iniファイルを消すと解決)

いまいち原因がわからないのだが、Mxplayを続けて実行すると音が出なくなり、プロセスが残るという現象が出て困った。

具体的には "Warning: mpxplay.ini is read-only!"というメッセージを出力されるので、mxplay.ini を思いきって消したら解決した。この現象はEmacsを経由しない場合、つまりWindowsのコマンドプロンプトからでも発生したので、Mxplay自身の問題というか仕様なのだろう。

キー押下で音を出す設定

Emacsでは、a や b などの普通のキーを押したときにself-insert-commandというコマンドが走る。よって、このコマンドに音を出す処理を advice として追加してやれば済みそうなのだが、実はうまくいかない(self-insert-commandは特別な扱いになっているようだ)。

そこで、self-insert-commandを自作の関数でラップし、キー押下時にラッパー関数のほうが実行されるようにする。さらに、ラッパー関数に advice として音を出す処理を追加する。

elispコード

以下を*scratch*バッファで実行すると、音が出るようになるはず。

(defcustom my-sound-dir
  (concat (getenv "HOME") "/.emacs.d/mp3/")
  "音声ファイルの場所")

;;; Can't advise SELF-INSERT-COMMAND, so create a wrapper procedure.
(defun self-insert-wrapper (n)
  (interactive "p")
  (self-insert-command n))

;;; Advise SELF-INSERT-WRAPPER to execute  after every keypress
(defadvice self-insert-wrapper (after ad-self-insert-wrapper activate)
  "入力された文字に応じて音声ファイルを選び、外部プロセスを呼ぶ"
  (let ((sndfile
         (concat my-sound-dir "b_" (format "%03d" (1+ (% (char-before) 10))) ".mp3")))
    (start-process
     "proc-sound"
     nil
     "C:/Program Files (x86)/MPXP157W/MPXPLAY" "-f0" "-xel" sndfile)))

;; Remap SELF-INSERT-COMMAND to be SELF-INSERT-WRAPPER.
(global-set-key [remap self-insert-command] 'self-insert-wrapper)

実際に使えるか、という問題

上のコードを実行して、実際に音を出しながらキーをタイプしてみたところ、ときどき砂時計が出てきてしまった… 残念ながら、実用に耐えない、という結論(とりあえず)。


ビープ音(ベル)だけ変える場合

ビープ音というのは Ctrl-g を押したときなどに鳴る音。これは頻繁に鳴るわけではないので、性能の問題は無視できるだろう。下記のコードを実行すれば、ランダムなピープ音が鳴るようになる。

(defcustom my-sound-dir
  (concat (getenv "HOME") "/.emacs.d/mp3/")
  "音声ファイルの場所")

(setq ring-bell-function
      (lambda ()
        (let ((sndfile (concat my-sound-dir "b_" (format "%03d" (1+ (random 10))) ".mp3")))
          (start-process
           "proc-sound"
           nil
           "C:/Program Files (x86)/MPXP157W/MPXPLAY" "-f0" "-xel" sndfile))))

ここで重要な組み込み関数/変数は、ring-bell-function。これにセットしたlambda式は、ビープ音を鳴らすタイミングで実行される。

0 件のコメント:

コメントを投稿