Android音频编解码Opus应用分析



一、引言

最近项目(AMA、DMA、GMA)中用到了语音编码Opus,在网上搜了一下,APP端的资料还是比较少,而且没有一个完整的例子。

二、Opus介绍

Opus是一个有损声音编码的格式,由IETF开发,没有任何专利或限制,适用于网络上的实时声音传输,标准格式为RFC 6716,其技术来源于Skype的SILK及Xiph.Org的CELT编码。

Opus集成了两种声音编码的技术:以语音编码为导向的SILK和低延迟的CELT。Opus可以无缝调节高低比特率。在编码器内部它在较低比特率时使用线性预测编码在高比特率时候使用变换编码(在高低比特率交界处也使用两者结合的编码方式)。Opus具有非常低的算法延迟(默认为22.5 ms),非常适合用于低延迟语音通话的编码,像是网络上的即时声音流、即时同步声音旁白等等,此外Opus也可以通过降低编码比特率,达成更低的算法延迟,最低可以到5 ms。在多个听觉盲测中,Opus都比MP3、AAC、HE-AAC等常见格式,有更低的延迟和更好的声音压缩率。

主要特性有:

  • 6 kb /秒到510 kb / s的比特率
  • 采样率从8 kHz(窄带)到48 kHz(全频)
  • 帧大小从2.5毫秒到60毫秒
  • 支持恒定比特率(CBR)和可变比特率(VBR)
  • 从窄带到全频段的音频带宽
  • 支持语音和音乐
  • 支持单声道和立体声
  • 支持多达255个频道(多数据流的帧)
  • 可动态调节比特率,音频带宽和帧大小
  • 良好的鲁棒性丢失率和数据包丢失隐藏(PLC)
  • 浮点和定点实现

官网:http://www.opus-codec.org/

GitHub: OPUS

三、Opus应用

3.1Download soucecode

通过官网或GitHub下载工程。

3.2 建立Opus JNI工程

JNI工程需封装接口及编写MakeFile、编译。

3.2.1 封装接口

接口封装的流程有:创建、设置、使用、销毁。

  • 创建一个OpusEncoder类型的对象。 opus_encoder_create()
  • 对编码对象进行参数设置。opus_encoder_ctl()。包括比特率,带宽,是否使用vbr等参数信息
  • 当然就是音频编码啦。opus_encode(),该函数返回编码的后的音频长度
  • 使用完成之后,别忘记删除这个OpusEncoder对象,opus_encoder_destroy()
//初始化语音,需要什么参数可以自定义参数个数
jint Java_com_xmamiga_opustool_OpusJni_OpusInit(JNIEnv *env,jobject thiz,jint compression){

   int err;
    encoder = opus_encoder_create(SAMPLE_RATE, CHANNELS, APPLICATION, &err);
   if (err<0)
   {
      return 0;
   }

   err = opus_encoder_ctl(encoder, OPUS_SET_BITRATE(BITRATE));
   if (err<0)
   {
      return 0;
   }

   opus_encoder_ctl(encoder, OPUS_SET_VBR(use_vbr));
   opus_encoder_ctl(encoder, OPUS_SET_VBR_CONSTRAINT(cvbr));
   opus_encoder_ctl(encoder, OPUS_SET_COMPLEXITY(compression));
   opus_encoder_ctl(encoder,   OPUS_SET_PACKET_LOSS_PERC(packet_loss_perc));
   decoder = opus_decoder_create(SAMPLE_RATE, CHANNELS, &err);
   if (err<0)
   {
      return 0;
   }
   return 1;
}
//编码,传入参数数量可自定义
jint Java_com_xmamiga_opustool_OpusJni_OpusEncode(JNIEnv *env,jobject thiz,  jshortArray lin, jint offset, jbyteArray encoded, jint size)
{
    int err;
    int i;
    int nbBytes;
    unsigned short cbits[480];
    unsigned short out[160];

    if(encoder == NULL){
        return 0;
    }
    //读取参数 数组
    (*env)->GetShortArrayRegion(env,lin,offset,size,out);
    nbBytes = opus_encode(encoder,out , size, cbits, 480);
    //设定压缩完的数据到数组中
    (*env)->SetByteArrayRegion(env,encoded, 0, nbBytes, (jshort*)cbits);

    return nbBytes;
}
//解码
jint Java_com_xmamiga_opustool_OpusJni_OpusDecode(JNIEnv *env,jobject thiz, jbyteArray encoded, jshortArray lin, int size)
{
    int err;
    int i;
    int frame_size;

    int nbBytes;
    unsigned char cbits[MAX_PACKET_SIZE];
    unsigned short out[160];

    unsigned short deout[160];
    if(decoder == NULL){
        return 0;
    }

    (*env)->GetByteArrayRegion(env, encoded,0,size,out);
    frame_size = opus_decode(decoder, out, size, deout, MAX_FRAME_SIZE, 0);

    if(frame_size <0){
        return 0;
    }

    int nSize = frame_size*CHANNELS;
    (*env)->SetShortArrayRegion(env,lin, 0, nSize, (jshort*)deout);

    return nSize;
}

