一次搞懂 Android 音频开发

在接触Android音频开发后,陆陆续续的看了不少的文章,如果说查缺补漏把这些文章梳理清楚,然后逐个整合,那么确实也能完整的推导出音频开发需要掌握的技术。但是对于初学者来说,可能在开发中产生很多障碍以及对知识一知半解。

所以为了系统的,有逻辑的,基于现实需求的将音频开发这块的知识展现给大家,同时也是为了对自己这段时间音频学习的总结,便有了这篇文章。

这里我们将由浅入深,以更贴近实际开发的步骤,让大家逐渐的对音视频有一个较为全面的了解。

再说一句题外话,音视频常规业务开发是一个难者不会,会者不难的技术模块,他真正的难点其实是涉及到的协议广泛,涉及到的概念特别多,但是这些都是固定的(就是说我们花时间就能掌握),大多数场景我们也可以根据业务需求分析出我们所需要使用的技术。所以这个部分的难点主要就是要博文强识。更近一步说,我们需要建立的是音视频的架构体系,知道整个业务的流程闭环是怎样的,这样在我们接触工作的时候就可以专点突破。

一、音频开发必备基础知识

1.1 声音的本质

声音是由物体振动而产生的,一切正在发声的物体都在振动。

1.1.1 我们听到声音的过程

我们听到声音的过程:
物体震动 -> 声波 -> 传播介质 -> 耳廓(收集声波)-> 外耳道(传递声波) -> 鼓膜(将声波转换成振动)
-> 听小骨(放大振动)
-> 耳蜗(将振动转换成电信号) -> 听觉神经(传递电信号) -> 大脑(形成听觉)

所以我们听见的并不是震动,而是震动转变成声波,我们又将声波转换成电信号。

人耳听到声音的过程其实和我们在计算机中处理音频的过程非常相像。
所以理解人耳的收音过程,对于计算机如何处理音频也有较大的参考价值。

1.1.2 声波的三要素

一个物体震动就会产生声波,声波的三要素是频率、振幅和波形

  • 频率代表音阶的高低
  • 振幅代表响度
  • 波形代表音色。
    在这里插入图片描述
    这里我们回想,初中时学的音叉实验,当我们敲动音叉时,音叉会不断的震动,进而产生声音。
    那么这个震动显然是有频率的,同时由于敲击力度不同,发出的声音响度也不同。
    在这里插入图片描述
    另外现实生活中,同一时间点,显然不可能只有一个声音源,一般都是多个音源同时发声,那么他们的波形显然也不会是上面这种规律的有周期的图形。

1.1.3 总结

在这里插入图片描述

1.2 音频数字化

上面讲到声音的本质是声波的形式,声音属于模拟信号,但便于计算机处理和存储的是数字信号;所以需要将模拟信号转成数字信号后进行存储,这一过程,即为音频数字化。我们在互联网上听到的声音,都是先经过录制后转为了数字音频,再传输到互联网上的。
音频数字化的常见技术方案是脉冲编码调制(PCM,Pulse Code Modulation),安卓采用的也是该方案

主要过程:采样 、量化 、编码

1.2.1 采样率

模拟信号的波形是无限光滑的,可以看成由无数个点组成,由于存储空间是相对有限的,数字编码过程中,必须要对波形的点进行采样。 采样就是每隔一段时间采集一次模拟信号的样本,在时间上将模拟信号离散化的过程。

根据采样定理(奈奎斯特–香农采样定理),只有当采样率高于声音信号最高频率的2倍时,才能把采集的声音信号唯一地还原成原来的声音;因此要按比声音最高频率高2倍以上的频率对声音进行采样;人耳能够听到的频率范围是20Hz~20kHz,所以采样频率一般为 44.1kHz,这样就可以保证采样声音达到20kHz也能被数字化,从而使得经过数字化处理之后,人耳听到的声音质量不会被降低。

  • 采样率表示每秒采集的样本数量,44.1kHz就是代表1秒会采样44100次;

1.2.2 量化

量化是指在幅度轴上对信号进行数字化,将每一个采样点的样本值数字化。
比如用16比特的二进制信号来表示声音的一个采样,而16比特所表示的 范围是[-32768,32767],共有2^16=625536个可能取值,因此最终模拟的音频信号在幅度上也分为了65536层;这里的16bit即为位深度(采样精度/采样大小):使用多少个二进制位来存储一个采样点的样本值;位深度越高,表示的振幅越精确;

