3、AudioRecord基础:创建、配置与数据读取

好,咱们进入第三章。这一章我打算把 AudioRecord 的底裤扒干净。

你可能会想:「AudioRecord 不就是 new 一下,然后 startRecording 吗?」

嗯,表面上看确实是这样。但实际项目中,很多人栽就栽在「创建配置」和「数据读取」这两个看似简单的环节上。我见过太多人因为 buffer 大小没设对,导致录音延迟高得离谱;也有人因为格式转换没处理好,音频数据直接变成噪音。

今天咱们就把这些坑一个一个填平。

3.1 AudioRecord 的创建与配置

先看最基础的——怎么创建一个 AudioRecord 实例。

// 最基本的创建方式
int sampleRate = 44100;
int channelConfig = AudioFormat.CHANNEL_IN_MONO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);

AudioRecord audioRecord = new AudioRecord(
    MediaRecorder.AudioSource.MIC,
    sampleRate,
    channelConfig,
    audioFormat,
    bufferSize
);

这段代码看起来简单,但每个参数都有讲究。我一个个说。

3.1.1 音频源的选择

第一个参数是 MediaRecorder.AudioSource。很多人直接用 MIC,但实际项目中,我建议你根据场景选:

音频源 适用场景 个人建议
MIC 通用录音 默认选项,但延迟不是最优
VOICE_RECOGNITION 语音识别 系统会做降噪处理,适合语音输入
UNPROCESSED 低延时场景 我个人做低延时项目时首选,绕过所有后处理
VOICE_COMMUNICATION VoIP通话 系统会优化回声消除,但会增加延迟
我的经验:如果你做的是实时音频处理(比如 K 歌、变声),用 UNPROCESSED。虽然原始数据可能有点「脏」,但延迟最低。降噪什么的,咱们自己算法处理。

3.1.2 采样率与通道配置

采样率这块,我踩过坑。以前有个项目,我设了 48000Hz,结果在某些低端手机上直接创建失败。

为什么会这样?因为不是所有设备都支持所有采样率。安全做法是:

  • 先用 AudioRecord.getMinBufferSize() 检测是否支持
  • 如果返回负数或 ERROR_BAD_VALUE,降级到 44100Hz
  • 再不行就 16000Hz

通道配置方面,我个人习惯用 CHANNEL_IN_MONO。为什么?

  • 双通道数据量翻倍,处理压力大
  • 大部分手机麦克风本身就是单声道
  • 低延时场景下,单声道足够用

3.1.3 BufferSize 的玄机

这个 bufferSize 是重中之重。很多人直接写死 1024 或 4096,这是大忌。

警告:千万不要自己拍脑袋定 buffer 大小!必须用 getMinBufferSize() 获取系统推荐值。

我曾经有个项目,为了「降低延迟」,把 buffer 设得很小。结果呢?录音数据频繁丢失,声音断断续续。后来才发现,buffer 太小会导致底层驱动来不及填充数据,直接丢帧。

那是不是 buffer 越大越好?也不是。buffer 越大,延迟越高。这是个 trade-off:

Buffer 大小 优点 缺点
偏小(接近最小值) 延迟低 容易丢数据,CPU 压力大
偏大(2倍最小值) 数据稳定 延迟高,不适合实时场景

我的做法是:用 getMinBufferSize() 的值,然后乘以 1.5 到 2 倍。既保证稳定性,又不会太延迟。

3.2 读取回调与循环读取

创建完 AudioRecord,接下来就是读数据了。两种方式:回调模式和循环读取模式。

3.2.1 回调模式(OnRecordPositionUpdateListener)

这种方式适合「被动接收」数据的场景。你设置一个监听器,系统有数据了就通知你。

audioRecord.setRecordPositionUpdateListener(new AudioRecord.OnRecordPositionUpdateListener() {
    @Override
    public void onMarkerReached(AudioRecord recorder) {
        // 到达标记位置时触发,一般用不到
    }

    @Override
    public void onPeriodicNotification(AudioRecord recorder) {
        // 周期性通知,可以在这里读数据
        int bytesRead = recorder.read(buffer, 0, buffer.length);
        // 处理数据...
    }
});

// 设置通知周期(必须设置,否则不会回调)
audioRecord.setPositionNotificationPeriod(1024);

说实话,我个人不太喜欢回调模式。原因有二:

  • 回调线程是系统内部的,你没法控制优先级
  • 如果处理慢了,会阻塞底层,导致丢数据

3.2.2 循环读取模式(推荐)

这才是我的主力方式。自己开一个线程,循环读数据。

class AudioCaptureThread extends Thread {
    private volatile boolean isRunning = true;
    private byte[] buffer = new byte[bufferSize];

    @Override
    public void run() {
        android.os.Process.setThreadPriority(
            android.os.Process.THREAD_PRIORITY_URGENT_AUDIO
        );

        while (isRunning) {
            int bytesRead = audioRecord.read(buffer, 0, buffer.length);
            if (bytesRead > 0) {
                // 把数据交给处理队列
                processAudioData(buffer, bytesRead);
            } else if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION) {
                // 录音状态异常,退出
                break;
            }
        }
    }

    public void stopCapture() {
        isRunning = false;
    }
}
关键点:注意线程优先级设置。我设了 THREAD_PRIORITY_URGENT_AUDIO,这样系统会优先调度这个线程,减少读取延迟。

