音频基础知识和录制

写作计划

学习入门 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 这两种格式的文件进行保存

参考资料

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容

  • 前言 说到视频,大家自己脑子里基本都会想起电影、电视剧、在线视频等等,也会想起一些视频格式 AVI、MP4、RMV...
    ForestSen阅读 22,423评论 10 202
  • 前些日子由于项目需要,一直在研究iOS CoreAudio相关的内容.在这里记录一些笔记.现实生活中,我们听到的声...
    brownfeng阅读 4,425评论 4 14
  • [TOC] 音视频&流媒体 是什么促使我要写这一篇音视频入门文章?那是因为和一妹子打赌码率的概念,结果输了;对一个...
    AllenWu阅读 4,627评论 1 25
  • 视频 视频实质:纯粹的视频(不包括音频)实质上就是一组帧图片,经过视频编码成为视频(video)文件再把音频(au...
    勇敢的_心_阅读 2,787评论 1 30
  • 世界以痛吻我 青春是段跌跌撞撞的旅行,拥有着后知后觉的美丽。人不轻狂枉少年,在这人...
    Nichkhun_872d阅读 148评论 0 0