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
相关产品推荐
相关产品推荐