这里有个细节:read() 方法是阻塞的。如果没数据,它会一直等。所以不用担心 while 循环空转消耗 CPU。

3.2.3 读取方式的对比

方式 优点 缺点
回调模式 代码简单,不用自己管理线程 线程不可控,容易丢数据
循环读取 完全可控,适合低延时 需要自己管理线程生命周期
直接读取(非阻塞) 可以配合 Selector 使用 需要轮询,CPU 占用高

我个人强烈推荐循环读取模式。你想想看,低延时场景下,每一毫秒都很关键。把线程控制权握在自己手里,心里踏实。

3.3 音频数据的格式转换

好,数据读出来了。但读出来的是原始 PCM 数据,很多时候需要转换格式。

3.3.1 PCM 数据格式

AudioRecord 读出来的数据,默认是 ENCODING_PCM_16BIT。什么意思?每个采样点用 2 个字节(short)表示,范围是 -32768 到 32767。

但有些场景需要其他格式:

  • WebRTC 处理:需要 16-bit 有符号 short 数组
  • 音频编码(如 AAC):需要 byte 数组
  • 浮点运算:需要 float 数组(归一化到 -1.0 ~ 1.0)

3.3.2 byte[] 转 short[]

这是最常见的转换。因为 read() 返回的是 byte 数组,但很多算法库要 short 数组。

public short[] byteToShort(byte[] audioData, int bytesRead) {
    int sampleCount = bytesRead / 2;
    short[] shortData = new short[sampleCount];

    ByteBuffer.wrap(audioData, 0, bytesRead)
        .order(ByteOrder.nativeOrder())
        .asShortBuffer()
        .get(shortData);

    return shortData;
}
注意字节序:Android 是小端模式(Little Endian)。如果你用 ByteBuffer,一定要设 ByteOrder.nativeOrder()。我见过有人直接强转,结果在高通和联发科设备上表现不一样。

3.3.3 short[] 转 float[]

做音频算法时,浮点运算更方便。比如滤波、FFT 这些。

public float[] shortToFloat(short[] shortData) {
    float[] floatData = new float[shortData.length];
    for (int i = 0; i < shortData.length; i++) {
        floatData[i] = shortData[i] / 32768.0f;
    }
    return floatData;
}

这里有个小技巧:除以 32768.0f 而不是 32767.0f。为什么?因为 short 范围是 -32768 到 32767,除以 32768 可以保证对称性,不会出现 -1.0 到 0.9999 这种不对称的情况。

3.3.4 采样率转换

这个比较复杂。比如你采集的是 44100Hz,但编码器要 16000Hz。怎么办?

我的建议是:不要自己写重采样算法。直接用 Android 的 AudioRecord 配合 AudioTrack 做重采样,或者用 MediaCodec 的格式转换能力。

如果非要自己转,可以用线性插值:

public short[] resample(short[] input, int inputSampleRate, int outputSampleRate) {
    double ratio = (double) outputSampleRate / inputSampleRate;
    int outputLength = (int) (input.length * ratio);
    short[] output = new short[outputLength];

    for (int i = 0; i < outputLength; i++) {
        double srcIndex = i / ratio;
        int srcIndexInt = (int) srcIndex;
        double frac = srcIndex - srcIndexInt;

        if (srcIndexInt + 1 < input.length) {
            output[i] = (short) (input[srcIndexInt] * (1 - frac) 
                + input[srcIndexInt + 1] * frac);
        } else {
            output[i] = input[srcIndexInt];
        }
    }
    return output;
}
注意:这个线性插值只是演示用。实际项目中,建议用 AudioRecord 直接设置目标采样率,或者用 MediaExtractor + MediaCodec 做专业重采样。自己写的重采样质量很难保证。

3.4 知识体系总览

说了这么多,咱们用一张图把 AudioRecord 的核心知识串起来:

AudioRecord 核心知识体系 创建与配置 • 音频源选择 • 采样率设置 • 通道配置 • BufferSize 计算 getMinBufferSize() UNPROCESSED 优先 单声道为主 数据读取 • 回调模式 • 循环读取(推荐) • 线程优先级设置 • 阻塞 vs 非阻塞 THREAD_PRIORITY_URGENT_AUDIO while 循环 + read() 错误状态处理 格式转换 • byte[] → short[] • short[] → float[] • 采样率转换 • 字节序处理 ByteOrder.nativeOrder() 除以 32768.0f 线性插值(演示) 核心原则:配置要稳、读取要快、转换要准

这张图把咱们今天讲的内容串起来了。左边是创建配置,中间是数据读取,右边是格式转换。三个环节环环相扣,任何一个出问题,整个音频链路都会受影响。

3.5 避坑总结

最后,我把这些年踩过的坑总结一下:

  1. 不要硬编码 buffer 大小——用 getMinBufferSize() 获取
  2. 不要在主线程读数据——开独立线程,设高优先级
  3. 不要忽略字节序——Android 是小端,转换时注意
  4. 不要自己写重采样——用系统 API 或专业库
  5. 不要用 MIC 做低延时——用 UNPROCESSED

嗯,这一章就到这儿。AudioRecord 的基础操作,说白了就是「创建 - 读取 - 转换」这三板斧。但每一板斧都有讲究,细节决定成败。

下一章咱们聊 AudioTrack,把音频输出的坑也填一填。


公众号:蓝海资料掘金营,微信deep3321