在接触Android音频开发后,陆陆续续的看了不少的文章,如果说查缺补漏把这些文章梳理清楚,然后逐个整合,那么确实也能完整的推导出音频开发需要掌握的技术。但是对于初学者来说,可能在开发中产生很多障碍以及对知识一知半解。
所以为了系统的,有逻辑的,基于现实需求的将音频开发这块的知识展现给大家,同时也是为了对自己这段时间音频学习的总结,便有了这篇文章。
这里我们将由浅入深,以更贴近实际开发的步骤,让大家逐渐的对音视频有一个较为全面的了解。
再说一句题外话,音视频常规业务开发是一个难者不会,会者不难的技术模块,他真正的难点其实是涉及到的协议广泛,涉及到的概念特别多,但是这些都是固定的(就是说我们花时间就能掌握),大多数场景我们也可以根据业务需求分析出我们所需要使用的技术。所以这个部分的难点主要就是要博文强识。更近一步说,我们需要建立的是音视频的架构体系,知道整个业务的流程闭环是怎样的,这样在我们接触工作的时候就可以专点突破。
声音是由物体振动而产生的,一切正在发声的物体都在振动。
我们听到声音的过程:
物体震动 -> 声波 -> 传播介质 -> 耳廓(收集声波)-> 外耳道(传递声波) -> 鼓膜(将声波转换成振动)
-> 听小骨(放大振动)
-> 耳蜗(将振动转换成电信号) -> 听觉神经(传递电信号) -> 大脑(形成听觉)
所以我们听见的并不是震动,而是震动转变成声波,我们又将声波转换成电信号。
人耳听到声音的过程其实和我们在计算机中处理音频的过程非常相像。
所以理解人耳的收音过程,对于计算机如何处理音频也有较大的参考价值。
一个物体震动就会产生声波,声波的三要素是频率、振幅和波形



上面讲到声音的本质是声波的形式,声音属于模拟信号,但便于计算机处理和存储的是数字信号;所以需要将模拟信号转成数字信号后进行存储,这一过程,即为音频数字化。我们在互联网上听到的声音,都是先经过录制后转为了数字音频,再传输到互联网上的。
音频数字化的常见技术方案是脉冲编码调制(PCM,Pulse Code Modulation),安卓采用的也是该方案。
主要过程:采样 、量化 、编码
模拟信号的波形是无限光滑的,可以看成由无数个点组成,由于存储空间是相对有限的,数字编码过程中,必须要对波形的点进行采样。 采样就是每隔一段时间采集一次模拟信号的样本,在时间上将模拟信号离散化的过程。
根据采样定理(奈奎斯特–香农采样定理),只有当采样率高于声音信号最高频率的2倍时,才能把采集的声音信号唯一地还原成原来的声音;因此要按比声音最高频率高2倍以上的频率对声音进行采样;人耳能够听到的频率范围是20Hz~20kHz,所以采样频率一般为 44.1kHz,这样就可以保证采样声音达到20kHz也能被数字化,从而使得经过数字化处理之后,人耳听到的声音质量不会被降低。
量化是指在幅度轴上对信号进行数字化,将每一个采样点的样本值数字化。
比如用16比特的二进制信号来表示声音的一个采样,而16比特所表示的 范围是[-32768,32767],共有2^16=625536个可能取值,因此最终模拟的音频信号在幅度上也分为了65536层;这里的16bit即为位深度(采样精度/采样大小):使用多少个二进制位来存储一个采样点的样本值;位深度越高,表示的振幅越精确;
这里面需要关注的是位深这个概念,就是存取一个采样点使用的数据位数大小。这个概念在视频中也会用到,比如存储一个像素所使用的位深。
编码涉及了很多种格式,通常说的音频裸数据格式就是PCM(脉冲编码调制)数据。PCM需要以下几个概念:采样格式(sampleFormat)、采样率 (sampleRate)、声道数(channel)。
采样格式:包含采样位深度、大小端模式、数据排列方式等;
如果采用16bit及以上的位数进行采样,那么就会涉及大小端模式。

