見出し画像

【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 (著)

お気軽にフォローやコメントしてください。けっこう喜びます。