You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PHP中如何合并Google Cloud TTS生成的LINEAR16编码音频文件?

解决Google Cloud TTS LINEAR16音频片段合并问题

问题根源

LINEAR16编码返回的是完整WAV格式音频(包含文件头+PCM原始数据),每个片段都带有独立的WAV头。直接拼接这些二进制内容时,音频播放器只会识别第一个WAV头,后续片段的头会被判定为无效数据,因此只播放第一段内容。而MP3是流式编码,无全局头限制,所以直接拼接即可正常播放。

解决方案

先剥离每个LINEAR16音频片段的WAV头,提取纯PCM数据;拼接所有PCM数据后,再为整体添加统一的WAV头。

具体实现代码

use Google\Cloud\TextToSpeech\V1\AudioConfig;
use Google\Cloud\TextToSpeech\V1\AudioEncoding;
use Google\Cloud\TextToSpeech\V1\TextToSpeechClient;
use Google\Cloud\TextToSpeech\V1\VoiceSelectionParams;
use Google\Cloud\TextToSpeech\V1\SynthesisInput;

$client = new TextToSpeechClient();
$synthesisInputText = new SynthesisInput();
$audioConfig = new AudioConfig();
$audioConfig->setAudioEncoding(AudioEncoding::LINEAR16);
$voice = new VoiceSelectionParams();

$voice->setLanguageCode('en-US');
$voice->setName('en-US-Neural2-A');

// 存储所有片段的纯PCM数据
$pcmData = [];
// 记录PCM采样参数(从第一个片段提取)
$sampleRate = null;
$bitsPerSample = null;
$channels = null;

foreach($txt_array as $k => $txt) {
    $synthesisInputText->setText($txt);
    $response = $client->synthesizeSpeech($synthesisInputText, $voice, $audioConfig);
    $audioContent = $response->getAudioContent();

    // 解析WAV头,提取PCM数据和参数
    $wavInfo = parseWavHeader($audioContent);
    if ($k === 0) {
        $sampleRate = $wavInfo['sampleRate'];
        $bitsPerSample = $wavInfo['bitsPerSample'];
        $channels = $wavInfo['channels'];
    }

    // 提取纯PCM数据(跳过WAV头)
    $pcm = substr($audioContent, $wavInfo['headerSize']);
    $pcmData[] = $pcm;
}

// 拼接所有PCM数据
$totalPcm = implode('', $pcmData);

// 生成统一的WAV头并拼接PCM数据
$finalAudio = buildWavHeader(strlen($totalPcm), $sampleRate, $bitsPerSample, $channels) . $totalPcm;

// 保存或输出最终音频
file_put_contents('merged_audio.wav', $finalAudio);

/**
 * 解析WAV文件头,获取关键参数和头长度
 */
function parseWavHeader(string $wavContent): array {
    $header = unpack('a4chunkId/VchunkSize/a4format/a4subchunk1Id/Vsubchunk1Size/vaudioFormat/vnumChannels/VsampleRate/VbyteRate/vblockAlign/vbitsPerSample', substr($wavContent, 0, 44));
    
    // 处理可能存在的额外子块(如LIST块)
    $subchunk2Offset = 44;
    if ($header['subchunk1Size'] > 16) {
        $subchunk2Offset += $header['subchunk1Size'] - 16;
    }
    
    // 找到data子块的起始位置
    while (true) {
        $subchunkId = substr($wavContent, $subchunk2Offset, 4);
        if ($subchunkId === 'data') {
            break;
        }
        $subchunkSize = unpack('V', substr($wavContent, $subchunk2Offset + 4, 4))[1];
        $subchunk2Offset += 8 + $subchunkSize;
    }
    
    return [
        'sampleRate' => $header['sampleRate'],
        'bitsPerSample' => $header['bitsPerSample'],
        'channels' => $header['numChannels'],
        'headerSize' => $subchunk2Offset + 8 // data子块头占8字节,之后是PCM数据
    ];
}

/**
 * 生成标准WAV文件头
 */
function buildWavHeader(int $pcmSize, int $sampleRate, int $bitsPerSample, int $channels): string {
    $byteRate = $sampleRate * $channels * $bitsPerSample / 8;
    $blockAlign = $channels * $bitsPerSample / 8;
    $chunkSize = 36 + $pcmSize;
    
    $header = pack(
        'a4Va4a4VvvVVvv',
        'RIFF',
        $chunkSize,
        'WAVE',
        'fmt ',
        16, // PCM格式的subchunk1Size固定为16
        1, // 音频格式:PCM=1
        $channels,
        $sampleRate,
        $byteRate,
        $blockAlign,
        $bitsPerSample
    );
    
    $header .= pack('a4V', 'data', $pcmSize);
    
    return $header;
}

关键说明

  • 解析WAV头:parseWavHeader函数会处理标准WAV头及可能存在的额外元数据块,确保准确提取PCM数据的起始位置。
  • 统一生成WAV头:buildWavHeader根据总PCM数据长度和采样参数生成全局WAV头,保证播放器能正确识别整个音频的参数。
  • 参数一致性:确保所有TTS生成的LINEAR16片段使用相同的采样率、位深和声道数(Google Cloud TTS默认是16位、单声道、24000Hz,除非主动修改),否则拼接后的音频会出现失真。

内容的提问来源于stack exchange,提问作者Simon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 21:05:11