Opus是一个开放格式的有损声音编码的格式,并在其使用上没有任何专利或限制。还可以处理各种音频应用,包括IP语音、视频会议、游戏内聊天、流音乐、甚至远程现场音乐表演。它可以从低比特率窄带语音扩展到非常高清音频的立体声音乐。支持的功能包括:
- 6 kb/秒到510 kb/秒的比特率;单一频道最高256 kb/秒
- 采样率从8 kHz(窄带)到48 kHz(全频)
- 帧大小从2.5毫秒到60毫秒
- 支持恒定比特率(CBR)、受约束比特率(CVBR)和可变比特率(VBR)
- 支持语音(SILK层)和音乐(CELT层)的单独或混合模式
- 支持单声道和立体声;支持多达255个音轨(多数据流的帧)
- 可动态调节比特率,音频带宽和帧大小
- 良好的鲁棒性丢失率和数据包丢失隐藏(PLC)
- 浮点和定点实现
来自:维基百科
本文章所用到的项目源码:https://github.com/inodevip/OpusLibAndroidDemo
编译Opus库为libopus.so文件
官网下载代码到本地
使用NDK-Build编译(已试过1.3.1同样适用)
解压下载好的opus-1.2.1.tar.gz
编写Android.mk文件到opus-1.2.1\
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)
编译
$ cd opus-1.2.1
$ ndk\ndk-build APP_BUILD_SCRIPT=Android.mk NDK_PROJECT_PATH=.
输出如下:
arm64-v8a库编译完成输出
armeabi-v7a库编译完成输出
x86库编译完成输出
x86_64库编译完成输出
在Android中使用so库
拷贝文件至Android项目中
opus-1.2.1/include ---> app\src\main\cpp
opus-1.2.1/libs/* ---> app\src\main\jniLibs\
拷贝头文件和so库
以下为JNI文件内容
JNIEXPORT jlong JNICALL Java_vip_inode_demo_opusaudiodemo_utils_OpusUtils_createEncoder
(JNIEnv *env, jobject thiz, jint sampleRateInHz, jint channelConfig, jint complexity) {
int error;
OpusEncoder *pOpusEnc = opus_encoder_create(sampleRateInHz, channelConfig,
OPUS_APPLICATION_RESTRICTED_LOWDELAY,
&error);
if (pOpusEnc) {
opus_encoder_ctl(pOpusEnc, OPUS_SET_VBR(0));//0:CBR, 1:VBR
opus_encoder_ctl(pOpusEnc, OPUS_SET_VBR_CONSTRAINT(true));
opus_encoder_ctl(pOpusEnc, OPUS_SET_BITRATE(32000));
opus_encoder_ctl(pOpusEnc, OPUS_SET_COMPLEXITY(complexity));//8 0~10
opus_encoder_ctl(pOpusEnc, OPUS_SET_SIGNAL(OPUS_SIGNAL_VOICE));
opus_encoder_ctl(pOpusEnc, OPUS_SET_LSB_DEPTH(16));
opus_encoder_ctl(pOpusEnc, OPUS_SET_DTX(0));
opus_encoder_ctl(pOpusEnc, OPUS_SET_INBAND_FEC(0));
opus_encoder_ctl(pOpusEnc, OPUS_SET_PACKET_LOSS_PERC(0));
}
return (jlong) pOpusEnc;
}
JNIEXPORT jlong JNICALL Java_vip_inode_demo_opusaudiodemo_utils_OpusUtils_createDecoder
(JNIEnv *env, jobject thiz, jint sampleRateInHz, jint channelConfig) {
int error;
OpusDecoder *pOpusDec = opus_decoder_create(sampleRateInHz, channelConfig, &error);
return (jlong) pOpusDec;
}
JNIEXPORT jint JNICALL Java_vip_inode_demo_opusaudiodemo_utils_OpusUtils_encode
(JNIEnv *env, jobject thiz, jlong pOpusEnc, jshortArray samples, jint offset,
jbyteArray bytes) {
OpusEncoder *pEnc = (OpusEncoder *) pOpusEnc;
if (!pEnc || !samples || !bytes)
return 0;
jshort *pSamples = env->GetShortArrayElements(samples, 0);
jsize nSampleSize = env->GetArrayLength(samples);
jbyte *pBytes = env->GetByteArrayElements(bytes, 0);
jsize nByteSize = env->GetArrayLength(bytes);
if (nSampleSize - offset < 320 || nByteSize <= 0)
return 0;
int nRet = opus_encode(pEnc, pSamples + offset, nSampleSize, (unsigned char *) pBytes,
nByteSize);
env->ReleaseShortArrayElements(samples, pSamples, 0);
env->ReleaseByteArrayElements(bytes, pBytes, 0);
return nRet;
}
JNIEXPORT jint JNICALL Java_vip_inode_demo_opusaudiodemo_utils_OpusUtils_decode
(JNIEnv *env, jobject thiz, jlong pOpusDec, jbyteArray bytes,
jshortArray samples) {
OpusDecoder *pDec = (OpusDecoder *) pOpusDec;
if (!pDec || !samples || !bytes)
return 0;
jshort *pSamples = env->GetShortArrayElements(samples, 0);
jbyte *pBytes = env->GetByteArrayElements(bytes, 0);
jsize nByteSize = env->GetArrayLength(bytes);
jsize nShortSize = env->GetArrayLength(samples);
if (nByteSize <= 0 || nShortSize <= 0) {
return -1;
}
int nRet = opus_decode(pDec, (unsigned char *) pBytes, nByteSize, pSamples, nShortSize, 0);
env->ReleaseShortArrayElements(samples, pSamples, 0);
env->ReleaseByteArrayElements(bytes, pBytes, 0);
return nRet;
}
JNIEXPORT void JNICALL Java_vip_inode_demo_opusaudiodemo_utils_OpusUtils_destroyEncoder
(JNIEnv *env, jobject thiz, jlong pOpusEnc) {
OpusEncoder *pEnc = (OpusEncoder *) pOpusEnc;
if (!pEnc)
return;
opus_encoder_destroy(pEnc);
}
JNIEXPORT void JNICALL Java_vip_inode_demo_opusaudiodemo_utils_OpusUtils_destroyDecoder
(JNIEnv *env, jobject thiz, jlong pOpusDec) {
OpusDecoder *pDec = (OpusDecoder *) pOpusDec;
if (!pDec)
return;
opus_decoder_destroy(pDec);
}
测试编解码
编码PCM为Opus (Kotlin)
override fun run() {
isRecorder = true
audioRecord.startRecording()
val file = File(opusAudioOpusPath)
val filePcm = File(opusAudioPcmPath)
val fileDir = File(file.parent)
if (!fileDir.exists()) {
fileDir.mkdirs()
}
if (file.exists()) {
file.delete()
}
if (filePcm.exists()) {
filePcm.delete()
}
file.createNewFile()
filePcm.createNewFile()
val fileOutputStream = FileOutputStream(file, true)
val filePcmOutputStream = FileOutputStream(filePcm, true)
val fileOpusBufferedOutputStream = BufferedOutputStream(fileOutputStream)//默认buffer大小8192
val filePcmBufferedOutputStream = BufferedOutputStream(filePcmOutputStream)
val opusUtils = OpusUtils()
val createEncoder = opusUtils.createEncoder(DEFAULT_AUDIO_SAMPLE_RATE, DEFAULT_OPUS_CHANNEL, 3)
while (isRecorder) {
val curShortSize = audioRecord.read(audioBuffer, 0, audioBuffer.size)
if (curShortSize > 0 && curShortSize <= audioBuffer.size) {
filePcmBufferedOutputStream.write(audioBuffer)//同时保存PCM以对比检查问题
val byteArray = ByteArray(audioBuffer.size / 8)//编码后大小减小8倍
val encodeSize = opusUtils.encode(createEncoder, Uilts.byteArrayToShortArray(audioBuffer), 0, byteArray)
if (encodeSize > 0) {
val decodeArray = ByteArray(encodeSize)
System.arraycopy(byteArray, 0, decodeArray, 0, encodeSize)
fileOpusBufferedOutputStream.write(decodeArray)//写入OPUS
} else {
}
}
}
opusUtils.destroyEncoder(createEncoder)
audioRecord.stop()
audioRecord.release()
filePcmBufferedOutputStream.close()
filePcmOutputStream.close()
fileOpusBufferedOutputStream.close()
fileOutputStream.close()
}
解码Opus为PCM (Kotlin)
private fun opusFileDecoder(needDecoder: Boolean = true) {
if (decodeOpusFilePath.isNullOrEmpty()) {
opusDecodeFinish()
return
}
val tntOpusUtils = OpusUtils.getInstant()
val decoderHandler = tntOpusUtils.createDecoder(DEFAULT_AUDIO_SAMPLE_RATE, DEFAULT_OPUS_CHANNEL)
val fis: FileInputStream
try {
fis = FileInputStream(decodeOpusFilePath)
} catch (e: Exception) {
opusDecodeFinish()
return
}
val bis = BufferedInputStream(fis)
while (!isCancel) {
val bufferArray = ByteArray(BUFFER_LENGTH)
var read: Int = -1
try {
read = bis.read(bufferArray, 0, bufferArray.size)
} catch (e: Exception) {
}
if (read < 0) {//已经读完了
Log.i(TAG, "OpusFileDecoder compare")
break
} else {
if (needDecoder) {
val decodeBufferArray = ShortArray(bufferArray.size * 4)
val size = tntOpusUtils.decode(decoderHandler, bufferArray, decodeBufferArray)
if (size > 0) {
val decodeArray = ShortArray(size)
System.arraycopy(decodeBufferArray, 0, decodeArray, 0, size)
opusDecode(decodeArray)//输出数据到接口
} else {
Log.e(TAG, "opusDecode error : $size")
break
}
} else {
opusDecode(byteArrayToShortArray(bufferArray))
}
}
}
tntOpusUtils.destroyDecoder(decoderHandler)
bis.close()
fis.close()
opusDecodeFinish()
}