在Android中使用Opus(编译使用Opus so库)

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文件

官网下载代码到本地

官方下载地址:http://opus-codec.org/downloads/

本文使用的是官方的1.2.1版本,不方便下载的也可以点击这里下载

使用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()
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,367评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,959评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,750评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,226评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,252评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,975评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,592评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,497评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,027评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,147评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,274评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,953评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,623评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,143评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,260评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,607评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,271评论 2 358

推荐阅读更多精彩内容

  • OpusEncoder 1:获得 OpusEncoder结构的大小 2:分配和初始化 encoder状态.一个编码...
    今忆Zoe阅读 6,564评论 0 3
  • 前言 关于Opus,我相信能找到这篇文章的人都应该知道他是用来干啥的了,这里我就不多做解释了,写这篇文章是为了加深...
    var_rain阅读 6,592评论 3 2
  • 什么是Opus? Opus官网介绍 Opus opus是完全开放的,免版税的,高度通用的音频编解码器。作品是无与伦...
    努力的菜鸟阅读 2,768评论 0 0
  • 简介 Opus是一款完全开放、免版税、功能多样的音频编解码器。它适用于互联网上的交互式语音和音乐传输,但也适用于存...
    JonorZhang阅读 31,381评论 9 17
  • [TOC] 音视频&流媒体 是什么促使我要写这一篇音视频入门文章?那是因为和一妹子打赌码率的概念,结果输了;对一个...
    AllenWu阅读 4,835评论 1 24