这里面需要关注的是位深这个概念,就是存取一个采样点使用的数据位数大小。这个概念在视频中也会用到,比如存储一个像素所使用的位深。

1.2.3 编码

编码涉及了很多种格式,通常说的音频裸数据格式就是PCM(脉冲编码调制)数据。PCM需要以下几个概念:采样格式(sampleFormat)、采样率 (sampleRate)、声道数(channel)。

采样格式:包含采样位深度、大小端模式、数据排列方式等;

如果采用16bit及以上的位数进行采样,那么就会涉及大小端模式。

  • 排列方式 Packed:
    对于双声道音频来说,Packed表示两个声道的数据交错存储,即:LRLRLRLR 的存储方式;
    在这里插入图片描述
  • 排列方式 Planar
    表示两个声道分开存储,也就是平铺分开,即:LLLLRRRR 的存储方式;

声道:单声道产生一组声波数据,双声道(立体声)产生两组声波数据。

1.3 音频流量的计算

对于声音格式,还有一个概念用来描述它的大小,称为比特率(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

二、录音

2.1 使用AudioRecord录音

有了上面的基础后我们再阅读代码的时候基本就会很流畅了,无非就是我们根据实际的业务需求去设置我们需要的音频参数。

2.2 AudioRecord的构造

这里我们可以使用 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();

2.2.1 setAudioSource

这个参数是设置被录音的音频源类型,一般来说我们录音的需求不一样,那么采用音频源类型也不一样。

Audio SourceValueremark
AUDIO_SOURCE_INVALID-1
DEFAULT0
MIC1麦克风
VOICE_UPLINK2录制上行音频 SystemApi
VOICE_DOWNLINK3录制下行音频 SystemApi
VOICE_CALL4录制上行+下行音频 SystemApi
CAMCORDER5麦克风音频源已调整为视频录制,方向与摄像头相同(如果可用)
VOICE_RECOGNITION6为语音识别调音的麦克风音频源
VOICE_COMMUNICATION7为语音通信(如VoIP)调谐的麦克风音频源。例如,它将利用回声消除或自动增益控制(如果可用)。
REMOTE_SUBMIX8用于传输系统混音的音频流到远端,这个声音会排除 AudioManager.STREAM_RING, AudioManager.STREAM_ALARM, and AudioManager.STREAM_NOTIFICATION.同时这些流会正常的播放。SystemApi
UNPROCESSED9如果可用的话,返回未经处理的音频流,和Default类似
VOICE_PERFORMANCE10低延迟用于满足实时音频处理
ECHO_REFERENCE1997回声抑制参考信号 ,SystemApi
RADIO_TUNER1998电台广播声音,SystemApi
HOTWORD1997抢占式的热词检测,SystemApi

备注:SystemApi - 的意思是需要

android.Manifest.permission.CAPTURE_AUDIO_OUTPUT

权限。此权限保留给系统组件使用,不可用于第三方应用程序。

2.2.2 AudioFormat

第二个参数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();
2.2.2.1 Encoding

由于有了第一个部分的基础支持我们现在再看这些代码就感觉很容易了,首先配置的编码格式,在AudioFormat中也为我们提供了很多的编码格式,这里给出部分编码的截图,全部内容大家可以自己在源码中查看。
在这里插入图片描述
这里涉及到编码的参数都是以

ENCODING_

作为开头的,所以大家进入到 android.media.AudioFormat 后很方便就可以看到。这里我们要着重说明的一下的是: ENCODING_PCM_16BIT
他的注释如下:

Audio data format: PCM 16 bit per sample. Guaranteed to be supported by devices.

也就是说这个参数是保证被所有设备支持的

注:

  1. 1.2.3 中我们说的数据排列方式,这里采用的是 Packed ,后面如无特殊说明都是Packed。
  2. 这里采用的是大端存储,所以当涉及到需要小端作为输入参数时,需要转换高低位。

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.

2.2.2.2 SampleRate

这个参数就是采样率和我们在 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;
        }
2.2.2.3 ChannelMask

