TeslaX93
TeslaX93

Reputation: 97

Is it possible to generate WAV files using PHP?

I want to write a small web application that generates short audio files (preferably WAV).

Audio files will be very simple, like this:

Eg. user input:

350 1000
500 1000

Output: two-seconds WAV file, first second is a 350Hz tone, second is 500Hz tone, like in this generator.

Is it possible to do it using PHP?

Upvotes: 1

Views: 3094

Answers (1)

Rob Ruchte
Rob Ruchte

Reputation: 3707

The WAV file format is simple enough that you can write a WAV file from scratch using PHP, provided you only want to output simple wave forms that can easily be generated programmatically.

Here is an example that will write sine waves to an 8bit 44.1kHz file using the input format you posted. I used the following as references:

Intro to Audio Programming, Part 2: Demystifying the WAV Format

A simple C# Wave editor, part 1: Background and analysis

<?php
/*
 * Set some input - format is [Hz, milliseconds], so [440, 1000] is 440Hz (A4) for 1 second
 */
$input = [
    [175, 1000],
    [350, 1000],
    [500, 1000],
    [750, 1000],
    [1000, 1000]
];

//Path to output file
$filePath = 'test.wav';

//Open a handle to our file in write mode, truncate the file if it exists
$fileHandle = fopen($filePath, 'w');

// Calculate variable dependent fields
$channels = 1; //Mono
$bitDepth = 8; //8bit
$sampleRate = 44100; //CD quality
$blockAlign = ($channels * ($bitDepth/8));
$averageBytesPerSecond = $sampleRate * $blockAlign;

/*
 * Header chunk
 * dwFileLength will be calculated at the end, based upon the length of the audio data
 */
$header = [
    'sGroupID' => 'RIFF',
    'dwFileLength' => 0,
    'sRiffType' => 'WAVE'
];

/*
 * Format chunk
 */
$fmtChunk = [
    'sGroupID' => 'fmt',
    'dwChunkSize' => 16,
    'wFormatTag' => 1,
    'wChannels' => $channels,
    'dwSamplesPerSec' => $sampleRate,
    'dwAvgBytesPerSec' => $averageBytesPerSecond,
    'wBlockAlign' => $blockAlign,
    'dwBitsPerSample' => $bitDepth
];

/*
 * Map all fields to pack flags
 * WAV format uses little-endian byte order
 */
$fieldFormatMap = [
    'sGroupID' => 'A4',
    'dwFileLength'=> 'V',
    'sRiffType' => 'A4',
    'dwChunkSize' => 'V',
    'wFormatTag' => 'v',
    'wChannels' => 'v',
    'dwSamplesPerSec' => 'V',
    'dwAvgBytesPerSec' => 'V',
    'wBlockAlign' => 'v',
    'dwBitsPerSample' => 'v' //Some resources say this is a uint but it's not - stay frosty.
];


/*
 * Pack and write our values
 * Keep track of how many bytes we write so we can update the dwFileLength in the header
 */
$dwFileLength = 0;
foreach($header as $currKey=>$currValue)
{
    if(!array_key_exists($currKey, $fieldFormatMap))
    {
        die('Unrecognized field '.$currKey);
    }

    $currPackFlag = $fieldFormatMap[$currKey];
    $currOutput = pack($currPackFlag, $currValue);
    $dwFileLength += fwrite($fileHandle, $currOutput);
}

foreach($fmtChunk as $currKey=>$currValue)
{
    if(!array_key_exists($currKey, $fieldFormatMap))
    {
        die('Unrecognized field '.$currKey);
    }

    $currPackFlag = $fieldFormatMap[$currKey];
    $currOutput = pack($currPackFlag, $currValue);
    $dwFileLength += fwrite($fileHandle, $currOutput);
}

/*
 * Set up our data chunk
 * As we write data, the dwChunkSize in this struct will be updated, be sure to pack and overwrite
 * after audio data has been written
 */
$dataChunk = [
    'sGroupID' => 'data',
    'dwChunkSize' => 0
];

//Write sGroupID
$dwFileLength += fwrite($fileHandle, pack($fieldFormatMap['sGroupID'], $dataChunk['sGroupID']));

//Save a reference to the position in the file of the dwChunkSize field so we can overwrite later
$dataChunkSizePosition = $dwFileLength;

//Write our empty dwChunkSize field
$dwFileLength += fwrite($fileHandle, pack($fieldFormatMap['dwChunkSize'], $dataChunk['dwChunkSize']));

/*
    8-bit audio: -128 to 127 (because of 2’s complement)
 */
$maxAmplitude = 127;

//Loop through input
foreach($input as $currNote)
{
    $currHz = $currNote[0];
    $currMillis = $currNote[1];

    /*
     * Each "tick" should be 1 second divided by our sample rate. Since we're counting in milliseconds, use
     * 1000/$sampleRate
     */
    $timeIncrement = 1000/$sampleRate;

    /*
     * Define how much each tick should advance the sine function. 360deg/(sample rate/frequency)
     */
    $waveIncrement = 360/($sampleRate/$currHz);

    /*
     * Run the sine function until we have written all the samples to fill the current note time
     */
    $elapsed = 0;
    $x = 0;
    while($elapsed<$currMillis)
    {
        /*
         * The sine wave math
         * $maxAmplitude*.95 lowers the output a bit so we're not right up at 0db
         */
        $currAmplitude = ($maxAmplitude)-number_format(sin(deg2rad($x))*($maxAmplitude*.95));

        //Increment our position in the wave
        $x+=$waveIncrement;

        //Write the sample and increment our byte counts
        $currBytesWritten = fwrite($fileHandle, pack('c', $currAmplitude));
        $dataChunk['dwChunkSize'] += $currBytesWritten;
        $dwFileLength  += $currBytesWritten;

        //Update the time counter
        $elapsed += $timeIncrement;
    }
}

/*
 * Seek to our dwFileLength and overwrite it with our final value. Make sure to subtract 8 for the
 * sGroupID and sRiffType fields in the header.
 */
fseek($fileHandle, 4);
fwrite($fileHandle, pack($fieldFormatMap['dwFileLength'], ($dwFileLength-8)));

//Seek to our dwChunkSize and overwrite it with our final value
fseek($fileHandle, $dataChunkSizePosition);
fwrite($fileHandle, pack($fieldFormatMap['dwChunkSize'], $dataChunk['dwChunkSize']));
fclose($fileHandle);

This is a proof of concept that illustrates how to create the files from your input, making it work in a web application is up to you.

Upvotes: 8

Related Questions