Android音视频开发之——音频非压缩编码和压缩编码(1)

thread?.start()

开启录音线程将录音数据通过AudioRecord写入文件

private fun writeData2File() {

var ret = 0

val byteArray = ByteArray(bufferSizeInBytes)

val file = File(externalCacheDir?.absolutePath + File.separator + filename)

if (file.exists()) {

file.delete()

} else {

file.createNewFile()

}

val fos = FileOutputStream(file)

while (status == Status.STARTING) {

ret = audioRecord?.read(byteArray, 0, bufferSizeInBytes)!!

if (ret!=AudioRecord.ERROR_BAD_VALUE || ret!=AudioRecord.ERROR_INVALID_OPERATION|| ret!=AudioRecord.ERROR_DEAD_OBJECT){

fos.write(byteArray)

}

}

fos.close()

}

释放资源

首先停止录制

if (null!=audioRecord && audioRecord?.state!=AudioRecord.STATE_UNINITIALIZED){

audioRecord?.stop()

}

然后停止线程

if (thread!=null){

thread?.join()

thread =null

}

最后释放AudioRecord

if (audioRecord != null) {

audioRecord?.release()

audioRecord = null

}

通过以上一个流程之后,就可以得到一个非压缩编码的PCM数据了。

但是这个数据在音乐播放器上一般是播放不了的,那么怎么验证我是否录制成功呢?当然是使用我们的AudioTrack进行播放看看是不是刚刚我们录制的声音了。

AudioTrack


由于AudioTrack是由Android SDK提供比较底层的播放API,也只能操作PCM裸数据,通过直接渲染PCM数据进行播放。当然如果想要使用AudioTrack进行播放,那就需要自行先将压缩编码格式文件解码。

AudioTrack的使用流程大致如下:

  • 根据音频参数创建AudioTrack

  • 调用play开始播放

  • 开启播放线程,循环想AudioTrack缓存区写入音频数据

  • 释放资源

创建AudioTrack

我们来看看AudioTrack的构造方法

public AudioTrack (int streamType,

int sampleRateInHz,

int channelConfig,

int audioFormat,

int bufferSizeInBytes,

int mode,

int sessionId)

  • streamType,Android手机上提供音频管理策略,按下音量键我们会发现由媒体声音管理,闹铃声音管理,通话声音管理等等,当系统有多个进程需要播放音频的时候,管理策略会决定最终的呈现效果,该参数的可选值将以常量的形式定义在类AudioManager中,主要包括以下内容:

· STREAM_VOCIE_CALL:电话声音

· STREAM_SYSTEM:系统声音

· STREAM_RING:铃声

· STREAM_MUSCI:音乐声

· STREAM_ALARM:警告声

· STREAM_NOTIFICATION:通知声

因为这里是播放音频,所以我们选择STREAM_MUSCI

  • sampleRateInHz,采样率,即播放的音频每秒钟会有多少次采样,可选用的采样频率列表为:8000、16000、22050、24000、32000、44100、48000等,一般采用人能听到最大音频的2倍,也就是44100Hz。

  • channelConfig,声道数的配置,可选值以常量的形式配置在类AudioFormat中,常用的是CHANNEL_IN_MONO(单声道)、CHANNEL_IN_STEREO(立体双声道)

  • audioFormat,采样格式,可选值以常量的形式定义在类AudioFormat中,分别为ENCODING_PCM_16BIT(16bit)、ENCODING_PCM_8BIT(8bit),一般采用16bit。

  • bufferSizeInBytes,其配置的是AudioTrack内部的音频缓冲区的大小,可能会因为生产厂家的不同而有所不同,为了方便AudioTrack提供了一个获取该值最小缓冲区大小的方法getMinBufferSize

  • mode,播放模式,AudioTrack提供了两种播放模式,可选的值以常量的形式定义在类AudioTrack中,一个是MODE_STATIC,需要一次性将所有的数据都写入播放缓冲区中,简单高效,通常用于播放铃声、系统提醒的音频片段;另一个是MODE_STREAM,需要按照一定的时间间隔不间断地写入音频数据,理论上它可以应用于任何音频播放的场景。

  • sessionId,AudioTrack都需要关联一个会话Id,在创建AudioTrack时可直接使用AudioManager.AUDIO_SESSION_ID_GENERATE,或者在构造之前通过AudioManager.generateAudioSessionId获取。

