当前位置: 首页 > 工具软件 > Opus codec > 使用案例 >

在Android中实现OPUS编码

申屠英韶
2023-12-01

将PCM转换成OPUS编码

Opus是一个有损声音编码的格式,由Xiph.Org基金会开发,之后由IETF(互联网工程任务组)进行标准化,目标是希望用单一格式包含声音和语音,取代Speex和Vorbis,且适用于网络上低延迟的即时声音传输,标准格式定义于RFC 6716文件。Opus格式是一个开放格式,使用上没有任何专利或限制。

采样率16k,位深度16bit,单声道的音频数据,用自动比特率编码成OPUS格式,并加上ogg封装之后,大小只有原来的1/13,这对于移动平台来说,为传输延时带来的好处是很明显的。

下面介绍两种对PCM进行编码的方法,仅供参考。

使用Concentus

在github上,有个Concentus项目,下面是其项目简介:

This project is an effort to port the Opus reference library to work natively in other languages, and to gather together any such ports that may exist. With this code, developers should be left with no excuse to use an inferior codec, regardless of their language or runtime environment.

这个项目提供了C#、Java、JavaScript这三种语言的编码方法。在Android平台上,可使用其提供的Java编码。

  1. 将Java/Concentus/src/main/java/org/concentus/下面的所有代码都复制到你的项目中。
  2. 在build.gradle中增加依赖:implementation ‘org.gagravarr:vorbis-java-core:0.8’

在/Java/ConcentusTestConsole/src/main/java/org/concentus/console/Program.java中,有Concentus的使用方法,下面分析其中的主要代码:

// 要读取的pcm格式的文件
FileInputStream fileIn = new FileInputStream("C:\\Users\\lostromb\\Documents\\Visual Studio 2015\\Projects\\Concentus-git\\AudioData\\48Khz Stereo.raw");

// 创建解码器并设置解码器的相关参数。其中bitrate越大,转换之后的文件音频质量越好,但是体积也越大。
// 如果不知道用什么,可以设置成OpusConstants.OPUS_AUTO。
OpusEncoder encoder = new OpusEncoder(48000, 2, OpusApplication.OPUS_APPLICATION_AUDIO);
encoder.setBitrate(96000);
encoder.setSignalType(OpusSignal.OPUS_SIGNAL_MUSIC);
encoder.setComplexity(10);

// 要输出的opus格式的文件
FileOutputStream fileOut = new FileOutputStream("C:\\Users\\lostromb\\Documents\\Visual Studio 2015\\Projects\\Concentus-git\\AudioData\\out.opus");

// 类似于wav文件44个字节的文件头,如果想要播放器能正确的读取opus格式的文件,需要在文件的开头及相关地方,储存声道,采样率等信息,所以需要构造一个OpusFile类型的变量,也就是一个标准的ogg封装。
// 构造OpusFile变量需要三样东西,一个OutputStream,存储声道、采样率等信息的OpusInfo,和存储标准tag的OpusTags。
OpusInfo info = new OpusInfo();
info.setNumChannels(2);
info.setSampleRate(48000);
OpusTags tags = new OpusTags();
//tags.setVendor("Concentus");
//tags.addComment("title", "A test!");
OpusFile file = new OpusFile(fileOut, info, tags);

// Opus转换的时候,对每次输入的数据量有要求,必须是2.5ms数据量的整数倍(这跟opus帧长度有关)。
// 我们输入的音频是48k,16bit,2声道的,1ms就是48个采样点 * 2字节/采样点 * 2声道 = 192字节。
// 这里packetSamples是每个声道的采样点数量,所以两个声道各960个采样点一共需要的内存是960个采样点 * 2字节/采样点 * 2声道 = 3840字节。
// 所以我们一帧的长度是5ms的音频数据。
int packetSamples = 960;
byte[] inBuf = new byte[packetSamples * 2 * 2];

// 这里是存放转换后的数据,要申请的足够大。
byte[] data_packet = new byte[1275];
long start = System.currentTimeMillis();

// 循环读取源文件进行转换
while (fileIn.available() >= inBuf.length) {
	int bytesRead = fileIn.read(inBuf, 0, inBuf.length);
	
	// 将byte数组转换成short数组
	short[] pcm = BytesToShorts(inBuf, 0, inBuf.length);
	
	// 编码,参数分别为,输入的pcm数据(byte数组),输入数据的offset,每个声道的采样点数量,输出的opus数据(short数组),输出数据的offset,输出的最大数量。
	int bytesEncoded = encoder.encode(pcm, 0, packetSamples, data_packet, 0, 1275);
	
	// 因为data_packet数组中,只有前bytedEncoded个元素是转换后的opus数据,所以只需要取这部分数据构建OpusAudioData,并写入OpusFile。
	byte[] packet = new byte[bytesEncoded];
	System.arraycopy(data_packet, 0, packet, 0, bytesEncoded);
	OpusAudioData data = new OpusAudioData(packet);
	file.writeAudioData(data);
}
file.close();

