PHP で、WAV 音声ファイルを操作してみる

# はじめに

音声ファイルを PHP で操作してみました。拡張していけば、VOCALOID っぽい、何かが作れるかもしれません？

作成したコードと実行結果の wav ファイルは、[github](https://github.com/yorozuone/qiita/tree/master/wavctrl) にアップしました。

wav ファイルは、重音テトさんの音源を利用したいと思います。音源は以下のサイトから入手しました。
[http://kasaneteto.jp/index.html](http://kasaneteto.jp/index.html)

重音テトさんの wav ファイルは、以下のような仕様になっていますので、今回のサンプルでは分かりやすさを優先して、この形式のデータだけを考慮して作ります。

* リニアPCM
* モノラルデータ
* サンプリング周波数、44100Hz
* 量子化16ビット

# wav ファイルを読み込む

wav ファイルを読み込み、前提としている仕様の wav ファイルかどうかどうかを確認します。仕様に合致すれば、wav ファイルの情報を取得してクラス変数に保存します。

wav ファイルの仕様は、[http://www.kk.iij4u.or.jp/~kondo/wave/](http://www.kk.iij4u.or.jp/~kondo/wave/) を参考にさせてもらいました。

```lang:wavctrl.php

	class WavCtrl {

		var $_d		= '';	// データ

		var $datasize	= 0;    // データサイズ
		var $fmtid	= 0;    // フォーマットID
		var $chsize	= 0;    // チャンネル数
		var $freq	= 0;    // サンプリング周波数

		function LoadFile($fn) {

			$this->_d = file_get_contents($fn);

			// 先頭4バイトが、RIFF でなければ、WAV ファイルではない。
			if (substr($this->_d, 0, 4) != 'RIFF') {
				return false;
			}

			// chunk 識別コード WAVE が存在するかどうか調査
			if (substr($this->_d, 8, 4) != 'WAVE') {
				return false;
			}

			// chunk 識別コード fmt が存在するかどうか調査
			if (substr($this->_d, 12, 4) != 'fmt ') {
				return false;
			}

			// chunk 識別コード data が存在するかどうか調査
			if (substr($this->_d, 36, 4) != 'data') {
				return false;
			}

			// フォーマットID を取得します
			// リニアPCMだけを対象にするので、それ以外はエラー
			$d = unpack('v', substr($this->_d, 20, 2));
			$this->fmtid = $d[1];

			if ($this->fmtid != 1) {
				return false;
			}

			// チャンネル数を取得
			// モノラルチャンネルだけを対象にします
			$d = unpack('v', substr($this->_d, 22, 2));
			$this->chsize = $d[1];

			if ($this->fmtid != 1) {
				return false;
			}

			// サンプリング周波数を取得
			// 44100hz のみを対象とします
			$d = unpack('V', substr($this->_d, 24, 4));
			$this->freq = $d[1];

			// データサイズを取得
			$d = unpack('V', substr($this->_d, 40, 4));
			$this->datasize = $d[1];

		}

	}
```

# ファイル保存

出来たファイルを保存する機能も追加しておきます

```lang:wavctrl.php
		function SaveFile($p1) {

			file_put_contents($p1, $this->_d);

		}
```

# WAV ファイルの連結

WAV ファイルを連結して、1つのファイルにするメソッドを作成します。連結することで、音が繋がって再生される WAV が作成できます。

```lang:wavctrl.php

		function WaveConnect(&$p1) {

			// WAVE ファイルのデータ部分だけ結合します
			$this->_d = $this->_d . substr($p1->_d, 44, $p1->datasize);

			// データサイズを更新します
			$this->datasize = strlen($this->_d) - 44;

			// 実際のデータのサイズも更新します
			$d = pack('V', strlen($this->_d) - 8);
			$this->_d[4] = $d[0];
			$this->_d[5] = $d[1];
			$this->_d[6] = $d[2];
			$this->_d[7] = $d[3];

			$d = pack('V', $this->datasize);
			$this->_d[40] = $d[0];
			$this->_d[41] = $d[1];
			$this->_d[42] = $d[2];
			$this->_d[43] = $d[3];

		}
```

こちらのサンプルをつかって、WAVE ファイルを連結してみます

```lang:wavctrl.php

	$w1 = new WavCtrl();
	$w1->LoadFile('_あ.wav');

	$w2 = new WavCtrl();
	$w2->LoadFile('_い.wav');

	$w3 = new WavCtrl();
	$w3->LoadFile('_う.wav');

	$w1->WaveConnect($w2);
	$w1->WaveConnect($w3);

	$w1->SaveFile('step1.wav');

```

step1.wav というファイルが作成され、再生すると「あ・い・う」という3音が連結されて再生されます。

# 音程を変える。

音程は、周波数を変更することによって調整します。

元になる、テトさんの音声を、ラ(440hz)の音で作成されていると仮定します。何故仮定するかというと、テトさんの音源の音階が何なのか書いてないのと、私が絶対音感を持たないためです。

たとえば、これを倍(880hz)にすると、1オクターブ高いラになります。

また、周波数を変更すると音の長さが変わります。音を低くすると、元の音より長く再生されるようになり、音を高くすると、元の音より短く再生されるようになります。

```lang:wavctrl.php

		// $p1 変換後の周波数を指定します。
		function WaveShift($p1) {

			$f1 = $p1 / 440;
			$f2 = 0;

			$dst = substr($this->_d, 0, 44);

			while(1) {

				$d = substr($this->_d, 44 + floor($f2) * 2, 2);

				$dst .= $d[0];
				$dst .= $d[1];

				if ($f2 > $this->datasize) {
					break;
				}

				$f2 += $f1;

			}

			// データ更新
			$this->_d = $dst;
			$this->datasize = strlen($this->_d) - 44;

			// 実際のデータのサイズも更新します
			$d = pack('V', strlen($this->_d) -  8);

			$this->_d[4] = $d[0];
			$this->_d[5] = $d[1];
			$this->_d[6] = $d[2];
			$this->_d[7] = $d[3];

			$d = pack('V', $this->datasize);

			$this->_d[40] = $d[0];
			$this->_d[41] = $d[1];
			$this->_d[42] = $d[2];
			$this->_d[43] = $d[3];

			// 変数解放
			unset($dst);

		}
```

音階をつけたい場合は、その音階に応じた周波数を指定すればよいわけです。

その周波数は、こちらを参考にさせていただきました

[http://www.yk.rim.or.jp/~kamide/music/notes.html](http://www.yk.rim.or.jp/~kamide/music/notes.html)

ド、レ、ミとしゃべってもらうには、以下のように、変換します

```lang:wavctrl.php

	$w1 = new WavCtrl();
	$w1->LoadFile('_あ.wav');

	$w2 = new WavCtrl();
	$w2->LoadFile('_い.wav');

	$w3 = new WavCtrl();
	$w3->LoadFile('_う.wav');

	$w1->WaveShift(261);
	$w2->WaveShift(329);
	$w3->WaveShift(391);

	$w1->WaveConnect($w2);
	$w1->WaveConnect($w3);

	$w1->SaveFile('step2.wav');
```

# wav ファイルを合成（ミックス)します

音を合成すると、複数の音を混ぜて聞くことができます

```lang:wavctrl.php

		function WavCtrl(&$p1) {

			$f2 = 0;

			while(1) {

				$src1 = 0;
				$src2 = 0;

				if ($f2 < $this->datasize) {
					$d = unpack('s', substr($this->_d, 44 + $f2, 2));
					$src1 = $d[1];
				}

				if ($f2 < $p1->datasize) {
					$d = unpack('s', substr($p1->_d,   44 + $f2, 2));
					$src2 = $d[1];
				}

				// ミックスします
				$src3 = floor($src1 + $src2);

				// 音割れ防止
				if ($src3 < -32768) {
					$src3 = -32768;
				}

				// 音割れ防止
				if ($src3 > 32767) {
					$src3 = 32767;
				}

				$d = pack('v', $src3);

				$this->_d[44 + $f2    ] = $d[0];
				$this->_d[44 + $f2 + 1] = $d[1];

				if (($f2 > $this->datasize) and ($f2 > $p1->datasize)) {
					break;
				}

				$f2 += 2;

			}

		}
```

音階を変更した音声を合成してみます。

```lang:wavctrl.php

	$w1 = new WavCtrl();
	$w1->LoadFile('_あ.wav');

	$w2 = new WavCtrl();
	$w2->LoadFile('_あ.wav');

	$w3 = new WavCtrl();
	$w3->LoadFile('_あ.wav');

	$w1->WaveShift(261);
	$w2->WaveShift(329);
	$w3->WaveShift(391);

	$w1->WaveMix($w2);
	$w1->WaveMix($w3);

	$w1->SaveFile('step3.wav');

```

step3.wav を再生すると、「あ」という発音で和音が聞こえると思います。音の長さが変わっているので、綺麗に途切れませんが、そのあたりを調整すれば、もっと、綺麗に合成できると思います

# 最後に

基本的な操作になるので、あまり、複雑なことはできませんが、音長変更や、音量変更、無音作成・・・などの、演奏をするのに必要な処理を加えれば、何か、面白いものが作れるかもしれません

何かの参考になればと思います

あと、重音テトさんの音源が、どの音階で作られているのか教えてもらえるとうれしいです。