上面这种构造方法已经被弃用了,现在基本使用如下构造(最小skd 版本需要>=21),参数内容与上基本一致:

public AudioTrack (AudioAttributes attributes,

AudioFormat format,

int bufferSizeInBytes,

int mode,

int sessionId)

通过AudioAttributes.Builder设置参数streamType

var audioAttributes = AudioAttributes.Builder()

.setLegacyStreamType(AudioManager.STREAM_MUSIC)

.build()

通过AudioFormat.Builder设置channelConfig,sampleRateInHz,audioFormat参数

var mAudioFormat = AudioFormat.Builder()

.setChannelMask(channel)

.setEncoding(audioFormat)

.setSampleRate(sampleRate)

.build()

切换播放状态

首先通过调用getState判断AudioRecord是否初始化成功,然后通过play切换成录播放状态

if (null!=audioTrack && audioTrack?.state != AudioTrack.STATE_UNINITIALIZED){

audioTrack?.play()

}

开启播放线程

开启播放线程

thread= Thread(Runnable {

readDataFromFile()

})

thread?.start()

将数据不断的送入缓存区并通过AudioTrack播放

private fun readDataFromFile() {

val byteArray = ByteArray(bufferSizeInBytes)

val file = File(externalCacheDir?.absolutePath + File.separator + filename)

if (!file.exists()) {

Toast.makeText(this, “请先进行录制PCM音频”, Toast.LENGTH_SHORT).show()

return

}

val fis = FileInputStream(file)

var read: Int

status = Status.STARTING

while ({ read = fis.read(byteArray);read }() > 0) {

var ret = audioTrack?.write(byteArray, 0, bufferSizeInBytes)!!

if (ret == AudioTrack.ERROR_BAD_VALUE || ret == AudioTrack.ERROR_INVALID_OPERATION || ret == AudioManager.ERROR_DEAD_OBJECT) {

break

}

}

fis.close()

}

释放资源

首先停止播放

if (audioTrack != null && audioTrack?.state != AudioTrack.STATE_UNINITIALIZED) {

audioTrack?.stop()

}

然后停止线程

if (thread!=null){

thread?.join()

thread =null

}

最后释放AudioTrack

if (audioTrack != null) {

audioTrack?.release()

audioTrack = null

}

经过这样几个步骤,我们就可以听到刚刚我们录制的PCM数据声音啦!这就是使用Android提供的AudioRecordAudioTrack对PCM数据进行操作。

但是仅仅这样是不够的,因为我们生活中肯定不是使用PCM进行音乐播放,那么怎么才能让音频在主流播放器上播放呢?这就需要我们进行压缩编码了,比如mp3或aac压缩编码格式。

MediaCodec编码AAC


AAC压缩编码是一种高压缩比的音频压缩算法,AAC压缩比通常为18:1;采样率范围通常是8KHz~96KHz,这个范围比MP3更广一些(MP3的范围一般是:16KHz~48KHz),所以在16bit的采样格式上比MP3更精细。

方便我们处理AAC编码,Android SDK中提供了MediaCodecAPI,可以将PCM数据编码成AAC数据。大概需要以下几个步骤:

  • 创建MediaCodec

  • MediaCodec配置音频参数

  • 启动线程,循环往缓冲区送入数据

  • 通过MediaCodec将缓冲区的数据进行编码并写入文件

  • 释放资源

创建MediaCodec

通过MediaCodec.createEncoderByType创建编码MediaCodec

mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)

配置音频参数

// 配置采样率和声道数

mediaFormat = MediaFormat.createAudioFormat(MINE_TYPE,sampleRate,channel)

// 配置比特率

mediaFormat?.setInteger(MediaFormat.KEY_BIT_RATE,bitRate)

// 配置PROFILE,其中属AAC-LC兼容性最好

mediaFormat?.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)

// 最大输入大小

mediaFormat?.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 10 * 1024)

mediaCodec!!.configure(mediaFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE)

mediaCodec?.start()

inputBuffers = mediaCodec?.inputBuffers

