Web Development

Create an audio stitching tool in PHP

One way to provide visually impaired users with the ability to use OCR-defeating passcodes is to stitch WAV files together. Phillip Perkins shows you how to deconstruct a group of WAV files and reconstruct one large WAV file by compiling the group.

One way to provide visually impaired users with the ability to use OCR-defeating passcodes is to stitch WAV files together. In this article, I demonstrate how to deconstruct a group of WAV files and reconstruct one large WAV file by compiling the group. (Note: I used ASP and Visual Basic to build this solution.)

Understanding Microsoft PCM WAV files

Microsoft PCM WAV files contain information such as the audio type, the audio format, and the audio data split into three "chunks." The first chunk contains ChunkID, ChunkSize, and Format. In a few bytes, this chunk informs any application reading the media file that it's a WAVE audio file.

The second chunk contains Subchunk1ID, Subchunk1Size, AudioFormat, NumChannels, SampleRate, ByteRate, BlockAlign, and BitsPerSample. Respectively, this is "fmt"; 16 (for PCM); 1 (no compression); 1 or 2 (Mono or Stereo); 8000, 44100, etc.; SampleRate * NumChannels * BitsPerSample/8; NumChannels * BitsPerSample/8; and 8, 16, etc.

The third chunk contains the Subchunk2ID, Subchunk2Size, and Data: "data", data size (NumSamples * NumChannels * BitsPerSample/8), and the audio data. To retrieve this information, you simply read and store the appropriate bytes in the form of an audio file. With PHP, you do this with the stream functions.

Using PHP's stream functions

By using a combination of fopen(), fread(), and fclose(), read these values in to local variables. A method that I'm particularly fond of is storing the file information in a structure for easy organization. You can do this with a class in PHP:

class FILESTRUCT {
    var $ChunkID;
    var $ChunkSize;
    var $Format;
    var $Subchunk1ID;
    var $Subchunk1Size;
    var $AudioFormat;
    var $NumChannels;
    var $SampleRate;
    var $ByteRate;
    var $BlockAlign;
    var $BitsPerSample;
    var $Subchunk2ID;
    var $Subchunk2Size;
    var $Data;

    function FILESTRUCT() {
        $this->ChunkID = array(0x0, 0x0, 0x0, 0x0);
        $this->ChunkSize = array(0x0, 0x0, 0x0, 0x0);
        $this->Format = array(0x0, 0x0, 0x0, 0x0);
        $this->Subchunk1ID = array(0x0, 0x0, 0x0, 0x0);
        $this->Subchunk1Size = array(0x0, 0x0, 0x0, 0x0);
        $this->AudioFormat = array(0x0, 0x0);
        $this->NumChannels = array(0x0, 0x0);
        $this->SampleRate = array(0x0, 0x0, 0x0, 0x0);
        $this->ByteRate = array(0x0, 0x0, 0x0, 0x0);
        $this->BlockAlign = array(0x0, 0x0);
        $this->BitsPerSample = array(0x0, 0x0);
        $this->Subchunk2ID = array(0x0, 0x0, 0x0, 0x0);
        $this->Subchunk2Size = array(0x0, 0x0, 0x0, 0x0);
        $this->Data = array();
    }
}

This file structure class contains information about the size of each section of the file. For instance, the ChunkID property is a four element array for containing four bytes of information. When I read in the binary data from the file, I can use the count() function to grab the number of bytes that makes up that portion of the file data. Given the basic information to retrieve the file data, you can start to conceptualize the end solution.

Stitching WAV files together

When a user visits a particular page, you want to provide the user with an audio file stitched together from multiple audio files. In order for this to happen, one of the input parameters needs to be an array of file paths. You must loop through these files, load the file information into the FILESTRUCT structures, and create a new FILESTRUCT structure that contains the information on the concatenated files. Then, using that FILESTRUCT structure, set the MIME type on the response and pump out the binary data.

I'll create a class to handle the file stitching operations. This is mostly because I use a class module in my Visual Basic solution to provide this functionality. The class will only contain one public method: StitchFiles. This method will accept a FILESTRUCT reference and an array of files:

class CStitcher {

    function StitchFiles(&$fsFile, &$sFiles) {
        ...
    }
}

This method contains the guts of the stitching process.

Implement a PHP WAV stitching solution

The great thing about WAV PCM files is that you can concatenate the binary audio data from two files without having to do some interesting binding routine. You simply stick the binary data from one file on the end of another file, adjust the Subchunk2Size value, and you've created one audio file composed of the two original files. The only drawback is the two files must be in the same format; however, for this application, that's not a big deal.