//释放语音对象
void Java_com_xmamiga_opustool_OpusJni_OpusClose(JNIEnv *env,jobject thiz)
{
    if(encoder !=NULL){
        opus_encoder_destroy(encoder);
    }

    if(encoder !=NULL){
        opus_decoder_destroy(decoder);
    }
}

3.2.2 MakeFile

MakeFile即.mk文件:

LOCAL_PATH := $(call my-dir)  #加载当前路径
include $(CLEAR_VARS)
include $(LOCAL_PATH)/celt_sources.mk   #加载celt 所有.c的 mk
include $(LOCAL_PATH)/silk_sources.mk  #加载silk 所有.c 的mk
include $(LOCAL_PATH)/opus_sources.mk #加载opus 所有.c 的mk
MY_MODULE_DIR       := newopus  #库的名称
LOCAL_MODULE        := $(MY_MODULE_DIR)
SILK_SOURCES += $(SILK_SOURCES_FIXED)
#编译的源代码.c
CELT_SOURCES += $(CELT_SOURCES_ARM)
SILK_SOURCES += $(SILK_SOURCES_ARM)
LOCAL_SRC_FILES     := Opusmain.c $(CELT_SOURCES) $(SILK_SOURCES) $(OPUS_SOURCES)

#LOCAL_LDLIBS        := -lm –llog  #加载系统的库 日志库

LOCAL_C_INCLUDES    := \
$(LOCAL_PATH)/include \
$(LOCAL_PATH)/silk \
$(LOCAL_PATH)/silk/fixed \
$(LOCAL_PATH)/celt
#附加编译选项
LOCAL_CFLAGS        := -DNULL=0 -DSOCKLEN_T=socklen_t -DLOCALE_NOT_USED -D_LARGEFILE_SOURCE=1 -D_FILE_OFFSET_BITS=64
LOCAL_CFLAGS        += -Drestrict='' -D__EMX__ -DOPUS_BUILD -DFIXED_POINT=1 -DDISABLE_FLOAT_API -DUSE_ALLOCA -DHAVE_LRINT -DHAVE_LRINTF -O3 -fno-math-errno
LOCAL_CPPFLAGS      := -DBSD=1
LOCAL_CPPFLAGS      += -ffast-math -O3 -funroll-loops
include $(BUILD_SHARED_LIBRARY)  #编译动态库设置
3.2.3 工程编译配置

工程编译配置生成so文件。

android {
    ......
    defaultConfig {
        ......
        ndk {
            moduleName "opus"    //生成的so名字
            ldLibs "log", "z", "m"
            abiFilters "armeabi", "armeabi-v7a", "x86"  //输出指定三种abi体系结构下的so库。目前可有可无。
        }
    }

    externalNativeBuild {
        ndkBuild {
            path 'src/main/jni/Android.mk'
        }
    }
}