outputBuffers = mediaCodec?.outputBuffers

启动线程

启动线程,循环读取PCM数据送入缓冲区

thread = Thread(Runnable {

val fis = FileInputStream(pcmFile)

fos = FileOutputStream(aacFile)

var read: Int

while ({ read = fis.read(byteArray);read }() > 0) {

encode(byteArray)

}

})

thread?.start()

AAC编码

将送入的PCM数据通过MediaCodec进行编码,大致流程如下:

  • 通过可用缓存去索引,获取可用输入缓冲区

  • 将pcm数据放入输入缓冲区并提交

  • 根据输出缓冲区索引,获取输出缓冲区

  • 创建输出数据data,并添加ADTS头部信息(有7byte)

  • outputBuffer编码后数据写入data(data有7byte偏移)

  • 将编码数据data写入文件

  • 重复以上过程

private fun encode(byteArray: ByteArray){

mediaCodec?.run {

//返回要用有效数据填充的输入缓冲区的索引, -1 无限期地等待输入缓冲区的可用性

val inputIndex = dequeueInputBuffer(-1)

if (inputIndex > 0){

// 根据索引获取可用输入缓存区

val inputBuffer = this@AACEncoder.inputBuffers!![inputIndex]

// 清空缓冲区

inputBuffer.clear()

// 将pcm数据放入缓冲区

inputBuffer.put(byteArray)

// 提交放入数据缓冲区索引以及大小

queueInputBuffer(inputIndex,0,byteArray.size,System.nanoTime(),0)

}

// 指定编码器缓冲区中有效数据范围

val bufferInfo = MediaCodec.BufferInfo()

// 获取输出缓冲区索引

var outputIndex = dequeueOutputBuffer(bufferInfo,0)

while (outputIndex>0){

// 根据索引获取可用输出缓存区

val outputBuffer =this@AACEncoder.outputBuffers!![outputIndex]

// 测量输出缓冲区大小

val bufferSize = bufferInfo.size

// 输出缓冲区实际大小,ADTS头部长度为7

val bufferOutSize = bufferSize+7

// 指定输出缓存区偏移位置以及限制大小

outputBuffer.position(bufferInfo.offset)

outputBuffer.limit(bufferInfo.offset+bufferSize)

// 创建输出空数据

val data = ByteArray(bufferOutSize)

// 向空数据先增加ADTS头部

addADTStoPacket(data, bufferOutSize)

// 将编码输出数据写入已加入ADTS头部的数据中

outputBuffer.get(data,7,bufferInfo.size)

// 重新指定输出缓存区偏移

outputBuffer.position(bufferInfo.offset)

// 将获取的数据写入文件

fos?.write(data)

// 释放输出缓冲区

releaseOutputBuffer(outputIndex,false)

// 重新获取输出缓冲区索引

outputIndex=dequeueOutputBuffer(bufferInfo,0)

}

}

}

释放资源

编码完成后,一定要释放所有资源,首先关闭输入输出流

fos?.close()

fis.close()

停止编码

if (mediaCodec!=null){

mediaCodec?.stop()

}

然后就是关闭线程

if (thread!=null){

thread?.join()

thread =null

}

最后释放MediaCodec

if (mediaCodec!=null){

mediaCodec?.release()

mediaCodec = null

mediaFormat = null

inputBuffers = null

outputBuffers = null

}

通过以上一个流程,我们就可以得到一个AAC压缩编码的音频文件,可以听一听是不是自己刚刚录制的。我听了一下我自己唱的一首歌,觉得我的还是可以的嘛,也不是那么五音不全~~

Android NDK


虽然我们通过压缩编码生成了AAC音频文件,但是有个问题:毕竟AAC音频不是主流的音频文件呀,我们最常见的是MP3的嘛,可不可以将PCM编码成MP3呢?

当然是可以的,但是Android SDK没有直接提供这样的API,只能使用Android NDK,通过交叉编译其他C或C++库来进行实现。

Android NDK 是由Google提供一个工具集,可让您使用 C 和 C++ 等语言实现应用。

