在开发上,习惯的将音频、视频功能的使用称之为多媒体,实际上如果讲的宽泛一些的话,相机的使用,比如拍照,录制视频等,也可以划分到多媒体的范畴里面。
从本节课开始,我们就来看看Android中多媒体的API使用和具体的功能。
本篇文章我们先从音频开发聊起。
在说音频开发之前,我们可以先想一想自己琢磨一下,哪些应用场景会用到音频开发。主要的应用场景大致包括:
如果我们要成系统的学习多媒体和音视频的开发,大致会有涉及到哪些方面的知识呢,归纳来看主要有一下几个方面的内容:
另外,如果要进行音频开发,需要了解一些音频的概念作为前置知识,一些常见的概念如下所示:
说到音视频多媒体,首先就有一个概念叫:媒体格式。也就是我们常说的不同格式的音视频文件。在Android这个开放系统平台中,支持的媒体格式还是很丰富的,详细内容如下:
总结来说,Android中常见的音频压缩格式有:MP3,AAC,OGG,WMA,Opus,FLAC,APE,m4a,AMR,等等。
首先认识两个基础的概念和API:
另外需要说一下,MediaPlayerl除了能够获取、解码以及播放音频和视频,而且只需很简单的设置即可以外。它还支持多种不同的媒体源,比如:
如果应用程序经常播放密集、急促而又短暂的音效(如游戏音效)那么使用MediaPlayer显得有些不太适合了。因为MediaPlayer存在如下缺点:
Android中除了MediaPlayer播放音频之外还提供了SoundPool来播放音效,SoundPool使用音效池的概念来管理多个短促的音效,例如它可以开始就加载20个音效,以后在程序中按音效的ID进行播放。SoundPool的特点和使用长江如下:
SoundPool的API说明如下:
AudioTrack属于更偏底层的音频播放,在Android的framework层有MediaPlayerService,其内部就是使用了AudioTrack。AudioTrack用于单个音频播放和管理,相比于MediaPlayer具有:精炼、高效的优点。因此,对于AutioTrack可以总结如下:
使用AudioTrack公有三个步骤:
共有三个步骤:
另外,其实AudioTrack以外,还有一个Audio系统,在该系统中主要包含三个核心的API,分别是:
Ringtone为铃声、通知和其他类似声音提供快速播放的方法,该种方式播放音频时,还会涉及到一个核心的管理类”RingtoneManager”,该类作为管理类提供系统铃声列表检索方法,并且RingtoneManager可以生成Ringtone实例。具体的Ringtone的使用步骤和相关的方法如下所示:
//1.通过铃声uri获取
static Ringtone getRingtone(Context context, Uri ringtoneUri)
//2.通过铃声检索位置获取
Ringtone getRingtone(int position)
1. // 两个构造方法
(Activity activity)
RingtoneManager(Context context)
2. // 获取指定声音类型(铃声、通知、闹铃等)的默认声音的Uri
static Uri getDefaultUri(int type)
3. // 获取系统所有Ringtone的cursor
Cursor getCursor()
4. // 获取cursor指定位置的Ringtone uri
Uri getRingtoneUri(int position)
5. // 判断指定Uri是否为默认铃声
static boolean isDefault(Uri ringtoneUri)
6. //获取指定uri的所属类型
static int getDefaultType(Uri defaultRingtoneUri)
7. //将指定Uri设置为指定声音类型的默认声音
static void setActualDefaultRingtoneUri(Context context, int type, Uri ringtoneUri)
8、//播放
void play()
9、//停止播放
void stop()
经过如上几种音效的播放方式的讲解,我们可以对音效的播放做简单的总结如下所示:
手机一般都有麦克风和摄像头,而Android系统就可以利用这些硬件来录制音视频了。为了增加对录制音视频的支持,Android系统提供了一个MediaRecorder的类。
与MediaPlayer类非常相似MediaRecorder也有它自己的状态图,MediaRecorder的各个状态介绍如下:
需要说明的是,与MediaPlayer相似,使用MediaRecorder录音录像时需要严格遵守状态函数调用的先后顺序,在不同的状态调用不同的函数,否则会出现异常。如上的文字描述可以转换为如下状态图:
音视频的原始数据非常庞大,难以存储和传输。要解决音视频数据的存储和传输问题,需要做如下处理:
而我们知道音视频编解码格式非常多(h264、h265、vp8、vp9、aac、opus……),实现每种编解码都需要引入外部库,导致项目臃肿、包体积过大且运行性能差。
因此Google提出了一套标准,这就是MediaCodec。具体来说,了解MediaCodec可以从以下几个方面来说:
关于MediaCodec的工作原理,可以参见下图所示:
工作步骤如下所示:
MediaCodec可以对三种数据进行操作,分别是:
MediaCodec存在三种状态:停止(stoped)、执行(executing)、释放(released)。
MediaCodec 发展
Android系统中关于MediaCodec的介绍,可以参考Android的官方网站提供的信息:https://developer.android.google.cn/reference/kotlin/android/media/MediaCodec
MediaCodec 是在 Android 4.1版本(API16 )中出现并可用的,它提供了一种极其原始的接口。MediaCodec类同时存在 Java和C++层中,但是只有前者是公共访问方法。
在Android 4.3 (API18)中,MediaCodec被扩展为通过 Surface 提供输入的方法(通过 createInputSurface方法),允许来自于相机的预览或者是经过OpenGL ES呈现。在该版本中,MediaCodec是第一个过了CTS测试的版本。所谓的CTS,全称是Compatibility Test Suite,主要是google推出的一种设备兼容性测试规范,用来保证不同设备一致的用户体验的规范。
除此之外,4.3版本还引入了 MediaMuxer。MediaMuxer允许将AVC编解码器(原始H.264基本流)的输出转换为.MP4格式,可以和音频流一起转码也可以单独转换。
MediaCodec通常与MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface和AudioTrack一起使用,几乎可以实现大部分音视频相关功能。主要的操作步骤如下所示:
代码中的体现如下:
- createEncoderByType/createDecoderByType
- configure
- start
- while(true) {
- dequeueInputBuffer
- queueInputBuffer
- dequeueOutputBuffer
- releaseOutputBuffer
}
- stop
- release
private static MediaCodec createAudioEncoder() throws IOException {
MediaCodec codec = MediaCodec.createEncoderByType(AUDIO_MIME);
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, AUDIO_MIME);
format.setInteger(MediaFormat.KEY_BIT_RATE, 64000);
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, 44100);
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
return codec;
}
...
while (!sawOutputEOS) {
if (!sawInputEOS) {
inputBufIndex = audioEncoder.dequeueInputBuffer(10_000);
if (inputBufIndex >= 0) {
ByteBuffer inputBuffer = audioInputBuffers[inputBufIndex];
//先清空缓冲区
inputBuffer.clear();
int bufferSize = inputBuffer.remaining();
if (bufferSize != rawInputBytes.length) {
rawInputBytes = new byte[bufferSize];
}
//读取
readRawAudioCount = fisRawAudio.read(rawInputBytes);
//判断是否到文件的末尾
if (readRawAudioCount == -1) {
readRawAudioEOS = true;
}
if (readRawAudioEOS) {
audioEncoder.queueInputBuffer(inputBufIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
sawInputEOS = true;
} else {
//放入缓冲区
inputBuffer.put(rawInputBytes, 0, readRawAudioCount);
rawAudioSize += readRawAudioCount;
//放入编码队列
audioEncoder.queueInputBuffer(inputBufIndex, 0, readRawAudioCount, audioTimeUs, 0);
audioTimeUs = (long) (1_000_000 * ((float) rawAudioSize / AUDIO_BYTES_PER_SAMPLE));
}
}
}
//输出端
outputBufIndex = audioEncoder.dequeueOutputBuffer(outBufferInfo, 10_000);
if (outputBufIndex >= 0) {
// Simply ignore codec config buffers.
if ((outBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
Log.i(TAG, "audio encoder: codec config buffer");
audioEncoder.releaseOutputBuffer(outputBufIndex, false);
continue;
}
if (outBufferInfo.size != 0) {
ByteBuffer outBuffer = audioOutputBuffers[outputBufIndex];
outBuffer.position(outBufferInfo.offset);
outBuffer.limit(outBufferInfo.offset + outBufferInfo.size);
//Log.v(TAG, String.format(" writing audio sample : size=%s , presentationTimeUs=%s", outBufferInfo.size, outBufferInfo.presentationTimeUs));
if (lastAudioPresentationTimeUs <= outBufferInfo.presentationTimeUs) {
lastAudioPresentationTimeUs = outBufferInfo.presentationTimeUs;
int outBufSize = outBufferInfo.size;
int outPacketSize = outBufSize + 7;
outBuffer.position(outBufferInfo.offset);
outBuffer.limit(outBufferInfo.offset + outBufSize);
byte[] outData = new byte[outPacketSize];
addADTStoPacket(outData, outPacketSize);
outBuffer.get(outData, 7, outBufSize);
fosAccAudio.write(outData, 0, outData.length);
//Log.v(TAG, outData.length + " bytes written.");
} else {
Log.e(TAG, "error sample! its presentationTimeUs should not lower than before.");
}
}
audioEncoder.releaseOutputBuffer(outputBufIndex, false);
if ((outBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
sawOutputEOS = true;
}
} else if (outputBufIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
audioOutputBuffers = audioEncoder.getOutputBuffers();
} else if (outputBufIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat audioFormat = audioEncoder.getOutputFormat();
Log.i(TAG, "format change : " + audioFormat);
}
}
...
以上是MediaCodec的编码执行操作。如果是解码,与编码过程相反即可完成。
总结
如果遇到一些要求更高的项目开发,对音频有高性能的需求,比如说:所需的不仅仅是简单的声音播放或录制功能。它们需要响应式实时系统行为。一些典型用例如:音频合成器、电子鼓、音乐学习应用、DJ 混音、音效、视频/音频会议等这类要求特别高的需求时。就要从更深层次的底层来提供功能支持,这里就会用到NDK开发。
首先来了解一下NDK,全称是Native Development Kit,翻译为原生开发工具包,主要的作用是可以让开发者在Android应用中利用C和c++代码的工具,可用以从自己的源代码构建,或者利用现有的预构建库。
本部分的内容可以在如下的Android官方网站中进行查看和学习:https://developer.android.google.cn/ndk/guides/audio
Android官方给提供了如下选择:
FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源程序。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec,为了保证高可移植性和编解码质量,libavcodec里很多code都是从头开发的。
只要是做音视频开发的开发者,几乎没有不知道FFmpeg库的。在github上可以找到FFmpeg的主页地址如下:https://github.com/FFmpeg/FFmpeg 官方网站的地址是:https://ffmpeg.org/
其中包含的库主要包括:
该程序最初在Linux平台上开发和使用,目前在windows、mac上均可以使用。
如果需要在Android中使用FFmpeg,需要进行集成。需要经过几个步骤:
Speex主要是针对语音的开源免费,无专利保护的一种音频压缩格式,是专门为码率在2-44kbps的语音压缩而设计。Speex的特点主要包括:
Slik算法主要的作用是实现语音和音频的编解码,其主要的特点是:
本篇文档,我们用很长的篇幅介绍了多媒体开发中的音频功能的开发和使用,在具体的开发和应用中,重点应该放在对整体知识的理解和架构的梳理上,不要拘泥于某个API的使用,参数的作用等。归根到底,不同的实现方案,不同的解决方案最终的落脚点和代码操作步骤几乎是相同的。再次回顾总结我们本篇内容: