文档章节

Android音频开发(2):如何采集一帧音频

乐搏学院
 乐搏学院
发布于 2016/10/18 15:34
字数 1736
阅读 11
收藏 1

本文重点关注如何在Android平台上采集一帧音频数据。阅读本文之前,建议先读一下我的上一篇文章《Android音频开发(1):基础知识》,因为音频开发过程中,经常要涉及到这些基础知识,掌握了这些重要的概念后,开发过程中的很多参数和流程就会更加容易理解。

 

Android SDK 提供了两套音频采集的API,分别是:MediaRecorder 和 AudioRecord,前者是一个更加上层一点的API,它可以直接把手机麦克风录入的音频数据进行编码压缩(如AMR、MP3等)并存成文件,而后者则更接近底层,能够更加自由灵活地控制,可以得到原始的一帧帧PCM音频数据。

 

如果想简单地做一个录音机,录制成音频文件,则推荐使用 MediaRecorder,而如果需要对音频做进一步的算法处理、或者采用第三方的编码库进行压缩、以及网络传输等应用,则建议使用 AudioRecord,其实 MediaRecorder 底层也是调用了 AudioRecord 与 Android Framework 层的 AudioFlinger 进行交互的。

 

音频的开发,更广泛地应用不仅仅局限于本地录音,因此,我们需要重点掌握如何利用更加底层的 AudioRecord API 来采集音频数据(注意,使用它采集到的音频数据是原始的PCM格式,想压缩为mp3,aac等格式的话,还需要专门调用编码器进行编码)。

 

1. AudioRecord 的工作流程

 

首先,我们了解一下 AudioRecord 的工作流程:

 

(1) 配置参数,初始化内部的音频缓冲区

(2) 开始采集

(3) 需要一个线程,不断地从 AudioRecord 的缓冲区将音频数据“读”出来,注意,这个过程一定要及时,否则就会出现“overrun”的错误,该错误在音频开发中比较常见,意味着应用层没有及时地“取走”音频数据,导致内部的音频缓冲区溢出。

(4) 停止采集,释放资源

 

2. AudioRecord 的参数配置

 

wKioL1bhXMew-y-lAAFNssMMHH8488.png

 

上面是 AudioRecord 的构造函数,我们可以发现,它主要是靠构造函数来配置采集参数的,下面我们来一一解释这些参数的含义(建议对照着我的上一篇文章来理解):

 

(1) audioSource

 

该参数指的是音频采集的输入源,可选的值以常量的形式定义在 MediaRecorder.AudioSource 类中,常用的值包括:DEFAULT(默认),VOICE_RECOGNITION(用于语音识别,等同于DEFAULT),MIC(由手机麦克风输入),VOICE_COMMUNICATION(用于VoIP应用)等等。

 

(2) sampleRateInHz

 

采样率,注意,目前44100Hz是唯一可以保证兼容所有Android手机的采样率。

 

(3) channelConfig

 

通道数的配置,可选的值以常量的形式定义在 AudioFormat 类中,常用的是 CHANNEL_IN_MONO(单通道),CHANNEL_IN_STEREO(双通道)

 

(4) audioFormat

 

这个参数是用来配置“数据位宽”的,可选的值也是以常量的形式定义在 AudioFormat 类中,常用的是 ENCODING_PCM_16BIT(16bit),ENCODING_PCM_8BIT(8bit),注意,前者是可以保证兼容所有Android手机的。

 

(5) bufferSizeInBytes

 

这个是最难理解又最重要的一个参数,它配置的是 AudioRecord 内部的音频缓冲区的大小,该缓冲区的值不能低于一帧“音频帧”(Frame)的大小,而前一篇文章介绍过,一帧音频帧的大小计算如下:

 

int size = 采样率 x 位宽 x 采样时间 x 通道数

 

采样时间一般取 2.5ms~120ms 之间,由厂商或者具体的应用决定,我们其实可以推断,每一帧的采样时间取得越短,产生的延时就应该会越小,当然,碎片化的数据也就会越多。

 

在Android开发中,AudioRecord 类提供了一个帮助你确定这个 bufferSizeInBytes 的函数,原型如下:

 

int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat);

 

不同的厂商的底层实现是不一样的,但无外乎就是根据上面的计算公式得到一帧的大小,音频缓冲区的大小则必须是一帧大小的2~N倍,有兴趣的朋友可以继续深入源码探究探究。

 