Android NDK 一般有两个用途,一个是进一步提升设备性能,以降低延迟,或运行计算密集型应用,如游戏或物理模拟;另一个是重复使用您自己或其他开发者的 C 或 C++ 库。当然我们使用最多的应该还是后者。

想使用Android NDK调试代码需要以下工具:

  • Android 原生开发套件 (NDK):这套工具使您能在 Android 应用中使用 C 和 C++ 代码。

  • CMake:一款外部编译工具,可与 Gradle 搭配使用来编译原生库。如果您只计划使用 ndk-build,则不需要此组件。

  • LLDB:Android Studio 用于调试原生代码的调试程序。

可以进入Tools > SDK Manager > SDK Tools 选择 NDK (Side by side) 和 CMake 应用安装

在应用以上选项之后,我们可以看到SDK的目录中多了一个ndk-bundle的文件夹,大致目录结构如下

  • ndk-build:该Shell脚本是Android NDK构建系统的起始点,一般在项目中仅仅执行这一个命令就可以编译出对应的动态链接库了,后面的编译mp3lame 就会使用到。

  • platforms:该目录包含支持不同Android目标版本的头文件和库文件,NDK构建系统会根据具体的配置来引用指定平台下的头文件和库文件。

  • toolchains:该目录包含目前NDK所支持的不同平台下的交叉编译器——ARM、x86、MIPS,其中比较常用的是ARM和x86。不论是哪个平台都会提供以下工具:

·CC:编译器,对C源文件进行编译处理,生成汇编文件。

·AS:将汇编文件生成目标文件(汇编文件使用的是指令助记符,AS将它翻译成机器码)。

·AR:打包器,用于库操作,可以通过该工具从一个库中删除或者增加目标代码模块。

·LD:链接器,为前面生成的目标代码分配地址空间,将多个目标文件链接成一个库或者是可执行文件。

·GDB:调试工具,可以对运行过程中的程序进行代码调试工作。

·STRIP:以最终生成的可执行文件或者库文件作为输入,然后消除掉其中的源码。

·NM:查看静态库文件中的符号表。

·Objdump:查看静态库或者动态库的方法签名。

了解Android NDK 之后,就可新建一个支持C/C++ 的Android项目了:

  • 在向导的 Choose your project 部分中,选择 Native C++ 项目类型。

  • 点击 Next。

  • 填写向导下一部分中的所有其他字段。

  • 点击 Next。

  • 在向导的 Customize C++ Support 部分中,您可以使用 C++ Standard 字段来自定义项目。使用下拉列表选择您想要使用哪种 C++ 标准化。选择 Toolchain Default 可使用默认的 CMake 设置。

  • 点击 Finish,同步完成之后会出现如下图所示的目录结构,即表示原生项目创建完成

编译Lame


LAME是一个开源的MP3音频压缩库,当前是公认有损质量MP3中压缩效果最好的编码器,所以我们选择它来进行压缩编码,那如何进行压缩编码呢?主流的由两种方式:

  • Cmake

  • ndk-build

下面就详细讲解这两种方式

Cmake编译Lame

配置Cmake之后可以直接将Lame代码运行于Android中

准备

下载Lame-3.100并解压大概得到如下目录

然后将里面的libmp3lame文件夹拷贝到我们上面创建的支持c/c++项目,删除其中的i386和vector文件夹,以及其他非.c 和 .h 后缀的文件

需要将以下文件进行修改,否则会报错

  • 将util.h中570行

extern ieee754_float32_t fast_log2(ieee754_float32_t x)

替换成

extern float fast_log2(float x)

  • 在id3tag.c和machine.h两个文件中,将HAVE_STRCHRHAVE_MEMCPY注释

#ifdef STDC_HEADERS

include <stddef.h>

include <stdlib.h>

include <string.h>

include <ctype.h>

#else

/*# ifndef HAVE_STRCHR

define strchr index

define strrchr rindex

endif

*/

char *strchr(), *strrchr();

/*# ifndef HAVE_MEMCPY

define memcpy(d, s, n) bcopy ((s), (d), (n))

endif*/

#endif

  • 在fft.c中,将47行注释

//#include “vector/lame_intrin.h”

  • 将set_get.h中24行

#include <lame.h>

替换成

#include “lame.h”

编写Mp3编码器

