2021-01-31

Emacs Lispから音を鳴らす

ふとEmacs Lispマニュアルの Sound Output のところが目に入ったので試してみました。サウンドまわりは大昔に試してあまり良くなかった記憶がありますがその後どれだけ改善されたかな。

マニュアルを読んでみると (play-sound '(sound :file "ファイルへのパス"))(play-sound '(sound :data "サウンドデータ")) で鳴るみたいです。

試しにサイン波でも生成して鳴らしてみましょうか。

対応形式は.wav(RIFF)または.auと書いてあります。.auの方がヘッダーがシンプルなのでそれにしましょう。

44.1KHzモノラルの16-bit PCMのヘッダーは文字列にすると ".snd\x00\x00\x00\x18\xff\xff\xff\xff\x00\x00\x00\x03\x00\x00\xac\x44\x00\x00\x00\x01" の24バイトですね(長さは不明ffffffffとしてあります)。各要素はビッグエンディアンな32-bit整数です。

16-bit PCMの各要素は負号付き整数みたいですね。-32767~0~32768。

うーん、どうしましょう。文字列バッファを先に確保して、そこにバイト列を埋めていく方が速度的に良さそうでしょうか? (適当に文字列結合しまくる方が作るのは楽そうですが)

ひとまず16-bit, 32-bit整数を文字列化する関数を作り……

(defun my-snd-aset-i32 (bytearray index x)
  (aset bytearray index (logand 255 (ash x -24)))
  (aset bytearray (+ index 1) (logand 255 (ash x -16)))
  (aset bytearray (+ index 2) (logand 255 (ash x -8)))
  (aset bytearray (+ index 3) (logand 255 x)))

(defun my-snd-aset-i16 (bytearray index x)
  (aset bytearray index (logand 255 (ash x -8)))
  (aset bytearray (+ index 1) (logand 255 x)))

サイン波を生成する関数を作り……

(defconst my-snd-byte-size-i16 2)
(defconst my-snd-amplitude-i16 32767)

(defun my-snd-aset-sin-wave (dst-buffer dst-index num-samples wavelength)
  (let ((2pi/wavelength (/ (* 2 pi) wavelength))
        (i 0))
    (while (< i num-samples)
      (my-snd-aset-i16 dst-buffer dst-index
                       (truncate (* my-snd-amplitude-i16
                                    ;;周期の誤差をどこまで許容するかよく分からないよね
                                    (sin (* i 2pi/wavelength)))))
      (setq dst-index (+ dst-index my-snd-byte-size-i16))
      (setq i (1+ i))))
  dst-buffer)

試しに440Hzのサイン波を含むauデータを生成する関数を作る。

(defun my-snd-generate-au-sin-wave-440hz-1sec ()
  (let* ((samples-per-sec 44100)
         (seconds 1)
         (num-samples (* seconds samples-per-sec))
         (data-size (* my-snd-byte-size-i16 num-samples))
         (data (make-string (+ 24 data-size) 0)))
    ;; header
    (my-snd-aset-i32 data 0 #x2e736e64)
    (my-snd-aset-i32 data 4 24) ;;Data offset
    (my-snd-aset-i32 data 8 data-size)
    (my-snd-aset-i32 data 12 3) ;;16-bit linear PCM
    (my-snd-aset-i32 data 16 samples-per-sec)
    (my-snd-aset-i32 data 20 1) ;;Channels
    ;; data
    (my-snd-aset-sin-wave data 24 num-samples (/ samples-per-sec 440.0))

    data))

鳴らしてみましょぅ。

(play-sound (list 'sound :data (string-as-unibyte ;;string-as-unibyte必要?
                                (my-snd-generate-au-sin-wave-440hz-1sec))))

あれ、結果は (error "Invalid sound specification") だって。

おかしいなデータが壊れているのかな。試しにファイルに書き出してみる。

(with-temp-file "~/tmp/sin440.au"
  (set-buffer-file-coding-system 'no-conversion)
  (insert (string-as-unibyte (my-snd-generate-au-sin-wave-440hz-1sec))))

プレイヤーで鳴らしたら「ポー」っとちゃんと鳴ります。

それならこのファイルをplay-soundで鳴らしてみる。

(play-sound '(sound :file "~/tmp/sin440.au"))

あれ鳴らない、と思ったらプレイヤーで開いていたからでした。プレイヤーを閉じたらEmacsからちゃんと鳴りました。

play-sound関数はsubr.elにあって、Emacsのサウンドサポートを確認してからplay-sound-internal関数を呼び出しています。play-sound-internal関数はsound.cにあって、最初の方でparse_sound関数を呼び出しています。parse_sound関数を見ると……

#else /* WINDOWSNT */
  /*
    Data is not supported in Windows.  Therefore a
    File name MUST be supplied.
  */
  if (!STRINGP (attrs[SOUND_FILE]))
    {
      return 0;
    }
#endif /* WINDOWSNT */

Windowsでは :data をサポートしていないんだって!


そういえば昔Emacsから音を鳴らしたくて↓こういうのを作ったのでした。

これは何万個もある音声ファイルをテキストファイルと付き合わせて確認する作業で活躍したのでした。今でも動くのかは不明。