PHP で PCM wav ファイル作成

適当な音声信号を PHP で生成した時に気軽に音を確かめるには wav 形式に落とせば良い。と思って作ったコードです。

↑こちらを参考にして PHP でも wav ファイルの作成を。

function makeWaveData

<?php
function makeWaveData($data, $nChannel, $sampleBits, $sampleRate) {
    $blockSize = $nChannel*($sampleBits/8);
    $bytePerSecs = $blockSize*$sampleRate;
    $formatId = 1; // linear PCM
    $fmtChunk = 'WAVEfmt ';
    $fmtChunk .= pack("V", 16); // fmt chunk length
    $fmtChunk .= pack("v", $formatId);
    $fmtChunk .= pack("vVV", $nChannel, $sampleRate, $bytePerSecs);
    $fmtChunk .= pack("vv", $blockSize, $sampleBits);
    // chunk
    $dataChunk = 'data'.pack('V', strlen($data)).$data;
    $riffLength = strlen($fmtChunk)+strlen($dataChunk);
    return 'RIFF'.pack("V", $riffLength).$fmtChunk.$dataChunk;
}

実験 (8bit monoral, 電話品質)

<?php

$sampleRate = 8000; // Phone quality
$nChannel = 1; // 1:monoral, 2:stereo
$toneA = 440; // Hz
$sampleBits = 8; // 8 or 16

$period = 3; // seconds;
$data = '';
$theta = 0;
$theta_delta = $toneA * 2 * M_PI / $sampleRate;

for ($i = 0 ; $i < $sampleRate * $period ; $i++) {
// unsigned 8-bit array
    $v = 0x80 + 0x40 * sin($theta);
    $data .= pack('C', $v);
    $theta += $theta_delta;
}

echo makeWaveData($data, $nChannel, $sampleBits, $sampleRate);

実験 (16bit monoral, CD音質)

<?php
// $sampleRate = 8000; // Phone quality
$sampleRate = 44100; // CD quality
$nChannel = 1; // 1:monoral, 2:stereo
$toneA = 440;
// $sampleBits = 8; // 8 or 16
$sampleBits = 16; // 8 or 16

$period = 3; // seconds;
$data = '';
$theta = 0;
$theta_delta = $toneA * 2 * M_PI / $sampleRate;

for ($i = 0 ; $i < $sampleRate * $period ; $i++) {
// unsigned 8-bit array
//    $v = 0x80 + 0x40 * sin($theta);
//    $data .= pack('C', $v);
// signed 16-bit array (little endian)   
    $v = 0 + 0x4000 * sin($theta);
    $data .= pack('v', $v); // acrovatic using for 'v'
    $theta += $theta_delta;
}

echo makeWaveData($data, $nChannel, $sampleBits, $sampleRate);

実験 (16bit stereo, CD音質)

  • short の扱いにちょっと自信ない。... 正しかったようです。
<?php
$sampleRate = 44100; // CD quality   
// $nChannel = 1; // 1:monoral, 2:stereo
$nChannel = 2; // 1:monoral, 2:stereo       
$toneA = 440;
$sampleBits = 16; // 8 or 16

$period = 3; // seconds;       
$data = '';
$theta = 0;
$theta_delta = $toneA * 2 * M_PI / $sampleRate;

for ($i = 0 ; $i < $sampleRate * $period ; $i++) {                               
// signed 16-bit array (little endian)
    $v = 0 + 0x4000 * sin($theta);
//    $data .= pack('v', $v); // acrovatic using for 'v'
    $data .= pack('vv', $v, 0 ); // L:$v, R:0      
    $theta += $theta_delta;
}

for ($i = 0 ; $i < $sampleRate * $period ; $i++) {                                 
// signed 16-bit array (little endian)
    $v = 0 + 0x4000 * sin($theta);
    $data .= pack('vv', 0, $v); // L:0, R:$v                                   
    $theta += $theta_delta;
}

echo makeWaveData($data, $nChannel, $sampleBits, $sampleRate);

追記 (2013/04/30)

8-bit samples are stored as unsigned bytes, ranging from 0 to 255. 16-bit samples are stored as 2's-complement signed integers, ranging from -32768 to 32767.

  • 8-bit サンプリングは unsigned bytes で 0〜255
  • 16-bit サンプリングは signed integers の 2 の補数表現で -32768 〜 32767.

正直なところ自信なかったけど正しかった。 :-)

$v = 0 + 0x4000 * sin($theta);
$data .= pack('v', $v); // acrovatic using for 'v'

の何がアクロバティックかというと PHP の pack は signed short を byte order 指定で使えないので、unsigned short を意味する 'v' を無理筋で使ってます。

概念的には、

$v = 0 + 0x4000 * sin($theta);
$v = $v + 0x10000; // 2の補数表現でバイナリ的にひっくり返す
$data .= pack('v', $v); // acrovatic using for 'v'

が正しいのですが、結果的には同じですので。0x10000 足してません。