Posted at

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

More than 3 years have passed since last update.


はじめに

音声ファイルを 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/ を参考にさせてもらいました。


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];

}

}



ファイル保存

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


wavctrl.php

        function SaveFile($p1) {

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

}



WAV ファイルの連結

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


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 ファイルを連結してみます


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オクターブ高いラになります。

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


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

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


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 ファイルを合成(ミックス)します

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


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;

}

}


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


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


最後に

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

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

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