3.3 应用层JNI接口封装

    private static OpusJni mInstance;

    public synchronized static OpusJni getInstance() {
        if (mInstance == null)
            mInstance = new OpusJni();
        return mInstance;
    }

    static
    {
        System.loadLibrary("opus");
    }
    public native int OpusInit(int complexity);  //complexity:0
    public native int OpusEncode(short[] src, int offset, byte[] out, int size); // 压缩数据,长度为320short->40byte
    public native int OpusDecode(byte[] src, short[] out, int size);// 解压缩数据,长度为40byte->320short
    public native void OpusClose(); // 释放内存

3.4 Opus编码

/**
     * 将raw转化为opus文件
     * @param inFileName
     * @param outFileName
     */
    public static void raw2Opus(String inFileName, String outFileName) {

        FileInputStream rawFileInputStream = null;
        FileOutputStream fileOutputStream = null;
        try {
            rawFileInputStream = new FileInputStream(inFileName);
            fileOutputStream = new FileOutputStream(outFileName);
            byte[] rawbyte = new byte[320];
            byte[] encoded = new byte[160];
            //将原数据转换成opus压缩的文件,opus只能编码160字节的数据,需要使用一个循环
            int readedtotal = 0;
            int size = 0;
            int encodedtotal = 0;
            while ((size = rawFileInputStream.read(rawbyte, 0, 320)) != -1) {
                readedtotal = readedtotal + size;
                short[] rawdata = ShortByteUtil.byteArray2ShortArray(rawbyte);
                int encodesize = OpusJni.getInstance().OpusEncode(rawdata, 0, encoded, rawdata.length);

                fileOutputStream.write(encoded, 0, encodesize);
                encodedtotal = encodedtotal + encodesize;
            }
            fileOutputStream.close();
            rawFileInputStream.close();
        } catch (Exception e) {

        }

    }

3.5 Opus解码

/**
     * 将opus文件转化为raw文件
     * @param inFileName
     * @param outFileName
     */
    public static void opus2Raw(String inFileName, String outFileName) {
        FileInputStream inAccessFile = null;
        FileOutputStream fileOutputStream = null;
        try {
            inAccessFile = new FileInputStream(inFileName);
            fileOutputStream = new FileOutputStream(outFileName);
            byte[] inbyte = new byte[20];
            short[] decoded = new short[160];
            int readsize = 0;
            int readedtotal = 0;
            int decsize = 0;
            int decodetotal = 0;
            while ((readsize = inAccessFile.read(inbyte, 0, 20)) != -1) {
                readedtotal = readedtotal + readsize;
                decsize = OpusJni.getInstance().OpusDecode(inbyte, decoded, readsize);

                fileOutputStream.write(ShortByteUtil.shortArray2ByteArray(decoded), 0, decsize*2);
                decodetotal = decodetotal + decsize;
            }
            fileOutputStream.close();
            inAccessFile.close();
        } catch (Exception e) {

        }
    }

3.6 初始化及销毁

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ......
        OpusJni.getInstance().OpusInit(0);
    }

    @Override
    protected void onDestroy(){
        super.onDestroy();
        OpusJni.getInstance().OpusClose();
    }

四、调试

调试踩过的坑:

  • 对于连续的一段声音,一定只能用一个解码器(不能创建之后释放再去创建解码器)
  • 声音不正常, 主要需要确认: 终端发过来的流/RTP收包部分/解码部分/重采样/混音部分/编码部分/发包部分
  • 定位问题,需要在每个地方都打桩写文件来确认,写opus文件的时候,需要注意格式(加8字节的头)

五、总结

通过对比,使用Opus效果比较好, 30%以内的丢包, 都能够保证语音能够正常听清楚。