声道:单声道产生一组声波数据,双声道(立体声)产生两组声波数据。
对于声音格式,还有一个概念用来描述它的大小,称为比特率(byteRate),即指单位时间内传输或处理的比特数量;单位是:比特每秒(bps),还有:千比特每秒(Kbps)、兆比特每秒(Mbps)等等。
以CD的音质为例:
位深度为16比特(2字节),采样率为44.1kHZ,声道数为2,这些信息就描述了CD的音质。对于1分钟CD音质的数据,比特率为:
44100 * 16 * 2 = 1378.125 Kbps
一分钟录音占用的存储空间为:
1378.125 * 60 / 8 / 1024 = 10.09MB
有了上面的基础后我们再阅读代码的时候基本就会很流畅了,无非就是我们根据实际的业务需求去设置我们需要的音频参数。
这里我们可以使用 AudioRecord.Builder 来创建AudioRecord对象。
mAudioRecord = new AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
.setAudioFormat(audioFormat)
.setBufferSizeInBytes(Config.AUDIO_CONFIG.READ_AUDIO_BUFFER_SIZE_BY_BYTES)
.build();
这个参数是设置被录音的音频源类型,一般来说我们录音的需求不一样,那么采用音频源类型也不一样。
| Audio Source | Value | remark |
|---|---|---|
| AUDIO_SOURCE_INVALID | -1 | |
| DEFAULT | 0 | |
| MIC | 1 | 麦克风 |
| VOICE_UPLINK | 2 | 录制上行音频 SystemApi |
| VOICE_DOWNLINK | 3 | 录制下行音频 SystemApi |
| VOICE_CALL | 4 | 录制上行+下行音频 SystemApi |
| CAMCORDER | 5 | 麦克风音频源已调整为视频录制,方向与摄像头相同(如果可用) |
| VOICE_RECOGNITION | 6 | 为语音识别调音的麦克风音频源 |
| VOICE_COMMUNICATION | 7 | 为语音通信(如VoIP)调谐的麦克风音频源。例如,它将利用回声消除或自动增益控制(如果可用)。 |
| REMOTE_SUBMIX | 8 | 用于传输系统混音的音频流到远端,这个声音会排除 AudioManager.STREAM_RING, AudioManager.STREAM_ALARM, and AudioManager.STREAM_NOTIFICATION.同时这些流会正常的播放。SystemApi |
| UNPROCESSED | 9 | 如果可用的话,返回未经处理的音频流,和Default类似 |
| VOICE_PERFORMANCE | 10 | 低延迟用于满足实时音频处理 |
| ECHO_REFERENCE | 1997 | 回声抑制参考信号 ,SystemApi |
| RADIO_TUNER | 1998 | 电台广播声音,SystemApi |
| HOTWORD | 1997 | 抢占式的热词检测,SystemApi |
备注:SystemApi - 的意思是需要
android.Manifest.permission.CAPTURE_AUDIO_OUTPUT
权限。此权限保留给系统组件使用,不可用于第三方应用程序。
第二个参数AudioFormat是我们配置录音参数的主要类,同样的他也为我们提供了Builder来快速创建对象。
AudioFormat audioFormat = new AudioFormat.Builder()
.setEncoding(Config.AUDIO_CONFIG.ENCODING_PCM)
.setSampleRate(Config.AUDIO_CONFIG.SAMPLE_RATE_IN_Hz)
.setChannelMask(Config.AUDIO_CONFIG.RECORD_CHANNEL_CONFIG)
.build();
由于有了第一个部分的基础支持我们现在再看这些代码就感觉很容易了,首先配置的编码格式,在AudioFormat中也为我们提供了很多的编码格式,这里给出部分编码的截图,全部内容大家可以自己在源码中查看。

