将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-16include $(LOCAL_PATH)/celt_sources.mk
include $(LOCAL_PATH)/silk_sources.mk
include $(LOCAL_PATH)/opus_sources.mkLOCAL_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-loopsinclude $(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包sinto('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( opuslibSHAREDIMPORTED )
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~10opus_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+毫秒。两者生成的文件大小基本一样。

在Android中实现OPUS编码相关推荐

  1. 在Android中使用Opus 1.3.1(Ndk编译使用Opus so库)

    Android中使用Opus 1.3.1 Opus是一个开放格式的有损声音编码的格式,并在其使用上没有任何专利或限制.还可以处理各种音频应用,包括IP语音.视频会议.游戏内聊天.流音乐.甚至远程现场音 ...

  2. url编码 android,Android中的URL编码

    您如何在Android中编码URL ? 我以为是这样的: final String encodedURL = URLEncoder.encode(urlAsString, "UTF-8&qu ...

  3. Android中常用的编码和解码(加密和解密)的问题

    1. URL Encoding     编码目的是为了在⺴址上可以包含中文等特殊字符 解码是为了把编码后的内容还原成原始的内容 格式如下%9C%3C%F3%98 规则: %hex_byte 就是将实际 ...

  4. 【Android 安装包优化】WebP 应用 ( Android 中使用 libwebp.so 库编码 WebP 图片 )

    文章目录 一.Android 中使用 libwebp.so 库编码 WebP 图片 二.完整代码示例 三.参考资料 一.Android 中使用 libwebp.so 库编码 WebP 图片 libwe ...

  5. java中base64编码加密和android中base64编码加密不一样?base64编码解析错误?

    在android的base64加密后得到: WwogIHsKICAgICJ0MSI6ICIwIiwKICAgICJ0MiI6ICIyNDM4NCIsCiAgICAidDMiOiAiIiwKICAgIC ...

  6. Android中使用MediaCodec视频编码异步实现

    Android中使用MediaCodec进行视频编解码异步实现 简单的介绍一下MediaCodec:本文主要讲述的是博主自己在用MediaCodec进行编解码过程中分别用同步和异步两种方式实现了硬编解 ...

  7. Android 中的安全机制

    1 Android 安全机制概述 Android 是一个权限分离的系统 . 这是利用 Linux 已有的权限管理机制,通过为每一个 Application 分配不同的 uid 和 gid , 从而使得 ...

  8. android音视频工程师,音视频学习 (十三) Android 中通过 FFmpeg 命令对音视频编辑处理(已开源)...

    ## 音视频学习 (十三) Android 中通过 FFmpeg 命令对音视频编辑处理(已开源) ## 视音频编辑器 ## 前言 有时候我们想对音视频进行加工处理,比如视频编辑.添加字幕.裁剪等功能处 ...

  9. 从Android中Activity之间的通信说开来

    引言 最近两个星期在研究android的应用开发,学习了android应用开发的基础知识,基本控件,基本布局,基本动画效果,数据存储,http访问internet等等基础知识. android中有一个 ...

最新文章

  1. 3、使用二进制方式搭建K8S集群
  2. 【Web安全】通过机器学习破解验证码图片
  3. number of databases available at XJTLU
  4. MySQL数据库以及其Python用法
  5. 面向对象4(匿名对象、内部类、包、修饰符、代码块)
  6. SQL语句错误:Operand should contain 1 column(s)【查询多个字段不用加括号】
  7. vue父组件向子组件传递多个数据
  8. 巴科斯范式BNF: Backus-Naur Form介绍
  9. 远程控制python
  10. 【译】2019年开始使用Typescript
  11. 华兴数控g71外圆循环编程_数控车床加工时的复合循环指令G70,G71,G72,G73
  12. android之TCP客户端框架
  13. [CF]Codeforces Round #546 (Div. 2)
  14. Fatal error in launcher: Unable to create process using ''之解决办法
  15. 安卓手机小说阅读器_粉笔免费小说阅读器app下载-粉笔免费小说阅读器手机版下载v1.0.1...
  16. Flutter 自定义CheckBox (用于兴趣爱好、风格选择)
  17. 哪里可以免费下载ps字体?【附字体安装教程】
  18. 使用linux批量引物设计,使用SSRMMD便捷、迅速与准确地进行:SSR位点检测,多态性SSR筛选,与批量SSR引物设计...
  19. 【Pygame实战】超有趣的泡泡游戏来袭——愿你童心不泯,永远快乐简单哦~
  20. 误差逆传播算法(BP算法)

热门文章

  1. STM32单片机启动过程详解
  2. JBuilder 无法启动的解决方法
  3. 机械行业ERP选型的两大原则
  4. HTC G6 无线上网 升级2.2系统
  5. 关于数据采集工作的一些感受
  6. Symbian操作系统历史简介
  7. 简易黑客初级教程:黑客技术,分享教学
  8. MavenNexus入门
  9. A股上市有什么条件?A股上市条件有哪些?
  10. 【解决方案】人脸识别/智能分析视频安防服务平台EasyCVR如何打造智慧人社局培训办事机构远程监控系统?