Common Lisp用のwavread

(追記:関連記事として「音ファイルを書き出すwavwrite関数(Lisp Advent Calendar 2022) - 丸井綜研」を書きました)

9月に「Common Lispで音ファイルを読み込む - 丸井綜研」というエントリでcl-wavとbodge-sndfileを試用しました。Apple Siliconではbodge-sndfileは使えないということで、cl-wavを使っています。ただ、チャンク形式のままのデータが戻ってくるので、もう少し便利に使えるようにMatlab風のwavreadを書いてみました。

WAVファイルの中身を表示してしまうと(ファイルが長いときに)大変なことになるので、REPLの中での表示行数を制限します。ここでは10行までを表示することにしました。

;; limit the number of lines to be displayed in REPL
(setf *print-lines* 10)

その上で以下のように実行して、EBass_E1.wavの中身を読み込んでsnd変数に代入します。

(ql:quickload :cl-wav)

;; read a sound file
(setf snd (wav:read-wav-file
           "EBass_E1.wav"
           :chunk-data-reader
           (wav:wrap-data-chunk-data-samples-reader)))

sndを表示すると次のような構造になっています。RIFF、fmt、dataの三つのチャンクが入っていることがわかります。

CL-USER> snd
((:CHUNK-ID "RIFF" :CHUNK-DATA-SIZE 2880036 :FILE-TYPE "WAVE")
 (:CHUNK-ID "fmt " :CHUNK-DATA-SIZE 16 :CHUNK-DATA
  (:COMPRESSION-CODE 1 :NUMBER-OF-CHANNELS 1 :SAMPLE-RATE 48000
   :AVERAGE-BYTES-PER-SECOND 96000 :BLOCK-ALIGN 2 :SIGNIFICANT-BITS-PER-SAMPLE
   16))
 (:CHUNK-ID "data" :CHUNK-DATA-SIZE 2880000 :CHUNK-DATA
  #(0.0 0.0 0.0 3.0517578e-5 -3.0517578e-5 3.0517578e-5 0.0 -3.0517578e-5
    9.1552734e-5 -9.1552734e-5 1.5258789e-4 -1.5258789e-4 1.5258789e-4
    -9.1552734e-5 9.1552734e-5 0.0 0.0 3.0517578e-5 6.1035156e-5 -6.1035156e-5
    1.5258789e-4 -9.1552734e-5 9.1552734e-5 0.0 3.0517578e-5 6.1035156e-5 ..)))

WAVファイルの中身を分析するのに使うのは各サンプルの振幅値("data"チャンク内の*CHUNK-DATA)と標本化周波数("fmt "チャンク内の:SAMPLE-RATE)でしょうか。さて、このファイルには3つのチャンクしか入っていませんが、ProToolsなどのDAWで出力したWAVファイルには拡張チャンクが入っていますし、放送業界で使われているBroadcast Wave Formatにもさまざまな拡張チャンクが入っています。なので、2つ目のチャンクリスト(fmt)の中の12番目の要素(16)というように場所を決め打ちした取り出し方をすると問題が生じます。チャンクIDで指定したチャンクのリストを取り出すことと、そのチャンク内のキーワードを検索してキーワードの直後にある値を取り出さないといけません。各チャンクは属性リスト(property list)になっているので、getfを使って簡単に取り出すことができます。