这个就是声道数了,这里我们可以理解为声道数是和声音的立体感相关的。在设备支持的情况下声道数越多我们听到的声音越立体。
该部分我们也可以在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);

即单声道与双声道。

2.2.3 BufferSizeInBytes

由于我们录音的时候,音频数据是以流的形式实时返回的,所以我们可以设置每次返回的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 是不同的!

2.3 开始录音

完成上面的参数配置之后我们就具备了录音能力。

2.3.1 录音状态

我们在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();

2.3.2 开始录音

这里我们需要新开一个线程来不断的从 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方法的三个参数为:

  • audioData – the array to which the recorded audio data is written.
  • offsetInBytes – index in audioData from which the data is written
  • expressed in bytes. sizeInBytes – the number of requested bytes.

由于我们并没有做特殊的处理或者要求,所以第二个参数为0,第三个参数就是我们声明tempAudioData 数组的长度。

当我们拿到原始的PCM数据流后,我们的录音功能就已经基本完成了。

三、播音

3.1 MediaPlayer和AudioTrack

如果说播放流媒体的话我们可以有两个选择:MediaPlayer和AudioTrack

这里我们需要了解一下他们的区别:

  1. MediaPlayer可以播放多种格式的声音文件,例如MP3,AAC,WAV,OGG,MIDI等。MediaPlayer会在framework层创建对应的音频解码器。
    而AudioTrack只能播放已经解码的PCM流,如果是文件的话只支持wav格式的音频文件,因为wav格式的音频文件大部分都是PCM流。AudioTrack不创建解码器,所以只能播放不需要解码的wav文件。

  2. 当然两者之间还是有紧密的联系的, MediaPlayer在framework层还是会创建AudioTrack,把解码后的PCM数流传递给AudioTrack,AudioTrack再传递给AudioFlinger进行混音,然后才传递给硬件播放。 所以是MediaPlayer包含了AudioTrack。

  3. 通过查看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音频世界的通用语言。

3.2 构造AudioTracker

这里我直接使用AudioTracker的构造方法:

public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat,
            int bufferSizeInBytes, int mode)

这里共涉及6个参数我们先将我们熟悉的过一下:

  • sampleRateInHz:采样率,录音的时候设置多少,播音的时候就设置多少。
  • channelConfig:声道数,设置方法同上。
  • audioFormat:数据存储编码格式,设置方法同上。
  • bufferSizeInBytes:每次写入buffer块的大小,AudioTracker也提供了和AudioRecord类似的getMinBufferSize方法。

再讲解一下两个新的参数。

3.2.1 streamType

这个参数的意思是流类型,他的注释让我们去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;

这里的代码也比较简单直接,就是告诉我们不同的流类型,系统会采用不同的声音设置进行播放。
所以我们根据具体的业务来设置就可以了。

3.2.2 mode

mode共有两个取值: MODE_STATIC 、MODE_STREAM

  • MODE_STATIC:在音频开始播放之前,音频数据仅从Java传输到Native层一次。
  • MODE_STREAM:播放音频时,音频数据从Java流式传输到Native层。

这里也比较好理解,就是一次传入还是以流式的方法不断的传入;显然这和我们的业务有关系,如果是播放PCM音频文件,我们可以使用MODE_STATIC;如果是实时的音频通讯业务就需要使用MODE_STREAM。

3.3 播放音频

再创建完 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);
}

3.4 stop和pause

  • AudioTrack#stop 停止播放音频数据。
    当在{@link#MODE_STREAM}模式下创建的实例上使用时,在播放最后一个写入的缓冲区后,音频将停止播放。要立即停止,请使用{@link#pause()},然后使用{@link#flush()}.放弃尚未播放的音频数据。

  • AudioTrack#pause 暂停播放音频数据。
    未回放的数据不会被丢弃。随后调用{@link#play}将播放此数据。请参阅{@link#flush()}以丢弃此数据。

四、编码与解码

编码和解码实际做的工作就是在音频参数确定后将一个数组转换成另外一个数组。

4.1 预备阶段

1.3中我们计算了:

  • 位深度为16比特(2字节)
  • 采样率为44.1kHZ
  • 声道数为2

信息描述的CD音质。对于1分钟CD音质的数据,其存储空间为:10.09MB;在网络中传播的话,数据量太大了;为了更便于存储和传输,一般都会使用某种音频编码对它进行编码压缩,然后再存成某种音频文件格式。同时在播放的时候通过解码还原成PCM再进行播放。

压缩分为无损压缩和有损压缩。

  • 无损压缩:解压后可以完全还原出原始数据;
  • 有损压缩:解压后不能完全还原出原始数据,会丢失一部分信息;一般是压缩掉冗余信号(冗余信号是指不能被人耳感知到的信号,包含人耳听觉范围之外的音频信号以及被掩蔽掉的音频信号等),不进行编码处理。

4.2 查看设备支持的编解码格式

在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() ? "解" : "编"));
        }
    }

