【Common Lisp】 Lispでサイン波を鳴らしてみる
目標
ポーっと鳴る電子音を作成します。サイン波です。こんな音です。
実装
1. PCMの定義
まずは、PCMというデータ形式で音データを作成します。とりあえず、クラスで作ってみます。モノラルのPCMです。
;; モノラルPCMの定義
;; fs: 標本化周波数
;; bits: 量子化精度
;; length: 長さ
;; sound-data:音データ
(defclass mono-pcm ()
(fs
bits
len
data
))
・標本化周波数とは、1秒あたりの標本化の回数とのことです。よくわからん。
・量子化精度とは、音データを記録する際の精度です。今回は16bitの精度で音データを作成します。すなわち「-32768 ~ 32767」の細かさの音データを作成します。
・長さ。これは標本化周波数と一致するものと考えます。
・音データ。ここに実際の音データを入れていきます。音データは長さぶんの配列を用意することになります。
2. 音データの作成
次に、音データを作成します。
サイン波を作ることで音データを作ります。
(defmethod make-sound (fs bits a f0 (pcm mono-pcm))
"音データを作成する"
(setf (slot-value pcm 'fs) fs)
(setf (slot-value pcm 'bits) bits)
(setf (slot-value pcm 'len) fs)
(setf (slot-value pcm 'data) (make-array (* (slot-value pcm 'len) 1)))
(dotimes (i (slot-value pcm 'len))
(setf (aref (slot-value pcm 'data) i) (* a (sin (/ (* 2 pi f0 i) (slot-value pcm 'fs)))))))
引数aは振幅です。aを変化させることで音の大小を調整します。
引数f0は周波数です。f0を変化させることで音の高低を調整します。
さきほど定義したPCMに音データをセットしていきます。
3. 音データのファイル出力
先ほど作ったPCMの音データをWAVEファイルとして出力します。ファイル出力用のメソッドを作成します。
(defmethod write-16bit-mono-wave (path (pcm MONO-PCM))
"16ビットモノラルのwaveファイルを作成する"
(with-open-file (out path :direction :output
:if-exists :supersede :element-type '(unsigned-byte 8))
(dolist (x '(#\R #\I #\F #\F)) (write-byte (char-code x) out))
(write-u32-2 out (+ 36 (* (slot-value pcm 'len) 2)))
(dolist (x '(#\W #\A #\V #\E)) (write-byte (char-code x) out))
(dolist (x '(#\f #\m #\t #\space)) (write-byte (char-code x) out))
(write-u32-2 out 16)
(write-u16-2 out 1)
(write-u16-2 out 1)
(write-u32-2 out (slot-value pcm 'fs))
(write-u32-2 out (/ (* (slot-value pcm 'fs)(slot-value pcm 'bits)) 8))
(write-u16-2 out (/ (slot-value pcm 'bits) 8))
(write-u16-2 out (slot-value pcm 'bits))
(dolist (x '(#\d #\a #\t #\a)) (write-byte (char-code x) out))
(write-u32-2 out (slot-value pcm 'len))
(dotimes (i (slot-value pcm 'len))
(let ((x (* (/ (+ (aref (slot-value pcm 'data) i) 1.0) 2.0) 65536.0)))
(cond ((> x 65535.0) (setf x 65535.0))
((< x 0.0) (setf x 0.0)))
(write-u16-2 out (round (- (+ x 0.5) 32768.0)))))))
あ、これバイナリ出力しないといけないのです。なので、自分でバイナリ出力用の関数を定義しました。
(defun write-u16 (out value)
"unsiged-16bit 0〜65535"
(write-byte (ldb (byte 8 8) value) out)
(write-byte (ldb (byte 8 0) value) out))
(defun write-u16-2 (out value)
"unsiged-16bit 0〜65535"
(write-byte (ldb (byte 8 0) value) out)
(write-byte (ldb (byte 8 8) value) out))
(defun write-u32 (out value)
"unsigned-32bit 0〜4294967295"
(write-byte (ldb (byte 8 24) value) out)
(write-byte (ldb (byte 8 16) value) out)
(write-byte (ldb (byte 8 8) value) out)
(write-byte (ldb (byte 8 0) value) out))
(defun write-u32-2 (out value)
"unsigned-32bit 0〜4294967295"
(write-byte (ldb (byte 8 0) value) out)
(write-byte (ldb (byte 8 8) value) out)
(write-byte (ldb (byte 8 16) value) out)
(write-byte (ldb (byte 8 24) value) out))
これは『実践Common Lisp』を参考に作りました。「write-u16」と「write-u16-2」のようにunsigned 16bit を書き込むための関数がふたつ定義してあります。これはリトルエンディアンとビックエンディアンの違いです。けれど、どっちがどっちだかわからなくて、2って名前をつけてしまいました。すいやせん。今回は2の方を使います。
4. 実行
最後に実行してみます。
(defparameter x nil)
(setf x (make-instance 'mono-pcm))
(make-sound 44100 16 0.5 500.0 x)
(write-16bit-mono-wave "~/hoge.wave" x)
副作用バリバリでLispっぽくない書き方ですが、ひとまずこんな感じですかね・・。まだ理解が足りない部分が多く、もう少し綺麗にまとめたいと思う。
参考文献
・『サウンドプログラミング入門――音響合成の基本とC言語による実装』青木直史 (著)
・『実践Common Lisp』Peter Seibel (著)
お気軽にフォローやコメントしてください。けっこう喜びます。