首先在自己的包下(我这里是com.coder.media,这个很重要,后面会用到),新建Mp3Encoder的文件,大概如下几个方法

  • init,将声道,比特率,采样率等信息传入

  • encode,根据init中提供的信息进行编码

  • destroy,释放资源

class Mp3Encoder {

companion object {

init {

System.loadLibrary(“mp3encoder”)

}

}

external fun init(

pcmPath: String,

channel: Int,

bitRate: Int,

sampleRate: Int,

mp3Path: String

): Int

external fun encode()

external fun destroy()

}

在cpp目录下新建两个文件

  • mp3-encoder.h

  • mp3-encoder.cpp

这两个文件中可能会提示错误异常,先不要管它,这是因为我们还没有配置CMakeList.txt导致的。

mp3-encoder.h中定义三个变量

FILE* pcmFile;

FILE* mp3File;

lame_t lameClient;

然后在mp3-encoder.c中分别实现我们在Mp3Encoder中定义的三个方法

首先导入需要的文件

#include <jni.h>

#include

#include “android/log.h”

#include “libmp3lame/lame.h”

#include “mp3-encoder.h”

#define LOGD(…) __android_log_print(ANDROID_LOG_DEBUG , “mp3-encoder”, VA_ARGS)

然后实现init方法

extern “C” JNIEXPORT jint JNICALL

Java_com_coder_media_Mp3Encoder_init(JNIEnv *env, jobject obj, jstring pcmPathParam, jint channels,

jint bitRate, jint sampleRate, jstring mp3PathParam) {

LOGD(“encoder init”);

int ret = -1;

const char* pcmPath = env->GetStringUTFChars(pcmPathParam, NULL);

const char* mp3Path = env->GetStringUTFChars(mp3PathParam, NULL);

pcmFile = fopen(pcmPath,“rb”);

if (pcmFile){

mp3File = fopen(mp3Path,“wb”);

if (mp3File){

lameClient = lame_init();

lame_set_in_samplerate(lameClient, sampleRate);

lame_set_out_samplerate(lameClient,sampleRate);

lame_set_num_channels(lameClient,channels);

lame_set_brate(lameClient,bitRate);

lame_init_params(lameClient);

ret = 0;

}

}

env->ReleaseStringUTFChars(mp3PathParam, mp3Path);

env->ReleaseStringUTFChars(pcmPathParam, pcmPath);

return ret;

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

Android高级架构师

由于篇幅问题,我呢也将自己当前所在技术领域的各项知识点、工具、框架等汇总成一份技术路线图,还有一些架构进阶视频、全套学习PDF文件、面试文档、源码笔记。

  • 330页PDF Android学习核心笔记(内含上面8大板块)

  • Android学习的系统对应视频

  • Android进阶的系统对应学习资料

  • Android BAT部分大厂面试题(有解析)

好了,以上便是今天的分享,希望为各位朋友后续的学习提供方便。觉得内容不错,也欢迎多多分享给身边的朋友哈。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。**

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-4EeS1w0P-1713289899751)]

[外链图片转存中…(img-DKFkbGiq-1713289899753)]

[外链图片转存中…(img-Vmmi1xJx-1713289899754)]

[外链图片转存中…(img-0nxyyEdu-1713289899755)]

[外链图片转存中…(img-Bf0G0uJy-1713289899756)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

Android高级架构师

由于篇幅问题,我呢也将自己当前所在技术领域的各项知识点、工具、框架等汇总成一份技术路线图,还有一些架构进阶视频、全套学习PDF文件、面试文档、源码笔记。

  • 330页PDF Android学习核心笔记(内含上面8大板块)

[外链图片转存中…(img-dMeK9qCz-1713289899757)]

[外链图片转存中…(img-G61JPUCR-1713289899758)]

  • Android学习的系统对应视频

  • Android进阶的系统对应学习资料

[外链图片转存中…(img-NbYt9wAV-1713289899759)]

  • Android BAT部分大厂面试题(有解析)

[外链图片转存中…(img-Fxkt37mR-1713289899760)]

好了,以上便是今天的分享,希望为各位朋友后续的学习提供方便。觉得内容不错,也欢迎多多分享给身边的朋友哈。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值