该方法就会为我们返回当前设备支持的编解码格式,以及他是软编软解还是硬编硬解。

4.3 Media codec AAC 编码

常见的音频编码格式,这里给大家两个网站可以自行阅读:

7种常见的音频格式简析:MP3,WMA,WAV,APE,FLAC,OGG,AAC
10大常见音频文件格式,你知道几个?

关于音频格式我们只需要记住他的特性即可,比如说 AMR 适合做即时通信类业务 ; OGG适合做铃声。
再给大家一个建议,关于格式的具体实现不需要特别的关注,在实现的时候按照协议标准去做即可。其实我们掌握了一个格式的封装之后,其他的格式基本上都可以照葫芦画瓢的写出来。

编码这个模块我们使用AAC编码作为讲解用例,AAC(Advanced Audio Coding)是新一代的音频有损压缩技术;

AAC编码的文件扩展名主要有3种:

  • .acc:传统的AAC编码,使用MPEG-2 Audio Transport Stream(ADTS)容器
  • .mp4:使用了MPEG-4 Part 14的简化版即3GPP Media Release 6 Basic(3gp6)进行封装的AAC编码
  • .m4a:为了区别纯音频MP4文件和包含视频的MP4文件而由Apple公司使用的扩展名;

特点:
在小于128Kbit/s的码率下表现优异,并且多用于视频中的音频编码。
适用场合:
128Kbit/s以下的音频编码,多用于视频中音频轨的编码。

Media codec 的详细使用和原理如果展开了讲的话内容比较多,这里推荐直接去google开发者官网去看。

https://developer.android.google.cn/reference/android/media/MediaCodec

4.3.1 创建编码器

所谓的编码就是将我们的PCM音频流变成另外一种数据格式,所以我们第一步就是要确定我们要进行的编码类型是什么:

       try {
            mediaCodec = MediaCodec
            .createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
        } catch (IOException e) {
            e.printStackTrace();
        }

这里我们采用的是AAC编码所以我们使用 MediaFormat.MIMETYPE_AUDIO_AAC , 和之前一样 MediaFormat同样为我们准备了大量的编解码类型,大家可以自己进入到MediaFormat中进行查看。

4.3.2 配置编码器

上一步虽然我们创建出了 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

4.3.3 MediaFormat

这里的 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 的原理,如果看过了就很好理解。

4.3.4 进行AAC编码

在正式编码前我们需要启动编码器和新建一个MediaCodec.BufferInfo

   mediaCodec.start();
   bufferInfo = new MediaCodec.BufferInfo();

一切准备就绪之后我们就可以编码了:

public byte[] offerEncoder(byte[] input);
4.3.4.1 向 Media codec 申请入队列Buffer块并填充数据

这里还是给大家简单的说一下 Media codec 的工作原理,方便大家对后续代码的理解。
先上一张虽然谁都会用,但是真的能帮助大家理解其工作原理的图:
在这里插入图片描述
MediaCodec采用了2个缓冲区队列(inputBuffer和outputBuffer),异步处理数据,

  1. 数据生成方(左侧Client)从input缓冲队列申请empty buffer—》dequeueinputBuffer
  2. 数据生产方(左侧Client)把需要编解码的数据copy到empty buffer,然后放入到input缓冲队列 —》queueInputBuffer
  3. MediaCodec从input缓冲区队列中取一帧进行编解码处理
  4. 编解码处理结束后,MediaCodec将原始inputbuffer置为empty后放回左侧的input缓冲队列,将编解码后的数据放入到右侧output缓冲区队列
  5. 消费方Client(右侧Client)从output缓冲区队列申请编解码后的buffer —》dequeueOutputBuffer
  6. 消费方client(右侧Client)对编解码后的buffer进行渲染或者播放
  7. 渲染/播放完成后,消费方Client再将该buffer放回到output缓冲区队列 —》releaseOutputBuffer

