Android开发Android开发经验谈Android音视频学习

音频基础知识和录制

2018-07-14  本文已影响14人  kamlin

写作计划

学习入门 Android 平台下的音视频技术, 我打算按照下图所示的步骤去学习:

从上图可以看出, 学习的步骤大概有三大部分(可以穿插学习):

  1. 音视频的录制
  2. 音视频在网络传输的流媒体协议
  3. 音视频的播放

音视频的录制

音频和视频的录制流程为:音频和视频录制 -> 将录制得到的音频和视频数据进行编码 -> 将编码后的音频和视频数据按照一定的格式进行合成得到视频文件。

流媒体协议

将得到的视频文件根据一定的协议进行网络传输,不同的协议适用不同的场景。

音视频的播放

从网络得到一个视频文件后, 我们需要处理的流程有:将视频文件按照一定的格式分离出其中的音频和视频 -> 对音频和视频进行解码 -> 音视频同步播放。

作为本专栏的开篇,我们先学习音频的基础知识和 Android 平台下音频的录制。

音频基础知识

自然界的声音经过麦克风采集后,可以得到模拟信号。接着,我们可以编写程序采集麦克风发出的模拟信号, 最后得到数字信号。在这个过程中,含有三种信号的转变:

关于声信号到模拟信号的转换,我们一般无需关心,手机的麦克风都帮我们转换好了。 我们只需关心的是从麦克风得到的模拟信号,程序如何去采集得到数字信号,最后保存为音频文件。

模拟信号到数字信号的转换


模拟信号一般通过脉冲编码调制(Pulse Code Modulation,PCM)方法转换为数字信号,这种方法包含三个步骤:

  1. 采样:模拟信号本身是一种连续信号,它在一定的时间范围内可以有无限多个不同的取值。而数字信号是指在取值上是离散的、不连续的信号。所谓的采样,就是将一段时间内的连续信号转为离散信号。在上图中,按照一定的频率, 对连续的模拟信号进行采集,然后记录下来。根据采样定理,按比声音最高频率的二倍进行采样,声音就能被完整地恢复。由于人耳能听到的频率范围是在 20~20KHz,所以采样率一般为 44.1kHz,这样才能保证声音达到 20KHz 时,也能够被完整地恢复。

  2. 量化:指采样得到后的数据,我们用多少位的二进制数字来表示声音的振幅。例如使用 16 比特的二进制信号来表示一个声音的采样,而 16bit 能表示的范围是: [-32768, 32767],一共有 65536 个取值。

  3. 编码:将采样量化后的数据按照一定的格式进行记录,比如顺序储存或者压缩储存。

音频开发中的重要参数

采样率

将模拟信号转为数字信号时,需要隔一定的时间对模拟信号进行一个采样,然后将这个采样用 01 来表示,也就是数字化的过程。 采样率表示,1S 内,对模拟信号进行多少次采样。采样频率越高,说明采样点之间越密集,记录这段音频所用的数据量就越大, 因此音质也就越好。

为了保证声音不失效,我们采样率通常设置为 44100Hz。常用的音频采样频率有:8kHz、11.025kHz、22.05kHz、16kHz、37.8kHz、44.1kHz、48kHz、96kHz、192kHz 等。

量化精度

对于一个采样点,需要用二进制数字来表示,这个二进制的精度可以是:4bit、8bit、16bit、32bit。 位数越多,表示的声音就越精细,声音的质量就越好。不过数据量也会变大。常见的位宽:8bit,16bit。

声道数

声道数一般表示声音录制时的音源数量或回放时相应的扬声器数量。常用的有:单通道和双通道。

帧间隔

音频不像视频那样,有一帧一帧的概念。它是约定一个时间为单位,然后这个时间内的数据为一帧,这个时间被称为采样时间。这个时间没有特别的标准,要看具体的编解码器。

计算一帧音频的大小

假设某通道的音频信号是采样率为8kHz,位宽为16bit,20ms一帧,双通道,则一帧音频数据的大小为:

int size = 8000 x 16bit x 0.02s  x 2 = 5120 bit = 640 byte

Android 音频录制

了解音频的基础知识后,我们可以开始来熟悉 Android 平台下音频录制的 API 了。Android 提供了两套音频采集的 API,分别是:

  1. MediaRecorder:比较上层的 API,它可以直接把手机麦克风的音频数据进行编码然后储存成文件。使用简单,但是支持的格式有限,并且不支持对音频进行进一步的处理,例如变声、混音等。
  2. AudioRecord:比较底层的一个 API,能够得到原始的 PCM音频数据。由于我们得到的是原始的 PCM 数据,我们可以对音频进行进一步的处理,例如编码、混音和变声等。

关于 MediaRecorder 的使用比较简单,这里不做介绍。接下来,主要介绍 AudioRecord 的使用套路。

AudioRecord 工作流程

AudioRecord的使用套路大体可以分为以下四个步骤:

  1. 根据配置参数,初始化音频内部的缓冲区
  2. 开始采集原始的音频数据
  3. 开辟一个 worker 线程, 不断地从 AudioRecord 中的缓冲区将音频数据读出来
  4. 停止采集,及时释放资源

