前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >音视频基础能力之 Android 音频篇 (一): 音频采集

音视频基础能力之 Android 音频篇 (一): 音频采集

原创
作者头像
声知视界
修改2024-11-19 10:14:41
修改2024-11-19 10:14:41
2550
举报
文章被收录于专栏:音频音频

一、概述

AudioRecord 是 Android 平台比较重要的类,也是 Java 接口中比较偏底层(平台)的接口,可以通过它从平台的音频输入硬件来获取原始音频 PCM 数据。它的工作原理是要需要通过应用侧轮询调用 read 接口来驱动,每调用一次,系统就会从硬件采集到的数据填充一次,至于传递数据的载体可以是 byte[] 数组 或者 ByteBuffer 。

二、所需权限

应用程序创建 AudioRecord 实例需要在AndroidManifest文件赋予 Manifest.permission.RECORD_AUDIO 权限。没有赋予这个权限,如果使用 AudioRecord.Builder 来构建的话,执行build()函数会抛出 UnsupportedOperationException 异常,即使您捕获了此异常,在获取 AudioRecord 状态(https://developer.android.google.cn/reference/android/media/AudioRecord?hl=en#getState(%29) 的时候,也是未初始化状态(STATE_UNINITIALIZED)。

代码语言:java
复制
<uses-permission android:name="android.permission.RECORD_AUDIO"/>

在 API 23 (Android 6.0) 之后,为了保护用户隐私,对于一些敏感权限(比如录音权限),应用需要在运行时动态申请。示例代码如下:

代码语言:java
复制
private static final int PERMISSION_REQUEST_CODE = 1;

// step1: 检查是否有录音权限
private boolean checkPermission() {
    int result = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO);
    return result == PackageManager.PERMISSION_GRANTED;
}

// step2: 请求录音权限
private void requestPermission() {
    ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, PERMISSION_REQUEST_CODE);
}

// step3: 处理权限请求结果
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    if (requestCode == PERMISSION_REQUEST_CODE) {
        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // 权限被授予,可以进行录音
            startRecording(); 
        } else {
            // 权限被拒绝,无法进行录音
            Toast.makeText(this, "录音权限被拒绝", Toast.LENGTH_SHORT).show();
        }
    }
}

三、初始化工作

3.1 确定硬件 buffer size

上文提及到,在创建 AudioRecord 对象的时候会初始化与其关联的 buffer , buffer size 是在构造它的时候传递过去的。所以第一步要做的事情就是确定 buffer size ,一般来说,设置硬件 buffer size 最大等于每一帧音频帧的 size ,这样处理起来比较方便。

$音频帧大小 = 通道数 每个采样点所占字节数 采样率 / 帧长$

举例说明:使用场景为双通道,每个采样点需要16bit,采样率48000,帧长(采样时间): 100ms,那么最终一帧数据的大小为:

代码语言:java
复制
//帧大小
final int bytesPerFrame = 2 * (16/8) * 48000 / 100; // 1920

//创建buffer
ByteBuffer buffer = ByteBuffer.allocateDirect(bytesPerFrame);

还有一个重要的步骤,就是获取硬件所支持的最小缓冲区大小 minHwBufferSize。因为我们想要设置缓冲区大小 ≥ 实际场景需要的音频帧大小,那如果音频帧大小要小于 minHwBufferSize 呢,所以我们需要做一些处理。如以下代码:

代码语言:java
复制
//获取硬件所支持的最小缓冲区大小
int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT);

if(minBufferSize < buffer.capacity()){
	minBufferSize = buffer.capacity();
}

//这样设置采集和处理音频数据比较平滑
int bufferSizeInBytes = Math.max(BUFFER_SIZE_FACTOR * minBufferSize, byteBuffer.capacity()); // BUFFER_SIZE_FACTOR 一般为2

3.2 构建 AudioRecord 对象

一共有两种方式可以构建出 AudioRecord 对象,分别是通过构造方法和构造者模式。

构造方法

代码语言:java
复制
// 1. 通过构造方法创建
AudioRecord audioRecord = null;
try {
	audioRecord = new AudioRecord(AudioSource.VOICE_COMMUNICATION, 48000, AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT, bufferSizeInBytes);
} catch (IllegalArgumentException e) {
	e.printStackTrace();
}

if(audioRecord == null || audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
	//构造失败	
}

构建者模式

代码语言:java
复制
//2. 通过构造者模式创建
AudioRecord audioRecord = null;
try {
	recorder = new AudioRecord.Builder()
	  .setAudioSource(AudioSource.VOICE_COMMUNICATION)
	  .setAudioFormat(new AudioFormat.Builder()
	    .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
	    .setSampleRate(48000)
	    .setChannelMask(AudioFormat.CHANNEL_IN_STEREO)
	    .build())
	  .setBufferSizeInBytes(bufferSizeInBytes)
	  .build();
} catch (UnsupportedOperationException e) {
	e.printStackTrace();	
}

if(audioRecord == null || audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
	//构造失败	
}

我们需要关注的是构建 AudioRecord 所需要的必要参数。

audioSource

音频源,定义了音频信号的默认输入设备和采集配置。具体常量见 AudioSource 这个类。

sampleRateInHz

采样率(单位:赫兹Hz),通常设置 44100Hz 可确保能够在几乎所有设备上都能正常工作,像其他的 22050Hz, 16000Hz, 11025Hz 也许只能在部分的设备上工作。

