头图

HarmonyOS Next 音视频之OPUS音频编码实战

背景

在聊天场景发送短语音消息需求中需要对发送的音频内容做编码压缩,最开始是用MP3编码器压缩的,后面语音消息要用于ASR模型的训练,需要使用OPUS编码器来处理语音类的信号。之前在Android上是不支持MP3和OPUS编码的,目前HarmonyOS 对MP3和OPUS编码都提供了支持,HarmonyOS 支持的编码器类型:

容器规格音频编码类型
mp4AAC、Flac
m4aAAC
flacFlac
aacAAC
mp3MP3
rawG711mu
amrAMR
oggopus

通过系统API进行opus编码后发现编码后音频文件无法播放,查看文件发现编码器没有自动做Muxer,查看系统API 发现系统Muxer 不支持ogg容器,目前支持的封装能力如下:

封装格式视频编解码类型音频编解码类型封面类型
mp4AVC(H.264)、HEVC(H.265)AAC、MPEG(MP3)jpeg、png、bmp
m4a-AACjpeg、png、bmp
mp3-MPEG(MP3)-

HarmonyOS 系统不支持 ogg容器,需要自己对容器做实现。

opus ogg封装介绍

ogg是以页(page)为单位将逻辑流组织链接起来,每个页都有pageheader和pagedata。页头中有如下的定义:

  1. capture_pattern页标识:ASCII字符,0x4f 'O' 0x67 'g' 0x67 'g' 0x53 'S',<font color=red>4个字节</font>大小,它标识着一个页的开始。
  2. stream_structure_version版本id:一般当前版本默认为0,<font color=red>1个字节</font>。
  3. header_type_flag类型标识:标识当前的页的类型,<font color=red>1个字节</font>,

     - 0x01:本页媒体编码数据与前一页属于同一个逻辑流的同一个packet,若此位没有设,表示本页是以一个新的packet开始的;
     - 0x02:表示该页为逻辑流的第一页,bos标识,如果此位未设置,那表示不是第一页;
     - 0x04:表示该页位逻辑流的最后一页,eos标识,如果此位未设置,那表示本页不是最后一页。
  4. granule_position:媒体编码相关的参数信息,<font color=red>8个字节</font>,对于音频流来说,它存储着到本页为止逻辑流在PCM输出中采样码的数目,可以由它来算得时间戳。对于视频流来说,它存储着到本页为止视频帧编码的数目。若此值为-1,那表示截止到本页,逻辑流的packet未结束。(小端)
  5. serial_number:当前页中的流的id,<font color=red>4个字节</font>,它是区分本页所属逻辑流与其他逻辑流的序号,我们可以通过这个值来划分流。(小端)
  6. page_seguence_number:本页在逻辑流的序号,<font color=red>4个字节</font>。
  7. CRC_cbecksum:循环冗余效验码效验,<font color=red>4个字节</font>,用来效验每页的有效性。
  8. number_page_segments:给定本页在segment_table域中出现的segement个数,<font color=red>1个字节</font>。
  9. segment_table:从字面看它就是一个表,表示着每个segment的长度,取值范围是0~255。由segment(1个segment就是1个字节)可以得到packet的值,每个packet的大小是以最后一个不等于255的segment结束的,从页头中的segment_table可以得到每个packet长度,举例:如果一组segment依次顺序为FF 45 FF FF FF 40FF 05FF FF FF 66(共4个packet,含12个segment,每个packet的长度是:FF 45【324】;FF FF FF 40【829】;FF 05【260】;FF FF FF 66【847】),那么第一个packet的长度为255+69 = 324,第二个packet大小829,同理。

    页头基本上就是由上述的参数组成,由此我们可以得到页头的长度和整个页的长度:

    header_size  = 27+number_page_segments ;(byte)
    page_size = header_size +segment_table中每个segment的大小;

    页头部格式:
    image.png

实现ogg封装

xiph提供了opus ogg封装的开源实现libopusenc,但是libopusenc依赖libopus库,所以要使用libopusenc实现ogg容器封装,简单的办法是直接基于libopusenc实现opus的编码和容器封装。

libopusenc的处理流程如下:

创建编码器

首先创建comments:

OggOpusComments *comments = ope_comments_create();  
ope_comments_add(comments, "ARTIST", "qingkouwei");  
ope_comments_add(comments, "TITLE", "qingkouwei-im");

可以自定义一些音频文件说明,接下来创建编码器:

OggOpusEnc *pEnc = ope_encoder_create_file(outputFilePath_, comments, inSamplerate, inChannel,  
                                              quality, &error);  
    if (pEnc) {    
        int ret = ope_encoder_ctl(pEnc, OPUS_SET_BITRATE(outBitrate));  
    }

参数有编码复用器输出文件路径,comment信息,以及采样率,声道数,音频质量等信息。

编码PCM数据

static napi_value encodePCMToOpusOggNative(napi_env env, napi_callback_info info)  
{  
    size_t argc = 1;  
    napi_value args[1] = {nullptr};  
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);  
        void* inputBuffer;   
size_t inputLength;   
napi_get_arraybuffer_info(env, args[0], &inputBuffer, &inputLength);   
ope_encoder_write(pEnc, (short *)inputBuffer, inputLength/2);  
    return nullptr;}

将TS层的二进制数据传给ope_encoder_write函数,ope_encoder_write函数会将编码后数据写入到enc创建时指定的路径。

关闭编码复用器

static napi_value closeOpusOggEncoderNative(napi_env env, napi_callback_info info)  
{  
   ope_encoder_drain(pEnc);  
    ope_encoder_destroy(pEnc);  
    if(comments != NULL){  
        ope_comments_destroy(comments);  
        comments = NULL;  
    }  
    return nullptr;  
}

释放enc,comments等对象,整体流程还是比较简单的,最终数据ogg容器文件用通用播放器可以正常播放:
image.png

总结

本文介绍了HarmonyOS中实现OPUS编码并进行OGG容器封装的方法,解决业务场景对音频编码的特殊要求诉求。


轻口味
35.2k 声望5.3k 粉丝

移动端十年老人,主要做IM、音视频、AI方向,目前在做鸿蒙化适配,欢迎这些方向的同学交流:wodekouwei