When you're parsing each audio file, read the binary audio data into a buffer array and add the next file's data to the end of the buffer array. Continue this process until you've read in each file. Create a new FILESTRUCT structure, fill in the format information, adjust the Subchunk2Size value, set the Data member to the buffer array, and write the contents of that FILESTRUCT to whatever output you need. In the case of a Web delivery system through PHP, that output will be the standard output. Here's the code to accomplish this task:

function StitchFiles(&$fsFile, &$sFiles) {

    $fsFiles = array(); //FILESTRUCT
    $lFileSize = 0;
    $lOffset = 0;
    $bData = array(); //byte
        
    for ($i = 0; $i < count($sFiles); $i++) {
        $fsFiles[$i] = new FILESTRUCT();
        SetFile($fsFiles[$i], $sFiles[$i]);
        $lSize = CalcLittleEndianValue($fsFiles[$i]->Subchunk2Size);
        $lFileSize = $lFileSize + $lSize;
        $bData = array_merge($bData, $fsFiles[$i]->Data);
        $lOffset = $lOffset + $lSize;
    }
    $fsFile->ChunkID = $GLOBALS["ChunkID_"];
    $fsFile->ChunkSize = GetLittleEndianByteArray(36 + $lFileSize);
    $fsFile->Format = $GLOBALS["FileFormat_"];
    $fsFile->Subchunk1ID = $GLOBALS["Subchunk1ID_"];
    $fsFile->Subchunk1Size = array(0x10, 0x0, 0x0, 0x0);
    $fsFile->AudioFormat = $GLOBALS["AudioFormat_"];
    $fsFile->NumChannels = $GLOBALS["Stereo_"];
    $fsFile->SampleRate = $GLOBALS["SampleRate_"];
    $fsFile->ByteRate = GetLittleEndianByteArray(
                                        
CalcLittleEndianValue($GLOBALS["SampleRate_"]) *
                                        CalcLittleEndianValue($GLOBALS["Stereo_"]) *
                                        
(CalcLittleEndianValue($GLOBALS["BitsPerSample_"]) / 8));
    $fsFile->BlockAlign =
array_splice(GetLittleEndianByteArray(CalcLittleEndianValue($GLOBALS["Stereo_"]
) * (CalcLittleEndianValue($GLOBALS["BitsPerSample_"]) / 8)), 0, 2);
    $fsFile->BitsPerSample = $GLOBALS["BitsPerSample_"];
    $fsFile->Subchunk2ID = $GLOBALS["Subchunk2ID_"];
    $fsFile->Subchunk2Size = GetLittleEndianByteArray($lFileSize);
    $fsFile->Data = $bData;
}

The first parameter of this call is a reference to a FILESTRUCT object. The second parameter is a reference to a list of filename strings. In my loop, I iterate through the list of filenames loading each one to a FILESTRUCT. I take the Data property from each FILESTRUCT and merge it to an existing buffer. After stepping through each FILESTRUCT, I set the properties of the FILESTRUCT parameter object through global variables—declared earlier in code. I change the Subchunk2Size property and set the Data property to the new buffer.

If you notice, I use some utility functions to take care of filling the FILESTRUCT structures, calculating values from little endian byte arrays, creating little endian byte arrays, and splitting string results to binary arrays. Here are those functions:

function SetFile(&$fsFile_, $sFileName) {
    $lSize = 1;
    if (file_exists($sFileName)) {
        $fil = fopen($sFileName, "rb");
        $contents = fread($fil, count($fsFile_->ChunkID));
            $fsFile_->ChunkID = bin_split($contents, 1);
        $contents = fread($fil, count($fsFile_->ChunkSize));
            $fsFile_->ChunkSize = bin_split($contents, 1);
        $contents = fread($fil, count($fsFile_->Format));
            $fsFile_->Format = bin_split($contents, 1);
        $contents = fread($fil, count($fsFile_->Subchunk1ID));
            $fsFile_->Subchunk1ID = bin_split($contents, 1);
        $contents = fread($fil, count($fsFile_->Subchunk1Size));
            $fsFile_->Subchunk1Size = bin_split($contents, 1);
        $contents = fread($fil, count($fsFile_->AudioFormat));
            $fsFile_->AudioFormat = bin_split($contents, 1);
        $contents = fread($fil, count($fsFile_->NumChannels));
            $fsFile_->NumChannels = bin_split($contents, 1);
        $contents = fread($fil, count($fsFile_->SampleRate));
            $fsFile_->SampleRate = bin_split($contents, 1);
        $contents = fread($fil, count($fsFile_->ByteRate));
            $fsFile_->ByteRate = bin_split($contents, 1);
        $contents = fread($fil, count($fsFile_->BlockAlign));
            $fsFile_->BlockAlign = bin_split($contents, 1);
        $contents = fread($fil, count($fsFile_->BitsPerSample));
            $fsFile_->BitsPerSample = bin_split($contents, 1);
        $contents = fread($fil, count($fsFile_->Subchunk2ID));
            $fsFile_->Subchunk2ID = bin_split($contents, 1);
        $contents = fread($fil, count($fsFile_->Subchunk2Size));
            $fsFile_->Subchunk2Size = bin_split($contents, 1);
        $lSize = CalcLittleEndianValue($fsFile_->Subchunk2Size);
            $contents = fread($fil, $lSize);
            $fsFile_->Data = bin_split($contents, 1);
        fclose($fil);
    }
}

