Laravel 项目中 PCM 音波文件转 WAV 音频文件案例 [经验分享]

需求#

前段时间,产品经理告诉一个需求,说是客户端录音之后实时转换成文字,后台上传一个录音文件后转换成文本,两个功能点属于个一个功能模块(其实就是后台和客户端产生的数据互通),我想了下说可以做,对接阿里云服务,客户端使用实时语音识别服务,并保留录音文件;后天使用录音文件识别服务,并保留音频文件且转码成 MP3 市客户端能播放该数据。

问题#

功能上第一版的时候实现,但是由于客户端在录音时需要调用阿里云的 SDK(别人的 SDK 一点不香),产生的录音文件就是音波,客户端需要转换成 WAV 或者其他格式(后台会统一走队列转码成 MP3 格式)。客户端请求阿里云得到文本数据后,然后提交时数据时需要将音波文件转换成 WAV 并上传给后端,这些流程走下来之后整个体验的流程就有点忙了,毕竟文件越大,上传接口就慢(带宽是最大的影响)。

优化背景#

知道问题后那就说下优化方案,客户端说录音的音波文件我们不转,让后段转,客户端一段一段上传音波文件给后台,后台合并后转换成 WAV 格式,成了 WAV 格式后客户端和后台所使用的播放器在队列没有完成转码任务时就已经可以播放该音频文件并可看文本信息

开干#

三前端开发围着我让实现,我只能撸了,最终撸了出来,然后产品让我整理下文档(程序员最讨厌给自己的代码写文档,也讨厌被人写的代码没文档)

结果#

具体使用方法这里就不做介绍,本文只是总结,具体信息可跳至仓库查阅

GitHub :PcmToWav

原理#

什么是 PCMWAV#

PCM :PCM(Pulse Code Modulation---- 脉码调制录音)。所谓 PCM 录音就是将声音等模拟信号变成符号化的脉冲列,再予以记录。 PCM 信号是由 10 等符号构成的数字信号,而未经过任何编码和压缩处理。与模拟信号比,它不易受传送系统的杂波及失真的影响。动态范围宽,可得到音质相当好的影响效果。

WAVWAV 是一种无损的音频文件格式, WAV 符合 PIFF (Resource Interchange File Format) 规范。所有的 WAV 都有一个文件头,这个文件头音频流的编码参数。WAV 对音频流的编码没有硬性规定,除了 PCM 之外,还有几乎所有支持 ACM 规范的编码都可以为 WAV 的音频流进行编码。

PCMWAV 的关系#

简单地说,PCM 是音频的原始数据,WAV 则是一种封装音频数据的容器,而且它的格式还很简单,只是在数据开头添加一些和音频数据相关的头信息。

规则#

首先我们看一下 WAV 的格式规则,下图所示

解决#

了解这些规则后,我们就可以撸代码吧

1、 ChunkID 占 4byte, 固定值 "RIFF"

$ChunkID = array(0x52, 0x49, 0x46, 0x46); // RIFF 16进制的0x52等于10进制中的82,82对应的ASCII码为R

2、 ChunkSize 占 4byte, 值为 4 + (8 + SubChunk1Size) + (8 + SubChunk2Size), 其中如果原始数据是 PCM, 简化为 36 + SubChunk2Size

$ChunkSize = array(0x0, 0x0, 0x0, 0x0);
$ChunkSize = self::getLittleEndianByteArray(36 + $dataSize);

3、 Format 占 4byte, 固定值 "WAVE"

$FileFormat = array(0x57, 0x41, 0x56, 0x45); // WAVE

4、 Subchunk1ID 占 4byte, 固定值 "ftm"(注意空格补全 4 位)

$Subchunk1ID = array(0x66, 0x6D, 0x74, 0x20); // fmt

5、 Subchunk1Size 占 4byte, 数据为 PCM 时,值为 16

$Subchunk1Size = array(0x10, 0x0, 0x0, 0x0); // 16 little endian

6、 AudioFormat 占 2byte, 数据为 PCM 时,值为 1, 其他值表示数据进行过某种压缩

$AudioFormat = array(0x1, 0x0); // PCM = 1 little endian

7、 NumChannels 占 2byte, 对应 AudioRecord 中的 channelConfig, 单声道 Mono = 1, 立体声 Stereo = 2

if ($numchannels == 2) {
    $fmt->NumChannels = array(0x2, 0x0); // 立体声为2
} else {
    $fmt->NumChannels = array(0x1, 0x0); // 单声道为1
}

8、 SampleRate 占 4byte, 对应 AudioRecord 中的 sampleRateInHz, 即采样频率,例如 8000, 16000, 44100

$SampleRate = self::getLittleEndianByteArray($samplerate);

9、 ByteRate 占 4byte, 值为 SampleRate * BlockAlign

self::getLittleEndianByteArray($samplerate * $numchannels * ($bitspersample / 8));

10、 BlockAlign 占 2byte, 值为 NumChannels * BitsPerSample / 8

self::getLittleEndianByteArray($numchannels * ($bitspersample / 8), true);

11、 BitsPerSample 占 2byte, 对应 AudioRecord 中的 audioFormat, 8bits = 8, 16bits = 16

self::getLittleEndianByteArray($bitspersample, true);

12、 Subchunk2ID 占 4byte, 固定值 "data", 即

$Subchunk2ID = array(0x64, 0x61, 0x74, 0x61); // data

13、 Subchunk2Size 占 4byte, 描述音频数据的长度,就是 pcm 文件大小

self::getLittleEndianByteArray(filesize($filename));

14、 Data 占 pcm 文件大小个 byte, 表示原始的 PCM 音频数据

getLittleEndianByteArray 方法说明

getLittleEndianByteArray 主要是将传递过来的数进行处理已转换成需要使用的数据,站几字节,就返回多少长度的数组

private static function getLittleEndianByteArray($lValue, $short = false)
    {
        $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);

        if ($short) { // 为 `true` 时返回长度为2的数组
            $tmp = array_slice($b, 0, 2);
            $b = $tmp;
        }

        return $b;
    }

整体思路#

整个文件的开头 44 字节信息也基本说明完了,下面就说下处理类文件的实现,这边处理的逻辑先临时创建一个只有 44 字节的文件,然后将 PCM 文件的数据追加进该文件,最终根据 WAV 的格式规则实际计算出真实的头部 44 字节信息并将文件修改指针指向文件开头,然后修改为新产生的数据

结语#

整体上好像跟 Laravel 不搭嘎,但是公司的产品确实是基于 Laravel 开发,项目中引入了 composer 安装。PHP 版面人少我就发这哈了 :joy: :joy:
写的不好,大佬们轻点虐 :relaxed::relaxed:

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 5年前 自动加精
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
讨论数量: 8

PHP 处理字节数据非常痛苦,不容易。

6年前 评论
Summer

排版清晰,不错不错

6年前 评论

@Wi1dcard 没办法,搬砖的谁管咱累不累,只要搬得动就行 :joy: :joy:

6年前 评论

file
这个字是不是打错了呀, 是 “实时 “的意思吗

6年前 评论

为什么不适用 ffmpeg?

5年前 评论

@隔壁老王
实际上是可以使用的,但是根据实际情况可能用不上,该文档只是做分享,并非最佳解决方案
1、底层有 Go 服务专门的去处理音视频转码(不支持 PCM 格式)
2、业务逻辑上是优先使未转码的文件能在客户端进行播放

5年前 评论