继 6 年前使用 js 实现的 mp4 封装之后,再次回顾编解码的知识是在23年8月接收到的私信,让补充下插件里的音频部分。
被迫回去翻了一下6年前的代码,然而发现当初提交的也没有音频的部分,而由于时间久远,早已忘记的差不多了,没能力赚这笔外快了。视频编码部分还是因为有保留的代码支持,才能捡回来一些。
背景
原文 js实现封装MP4格式文件并下载 中,因为近几年的技术更新与变化,一些重要的资料网站也被关停了。然而,我现在又要开始做音视频编码了。
作为一名前端开发者,我现在需要在鸿蒙 5.0 下实现音频编码。关于为什么不说编解码,因为目前的需求,收到的音频数据流是 PCM 这样的基础格式,可以通过编码封装为各种其他的如 wav、mp3、aac 等其他可进行播放的音频格式。
音频常见格式
对音频进行编码常见的格式有:
格式 | 类型 | 说明 |
---|---|---|
PCM | 无压缩 | 一种将模拟信号的数字化方法,无损编码。 |
WAV | 无压缩 | 有多种实现方式,但是都不会进行压缩操作。其中一种实现就是在 PCM 数据格式的前面加上 44 字节,分别用来描述 PCM 的采样率、声道数、数据格式等信息。音质非常好,大量软件都支持。 |
MP3 | 有损压缩 | 音质在 128 Kbps 以上表现还不错,压缩比比较高,大量软件和硬件都支持,兼容性好。 |
AAC | 有损压缩 | 在小于 128 Kbps 的码率下表现优异,并且多用于视频中的音频编码。 |
OPUS | 有损压缩 | 可以用比 MP3 更小的码率实现比 MP3 更好的音质,高中低码率下均有良好的表现,兼容性不够好,流媒体特性不支持。适用于语音聊天的音频消息场景。 |
PCM 是音频原始数据的基础格式,并不支持直接用于播放,但可以将其通过编码转换成其他支持播放的格式文件,也可将一些格式文件解码成PCM后再进行编码来实现不同格式的音频文件转换;AAC 则在短视频和直播场景广泛使用。
我们能直接获取到的默认音频格式为音频裸数据格式,即 PCM 格式,因此并不需要对 PCM 进行额外的编码,但需要获取到对应的配置,用于将 PCM 格式的音频转换为其他格式的音频,比如我们本文中的 WAV 格式。
WAV
WAV 全称 Waveform Audio File Format,是微软公司开发的一种无损声音文件格式,也叫波形声音文件,是最早的数字音频格式,被 Windows 平台及其应用程序广泛支持。
WAV 符合 RIFF(Resource Interchange File Format) 规范,所有的WAV都由 44字节 文件头 和 PCM 数据 组成,这个文件头包含语音信号的所有参数信息(声道数、采样率、量化位数、比特率....) 44个字节的 头文件由 3个区块组成:
- RIFF chunk:WAV文件标识
- Format chunk: 声道数、采样率、量化位数、等信息
- Data chunk:存放 PCM 数据
根据上图,所有区块的内容如下:
RIFF 区块
名称 | 字节数 | 内容 | 描述 |
---|---|---|---|
ID | 4Byte | 'RIFF' | RIFF 标识 |
Size | 4Byte | fileSize - 8 | 整个文件的长度减去ID 和Size 的长度 |
Type | 4Byte | 'WAVE' | WAVE 格式类型。表示后面需要两个子块:Format 区块和Data 区块 |
format 区块
名称 | 字节数 | 内容 | 描述 |
---|---|---|---|
ID | 4Byte | 'fmt ' | fmt 标识 |
Size | 4Byte | 16 | 区块长度 fmt (不包含ID 和Size 部分的长度) |
AudioFormat | 2Byte | 音频格式 | PCM = 1(即线性量化),1 以外的值表示一些压缩形式。 |
NumChannels | 2Byte | 声道数 | 音频数据的声道数,1:单声道,2:双声道 |
SampleRate | 4Byte | 采样率 | 音频数据的采样率 |
ByteRate | 4Byte | 每秒数据字节数 | 音频数据的码率:采样率 * 通道数 * 位深 / 8 |
BlockAlign | 2Byte | 数据块对齐 | 一个样本的字节数:通道数 * 位深 / 8 |
BitsPerSample | 2Byte | 采样位数 | 位深:8 位 = 8,16 位 = 16,等等 |
data 区块
名称 | 字节数 | 内容 | 描述 |
---|---|---|---|
ID | 4Byte | 'data' | data 标识 |
Size | 4Byte | 音频数据长度 | 音频数据长度,一般为ByteRate * 时间(s),若为文件,则可直接取文件长度 |
Data | NByte | 音频数据 | 音频数据内容 |
根据上述的WAV 格式标准,我们就可以在鸿蒙上实现一个将 PCM 格式文件编码成 WAV 格式文件的功能函数了
实现
WAV 文件格式还是比较清晰的,因此实现上也比较简单,将 PCM 文件的内容读取出来,按照上述的格式,在 WAV 文件中写入文件头,再将 PCM 文件内容续写到 WAV 文件中即可完成 PCM 到 WAV 格式的音频文件转换。
由于鸿蒙 ArkTS 对前端的友好性,对于前端来说,实现上也变得更加简单。
ArkTS
import fs, { ReadOptions } from '@ohos.file.fs';
export class PcmToWavUtil {
//采样率
private mSampleRate: number = 0
// 声道数
private mChannel: number = 0;
/**
* @param sampleRate sample rate、采样率
* @param channel channel、声道
*/
constructor(sampleRate:number, channel: number) {
this.mSampleRate = sampleRate;
this.mChannel = channel;
}
/**
* pcm文件转wav文件函数
* @param src 源文件
* @param dest 目标文件
*/
public pcmToWav(src: string, dest: string) {
const inFile: fs.File = fs.openSync(src, fs.OpenMode.READ_ONLY);
const outFile: fs.File = fs.openSync(dest, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
let byteRate = 16 * this.mSampleRate * this.mChannel / 8;
const inFileStat = fs.statSync(inFile.fd)
// 音频文件
let audioDataSize = inFileStat.size;
let totalDataLen = audioDataSize + 36;
// 1. wav 文件头编写
this.writeWaveFileHeader(outFile, audioDataSize, totalDataLen, byteRate);
// 2. 写入 pcm 数据
this.writePcmData(inFile, outFile, audioDataSize)
}
// pcm 数据写入到 pcm 文件函数
private writePcmData(inFile: fs.File, outFile: fs.File, audioDataSize: number) {
// 写入 pcm 数据
let readSize = 0
let data = new ArrayBuffer(audioDataSize);
let readOptions: ReadOptions = {
offset: readSize,
length: audioDataSize
};
let readLen = fs.readSync(inFile.fd, data, readOptions);
while (readLen > 0) {
readSize += readLen;
fs.writeSync(outFile.fd, data, { length: readLen});
readOptions.offset = readSize;
readLen = fs.readSync(inFile.fd, data, readOptions);
}
fs.closeSync(inFile.fd)
fs.closeSync(outFile.fd)
}
//写入wav文件头函数
private writeWaveFileHeader(
out: fs.File,
audioDataSize: number,
totalDataLen: number,
byteRate: number
) {
const header = new ArrayBuffer(44);
const dv = new DataView(header);
const bitsPerSample = 16; // 当前位深是16
// 写入RIFF块
this.writeString(dv, 0, 'RIFF');
dv.setUint32(4, totalDataLen, true);
this.writeString(dv, 8, 'WAVE');
// 写入fmt块
this.writeString(dv, 12, 'fmt ');
dv.setUint32(16, 16, true); // fmt块大小
dv.setUint16(20, 1, true); // 格式类别 (PCM)
dv.setUint16(22, this.mChannel, true); // 通道数
dv.setUint32(24, this.mSampleRate, true); // 采样率
dv.setUint32(28, byteRate, true); // ByteRate 码率
dv.setUint16(32, this.mChannel * bitsPerSample / 8, true); // BlockAlign
dv.setUint16(34, bitsPerSample, true); // 位深
// 写入data块
this.writeString(dv, 36, 'data');
dv.setUint32(40, audioDataSize, true); // 数据块大小
// 写入
fs.writeSync(out.fd, new Uint8Array(header).buffer, {
length: 44
})
}
private writeString(dv: DataView, offset: number, str: string) {
for (let i = 0; i < str.length; i++) {
dv.setUint8(offset + i, str.charCodeAt(i));
}
}
}
最后
如今随着 ffmpeg 的发展,已经可以实现各种音视频的直接转换,能够直接输入一个音视频文件,通过一系列的指令和参数,实现一键粗暴生成想要的音视频格式了。但站在开发者的角度,从很多方面考虑,在能使用ffmpeg的情况下还是很原因使用的,除非场景或需求上由于一些条条框框的限制上不允许,比如这里的 wav 格式编码,可能直接通过代码实现只需要百来行代码,但引入 ffmpeg 这么一个庞然大物是否值得呢。
基于 HarmonyOS NEXT 广泛应用的 ArkTS 语言,众多前端技术得以在鸿蒙系统上顺畅运用。例如,在上述音频编码实现中,DataView 类和 fs 模块的表现与前端中的 DataView 以及 Node 环境下的 fs 模块的使用上高度相似,这使得在功能实现过程中减少了一些技术障碍。
就目前来看,HarmonyOS NEXT 在一定情况下为前端开发者拓展了新的领域方向,提供了更多选择的可能性。