实际开发中,强烈建议由该函数计算出需要传入的 bufferSizeInBytes,而不是自己手动计算。

 

3. 音频的采集线程

 

当创建好了 AudioRecord 对象之后,就可以开始进行音频数据的采集了,通过下面两个函数控制采集的开始/停止:

 

AudioRecord.startRecording();

AudioRecord.stop();

 

一旦开始采集,必须通过线程循环尽快取走音频,否则系统会出现 overrun,调用的读取数据的接口是:

 

AudioRecord.read(byte[] audioData, int offsetInBytes, int sizeInBytes);

 

4. 示例代码

 

我将 AudioRecord 类的接口简单封装了一下,提供了一个 AudioCapturer 类,可以到我的Github下载:https://github.com/Jhuster/Android/blob/master/Audio/AudioCapturer.java

 

这里也贴出来一份:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

/*

 *  COPYRIGHT NOTICE  

 *  Copyright (C) 2016, Jhuster <lujun.hust@gmail.com>

 *  https://github.com/Jhuster/Android

 *   

 *  @license under the Apache License, Version 2.0 

 *

 *  @file    AudioCapturer.java

 *  

 *  @version 1.0     

 *  @author  Jhuster

 *  @date    2016/03/10    

 */

import android.media.AudioFormat;

import android.media.AudioRecord;

import android.media.MediaRecorder;

import android.util.Log;

 

public class AudioCapturer {

 

    private static final String TAG = "AudioCapturer";

     

    private static final int DEFAULT_SOURCE = MediaRecorder.AudioSource.MIC;

    private static final int DEFAULT_SAMPLE_RATE = 44100;

    private static final int DEFAULT_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;

    private static final int DEFAULT_AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;

 

    private AudioRecord mAudioRecord;

    private int mMinBufferSize = 0;

     

    private Thread mCaptureThread;  

    private boolean mIsCaptureStarted = false;

    private volatile boolean mIsLoopExit = false;

 

    private OnAudioFrameCapturedListener mAudioFrameCapturedListener;

 

    public interface OnAudioFrameCapturedListener {

        public void onAudioFrameCaptured(byte[] audioData);

    }  

 

    public boolean isCaptureStarted() {     

        return mIsCaptureStarted;

    }

 

    public void setOnAudioFrameCapturedListener(OnAudioFrameCapturedListener listener) {

        mAudioFrameCapturedListener = listener;

    }

 

    public boolean startCapture() {

        return startCapture(DEFAULT_SOURCE, DEFAULT_SAMPLE_RATE, DEFAULT_CHANNEL_CONFIG,

            DEFAULT_AUDIO_FORMAT);

    }

 

    public boolean startCapture(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat) {

 

        if (mIsCaptureStarted) {

            Log.e(TAG, "Capture already started !");

            return false;

        }

     

        mMinBufferSize = AudioRecord.getMinBufferSize(sampleRateInHz,channelConfig,audioFormat);

        if (mMinBufferSize == AudioRecord.ERROR_BAD_VALUE) {

            Log.e(TAG, "Invalid parameter !");

            return false;

        }

        Log.d(TAG , "getMinBufferSize = "+mMinBufferSize+" bytes !");

         

        mAudioRecord = new AudioRecord(audioSource,sampleRateInHz,channelConfig,audioFormat,mMinBufferSize);            

        if (mAudioRecord.getState() == AudioRecord.STATE_UNINITIALIZED) {

            Log.e(TAG, "AudioRecord initialize fail !");

        return false;

        }      

 

        mAudioRecord.startRecording();

 

        mIsLoopExit = false;

        mCaptureThread = new Thread(new AudioCaptureRunnable());

        mCaptureThread.start();

 

        mIsCaptureStarted = true;

 

        Log.d(TAG, "Start audio capture success !");

 

        return true;

    }

 