function CalcLittleEndianValue(&$bValue) {
    $lSize_ = 0;
    for ($iByte = 0; $iByte < count($bValue); $iByte++) {
        $lSize_ += ($bValue[$iByte] * pow(16, ($iByte * 2)));
    }
    return $lSize_;
}

function GetLittleEndianByteArray($lValue) {
    $running = 0;
    $b = array(0, 0, 0, 0);
    $running = $lValue / pow(16,6);
    $b[3] = floor($running);
    $running -= $b[3];
    $running *= 256;
    $b[2] = floor($running);
    $running -= $b[2];
    $running *= 256;
    $b[1] = floor($running);
    $running -= $b[1];
    $running *= 256;
    $b[0] = round($running);
    return $b;
}


function bin_split($text, $c)
{
 $arr = array();
 $len = strlen($text);
 $a = 0;
 while($a < $len)
 {
  if ($a + $c > $len)
  {
   $c = $len - $a;
  }
  $arr[$a] = ord(substr($text, $a, $c));
  $a += $c;
 }
 return $arr;
}

In order to implement this class's functionality, you create a CStitcher instance and call the StitchFiles() method on the instance, passing the appropriate parameters:

$ChunkID_ = array(0x52, 0x49, 0x46, 0x46);       //"RIFF" big endian
$FileFormat_ = array(0x57, 0x41, 0x56, 0x45);    //"WAVE" big endian
$Subchunk1ID_ = array(0x66, 0x6D, 0x74, 0x20);   //"fmt" big endian
$AudioFormat_ = array(0x1, 0x0);                 //PCM = 1 little endian
$Stereo_ = array(0x2, 0x0);                      //Stereo = 2 little endian
$Mono_ = array(0x1, 0x0);                        //Mono = 1 little endian
$SampleRate_ = array(0x44, 0xAC, 0x0, 0x0);      //44100 little endian
$BitsPerSample_ = array(0x10, 0x0);              //16 little endian
$Subchunk2ID_ = array(0x64, 0x61, 0x74, 0x61);   //"data" big endian

$files = array("C:\\Inetpub\\wwwroot\\Test\\PHP\\1.wav",
 "C:\\Inetpub\\wwwroot\\Test\\PHP\\2.wav");
$Stitcher = new CStitcher();
$file = new FILESTRUCT();
$Stitcher->StitchFiles($file, $files);

You'll use the following code to write the binary data to HTTP output:

header('Content-type: audio/x-wav', true);
header('Content-Disposition: attachment;filename=stitch.wav');

foreach($file->ChunkID as $val) {
    print chr($val);
}
foreach($file->ChunkSize as $val) {
    print chr($val);
}
foreach($file->Format as $val) {
    print chr($val);
}
foreach($file->Subchunk1ID as $val) {
    print chr($val);
}
foreach($file->Subchunk1Size as $val) {
    print chr($val);
}
foreach($file->AudioFormat as $val) {
    print chr($val);
}
foreach($file->NumChannels as $val) {
    print chr($val);
}
foreach($file->SampleRate as $val) {
    print chr($val);
}
foreach($file->ByteRate as $val) {
    print chr($val);
}
foreach($file->BlockAlign as $val) {
    print chr($val);
}
foreach($file->BitsPerSample as $val) {
    print chr($val);
}
foreach($file->Subchunk2ID as $val) {
    print chr($val);
}
foreach($file->Subchunk2Size as $val) {
    print chr($val);
}
foreach($file->Data as $val) {
    print chr($val);
}

Here's some HTML for you to test your efforts:

<html>
<head>
<script language="JavaScript">
function play_soundbyte() {
    bgsound1.src = "wavstitch.php";
}
</script>
</head>
<bgsound id="bgsound1" loop="1">
<body>

<a href="javascript:play_soundbyte();">Click here to play soundbyte.</a>

</body>
</html>

If you like, you can download the source code for this article, as well as some sample WAV files that you can use in your own solution. Also, if you're interested in learning more about the WAV PCM format, check out the site where I found my information.

Keep your developer skills sharp by automatically signing up for TechRepublic's free Web Development Zone newsletter, delivered each Tuesday.

0 comments

Editor's Picks