根据配置参数,初始化音频内部的缓冲区

//默认采样率,44100Hz 可以保证兼容所有 Android 手机的采样率。
private static final int DEFAULT_SAMPLE_RATE = 44100; 

//通道数:单通道,AudioFormat.CHANNEL_IN_STEREO 表示双通道
private static final int DEFAULT_CHANNEL = AudioFormat.CHANNEL_IN_MONO;
 
//16 位量化位宽
private static final int SIMPLE_FORMAT = AudioFormat.ENCODING_PCM_16BIT; 

//声音从麦克风采集而来,可选的值以常量的形式定义在 MediaRecorder.AudioSource 类中,常用的值包括:DEFAULT(默认),
//VOICE_RECOGNITION(用于语音识别,等同于DEFAULT),MIC(由手机麦克风输入),
//VOICE_COMMUNICATION(用于VoIP应用)等等。
private static final int DEFAULT_SOURCE_MIC = MediaRecorder.AudioSource.MIC; 
private AudioRecord createAudioRecord(int audioSource, int simpleRate, int channels, int audioFormat) {
    //获取一帧音频帧的大小
     int minBufferSize = AudioRecord.getMinBufferSize(simpleRate, channels, audioFormat); 
     if (minBufferSize == AudioRecord.ERROR_BAD_VALUE) {
         Log.d(TAG, "获取音频帧大小失败!");
         return null;
     }
    int audioRecordBufferSize = minBufferSize * 4; //AudioRecord内部缓冲设置为4帧音频帧的大小
    AudioRecord audioRecord = new AudioRecord(audioSource, simpleRate, 
          channels, audioFormat, audioRecordBufferSize);
    if (audioRecord.getState() == AudioRecord.STATE_UNINITIALIZED) {
        Log.d(TAG, "初始化AudioRecord失败!");
        return null;
    }
    return audioRecord;
}

初始化AudioRecord时,我们需要先确定一帧音频占用的大小,Android提供了AudioRecord.getMinBufferSize 函数给我们确定。不建议手动算音频帧的大小。

audioRecordBufferSize 配置的是 AudioRecord 内部的音频缓冲区的大小,该缓冲区的值不能低于一帧音频帧的大小,我这里配置为4帧的大小。

开始采集原始的音频数据

 mAudioRecord.startRecording(); //开始采集数据

开辟一个 worker 线程,不断地从 AudioRecord 中的缓冲区将音频数据读出来

当我们调用 AudioRecordstartRecording() 方法后,AudioRecord就会开始帮我们采集数据,然后存放在内部的缓冲区,等待客户端去拿走数据。这时候,我们应该开辟一个 worker 线程,不断从 AudioRecord 内部缓冲区将音频数据读出来。来个比较生动的图:

拿到后的PCM数据,我们一般通过接口回调给外部使用。外部使用者可以使用 AudioTrick 进行实时播放,或者进行再一次编码,然后保存成一个音频文件。

private OnAudioCaptureListener listener;

public interface OnAudioCaptureListener {
    void onAudioFrameCaptured(byte[] audioData);
}

public void setOnAudioCaptureListener(OnAudioCaptureListener listener) {
    this.listener = listener;
}

private class AudioCaptureRunnable implements Runnable {

    @Override
    public void run() {
        while (!isExit) {
            byte[] buffer = new byte[1024 * 2]; //每次拿2k
            int result = mAudioRecord.read(buffer, 0, buffer.length);
            if (result == AudioRecord.ERROR_BAD_VALUE) {
                Log.d(TAG, "run: ERROR_BAD_VALUE");
            } else if (result == AudioRecord.ERROR_INVALID_OPERATION) {
                Log.d(TAG, "run: ERROR_INVALID_OPERATION");
            } else {
                if (listener != null) {
                    Log.d(TAG, "run: capture buffer length is " + result);
                    listener.onAudioFrameCaptured(buffer);
                }
            }
       }
   }
}

停止采集, 及时释放资源

当我们录制完时,需要停止采集然后及时释放掉 native 层的资源。

    public boolean stop() {
        if (!isStart) {
            return false;
        }

        isExit = true;

        try {
            captureThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        if (mAudioRecord.getState() == AudioRecord.RECORDSTATE_RECORDING) {
            mAudioRecord.stop(); // 不是必须调用的, 因为release内部也会调用
        }
        mAudioRecord.release(); //释放掉资源
        mAudioRecord = null;
        isStart = false;
        Log.d(TAG, "stop: stop successfully");
        return true;
    }

最后,附上完整代码地址。欢迎 startfollow

总结

这篇文章主要总结了自己学习音视频的一个大体计划、音频的一些基础知识和 Android 平台下的 AudioRecord 的使用姿势。

下一步

  1. 使用 AudioTrick 来播放采集得到的 PCM 数据

  2. 将采集得到的 PCM 数据编码成 AACWAV 这两种格式的文件进行保存

参考资料

由于本人能力水平限制,文章中难免会有错误。如果大家发现文章有不足之处,欢迎指出。

上一篇下一篇

猜你喜欢

热点阅读