long end = System.currentTimeMillis();
System.out.println("Time was " + (end - start) + "ms");
fileIn.close();
//fileOut.close();
System.out.println("Done!");

使用libopus

我是准备把libopus封装成一个标准的aar,这样以后其他项目就可以直接用。这里会用到一些java native的基础知识。

下载代码

libopus是xiph.org基金会提供的官方的编码解码库,提供了源码,可以从其官方网站https://opus-codec.org下载到。最新的版本是1.3.1。虽然在github上也有仓库,但是github上的版本比较老了,还是建议去官方网站下载。

编译成so

下载的代码是c代码,需要使用ndk编译成Android系统用的arm版本。在解压后的opus-1.3.1目录中,新建一个Android.mk文件,然后将下面的内容拷贝到Android.mk中:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
#我使用的是NDK 18
#NDK 17及以上不再支持ABIs [mips64, armeabi, mips]
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64
APP_CPPFLAGS += -std=c++11
APP_STL := gnustl_shared
APP_PLATFORM := android-16

include $(LOCAL_PATH)/celt_sources.mk
include $(LOCAL_PATH)/silk_sources.mk
include $(LOCAL_PATH)/opus_sources.mk

LOCAL_MODULE        := opus

# Fixed point sources
SILK_SOURCES        += $(SILK_SOURCES_FIXED)

# ARM build
CELT_SOURCES        += $(CELT_SOURCES_ARM)
SILK_SOURCES        += $(SILK_SOURCES_ARM)
LOCAL_SRC_FILES     := \
    $(CELT_SOURCES) $(SILK_SOURCES) $(OPUS_SOURCES) $(OPUS_SOURCES_FLOAT)

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 -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)

这段代码是从https://www.jianshu.com/p/927cdab568af找到的,他是在1.2.1上编写的,在1.3.1上也能用。

然后用下面的指令进行编译

ndk-build APP_BUILD_SCRIPT=Android.mk NDK_PROJECT_PATH=.

其中ndk-build是ndk下的编译程序,如果设置了环境变量就可以直接用,没有设置环境变量,就需要写全路径。

我用的ndk版本是22.1.7171670。不过似乎是不挑版本,用其他的也行。

编译出来的库文件都在lib目录下,拷贝出来备用。

配置Android Studio项目

在Android Studio中新建一个project,用Empty Activity就行。

新建的项目要编译出aar,需要做下面的修改:

  1. 把Module的build.gradle的第一行改成下面这样:
plugins {
    id 'com.android.library'
}
  1. 删除Module的build.gradle中的applicationID。
  2. 删除AndroidManifest.xml中的整个application节点。
  3. 删除MainActivity。
  4. 在Module的build.gradle中增加以下代码:
task makeJar(type: Copy) {
    delete('build/*.aar') //删除之前的旧jar包
    from('build/outputs/aar/') //从这个目录下取出默认jar包s
    into('build/') //将jar包输出到指定目录下
    include('app-debug.aar')
    rename('app-debug.aar', 'OpusTool_' + android.defaultConfig.versionName + '.aar') //自定义jar包的名字
}

这段代码就是在gradle中新建了一个task。Gradle完成sync后,在Gradle窗口中,Tasks->other下,就会看到一个makeJar任务,双击就会在生成aar包并拷贝到build目录下。

  1. Analyze->Run Inspection by Name,在弹框中输入unused ressources。然后等待分析完成,删除未使用的资源文件。这一步是防止没用的资源被打包到aar中,在引用这个aar的项目中造成冲突。

  2. 在Project窗口中右键,选择“Add C++ to Module”,等待项目配置完成。

  3. 在src/main/cpp/下面新建目录opuslib,将前面拷贝出来的lib文件下下的所有文件夹拷贝进来。注意,我用的Android Studio版本是Arctic Fox|2020.3.1,这个版本比较新。在老的版本中,第三方so是拷贝到lib目录,并需要在build.gradle中显示指定。但是新的版本在CMakeLists.txt中指定路径,不能在build.gradle中指定,并且不能放在lib目录下面,不然编译的时候会报冲突。

  4. 修改CMakeLists.txt:


# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.10.2)

# Declares and names the project.

// 修改project名称
// start ===============
project("opustool")
// stop ================

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
             opustool

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             opustool.cpp)

// 增加第三方库声明
// start =======================================================================
add_library( opuslib
             SHARED
             IMPORTED )
set_target_properties( # Specifies the target library.
             opuslib

             # Specifies the parameter you want to define.
             PROPERTIES IMPORTED_LOCATION

             # Provides the path to the library you want to import.
             ${PROJECT_SOURCE_DIR}/opuslib/${ANDROID_ABI}/libopus.so )

include_directories(${PROJECT_SOURCE_DIR}/opuslib/include/)
// stop ========================================================================

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