channelConfig

描述音频通道的配置,一般配置单通道(AudioFormat.CHANNEL_IN_MONO) 和双声道 (AudioFormat.CHANNEL_IN_STEROR)。

audioFormat

音频格式,这里表示采集音频数据的精度,参数可选:AudioFormat.ENCODING_PCM_8BIT,AudioFormat.ENCODING_PCM_16BIT,AudioFormat.ENCODING_PCM_FLOAT。

bufferSizeInBytes

缓存 buffer 的大小(单位: 字节),上文 3.1 章节已提及过。

四、采集音频数据

  • 开启音频采集

通过调用 startRecording 接口来控制硬件开启采集状态,可以通过通过 AudioRecord 对象内部的 recordingState 状态来判断是否开启成功。

代码语言:kotlin
复制
var result = ErrorCode.SV_NO_ERROR
try {
  audioRecord?.startRecording()
} catch (err: IllegalStateException) {
  err.printStackTrace()
  result = ErrorCode.SV_START_ERROR
}

if(audioRecord!!.recordingState != AudioRecord.RECORDSTATE_RECORDING) {
  //通过 recordingState 状态来判断是否开启成功
}
  • 创建采集线程
代码语言:kotlin
复制
captureThread = AudioCaptureThread()
captureThread?.start() ?: { result = ErrorCode.SV_START_ERROR }
  • 读取音频数据

其实 read 函数这里有个细节的地方,来看看 read 函数的定义:

代码语言:kotlin
复制
public int read(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes);

public int read(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes) {
    return read(audioData, offsetInBytes, sizeInBytes, READ_BLOCKING);
}

@IntDef({
    READ_BLOCKING,
    READ_NON_BLOCKING
})
@Retention(RetentionPolicy.SOURCE)
public @interface ReadMode {}
public int read(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes, @ReadMode int readMode)

如果不带参数 readMode 的情况,默认走的是阻塞模式,调用 read 会一直等待系统那边填充结束后才返回给你。所以走阻塞的情况一般需要创建一条采集线程,以防止阻塞住调用线程。具体怎么选择?一般来说,如果采集的 buffer size 比较小,则选择阻塞模式。buffer size 比较大,可以选择非阻塞,因为非阻塞模式,系统并没有提供填充结束的回调通知,还需要开发者自行的计算采集间隔时间。

代码语言:kotlin
复制
while (keepAlive) {
    val readSize = audioRecord!!.read(byteBuffer, byteBuffer.capacity())
    if(readSize == byteBuffer.capacity()) {
        val data = Arrays.copyOf(byteBuffer.array(), byteBuffer.capacity())
        fos?.write(data)
    } else {
        // read failed.
    }
}

五、停止采集

  • 停止采集,停止采集线程,将 keepAlive 变量置为 false 即可。
  • 调用 stop 接口,关闭硬件采集。
代码语言:kotlin
复制
    runCatching {
        audioRecord?.stop()
    }

六、其他接口

上文主要讲述了使用 AudioRecord 接口的整体调用姿势,下面来讲讲 AudioRecord 的几个平时开发很少用,却很实用的接口。

6.1 分段录制

相关配套的接口如下,其中 markerPosition 和 period 区别在于是否需要周期性回调。这类接口的使用场景一般:分段录制,采集进度回调显示等。

代码语言:kotlin
复制
// 设置标记帧,采集了 markerInFrames 帧数就回调 OnRecordPositionUpdateListener#onMarkerReached
public int setNotificationMarkerPosition(int markerInFrames)

// 设置一个采集周期,采集到的音频帧等于 periodInFrames 就触发回调 OnRecordPositionUpdateListener#onPeriodicNotification
public int setPositionNotificationPeriod(int periodInFrames);

public interface OnRecordPositionUpdateListener  {
    /**
     * Called on the listener to notify it that the previously set marker has been reached
     * by the recording head.
     */
    void onMarkerReached(AudioRecord recorder);

    /**
     * Called on the listener to periodically notify it that the record head has reached
     * a multiple of the notification period.
     */
    void onPeriodicNotification(AudioRecord recorder);
}

6.2 音频路由相关

设置音频路由偏好

代码语言:kotlin
复制
// 指定采集的音频输入设备偏好
public boolean setPreferredDevice(AudioDeviceInfo deviceInfo);

监听音频路由

代码语言:kotlin
复制
// 添加音频路由监听
public void addOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener, android.os.Handler handler);
// 取消音频路由监听
public void removeOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener);

public interface OnRoutingChangedListener {
    public void onRoutingChanged(AudioRouting router);
}

最后

本人比较详细的讲解了 AudioRecord 的运行机制以及使用方面的介绍,下面是 github 上的示例代码链接。

https://github.com/Sound-Vision/audio_record

如果您觉得以上内容对您有所帮助的话,可以关注下我们运营的公众号 声知视界 ,会定期的推送 音视频技术、移动端技术 为主轴的 科普类、基础知识类、行业资讯类等相关文章。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、概述
  • 二、所需权限
  • 三、初始化工作
    • 3.1 确定硬件 buffer size
    • 3.2 构建 AudioRecord 对象
  • 四、采集音频数据
  • 五、停止采集
  • 六、其他接口
    • 6.1 分段录制
    • 6.2 音频路由相关
  • 最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档