首页 > 解决方案 > 是否可以使用 PHP 生成 WAV 文件?

问题描述

我想编写一个生成短音频文件(最好是 WAV)的小型 Web 应用程序。

音频文件将非常简单,如下所示:

例如。用户输入:

350 1000
500 1000

输出:两秒 WAV 文件,第一秒是 350Hz 音调,第二秒是 500Hz 音调,就像在这个发生器中一样。

是否可以使用PHP来做到这一点?

标签: phpaudiobinaryfiles

解决方案


WAV 文件格式非常简单,您可以使用 PHP 从头开始​​编写 WAV 文件,前提是您只想输出可以通过编程轻松生成的简单波形。

这是一个使用您发布的输入格式将正弦波写入 8 位 44.1kHz 文件的示例。我使用以下内容作为参考:

音频编程介绍,第 2 部分:揭开 WAV 格式的神秘面纱

一个简单的 C# Wave 编辑器,第 1 部分:背景和分析

<?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);

这是一个概念证明,说明了如何根据您的输入创建文件,使其在 Web 应用程序中工作取决于您。


推荐阅读