这里涉及到编码的参数都是以
ENCODING_
作为开头的,所以大家进入到 android.media.AudioFormat 后很方便就可以看到。这里我们要着重说明的一下的是: ENCODING_PCM_16BIT
他的注释如下:
Audio data format: PCM 16 bit per sample. Guaranteed to be supported by devices.
也就是说这个参数是保证被所有设备支持的。
注:
1.2.3 中我们说的数据排列方式,这里采用的是 Packed ,后面如无特殊说明都是Packed。The audio sample is a 16 bit signed integer typically stored as a Java short in a short array, but when the short is stored in a ByteBuffer, it is native endian (as compared to the default Java big endian).The short has full range from [-32768, 32767], and is sometimes interpreted as fixed point Q.15 data.
这个参数就是采样率和我们在 1.2.1 中所提到的概念一致,即每秒进行多少次采样。
一般常见的采样率规格如下:
Pair<Integer, Integer> SAMPLE_RATE_96000 = new Pair<>(0x00, 96000);
Pair<Integer, Integer> SAMPLE_RATE_88200 = new Pair<>(0x01, 88200);
Pair<Integer, Integer> SAMPLE_RATE_64000 = new Pair<>(0x02, 64000);
Pair<Integer, Integer> SAMPLE_RATE_48000 = new Pair<>(0x03, 48000);
Pair<Integer, Integer> SAMPLE_RATE_44100 = new Pair<>(0x04, 44100);
Pair<Integer, Integer> SAMPLE_RATE_32000 = new Pair<>(0x05, 32000);
Pair<Integer, Integer> SAMPLE_RATE_24000 = new Pair<>(0x06, 24000);
Pair<Integer, Integer> SAMPLE_RATE_22050 = new Pair<>(0x07, 22050);
Pair<Integer, Integer> SAMPLE_RATE_16000 = new Pair<>(0x08, 16000);
Pair<Integer, Integer> SAMPLE_RATE_12000 = new Pair<>(0x09, 12000);
Pair<Integer, Integer> SAMPLE_RATE_11025 = new Pair<>(0x0A, 11025);
Pair<Integer, Integer> SAMPLE_RATE_8000 = new Pair<>(0x0B, 8000);
这里我们要注意的是,采样率是有最大最小值的,我们所给的参数需要再这个区间之内:
public static final int SAMPLE_RATE_HZ_MIN = AudioSystem.SAMPLE_RATE_HZ_MIN;
public static final int SAMPLE_RATE_HZ_MAX = AudioSystem.SAMPLE_RATE_HZ_MAX;
如果不设置在该区间会报错:
public Builder setSampleRate(int sampleRate) throws IllegalArgumentException {
if (((sampleRate < SAMPLE_RATE_HZ_MIN) || (sampleRate > SAMPLE_RATE_HZ_MAX)) &&
sampleRate != SAMPLE_RATE_UNSPECIFIED) {
throw new IllegalArgumentException("Invalid sample rate " + sampleRate);
}
mSampleRate = sampleRate;
mPropertySetMask |= AUDIO_FORMAT_HAS_PROPERTY_SAMPLE_RATE;
return this;
}
这个就是声道数了,这里我们可以理解为声道数是和声音的立体感相关的。在设备支持的情况下声道数越多我们听到的声音越立体。
该部分我们也可以在AudioFormat内通过 CHANNEL_ 查看。
这里需要特别说一下的是:
public static final int CHANNEL_IN_MONO = CHANNEL_IN_FRONT;
public static final int CHANNEL_IN_STEREO = (CHANNEL_IN_LEFT | CHANNEL_IN_RIGHT);
即单声道与双声道。
由于我们录音的时候,音频数据是以流的形式实时返回的,所以我们可以设置每次返回的Buffer大小。
这大小决定了返回间隙! 因为采样点、声道数、位深确定后,每秒产生的数据量就确定了,那么每次返回的数据块大小确定后,就可以估算出1秒钟会返回多少次,即可以知道单个音频数据块的返回间隙。这里我们有个初步的概念即可,在后面的音频传输部分我们会用到。
在设置该参数的时候有如下注释:
Sets the total size (in bytes) of the buffer where audio data is written during the recording. New audio data can be read from this buffer in smaller chunks than this size. See getMinBufferSize(int,int, int) to determine the minimum required buffer size for the successful creation of an AudioRecord instance.
这里就是说我们设置的大小不能小于 MinBufferSize 的大小,并且我们可以使用 getMinBufferSize 来获取这个大小。
int AudioRecord.getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat);
这个代码现在看起来也不在有什么难度了,将之前已经确定的 采样率、声道数、数据格式设置进去之后就会得到该录音参数下的 MinBufferSize ,我们设置的 BufferSizeInBytes 大于 MinBufferSize 即可。
注:不同的音频参数下 MinBufferSize 是不同的!
完成上面的参数配置之后我们就具备了录音能力。
我们在AudioRecord内可以查看录音状态:
public static final int STATE_UNINITIALIZED = 0;
public static final int STATE_INITIALIZED = 1;
public static final int RECORDSTATE_STOPPED = 1; // matches SL_RECORDSTATE_STOPPED
public static final int RECORDSTATE_RECORDING = 3;// matches SL_RECORDSTATE_RECORDING
简单来说可以分成两对,即是否初始化了;和是否处于录音中。
在开始录音之前,为了增加代码的健壮性,我们可以先检查一下AudioRecord是否处于录音状态:
int recordingState = mAudioRecord.getRecordingState();
这里我们需要新开一个线程来不断的从 AudioRecord 中读取音频数据:
while (mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
byte[] tempAudioData = new byte[Config.AUDIO_CONFIG.READ_AUDIO_BUFFER_SIZE_BY_BYTES];
int bufferReadResult = mAudioRecord.read(tempAudioData, 0, Config.AUDIO_CONFIG.READ_AUDIO_BUFFER_SIZE_BY_BYTES);
if (bufferReadResult < 0) {
Log.w(TAG, "getRecordAndRTPSendRunnable bufferReadResult = " + bufferReadResult);
break;
}
}
read方法的三个参数为:
由于我们并没有做特殊的处理或者要求,所以第二个参数为0,第三个参数就是我们声明tempAudioData 数组的长度。
当我们拿到原始的PCM数据流后,我们的录音功能就已经基本完成了。
如果说播放流媒体的话我们可以有两个选择:MediaPlayer和AudioTrack
这里我们需要了解一下他们的区别:
MediaPlayer可以播放多种格式的声音文件,例如MP3,AAC,WAV,OGG,MIDI等。MediaPlayer会在framework层创建对应的音频解码器。
而AudioTrack只能播放已经解码的PCM流,如果是文件的话只支持wav格式的音频文件,因为wav格式的音频文件大部分都是PCM流。AudioTrack不创建解码器,所以只能播放不需要解码的wav文件。
当然两者之间还是有紧密的联系的, MediaPlayer在framework层还是会创建AudioTrack,把解码后的PCM数流传递给AudioTrack,AudioTrack再传递给AudioFlinger进行混音,然后才传递给硬件播放。 所以是MediaPlayer包含了AudioTrack。
通过查看API可以知道,MediaPlayer提供了5个setDataSource方法,分为三类一类是传递播放文件的字符串路径作为参数,例如直接取sd卡里mp3文件的路径,一类是传递播放文件的FileDescriptor文件描述符作为播放的id,例例如从db中查询的音频文件的id,就可以直接赋给MediaPlayer进行播放。还有一类是Uri类型的资源文件,用于播放content uri文件。而AudioTracker的write方法支持PCM音频缓冲区流式传输到音频接收器以进行播放。
public int write(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes,
@WriteMode int writeMode)
可以看出在Android端的播音最后都是通过AudioTrack播放PCM数据实现的。可以将PCM看成Android音频世界的通用语言。
这里我直接使用AudioTracker的构造方法:
public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat,
int bufferSizeInBytes, int mode)
这里共涉及6个参数我们先将我们熟悉的过一下:
再讲解一下两个新的参数。
这个参数的意思是流类型,他的注释让我们去AudioManager中进行查看。
/** Used to identify the volume of audio streams for phone calls */
public static final int STREAM_VOICE_CALL = AudioSystem.STREAM_VOICE_CALL;
/** Used to identify the volume of audio streams for system sounds */
public static final int STREAM_SYSTEM = AudioSystem.STREAM_SYSTEM;
/** Used to identify the volume of audio streams for the phone ring */
public static final int STREAM_RING = AudioSystem.STREAM_RING;
/** Used to identify the volume of audio streams for music playback */
public static final int STREAM_MUSIC = AudioSystem.STREAM_MUSIC;
/** Used to identify the volume of audio streams for alarms */
public static final int STREAM_ALARM = AudioSystem.STREAM_ALARM;
/** Used to identify the volume of audio streams for notifications */
public static final int STREAM_NOTIFICATION = AudioSystem.STREAM_NOTIFICATION;
这里的代码也比较简单直接,就是告诉我们不同的流类型,系统会采用不同的声音设置进行播放。
所以我们根据具体的业务来设置就可以了。
mode共有两个取值: MODE_STATIC 、MODE_STREAM
这里也比较好理解,就是一次传入还是以流式的方法不断的传入;显然这和我们的业务有关系,如果是播放PCM音频文件,我们可以使用MODE_STATIC;如果是实时的音频通讯业务就需要使用MODE_STREAM。
再创建完 AudioTrack 后我们就可以调用 play() 方法进行播放了。这里的播放是在独立线程中进行的:
public void play()
throws IllegalStateException {
if (mState != STATE_INITIALIZED) {
throw new IllegalStateException("play() called on uninitialized AudioTrack.");
}
//FIXME use lambda to pass startImpl to superclass
final int delay = getStartDelayMs();
if (delay == 0) {
startImpl();
} else {
new Thread() {
public void run() {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
baseSetStartDelayMs(0);
try {
startImpl();
} catch (IllegalStateException e) {
// fail silently for a state exception when it is happening after
// a delayed start, as the player state could have changed between the
// call to start() and the execution of startImpl()
}
}
}.start();
}
}
同时如果我们使用的是流式的方式进行播放,需要不断的将数据写入到AudioTracker中:
while (true) {
mAudioTrack.write(pcmData, 0, pcmData.length);
}
AudioTrack#stop 停止播放音频数据。
当在{@link#MODE_STREAM}模式下创建的实例上使用时,在播放最后一个写入的缓冲区后,音频将停止播放。要立即停止,请使用{@link#pause()},然后使用{@link#flush()}.放弃尚未播放的音频数据。
AudioTrack#pause 暂停播放音频数据。
未回放的数据不会被丢弃。随后调用{@link#play}将播放此数据。请参阅{@link#flush()}以丢弃此数据。
编码和解码实际做的工作就是在音频参数确定后将一个数组转换成另外一个数组。
在1.3中我们计算了:
信息描述的CD音质。对于1分钟CD音质的数据,其存储空间为:10.09MB;在网络中传播的话,数据量太大了;为了更便于存储和传输,一般都会使用某种音频编码对它进行编码压缩,然后再存成某种音频文件格式。同时在播放的时候通过解码还原成PCM再进行播放。
压缩分为无损压缩和有损压缩。
在Android中如果不借住其他三方,我们主要使用MediaCodec进行编解码,但是音频和视频的编码格式是非常丰富的,并且由于设备的不同导致终端支持的编解码能力也不同,所以一般在我们需要进行编解码工作之前可以先查看一下我们设备支持的编解码格式,避免由于设备问题导致我们的编解码功能失效。
这里我将该方法封装成了工具类,大家需要的话可以直接拿去用:
public static void getSupportMediaType() {
MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
MediaCodecInfo[] supportCodes = mediaCodecList.getCodecInfos();
Log.d(TAG, "Support mediatypes:");
for (MediaCodecInfo codec : supportCodes) {
String name = codec.getName();
Log.d(TAG, name +" "+ (name.startsWith("OMX.google") ? "软" : "硬") + (codec.isEncoder() ? "解" : "编"));
}
}
该方法就会为我们返回当前设备支持的编解码格式,以及他是软编软解还是硬编硬解。
常见的音频编码格式,这里给大家两个网站可以自行阅读:
7种常见的音频格式简析:MP3,WMA,WAV,APE,FLAC,OGG,AAC
10大常见音频文件格式,你知道几个?
关于音频格式我们只需要记住他的特性即可,比如说 AMR 适合做即时通信类业务 ; OGG适合做铃声。
再给大家一个建议,关于格式的具体实现不需要特别的关注,在实现的时候按照协议标准去做即可。其实我们掌握了一个格式的封装之后,其他的格式基本上都可以照葫芦画瓢的写出来。
编码这个模块我们使用AAC编码作为讲解用例,AAC(Advanced Audio Coding)是新一代的音频有损压缩技术;
AAC编码的文件扩展名主要有3种:
特点:
在小于128Kbit/s的码率下表现优异,并且多用于视频中的音频编码。
适用场合:
128Kbit/s以下的音频编码,多用于视频中音频轨的编码。
Media codec 的详细使用和原理如果展开了讲的话内容比较多,这里推荐直接去google开发者官网去看。
https://developer.android.google.cn/reference/android/media/MediaCodec
所谓的编码就是将我们的PCM音频流变成另外一种数据格式,所以我们第一步就是要确定我们要进行的编码类型是什么:
try {
mediaCodec = MediaCodec
.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
} catch (IOException e) {
e.printStackTrace();
}
这里我们采用的是AAC编码所以我们使用 MediaFormat.MIMETYPE_AUDIO_AAC , 和之前一样 MediaFormat同样为我们准备了大量的编解码类型,大家可以自己进入到MediaFormat中进行查看。
上一步虽然我们创建出了 mediaCodec 对象,但是他并不知道我们要交给他的PCM数据格式是什么样的,所以这里我们和使用AudioTrack一样需要进行音频参数的配置。
public void configure(
@Nullable MediaFormat format,
@Nullable Surface surface, @Nullable MediaCrypto crypto,
@ConfigureFlag int flags) {
configure(format, surface, crypto, null, flags);
}
由于我们不涉及到Surface和数据的加解密,我们这两参数传null。
flags这个参数如果传递代表是编码器,由于我们现在进行的就是编码所以我们传 MediaCodec.CONFIGURE_FLAG_ENCODE
这里的 MediaFormat 和我们前面说的 AudioFormat 不太一样。具体的我们看下下面的代码:
public static final @NonNull MediaFormat createAudioFormat(
@NonNull String mime,
int sampleRate,
int channelCount) {
MediaFormat format = new MediaFormat();
format.setString(KEY_MIME, mime);
format.setInteger(KEY_SAMPLE_RATE, sampleRate);
format.setInteger(KEY_CHANNEL_COUNT, channelCount);
return format;
}
public MediaFormat() {
mMap = new HashMap();
}
从代码中我们可以看出 MediaFormat 实际上是一个 HashMap 里面通过key-value的形式存储和编码相关的参数格式。
那么此处我们设置的 MediaFormat 为:
MediaFormat mediaFormat = MediaFormat.createAudioFormat(
MediaFormat.MIMETYPE_AUDIO_AAC,
Config.AUDIO_CONFIG.SAMPLE_RATE_IN_Hz,
Config.CODEC_CANNEL_COUNT);
mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, Config.AUDIO_CONFIG.BITRATES);
mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, Config.AUDIO_CONFIG.READ_AUDIO_BUFFER_SIZE_BY_BYTES);
这里我们不再介绍我们熟悉的: mime、sampleRate、format
主要看下新的内容:
MediaFormat.KEY_AAC_PROFILE
只有使用AAC格式时才使用的字段,该字段用于描述AAC的级别,可以理解为AAC的实现算法。
在 MediaCodecInfo.CodecProfileLevel 已经帮我们定义好了,所以直接引用即可。
MediaFormat.KEY_BIT_RATE
比特率,即我们压缩后每秒产生的数据大小,bitrate in bits/sec。
MediaFormat.KEY_MAX_INPUT_SIZE
Media codec 数据缓冲区的最大字节大小,这里涉及Media codec 的原理,如果看过了就很好理解。
在正式编码前我们需要启动编码器和新建一个MediaCodec.BufferInfo。
mediaCodec.start();
bufferInfo = new MediaCodec.BufferInfo();
一切准备就绪之后我们就可以编码了:
public byte[] offerEncoder(byte[] input);
这里还是给大家简单的说一下 Media codec 的工作原理,方便大家对后续代码的理解。
先上一张虽然谁都会用,但是真的能帮助大家理解其工作原理的图:

