音を再生しながら録音する(フルデュプレックス版)

昨年末にcl-portaudioを使ってCommon Lispで音の入出力をする記事を書きました。

marui.hatenablog.com

この記事ではwith-default-audio-streamを使っていたため、デフォルトで指定されているオーディオデバイスが使われていました。どのデバイスがデフォルトになるかはprint-devicesを使えば分かります。手元のWindows環境で実行すると以下のように表示されました。device 1番と5番のところ、[Default Input] / [Default Output]と表示されているデバイスがデフォルトです(コントロールパネルの「サウンド」で指定しているものがデフォルトになるみたいです)。

CL-USER> (pa:print-devices)
PortAudio version number = 1246976
PortAudio version text = PortAudio V19.7.0-devel, revision unknown
Number of devices = 40
---------------------- device 0
Name                        = Microsoft Sound Mapper - Input
Host API                    = MME
Max inputs = 2, Max outputs = 0
Default low input latency   =   0.0900
Default low output latency  =   0.0900
Default high input latency  =   0.1800
Default high output latency =   0.1800
Default sample rate         = 44100.0000
Supported standard sample rates
 for half-duplex float 32 bit 2 channel input = 
 8000.00,  9600.00, 11025.00, 12000.00, 16000.00, 22050.00, 24000.00, 32000.00, 44100.00, 48000.00, 88200.00, 96000.00, 192000.00, 
---------------------- device 1
[ Default Input ]
Name                        = Analog (1+2) (RME Babyface Pro)
Host API                    = MME
Max inputs = 2, Max outputs = 0
Default low input latency   =   0.0900
Default low output latency  =   0.0900
Default high input latency  =   0.1800
Default high output latency =   0.1800
Default sample rate         = 44100.0000
Supported standard sample rates
 for half-duplex float 32 bit 2 channel input = 
 8000.00,  9600.00, 11025.00, 12000.00, 16000.00, 22050.00, 24000.00, 32000.00, 44100.00, 48000.00, 88200.00, 96000.00, 192000.00, 

(略)

---------------------- device 5
[ Default Output ]
Name                        = Analog (3+4) (RME Babyface Pro)
Host API                    = MME
Max inputs = 0, Max outputs = 2
Default low input latency   =   0.0900
Default low output latency  =   0.0900
Default high input latency  =   0.1800
Default high output latency =   0.1800
Default sample rate         = 44100.0000
Supported standard sample rates
 for half-duplex float 32 bit 2 channel output = 
 8000.00,  9600.00, 11025.00, 12000.00, 16000.00, 22050.00, 24000.00, 32000.00, 44100.00, 48000.00, 88200.00, 96000.00, 192000.00, 

(略)

---------------------- device 22
[ Default #<HOST-API-INFO {110848FFA3}> Input,[ Default #<HOST-API-INFO {11084B7FD3}> Output ]
Name                        = ASIO Fireface USB
Host API                    = ASIO
Max inputs = 12, Max outputs = 12
Default low input latency   =   0.0232
Default low output latency  =   0.0232
Default high input latency  =   0.0232
Default high output latency =   0.0232
Default sample rate         = 44100.0000
Supported standard sample rates
 for half-duplex float 32 bit 12 channel input = 
32000.00, 44100.00, 48000.00, 88200.00, 96000.00, 192000.00, 
Supported standard sample rates
 for half-duplex float 32 bit 12 channel output = 
32000.00, 44100.00, 48000.00, 88200.00, 96000.00, 192000.00, 
Supported standard sample rates
 for full-duplex float 32 bit 12 channel input, 12 channel output = 
32000.00, 44100.00, 48000.00, 88200.00, 96000.00, 192000.00, 
---------------------- device 23
Name                        = ASIO Sonic Port VX
Host API                    = ASIO
Max inputs = 2, Max outputs = 2
Default low input latency   =   0.0029
Default low output latency  =   0.0029
Default high input latency  =   0.0464
Default high output latency =   0.0464
Default sample rate         = 44100.0000
Supported standard sample rates
 for half-duplex float 32 bit 2 channel input = 
44100.00, 48000.00, 
Supported standard sample rates
 for half-duplex float 32 bit 2 channel output = 
44100.00, 48000.00, 
Supported standard sample rates
 for full-duplex float 32 bit 2 channel input, 2 channel output = 
44100.00, 48000.00, 

(略)

音を再生しながら録音するような場面では、入出力が同期していることが理想です。WindowsだとASIO対応のデバイスを使用したいところですが、少なくともフルデュプレックスになっていることが望ましいです。上記の出力結果だと22番と23番にfull-duplexという記載がありますね。

フルデュプレックスになっているデバイスであることを自前のプログラムで確認するには、入力・出力ともにチャンネルの割り当てがあるかを見るとよいです。以下のように、全デバイスを次々と見て行って、入出力両方に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))))))))