    public void stopCapture() {

 

        if (!mIsCaptureStarted) {

            return;

        }

 

        mIsLoopExit = true;      

        try {

            mCaptureThread.interrupt();

            mCaptureThread.join(1000);

        

        catch (InterruptedException e) {    

            e.printStackTrace();

        }

 

        if (mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {

            mAudioRecord.stop();                       

        }

 

        mAudioRecord.release();    

     

        mIsCaptureStarted = false;

        mAudioFrameCapturedListener = null;

 

        Log.d(TAG, "Stop audio capture success !");

    }

 

    private class AudioCaptureRunnable implements Runnable {      

     

        @Override

        public void run() {

 

            while (!mIsLoopExit) {

 

                byte[] buffer = new byte[mMinBufferSize];

 

                int ret = mAudioRecord.read(buffer, 0, mMinBufferSize);               

                if (ret == AudioRecord.ERROR_INVALID_OPERATION) {

                    Log.e(TAG , "Error ERROR_INVALID_OPERATION");

                

                else if (ret == AudioRecord.ERROR_BAD_VALUE) {

                    Log.e(TAG , "Error ERROR_BAD_VALUE");

                

                else 

                    if (mAudioFrameCapturedListener != null) {

                        mAudioFrameCapturedListener.onAudioFrameCaptured(buffer);

                    }   

                    Log.d(TAG , "OK, Captured "+ret+" bytes !");

                }                                                      

            }      

        }    

    }

}

 

使用前要注意,添加如下权限:

 

<uses-permission android:name="android.permission.RECORD_AUDIO" />

结束语

免费学习更多精品课程,登录乐搏学院官网http://h.learnbo.cn/

原创本文出自 “Jhuster的专栏” 博客,请务必保留此出处http://ticktick.blog.51cto.com/823160/1749719

本文转载自:

乐搏学院
粉丝 9
博文 526
码字总数 707467
作品 0
丰台
程序员
私信 提问
Android音视频之使用MediaCodec编解码AAC

现在的短视频、音视频通话都离不开编码和解码,今天就来聊一下音频的编解码。 1. 音频的基本概念 在音频开发中,有些基本概念是需要了解的。 采样率(SampleRate):每秒采集声音的数量,它用...

落英坠露
05/03
0
0
直播,音视频编码器和解码器(EasyDarwin)-Android

使用摄像头采集视频数据,并通过MediaCodec进行H264编码,之后打包成RTSP格式并上传的。 TextuewView也提供了一个setTransform方法,该方法接收一个matrix参数,使用该参数对当前的渲染内容进...

shareus
2018/05/18
0
0
《视频直播技术详解》系列之一:视频采集和处理

直播中的各个环节: 1.采集 采集是播放环节中的第一环,iOS 系统因为软硬件种类不多,硬件适配性较好,所以比较简单。Android 则不同,市面上硬件机型非常多,难以做到一个库适配所有硬件。P...

ljianbing
2018/06/26
0
0
Android AudioTrack播放(解码)音频

-- MediaPlayer,AudioTrack 1.MediaPlayer能够播放多种格式的声音文件,比如MP3,AAC,WAV,OGG,MIDI等。MediaPlayer包括了AudioTrack。 2.AudioTrack仅仅能播放已经解码的PCM流,假设是文...

desaco
2018/12/26
0
0
Android手机直播系统开发介绍

近两年直播热的兴起也带动了直播开发行业的崛起,每个人都想要去吃直播开发这块蛋糕。但这块蛋糕也不是这么容易吃到的,在激烈的市场竞争下,有几个大的直播平台始终占据着市场中较大的份额,...

直播开发
2018/09/07
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Mybatis Plus删除

/** @author beth @data 2019-10-17 00:30 */ @RunWith(SpringRunner.class) @SpringBootTest public class DeleteTest { @Autowired private UserInfoMapper userInfoMapper; /** 根据id删除......

一个yuanbeth
今天
4
0
总结

一、设计模式 简单工厂:一个简单而且比较杂的工厂,可以创建任何对象给你 复杂工厂:先创建一种基础类型的工厂接口,然后各自集成实现这个接口,但是每个工厂都是这个基础类的扩展分类,spr...

BobwithB
今天
5
0
java内存模型

前言 Java作为一种面向对象的,跨平台语言,其对象、内存等一直是比较难的知识点。而且很多概念的名称看起来又那么相似,很多人会傻傻分不清楚。比如本文我们要讨论的JVM内存结构、Java内存模...

ls_cherish
今天
4
0
友元函数强制转换

友元函数强制转换 p522

天王盖地虎626
昨天
5
0
js中实现页面跳转(返回前一页、后一页)

本文转载于:专业的前端网站➸js中实现页面跳转(返回前一页、后一页) 一:JS 重载页面,本地刷新,返回上一页 复制代码代码如下: <a href="javascript:history.go(-1)">返回上一页</a> <a h...

前端老手
昨天
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部