MediaCodec采用了2个缓冲区队列(inputBuffer和outputBuffer),异步处理数据,
下面再看代码就简单很多了:
int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferIndex);
inputBuffer.put(input);
inputBuffer.limit(input.length);
long pts = computePresentationTime(presentationTimeUs);
mediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, pts, 0);
presentationTimeUs += 1;
}
这个步骤简单来说就是和 Media codec 申请可用的InputBuffer,然后将我们的数据填充进InputBuffer,再然后将我们的数据放入到 Media codec 的输入队列。
其中需要注意的有dequeueInputBuffer(-1),参数表示需要等待的毫秒数
int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
if (outputBufferIndex < 0) {
Log.w(TAG, "offerDecoder dequeueOutputBuffer = " + outputBufferIndex);
}
while (outputBufferIndex >= 0) {
int outBitsSize = bufferInfo.size;
int outPacketSize = outBitsSize + 7;
ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferIndex);
outputBuffer.position(bufferInfo.offset);
outputBuffer.limit(bufferInfo.offset + outBitsSize);
//添加ADTS头
byte[] outData = new byte[outPacketSize];
addADTStoPacket(outData,
outPacketSize,
MediaCodecInfo.CodecProfileLevel.AACObjectLC,
Config.AUDIO_CONFIG.SAMPLE_RATE_INDEX
);
outputBuffer.get(outData, 7, outBitsSize);
outputBuffer.position(bufferInfo.offset);
outputStream.write(outData);
mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
}
这里和我们申请入队列类似,我们申请出队列,并将我们 4.3.4 中开头提到的 bufferInfo 传递进去,然后拿到可用的输出Buffer索引。
这里我们看一下 BufferInfo 的实现:
public final static class BufferInfo {
public int offset;
public int size;
public long presentationTimeUs;
public int flags;
public void set(int newOffset, int newSize, long newTimeUs, @BufferFlag int newFlags) {
offset = newOffset;
size = newSize;
presentationTimeUs = newTimeUs;
flags = newFlags;
}
public BufferInfo dup() {
BufferInfo copy = new BufferInfo();
copy.set(offset, size, presentationTimeUs, flags);
return copy;
}
};
他并不是输出数据,他是对输出数据的描述,也就是元数据,即描述输出数据的类对象。
在往下看就是 outputBufferIndex ,他代表可用的输出数据块的索引,所以他必须大于0,也就是说他小于0时是无效的!
剩下的就是基于本次输入,我们不断的向 Media codec 申请剩余的输出数据块,可能一次输出不完。
这也就是while循环内的内容。
这里需要再特殊说明一下的是在拿到音频数据后我们为每个音频数据块添加了 ADTS 头:
addADTStoPacket(outData,
outPacketSize,
MediaCodecInfo.CodecProfileLevel.AACObjectLC,
Config.AUDIO_CONFIG.SAMPLE_RATE_INDEX
);
这个逻辑虽然说是AAC独有的,但是对我们做其他的编码工作是有一定启发的,即拿到原始流后并不是立即存储到输出流中,而是可能需要做一些附加信息的特殊处理。所以转码是一方面,转码之后的处理我们也需要关注,这就和具体的实现要求有关了。
至此,我们经过了:创建 Media codec->配置 Media codec->启动 Media codec->向 Media codec申请可用的输入Buffer块 -> 对 Buffer块填充需要编码的数据 ->提交 Buffer块 ->申请完成编码的Buffer块->处理原始的编码块->返回处理完的编码块
等一系列的步骤,完成了编码的工作,工作步骤虽然不少,但是理解了其原理还是比较简单的。
再完成编码之后,我们首先想到的就是解码工作了,其实不然,这里建议大家如果做的是标准的编码工作,可以先使用播放器播放一下我们的编码内容。比如说我们上面制作的AAC编码,可以在其编码完成后把他存储到文件中,然后使用第三方的播放器进行播放,这样就预先验证了我们编码工作的正确性,验证通过后就可以放心的进行解码工作了。
上面说过,使用文件的方式验证编码结果其实还有另外一个好处,就是帮我们快速的构建MediaFormat。通过上面的学习我们基本上已经感受到了,录音和播音其实是一个对称的过程,除了通有参数外,只是加了几个功能本身需要的参数。
配置其他参数这块其实并不是很困难,一般来说我们通过阅读源码中的注释就可以解决,但是有没有更无脑更简单的方法那,有的,这就是MediaExtractor。并且MediaExtractor 非常的强大,他不仅可以帮我们分析音频文件的MediaFormat,还能帮我们分析例如MP4这样多种格式封装的文件。
使用 MediaExtractor 也是非常简单的:
MediaExtractor extractor = new MediaExtractor();//实例一个MediaExtractor
try {
extractor.setDataSource(mFile.getAbsolutePath());//设置添加媒体文件路径
} catch (IOException e) {
e.printStackTrace();
}
int count = extractor.getTrackCount();//获取轨道数量
Log.d(TAG, "轨道数量 = "+count);
for (int i = 0; i < count; i++){
MediaFormat mediaFormat = extractor.getTrackFormat(i);
Log.d(TAG, i+"编号通道格式 = "+mediaFormat.getString(MediaFormat.KEY_MIME));
}
简单来说就是:构建 MediaExtractor 对象,传入需要分析音视频文件路径,得到文件的Track,然后得到对应的MediaFormat。
try {
mediaCodec =MediaCodec.createDecoderByType(mediaFormat.getString(MediaFormat.KEY_MIME));
} catch (IOException e) {
e.printStackTrace();
}
这里我们使用 MediaCodec 创建了解码器,传入的参数是 mediaFormat 中的 MediaFormat.KEY_MIME;如果对编码那里还有印象的话,MediaCodec 和MediaFormat使用的 MIME_TYPE其实是一样的。
剩下的就简单了,传入mediaFormat就可以了。最后一个参数Flag不是编码不传有效值,所以我们传0。
mediaCodec.configure(mediaFormat, null, null, 0);
解码的实现和编码的实现原理差不多,其实都是将输入数组转成另外一个数组,所以这里就不在贴代码了。但是有一点我们需要关注的是,在编码的时候我们为了符合AAC编码格式给每个AAC音频数据块都加了ADTS头,那么我们解码的时候就需要去掉这个头,因为解码和编码是对称的。同样的我们做其他解码工作时,也应该关注一下,我们收到的编码数据是否是可以直接进行解码的,避免由于数据的错误,造成解码错误。
这个模块主要是介绍 :Media codec、MediaFormat、BufferInfo、ByteBuffer 、MediaExtractor 的使用,这里的建议是,上面的每个模块都阅读一下常用方法的注释,方法的注释是最好的老师,强过在网上查找资料。当然实验是检验真理的唯一标准,多做实验可以更好的了解音视频编解码的相关功能。
如果只是简单的录音存储文件,这对大多数的开发者来说都是比较简单的,通过上面的学习我们可以快速的完成这样的功能。根据业务需求确定音频参数后,我们只需要完成AudioRecord、AudioTracker的创建和配置;同样的使用Media codec + MediaForamt + MediaExtractor 完成编解码器的创建于配置,剩下的就是录音编码生成录音文件,然后解码播放录音文件了。
但是我们仍然要思考一个问题,如果我们做实时音频传输,如对讲功能,是否能按照上面的方式来完成。我们不妨先进行一个对比:
| 数据来源方式 | 有序 | 丢失 | 传输大小限制 | 实时性保证 |
|---|---|---|---|---|
| 文件 | 有序 | 不丢失 | 不涉及 | 保证 |
| TCP | 有序 | 不丢失 | 无限制 | 不保证 |
| UDP | 无序 | 丢失 | 有限制 | 保证 |
这里我们主要把关注点放在TCP传输和UDP传输这两个点上就可以了。
UDP和TCP的一个主要不同就是UDP传输是不保证到达顺序的,即当我们连续的发送音频数据时可能是后面的语句先到达,而前面的语句后到达,举一个例子:
比如说我现在要传:
一 二 三 四
如果使用TCP那么对端收到的也是“一 二 三 四”,但是UDP收到的就可能是:
二 一 四 三
这显然不是我们希望的,这里的例子是为了大家更好的理解这个问题场景。
实际中音频数据一秒钟音频生成的数据块和我们之前设置的录音参数有关,一般来说数据块都在10个左右(也可能更多或者更少),换句话说我们匀速说话时一个字的传输是由几个块组成的,当这些块乱序时,虽然不会像上面例子那样导致传递信息的内容改变,但是会对用户的体验产生影响。这里我们可以回想一下之前说的声波曲线,如果声波曲线乱序之后,本应该平滑变化的曲线就会可能变成前高后低并且不再平滑,这个时候的音频听起来就会感觉很怪异。
其实解决这个问题的方式也比较简单,就是在接收端加一个缓冲队列,然后我们对收到的数据做排序,但是仍然需要注意下面几个点:
UDP并非保证达到,音频数据块丢失是非常常见的,而且随着业务的进展是必然发生的。所以这个问题也是我们必须要解决的问题。
结合现在的网络带宽和我们前面编码后的音频数据大小,这里对于音频一般采取的是添加数据冗余的方式来解决丢失的问题。举个简单的例子:
比如说我现在要传递: 1、2、3、4、5 五个数字,我可以再传一个15,即1-5的总和,那么只要总和不丢,1-5这五个数字丢失任意一个我们都能将它还原出来。这里的列子非常的简单,实际的算法比这个要复杂很多,如这里面的冗余数据不能丢,并且有效的数据也只能丢1个,这些都是限制,同样的这些也是FEC算法需要解决的问题。
但是我们可以推断出来的是,冗余越多,我们能将丢失数据还原回来的可能性就越大。显然我们也不能将冗余设置的太大,那么如何设置有效的冗余就是一个我们需要思考的问题。
这里就引入了一个新的概念丢包率,即一段时间内发送的包总数和收到的包总数之差再比上发送的包总数。简单来说就是发送了100个包,对面只收到了80个那么丢包率是20%,此时我们将冗余比例设置的稍微大于丢包率就可以了。
下面给出丢包这块的重点:
至于FEC前向纠错算法的选择,可以直接移植WebRTC中使用的算法,也可以自己在Github中找现成的,总之结合业务,选择最适合自己的才是重点。
使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta
在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/
我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当
我已经在Sinatra上创建了应用程序,它代表了一个简单的API。我想在生产和开发上进行部署。我想在部署时选择,是开发还是生产,一些方法的逻辑应该改变,这取决于部署类型。是否有任何想法,如何完成以及解决此问题的一些示例。例子:我有代码get'/api/test'doreturn"Itisdev"end但是在部署到生产环境之后我想在运行/api/test之后看到ItisPROD如何实现? 最佳答案 根据SinatraDocumentation:EnvironmentscanbesetthroughtheRACK_ENVenvironm
我们的git存储库中目前有一个Gemfile。但是,有一个gem我只在我的环境中本地使用(我的团队不使用它)。为了使用它,我必须将它添加到我们的Gemfile中,但每次我checkout到我们的master/dev主分支时,由于与跟踪的gemfile冲突,我必须删除它。我想要的是类似Gemfile.local的东西,它将继承从Gemfile导入的gems,但也允许在那里导入新的gems以供使用只有我的机器。此文件将在.gitignore中被忽略。这可能吗? 最佳答案 设置BUNDLE_GEMFILE环境变量:BUNDLE_GEMFI
这似乎非常适得其反,因为太多的gem会在window上破裂。我一直在处理很多mysql和ruby-mysqlgem问题(gem本身发生段错误,一个名为UnixSocket的类显然在Windows机器上不能正常工作,等等)。我只是在浪费时间吗?我应该转向不同的脚本语言吗? 最佳答案 我在Windows上使用Ruby的经验很少,但是当我开始使用Ruby时,我是在Windows上,我的总体印象是它不是Windows原生系统。因此,在主要使用Windows多年之后,开始使用Ruby促使我切换回原来的系统Unix,这次是Linux。Rub
我正在玩HTML5视频并且在ERB中有以下片段:mp4视频从在我的开发环境中运行的服务器很好地流式传输到chrome。然而firefox显示带有海报图像的视频播放器,但带有一个大X。问题似乎是mongrel不确定ogv扩展的mime类型,并且只返回text/plain,如curl所示:$curl-Ihttp://0.0.0.0:3000/pr6.ogvHTTP/1.1200OKConnection:closeDate:Mon,19Apr201012:33:50GMTLast-Modified:Sun,18Apr201012:46:07GMTContent-Type:text/plain
无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD
在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList()Obt
@作者:SYFStrive @博客首页:HomePage📜:微信小程序📌:个人社区(欢迎大佬们加入)👉:社区链接🔗📌:觉得文章不错可以点点关注👉:专栏连接🔗💃:感谢支持,学累了可以先看小段由小胖给大家带来的街舞👉微信小程序(🔥)目录自定义组件-behaviors 1、什么是behaviors 2、behaviors的工作方式 3、创建behavior 4、导入并使用behavior 5、behavior中所有可用的节点 6、同名字段的覆盖和组合规则总结最后自定义组件-behaviors 1、什么是behaviorsbehaviors是小程序中,用于实现