// 这里增加一个opuslib
target_link_libraries( # Specifies the target library.
                       opustool opuslib

        # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

  1. 将默认生成的app.cpp重命名为opustool.cpp。

  2. 新建一个JniClient类,然后在其中拷贝以下代码:

package com.toycloud.opustool;

public class JniClient {
    private static JniClient sInstance;

    private JniClient(){}

    public static JniClient getInstance() {
        if (sInstance == null) {
            sInstance = new JniClient();
        }
        return sInstance;
    }

    static {
        System.loadLibrary("opustool");
    }

    /**
     * 创建编码器
     *
     * @param sampleRate    音频的采样率
     * @param channelConfig 音频的声道数
     * @return 编码器的句柄
     */
    public long createEncoder(int sampleRate, int channelConfig) {
        return nativeCreateEncoder(sampleRate, channelConfig);
    }

    /**
     * 编码音频数据
     *
     * @param opusEncoder 编码器句柄
     * @param in          输入的PCM音频,按照OPUS的要求,音频的数据量应为2.5ms,5ms,10ms,20ms,40ms或者60ms。
     * @param out         输出的OPUS音频
     * @return 输出的数据量
     */
    public int encode(long opusEncoder, byte[] in, byte[] out) {
        return nativeEncode(opusEncoder, in, out);
    }

    /**
     * 销毁编码器
     *
     * @param opusEncoder 编码器句柄
     */
    public void destroyEncoder(long opusEncoder) {
        nativeDestroyEncoder(opusEncoder);
    }

    private native long nativeCreateEncoder(int sampleRate, int channelConfig);

    private native int nativeEncode(long opusEncoder, byte[] in, byte[] out);

    private native void nativeDestroyEncoder(long opusEncoder);
}

  1. 在上面的native方法上按option+enter(windows是alt+enter),就会在opustool.cpp中自动生成对应的c++函数声明。
  2. 下面就直接贴一下C++的代码,比较简单,如果有不明白的,可以去头文件看一下函数声明,里面解释很清楚。
extern "C"
JNIEXPORT jlong JNICALL
Java_com_toycloud_opustool_JniClient_nativeCreateEncoder(JNIEnv *env, jobject thiz, jint sample_rate,
                                                   jint channel_config) {
    int err;
    opus_int32 skip = 0;
    OpusEncoder *pOpusEnc = opus_encoder_create(sample_rate, channel_config,
                                                OPUS_APPLICATION_RESTRICTED_LOWDELAY, &err);
    if (pOpusEnc) {
        opus_encoder_ctl(pOpusEnc, OPUS_SET_BITRATE(OPUS_AUTO));
        opus_encoder_ctl(pOpusEnc, OPUS_SET_COMPLEXITY(10));//8    0~10
        opus_encoder_ctl(pOpusEnc, OPUS_SET_SIGNAL(OPUS_SIGNAL_VOICE));
    }
    return (jlong) pOpusEnc;
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_toycloud_opustool_JniClient_nativeEncode(JNIEnv *env, jobject thiz, jlong opus_encoder,
                                                  jbyteArray in, jbyteArray out) {
    OpusEncoder *pEnc = (OpusEncoder *) opus_encoder;
    if (!pEnc || !in || !out) {
        return 0;
    }
    jbyte *pIn = env->GetByteArrayElements(in, 0);
    jsize pInSize = env->GetArrayLength(in);
    jbyte *pOut = env->GetByteArrayElements(out, 0);
    jsize pOutSize = env->GetArrayLength(out);

    opus_int16 *pcm =(opus_int16 *)pIn;
    int frame_size = pInSize >> 1;
    unsigned char *data = (unsigned char *)pOut;
    opus_int32 max_data_bytes = pOutSize;
    int nRet = opus_encode(pEnc, pcm, frame_size, data, max_data_bytes);
    env->ReleaseByteArrayElements(in, pIn, 0);
    env->ReleaseByteArrayElements(out, pOut, 0);
    return nRet;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_toycloud_opustool_JniClient_nativeDestroyEncoder(JNIEnv *env, jobject thiz, jlong opus_encoder) {
    OpusEncoder *pEnc = (OpusEncoder *) opus_encoder;
    if (!pEnc)
        return;
    opus_encoder_destroy(pEnc);
}
  1. 在gradle窗口中双击makeJar就能生成aar包了。

在其他项目中引用

引用的方法比较简单,将aar拷贝到lib目录,然后在build.gradle中声明即可,使用方法与Concentus类似,这里就不赘述了。

需要注意的是,这里编码后的数据也需要和Concentus一样,再增加ogg封装。

如果需要的是生成byte数组,而不是写文件,就将FileOutputStream换成ByteArrayOutputStream(这个也继承了OutputStream),在转换完成之后,调用其toByteArray()接口即可。

两种方案的比较

生成的文件可以在https://www.aconvert.com/cn/format/opus/在线转换成mp3并试听。

我用两种方案对同一个20秒的wav文件进行编码,实测发现Concentus需要用1000多毫秒,用自己封装的aar只需要300+毫秒。两者生成的文件大小基本一样。

 类似资料: