cl-portaudioを使った音の入出力

前回の記事でCommon Lisp上でPortAudioが使えるようになりました。

marui.hatenablog.com

今回は音の入出力をやってみます。基本的にはcl-portaudioのドキュメントに載っている「入力と出力を15秒間だけつなげる」というデモプログラムを実行するだけです。ただ、それだけだとつまらないので、フルデュプレックスのオーディオインターフェース(以下デバイス)を自動的に選択するようにしたり、シンタックスシュガー的なマクロを使用しないでやってみようと思います。

バイスリストを見る

まずは前回同様にデバイスリストを見てみます。

(pa:with-audio
  (dotimes (k (pa:get-device-count))
    (let ((dinfo (pa:get-device-info k)))
      (format t "~A: ~A~%" k (pa:device-info-name dinfo)))))

今日はWindowsデスクトップを使用していて、MME、WDM-KS、WASAPI、DirectSound、ASIOといったAPIそれぞれにデバイスが表示されるので、その数もすごいことになっています。

0 : Microsoft Sound Mapper - Input
1 : Digital Audio Interface (Line 6
2 : Microsoft Sound Mapper - Output
3 : スピーカー (Line 6 Sonic Port VX)
4 : スピーカー (Realtek(R) Audio)
5 : プライマリ サウンド キャプチャ ドライバー
6 : Digital Audio Interface (Line 6 Sonic Port VX)
7 : プライマリ サウンド ドライバー
8 : スピーカー (Line 6 Sonic Port VX)
9 : スピーカー (Realtek(R) Audio)
10: ASIO Sonic Port VX
11: スピーカー (Realtek(R) Audio)
12: スピーカー (Line 6 Sonic Port VX)
13: Digital Audio Interface (Line 6 Sonic Port VX)
14: Line in at front panel (Green) (Line in at front panel (Green))
15: Speakers (Realtek HDA Primary output)
16: Mic in at front panel (Pink) (Mic in at front panel (Pink))
17: Line in at rear panel (Blue) (Line in at rear panel (Blue))
18: Line in at rear panel (Green) (Line in at rear panel (Green))
19: Mic in at rear panel (Pink) (Mic in at rear panel (Pink))
20: ステレオ ミキサー (Realtek HD Audio Stereo input)
21: Headphones (Realtek HD Audio 2nd output)
22: Digital Audio Interface (Line 6 Sonic Port VX)
23: Speaker (Line 6 Sonic Port VX)

この中でフルデュプレックスなのは10番の「ASIO Sonic Port VX」だけです。

フルデュプレックスの話

フルデュプレックス(full-duplex)というのは、入力と出力の両方を同時に一台のデバイスで扱うことができるもので、入出力の同期がされるところが特徴です。スピーカだけ、マイクだけ、という片方向デバイスシンプレックス(simplex)と言うのでしょうか。Sonic Port VXでもASIOではなくMMEなどからアクセスするとシンプレックス2つとして扱われるようです。ちなみにハーフデュプレックス(half-duplex)というのもあって、トランシーバーのように片方の人が話しているあいだ、もう一方は聞くだけになるものの、双方向の通信はできる回線のことです。音響関連だとインカムとかトークバックとかはハーフデュプレックスのような動作をしますが、オーディオインターフェースでハーフデュプレックス動作するものは聞いたことがありません。(cl-portaudioのソースコードの中ではシンプレックス動作をするデバイスをハーフデュプレックスと呼んでいますね……)

フルデュプレックスになっていないデバイスだと、入力と出力の同期がとれず、録音・再生のあいだでタイミングのズレが変化する(ある日はズレが短いのに別の日はズレが大きくなるというような)ことがあるので、信頼して使用することができません。さすがに再生しているあいだに音が飛んだりということはないので、再生するだけ・録音するだけであればフルデュプレックスでなくても大丈夫です。ビデオ会議システムのように話すときと聞くときが同時に行われなくてもいいし、そもそもネットワーク回線の遅延が大きくて音楽用途に使いにくい場合にもフルデュプレックスの必要はなさそうです。バックトラックを再生しながら、それに合わせて楽器を録音したいというような、どうしても再生・録音の同期を取りたいというときに使います。(録音と再生のズレがゼロになるというわけではなく、一定のズレになるので、そのズレはあとから補正します)

フルデュプレックスデバイスの判定と抽出

フルデュプレックスになっているかどうかは「入力チャンネル・出力チャンネルがともに1以上になっている」ことで調べられます。前回やったようにデバイス情報も表示させてみましょう。

(pa:with-audio
  (dotimes (k (pa:get-device-count))
    (let ((dinfo (pa:get-device-info k)))
      (when (and (> (pa:device-info-max-input-channels dinfo) 0)
                 (> (pa:device-info-max-output-channels dinfo) 0))
        (format t (concatenate 'string
                               "~A: ~A~%"
                               "            Host API: ~A~%"
                               "       Sampling Freq: ~A Hz~%"
                               "  Number of Channels: ~A in / ~A out~%"
                               "       Input Latency: ~A - ~A ms~%"
                               "      Output Latency: ~A - ~A ms~%~%")
                k
                (pa:device-info-name dinfo)
                (pa:host-api-info-name (pa:get-host-api-info
                                        (pa:device-info-host-api dinfo)))
                (pa:device-info-default-sample-rate dinfo)
                (pa:device-info-max-input-channels dinfo)
                (pa:device-info-max-output-channels dinfo)
                (round (* 1000 (pa:device-info-default-low-input-latency dinfo)))
                (round (* 1000 (pa:device-info-default-high-input-latency dinfo)))
                (round (* 1000 (pa:device-info-default-low-output-latency dinfo)))
                (round (* 1000 (pa:device-info-default-high-output-latency dinfo))))))))

出力されるのは以下のデバイスひとつだけでした。

10: ASIO Sonic Port VX
            Host API: ASIO
       Sampling Freq: 44100.0d0 Hz
  Number of Channels: 2 in / 2 out
       Input Latency: 23 - 46 ms
      Output Latency: 23 - 46 ms

音の入出力

では、これを使って入力音をそのまま出力するプログラムを見ていきましょう。あらかじめいくつかの定数を決めておきます。

(defconstant +frames-per-buffer+ 1024)
(defconstant +sample-rate+ 44100d0)
(defconstant +seconds+ 15)
(defconstant +sample-format+ :float)
(defconstant +num-channels+ 2)

ここまでは(pa:with-audio ... )を使っていました。これは(pa:initialize)(pa:terminate)と同等のことをするものです。いろいろと面倒を見てくれて便利ではあるのですが、初期化にけっこう時間がかかるので、プログラム起動時に(pa:initialize)、プログラム終了時に(pa:terminate)をしてあげても良いでしょう。

(pa:initialize)

(setq adev
      (loop for k below (pa:get-device-count)
            if (and (> (pa:device-info-max-input-channels (pa:get-device-info k)) 0)
                    (> (pa:device-info-max-output-channels (pa:get-device-info k)) 0))
              return k))

ここで、フルデュプレックスのデバイスのうち最初に見つかったもののデバイス番号がadevに入りました。

次に(pa:make-stream-parameters)でストリーム情報を作り、デバイス番号、チャンネル数、サンプルの型、レイテンシーなどを設定します。(pa:with-audio-stream ... )を使ってもいいのですが、ここでは直接ストリームを開いて、閉じて、といったことをしています。

dotimesのところで15秒間ぶんのループを回し、read-streamwrite-streamで入力を出力にそのまま流しています。この部分を無限ループにして音を加工する処理を入れてあげれば、リアルタイム動作するエフェクタを作ることができそうです。

(let ((input-parameters (pa:make-stream-parameters))
      (output-parameters (pa:make-stream-parameters))
      (astream nil))
  (setf (pa:stream-parameters-device input-parameters) adev
        (pa:stream-parameters-channel-count input-parameters) +num-channels+
        (pa:stream-parameters-sample-format input-parameters) +sample-format+
        (pa:stream-parameters-suggested-latency input-parameters) (pa:device-info-default-low-input-latency (pa:get-device-info adev)))
  (setf (pa:stream-parameters-device output-parameters) adev
        (pa:stream-parameters-channel-count output-parameters) +num-channels+
        (pa:stream-parameters-sample-format output-parameters) +sample-format+
        (pa:stream-parameters-suggested-latency output-parameters) (pa:device-info-default-low-output-latency (pa:get-device-info adev)))
  (format t "~%=== Wire on. Will run ~D seconds . ===~%" +seconds+)
  (setf astream (pa:open-stream input-parameters output-parameters +sample-rate+ +frames-per-buffer+ '(:clip-off)))
  (pa:start-stream astream)
  (dotimes (i (round (/ (* +seconds+ +sample-rate+) +frames-per-buffer+)))
    (pa:write-stream astream (pa:read-stream astream)))
  (pa:abort-stream astream)
  (pa:close-stream astream))

(pa:terminate)

最後に終了処理(pa:terminate)を忘れないようにします。手元の環境で試したところ、問題なく動作しました。(実行するときには、ハウリングを避けるためにスピーカからの出力レベルに注意してください)

ゆくゆくは「ファイルを読み込んで、スピーカで再生しながら、マイクから録音して、ファイルに保存」をやりたいです(参考:t-sinさんの発表スライド)。そこまでできれば、昨年作ったTSP信号を使ってインパルス応答の計測ができるようになります。以下の記事でJuliaでやったことを、Common Lispでも実現したいというわけです。

marui.hatenablog.com