下面再看代码就简单很多了:

  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),参数表示需要等待的毫秒数

  • -1表示一直等
  • 0表示不需要等,传0的话程序不会等待,但是有可能会丢帧
  • 正数表示等待时间。
4.3.4.2 向 Media codec 申请出队列Buffer块,拿到解码数据
        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独有的,但是对我们做其他的编码工作是有一定启发的,即拿到原始流后并不是立即存储到输出流中,而是可能需要做一些附加信息的特殊处理。所以转码是一方面,转码之后的处理我们也需要关注,这就和具体的实现要求有关了。

4.3.5 编码总结

至此,我们经过了:创建 Media codec->配置 Media codec->启动 Media codec->向 Media codec申请可用的输入Buffer块 -> 对 Buffer块填充需要编码的数据 ->提交 Buffer块 ->申请完成编码的Buffer块->处理原始的编码块->返回处理完的编码块

等一系列的步骤,完成了编码的工作,工作步骤虽然不少,但是理解了其原理还是比较简单的。

4.4 Media codec AAC 解码

4.4.1 验证编码

再完成编码之后,我们首先想到的就是解码工作了,其实不然,这里建议大家如果做的是标准的编码工作,可以先使用播放器播放一下我们的编码内容。比如说我们上面制作的AAC编码,可以在其编码完成后把他存储到文件中,然后使用第三方的播放器进行播放,这样就预先验证了我们编码工作的正确性,验证通过后就可以放心的进行解码工作了。

4.4.2 MediaExtractor

上面说过,使用文件的方式验证编码结果其实还有另外一个好处,就是帮我们快速的构建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。

4.4.3 创建解码器

   try {
        mediaCodec =MediaCodec.createDecoderByType(mediaFormat.getString(MediaFormat.KEY_MIME));
   } catch (IOException e) {
        e.printStackTrace();
   }

这里我们使用 MediaCodec 创建了解码器,传入的参数是 mediaFormat 中的 MediaFormat.KEY_MIME;如果对编码那里还有印象的话,MediaCodec 和MediaFormat使用的 MIME_TYPE其实是一样的。

4.4.4 配置解码器

剩下的就简单了,传入mediaFormat就可以了。最后一个参数Flag不是编码不传有效值,所以我们传0。

   mediaCodec.configure(mediaFormat, null, null, 0);

4.4.5 开始解码

解码的实现和编码的实现原理差不多,其实都是将输入数组转成另外一个数组,所以这里就不在贴代码了。但是有一点我们需要关注的是,在编码的时候我们为了符合AAC编码格式给每个AAC音频数据块都加了ADTS头,那么我们解码的时候就需要去掉这个头,因为解码和编码是对称的。同样的我们做其他解码工作时,也应该关注一下,我们收到的编码数据是否是可以直接进行解码的,避免由于数据的错误,造成解码错误。

4.5 总结

这个模块主要是介绍 :Media codec、MediaFormat、BufferInfo、ByteBuffer 、MediaExtractor 的使用,这里的建议是,上面的每个模块都阅读一下常用方法的注释,方法的注释是最好的老师,强过在网上查找资料。当然实验是检验真理的唯一标准,多做实验可以更好的了解音视频编解码的相关功能。

五、音频传输

如果只是简单的录音存储文件,这对大多数的开发者来说都是比较简单的,通过上面的学习我们可以快速的完成这样的功能。根据业务需求确定音频参数后,我们只需要完成AudioRecord、AudioTracker的创建和配置;同样的使用Media codec + MediaForamt + MediaExtractor 完成编解码器的创建于配置,剩下的就是录音编码生成录音文件,然后解码播放录音文件了。

但是我们仍然要思考一个问题,如果我们做实时音频传输,如对讲功能,是否能按照上面的方式来完成。我们不妨先进行一个对比:

数据来源方式有序丢失传输大小限制实时性保证
文件有序不丢失不涉及保证
TCP有序不丢失无限制不保证
UDP无序丢失有限制保证

这里我们主要把关注点放在TCP传输和UDP传输这两个点上就可以了。

5.1 数据乱序

UDP和TCP的一个主要不同就是UDP传输是不保证到达顺序的,即当我们连续的发送音频数据时可能是后面的语句先到达,而前面的语句后到达,举一个例子:

比如说我现在要传:

一 二 三 四

如果使用TCP那么对端收到的也是“一 二 三 四”,但是UDP收到的就可能是:

二 一 四 三

这显然不是我们希望的,这里的例子是为了大家更好的理解这个问题场景。

实际中音频数据一秒钟音频生成的数据块和我们之前设置的录音参数有关,一般来说数据块都在10个左右(也可能更多或者更少),换句话说我们匀速说话时一个字的传输是由几个块组成的,当这些块乱序时,虽然不会像上面例子那样导致传递信息的内容改变,但是会对用户的体验产生影响。这里我们可以回想一下之前说的声波曲线,如果声波曲线乱序之后,本应该平滑变化的曲线就会可能变成前高后低并且不再平滑,这个时候的音频听起来就会感觉很怪异。

其实解决这个问题的方式也比较简单,就是在接收端加一个缓冲队列,然后我们对收到的数据做排序,但是仍然需要注意下面几个点:

  • 当前收到的数据序列号小于正在播放的音频数据块序列号,那么代表这个帧已经没有意义了,需要丢弃。
  • 排序算法的选择,发生乱序的情况可能有两种:一个是当前到达的数据块大于队尾的数据块,那么此时直接放在队尾就可以了;一个是当前到达的数据块小于队尾的数据块,一般来说和队尾的距离不会太大,我们从后往前找位置会更快。所以根据业务场景采取尾插法进行排序,效率更高。
  • 缓存大小的选择,这里建议结合业务,一般来说肯定是缓存队列越大,我们对原音频数据的序列还原就越好,但是同样的,开始播音的时间会延长。所以使用一个合理的缓存队列也是十分有必要的。
  • 数据结构的选择,需要选择线程安全的数据容器,尤其需要关注同时读写造成的问题。如上一个音频数据块播放完毕后,正在移出下一个音频数据块,而此时又接收到新的音频数据块进行插入,这就会出现同时读写,如果是线程不安全的容器如ArrayList就会报错。

5.2 丢包

UDP并非保证达到,音频数据块丢失是非常常见的,而且随着业务的进展是必然发生的。所以这个问题也是我们必须要解决的问题。

结合现在的网络带宽和我们前面编码后的音频数据大小,这里对于音频一般采取的是添加数据冗余的方式来解决丢失的问题。举个简单的例子:
比如说我现在要传递: 1、2、3、4、5 五个数字,我可以再传一个15,即1-5的总和,那么只要总和不丢,1-5这五个数字丢失任意一个我们都能将它还原出来。这里的列子非常的简单,实际的算法比这个要复杂很多,如这里面的冗余数据不能丢,并且有效的数据也只能丢1个,这些都是限制,同样的这些也是FEC算法需要解决的问题。

但是我们可以推断出来的是,冗余越多,我们能将丢失数据还原回来的可能性就越大。显然我们也不能将冗余设置的太大,那么如何设置有效的冗余就是一个我们需要思考的问题。

这里就引入了一个新的概念丢包率,即一段时间内发送的包总数和收到的包总数之差再比上发送的包总数。简单来说就是发送了100个包,对面只收到了80个那么丢包率是20%,此时我们将冗余比例设置的稍微大于丢包率就可以了。

下面给出丢包这块的重点:

  • 了解FEC算法的本质就可以了,即通过增加冗余数据,还原出丢失的数据
  • 冗余越多,还原丢失数据的可能性就越大
  • 冗余数据大小的选择和丢包率有关
  • 丢包率应该是基于本次传输的发送和到达情况计算的
  • 丢包率应该是动态的,所以我们冗余数据大小也应该是动态的

至于FEC前向纠错算法的选择,可以直接移植WebRTC中使用的算法,也可以自己在Github中找现成的,总之结合业务,选择最适合自己的才是重点。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值