【harmonyOS NEXT 下的前端开发者】WAV音频编码实现

继 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 整个文件的长度减去IDSize的长度
Type 4Byte 'WAVE' WAVE 格式类型。表示后面需要两个子块:Format区块和Data区块

format 区块

名称 字节数 内容 描述
ID 4Byte 'fmt ' fmt 标识
Size 4Byte 16 区块长度 fmt (不包含IDSize 部分的长度)
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 在一定情况下为前端开发者拓展了新的领域方向,提供了更多选择的可能性。

附录

WAVE介绍

相关推荐
Hello-Mr.Wang4 分钟前
vue3中开发引导页的方法
开发语言·前端·javascript
程序员凡尘32 分钟前
完美解决 Array 方法 (map/filter/reduce) 不按预期工作 的正确解决方法,亲测有效!!!
前端·javascript·vue.js
编程零零七4 小时前
Python数据分析工具(三):pymssql的用法
开发语言·前端·数据库·python·oracle·数据分析·pymssql
(⊙o⊙)~哦6 小时前
JavaScript substring() 方法
前端
无心使然云中漫步6 小时前
GIS OGC之WMTS地图服务,通过Capabilities XML描述文档,获取matrixIds,origin,计算resolutions
前端·javascript
Bug缔造者6 小时前
Element-ui el-table 全局表格排序
前端·javascript·vue.js
xnian_7 小时前
解决ruoyi-vue-pro-master框架引入报错,启动报错问题
前端·javascript·vue.js
麒麟而非淇淋8 小时前
AJAX 入门 day1
前端·javascript·ajax
2401_858120538 小时前
深入理解MATLAB中的事件处理机制
前端·javascript·matlab
阿树梢8 小时前
【Vue】VueRouter路由
前端·javascript·vue.js