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 协议》,转载必须注明作者和本文链接
本帖由系统于 4年前 自动加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 8

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

5年前 评论
Summer

排版清晰,不错不错

5年前 评论

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

5年前 评论

@Summer 大佬,请多指教 :see_no_evil:

5年前 评论

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

5年前 评论

@一念沧海一念桑田 是的,打错字了,已改正 :kissing_heart:

5年前 评论

为什么不适用ffmpeg?

4年前 评论

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

4年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!