はじめに
音声ファイルを PHP で操作してみました。拡張していけば、VOCALOID っぽい、何かが作れるかもしれません?
作成したコードと実行結果の wav ファイルは、github にアップしました。
wav ファイルは、重音テトさんの音源を利用したいと思います。音源は以下のサイトから入手しました。
http://kasaneteto.jp/index.html
重音テトさんの wav ファイルは、以下のような仕様になっていますので、今回のサンプルでは分かりやすさを優先して、この形式のデータだけを考慮して作ります。
- リニアPCM
- モノラルデータ
- サンプリング周波数、44100Hz
- 量子化16ビット
wav ファイルを読み込む
wav ファイルを読み込み、前提としている仕様の wav ファイルかどうかどうかを確認します。仕様に合致すれば、wav ファイルの情報を取得してクラス変数に保存します。
wav ファイルの仕様は、http://www.kk.iij4u.or.jp/~kondo/wave/ を参考にさせてもらいました。
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];
}
}
ファイル保存
出来たファイルを保存する機能も追加しておきます
function SaveFile($p1) {
file_put_contents($p1, $this->_d);
}
WAV ファイルの連結
WAV ファイルを連結して、1つのファイルにするメソッドを作成します。連結することで、音が繋がって再生される WAV が作成できます。
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 ファイルを連結してみます
$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オクターブ高いラになります。
また、周波数を変更すると音の長さが変わります。音を低くすると、元の音より長く再生されるようになり、音を高くすると、元の音より短く再生されるようになります。
// $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
ド、レ、ミとしゃべってもらうには、以下のように、変換します
$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 ファイルを合成(ミックス)します
音を合成すると、複数の音を混ぜて聞くことができます
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;
}
}
音階を変更した音声を合成してみます。
$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 を再生すると、「あ」という発音で和音が聞こえると思います。音の長さが変わっているので、綺麗に途切れませんが、そのあたりを調整すれば、もっと、綺麗に合成できると思います
最後に
基本的な操作になるので、あまり、複雑なことはできませんが、音長変更や、音量変更、無音作成・・・などの、演奏をするのに必要な処理を加えれば、何か、面白いものが作れるかもしれません
何かの参考になればと思います
あと、重音テトさんの音源が、どの音階で作られているのか教えてもらえるとうれしいです。