(defun wav-select-chunk (id chnks)
  "Extract only the chunk with specified chunk ID.
   Example: (wav-select-chunk \"fmt \" snd)"
  (car (member id chnks
               :test #'equal
               :key #'(lambda (x) (getf x :chunk-id)))))

(defun wav-select-key (key chnk)
  "Extract only the data with specified keyword.
   Example: (wav-select-key :sample-rate fmt)"
  (getf chnk key))

これらを使うと以下のようにフォーマットチャンクだけを取り出したり、その中の一つの値を取り出したりできるようになります。

CL-USER> (wav-select-chunk "fmt " snd)
(:CHUNK-ID "fmt " :CHUNK-DATA-SIZE 16 :CHUNK-DATA
 (:COMPRESSION-CODE 1 :NUMBER-OF-CHANNELS 1 :SAMPLE-RATE 48000
  :AVERAGE-BYTES-PER-SECOND 96000 :BLOCK-ALIGN 2 :SIGNIFICANT-BITS-PER-SAMPLE
  16))

CL-USER> (wav-select-key :chunk-data
           (wav-select-chunk "fmt " snd))
(:COMPRESSION-CODE 1 :NUMBER-OF-CHANNELS 1 :SAMPLE-RATE 48000
 :AVERAGE-BYTES-PER-SECOND 96000 :BLOCK-ALIGN 2 :SIGNIFICANT-BITS-PER-SAMPLE 16)

CL-USER> (wav-select-key :number-of-channels
           (wav-select-key :chunk-data
             (wav-select-chunk "fmt " snd)))
1

以上をまとめてwavread関数を作ります。返り値は振幅値と標本化周波数に加えてフォーマットチャンクも入れておきましょう。

(defun wavread (filename)
  "Open a WAV file and extract sample data, sampling rate, and format
   chunk.
   Example: (multiple-value-bind (x fs fmt)
                (wavread \"EBass_E1.wav\")
              fmt)"
  (let* ((snd (wav:read-wav-file
               filename
               :chunk-data-reader
               (wav:wrap-data-chunk-data-samples-reader)))
         (fmt (wav-select-key :chunk-data (wav-select-chunk "fmt " snd)))
         (nch (wav-select-key :number-of-channels fmt))
         (fs  (wav-select-key :sample-rate fmt))
         (nbit (wav-select-key :significant-bits-per-sample fmt))
         (data (wav-select-key :chunk-data (wav-select-chunk "data" snd))))
    (values data fs fmt)))

同じファイルをwavreadで読み込んでみます。

CL-USER> (multiple-value-bind (x fs fmt) (wavread "EBass_E1.wav")
           x)
#(0.0 0.0 0.0 3.0517578e-5 -3.0517578e-5 3.0517578e-5 0.0 -3.0517578e-5
  9.1552734e-5 -9.1552734e-5 1.5258789e-4 -1.5258789e-4 1.5258789e-4
  -9.1552734e-5 9.1552734e-5 0.0 0.0 3.0517578e-5 6.1035156e-5 -6.1035156e-5
  1.5258789e-4 -9.1552734e-5 9.1552734e-5 0.0 3.0517578e-5 6.1035156e-5
  3.0517578e-5 0.0 3.0517578e-5 6.1035156e-5 0.0 9.1552734e-5 0.0 3.0517578e-5
  9.1552734e-5 -3.0517578e-5 9.1552734e-5 3.0517578e-5 3.0517578e-5
  6.1035156e-5 6.1035156e-5 3.0517578e-5 6.1035156e-5 0.0 1.2207031e-4 0.0
  9.1552734e-5 3.0517578e-5 3.0517578e-5 9.1552734e-5 3.0517578e-5 6.1035156e-5
  6.1035156e-5 3.0517578e-5 1.2207031e-4 -3.0517578e-5 1.5258789e-4
  -3.0517578e-5 1.5258789e-4 0.0 9.1552734e-5 3.0517578e-5 9.1552734e-5 ..)

CL-USER> (multiple-value-bind (x fs fmt) (wavread "EBass_E1.wav")
           fs)
48000

CL-USER> (multiple-value-bind (x fs fmt) (wavread "EBass_E1.wav")
           fmt)
(:COMPRESSION-CODE 1 :NUMBER-OF-CHANNELS 1 :SAMPLE-RATE 48000
 :AVERAGE-BYTES-PER-SECOND 96000 :BLOCK-ALIGN 2 :SIGNIFICANT-BITS-PER-SAMPLE 16)

うまく行っているようです。(ここでは多値で返していますが、使いにくいので後日リストで返すようにしました。)