実行すると以下のような表示です。ASIOでアクセスできるデバイスが見つかりました。

22: ASIO Fireface USB
            Host API: ASIO
       Sampling Freq: 44100.0d0 Hz
  Number of Channels: 12 in / 12 out
       Input Latency: 23 - 23 ms
      Output Latency: 23 - 23 ms

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

最初に見つかったものを使用できるようにしましょう。デバイス番号(上記なら22番)が戻ってくる関数にします。

(defun get-first-duplex-devno ()
  (pa:with-audio
    (let ((devcount (pa:get-device-count)))
      (loop for k below devcount
            do (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))
                   (return (values k (pa:device-info-name dinfo)))))))))

with-default-audio-streamで入出力チャンネルを両方1以上に指定した場合にはフルデュプレックスのデバイスをつかんでくれると嬉しいのですが、そうはなっていないようです。なので、指定したデバイスのストリームを開くようにします。上記で得られたデバイス番号をはじめとするパラメータ群を準備し、サンプリング周波数を指定してストリームを開きます。あとは前回記事と同様に、ループを回して再生~録音をしてあげればOKです。

(defun play-and-record-duplex (x fs &optional (frames-per-buffer 1024))
  (pa:with-audio
    (multiple-value-bind (devno devname) (get-first-duplex-devno)
      ;;(format t "Using ~A (~A) for audio in/out.~%" devname devno)
      (let ((input-parameters (pa:make-stream-parameters))
            (output-parameters (pa:make-stream-parameters)))
        (setf (pa:stream-parameters-device input-parameters) devno
              (pa:stream-parameters-channel-count input-parameters) 1
              (pa:stream-parameters-sample-format input-parameters) :float
              (pa:stream-parameters-suggested-latency input-parameters) (pa:device-info-default-low-input-latency (pa:get-device-info devno))
              (pa:stream-parameters-device output-parameters) devno
              (pa:stream-parameters-channel-count output-parameters) 1
              (pa:stream-parameters-sample-format output-parameters) :float
              (pa:stream-parameters-suggested-latency output-parameters) (pa:device-info-default-low-output-latency (pa:get-device-info devno)))
        (pa:with-audio-stream (st input-parameters output-parameters
                               :sample-rate (coerce fs 'double-float)
                               :frames-per-buffer frames-per-buffer)
          (let* ((recorded (make-array (* 1 frames-per-buffer (ceiling (/ (length x) frames-per-buffer))) :element-type 'float :initial-element 0.0))
                 (playbuf (make-array frames-per-buffer :element-type 'float :initial-element 0.0))
                 (playing (make-array (length recorded) :element-type 'float :initial-element 0.0)))
            (dotimes (i (length x))
              (setf (aref playing i) (aref x i)))
            (dotimes (i (ceiling (/ (length recorded) frames-per-buffer)))
              (loop for k from 0 below frames-per-buffer
                    do (setf (aref playbuf k) (aref playing (+ (* i frames-per-buffer) k))))
              (pa:write-stream st playbuf)
              (let ((recbuf (pa:read-stream st)))
                (loop for k from 0 below frames-per-buffer
                      do (setf (aref recorded (+ (* i frames-per-buffer) k))
                               (aref recbuf k)))))
            ;;(format t "Done playback/recording.")
            recorded))))))