AudioRecord音频录制流程深度解析

上一篇我们追了音频的"顺流"------数据从 App 写入 Ring Buffer,由 AudioFlinger 消费输出。这一篇我们转换视角,看音频的"逆流":AudioRecord

如果说 AudioTrack 是"喇叭",AudioRecord 就是"麦克风"。数据方向完全相反------硬件产生的 PCM 数据,经过 Audio HAL → RecordThread → Ring Buffer,最终由你的 read() 调用取走。但底层机制------共享内存、零拷贝、无锁同步------和 AudioTrack 出奇地相似,毕竟同一套基础设施,只是数据方向反了。

本文主要内容:

  1. AudioRecord API 核心参数、音频源类型解析
  2. 创建流程 :权限检查 → JNI → Native → AudioFlinger::openRecord()
  3. RecordThread 采集机制:如何从 HAL 周期性读取数据
  4. 共享内存 Ring Buffer:录音端的生产消费模型(与 AudioTrack 对比)
  5. read() 内部实现:阻塞与非阻塞、时间戳同步
  6. 实战案例:录音机 + 实时音频处理 + PCM Dump 调试

源码版本 :Android 15 AOSP 关键路径frameworks/base/media/java/android/media/AudioRecord.javaframeworks/av/media/libaudioclient/AudioRecord.cppframeworks/av/services/audioflinger/RecordTracks.h


AudioRecord API 基础

音频源类型:选对 AudioSource 很关键

AudioRecord 最重要的参数是 AudioSource,它不只是标签,直接影响录音路径和信号处理

java 复制代码
// 常用音频源对比
AudioSource.MIC                    // 原始麦克风输入,无任何处理
AudioSource.VOICE_COMMUNICATION    // 针对 VoIP 优化:噪声消除 + 回声消除
AudioSource.VOICE_RECOGNITION      // 针对语音识别优化:低延迟 + 高清晰度
AudioSource.CAMCORDER              // 相机录像:可能使用辅助麦克风降低风噪
AudioSource.VOICE_CALL             // 录制通话音频(需特殊权限)
AudioSource.UNPROCESSED            // 完全原始信号,无任何DSP处理(Android 7+)
AudioSource.VOICE_PERFORMANCE      // 针对现场表演:最低延迟(Android 10+)

踩坑经验 :做语音识别 App 时用了 MIC,结果在嘈杂环境下识别率很低。换成 VOICE_RECOGNITION 后,系统会自动启用降噪算法,识别率明显提升。代价是多了一点处理延迟,但对识别场景来说完全值得。

基础用法:Builder 模式

java 复制代码
// 第一步:计算最小缓冲区大小
int minBufferSize = AudioRecord.getMinBufferSize(
    16000,                              // 采样率:16kHz(语音识别常用)
    AudioFormat.CHANNEL_IN_MONO,        // 单声道(录音通常用单声道)
    AudioFormat.ENCODING_PCM_16BIT      // 16-bit PCM
);

if (minBufferSize == AudioRecord.ERROR_BAD_VALUE) {
    // 参数不支持,需要换参数
    throw new IllegalArgumentException("不支持的录音参数");
}

// 第二步:创建 AudioRecord(推荐 Builder 方式,Android 6+)
AudioRecord recorder = new AudioRecord.Builder()
    .setAudioSource(MediaRecorder.AudioSource.VOICE_RECOGNITION)
    .setAudioFormat(new AudioFormat.Builder()
        .setSampleRate(16000)
        .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
        .setChannelMask(AudioFormat.CHANNEL_IN_MONO)
        .build())
    .setBufferSizeInBytes(minBufferSize * 2)  // 实际缓冲区建议 2x 最小值
    .build();

// 第三步:检查是否成功初始化
if (recorder.getState() != AudioRecord.STATE_INITIALIZED) {
    throw new IllegalStateException("AudioRecord 初始化失败");
}

参数选择指南

场景 采样率 声道 格式 AudioSource
语音识别 16000 Hz MONO PCM_16BIT VOICE_RECOGNITION
电话录音 8000 Hz MONO PCM_16BIT VOICE_COMMUNICATION
高清录音 44100 Hz STEREO PCM_16BIT MIC
专业录音 48000 Hz STEREO PCM_FLOAT UNPROCESSED
游戏/现场 44100 Hz MONO PCM_16BIT VOICE_PERFORMANCE

注意CHANNEL_IN_STEREO 在部分低端设备上可能不支持,生产环境需做降级处理。


AudioRecord 创建流程深度解析

与 AudioTrack 高度对称,AudioRecord 的创建同样跨越三个进程边界。

第一层:权限检查------比 AudioTrack 多了一道门

这是 AudioRecord 与 AudioTrack 最大的不同之处。录音涉及隐私,Android 在 Java 层就做了强制权限检查:

java 复制代码
// frameworks/base/media/java/android/media/AudioRecord.java
public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig,
        int audioFormat, int bufferSizeInBytes) {
    // ...
    // 1. 权限检查(API 23+ 需要运行时权限)
    // 注意:这里只是参数校验,实际权限在 AudioPolicyService 里检查
    if (audioSource == MediaRecorder.AudioSource.DEFAULT) {
        mRecordSource = MediaRecorder.AudioSource.MIC;
    } else {
        mRecordSource = audioSource;
    }

    // 2. 触发 Native 初始化
    int initResult = native_setup(
        new WeakReference<AudioRecord>(this),
        mRecordSource,
        sampleRateInHz, channelMask, audioFormat,
        buffSizeInBytes, sessionId,
        getCurrentOpPackageName(), 0);
}

实际权限检查在哪里?AudioPolicyService::getInputForAttr() 里,它会调用 PermissionController 验证调用方是否有 android.permission.RECORD_AUDIO。如果没有权限,openRecord() 会返回 PERMISSION_DENIED,整个 native_setup() 失败。

第二层:Native AudioRecord::set() 的关键步骤

cpp 复制代码
// frameworks/av/media/libaudioclient/AudioRecord.cpp
status_t AudioRecord::set(
        audio_source_t inputSource,
        uint32_t sampleRate,
        audio_format_t format,
        audio_channel_mask_t channelMask,
        size_t frameCount,
        ...) {

    // 1. 参数校验
    if (!audio_is_valid_format(format) || !audio_is_linear_pcm(format)) {
        ALOGE("Invalid format %#x", format);
        return BAD_VALUE;
    }

    // 2. 通过 AudioSystem 查询 AudioPolicyService,获取合适的输入设备
    //    这里会触发路由决策:MIC → 哪个物理麦克风?
    audio_io_handle_t input = AUDIO_IO_HANDLE_NONE;
    status_t status = AudioSystem::getInputForAttr(
        &mAttributes, &input, ..., &selectedDeviceId, ...);

    // 3. 建立与 AudioFlinger 的连接
    status = openRecord_l(0 /*epoch*/, mOpPackageName);

    return NO_ERROR;
}

第三层:openRecord_l() ------ Binder 调用 AudioFlinger

cpp 复制代码
// frameworks/av/media/libaudioclient/AudioRecord.cpp
status_t AudioRecord::openRecord_l(const Modulo<uint32_t> &epoch, ...) {
    // 1. 获取 AudioFlinger Binder 代理
    const sp<IAudioFlinger>& audioFlinger = AudioSystem::get_audio_flinger();

    // 2. 构造请求(与 AudioTrack 非常相似)
    IAudioFlinger::CreateRecordInput input;
    input.attr = mAttributes;
    input.config.sample_rate = mSampleRate;
    input.config.channel_mask = mChannelMask;
    input.config.format = mFormat;
    input.frameCount = mReqFrameCount;
    input.sessionId = mSessionId;

    // 3. Binder IPC → AudioFlinger.createRecord()
    IAudioFlinger::CreateRecordOutput output;
    sp<media::IAudioRecord> record = audioFlinger->createRecord(input, output, &status);

    // 4. 拿到共享内存,进行 mmap
    sp<IMemory> iMem = record->getCblk();
    void* iMemPointer = iMem->unsecurePointer();

    // 5. mCblk:录音控制块
    mCblk = static_cast<audio_track_cblk_t*>(iMemPointer);
    mBuffer = (char*)iMemPointer + sizeof(audio_track_cblk_t);

    return NO_ERROR;
}

AudioFlinger 端:createRecord() 的工作

cpp 复制代码
// frameworks/av/services/audioflinger/AudioFlinger.cpp
sp<media::IAudioRecord> AudioFlinger::createRecord(
        const CreateRecordInput& input,
        CreateRecordOutput& output,
        status_t *status) {

    // 1. 权限二次验证(audioserver 进程再次检查)
    if (!recordingAllowed(input.attr, input.clientPid, input.clientUid)) {
        *status = PERMISSION_DENIED;
        return nullptr;
    }

    // 2. 找到对应的 RecordThread
    //    RecordThread 是在 Audio HAL openInputStream 时创建的
    sp<RecordThread> thread = checkRecordThread_l(input.inputId);

    // 3. 在 RecordThread 上创建 RecordTrack
    sp<RecordThread::RecordTrack> recordTrack = thread->createRecordTrack_l(
        client, sampleRate, format, channelMask,
        &frameCount, sessionId, ...);

    // 4. 分配共享内存(同 AudioTrack)
    //    audio_track_cblk_t + PCM 数据缓冲区
    sp<MemoryDealer> heap = new MemoryDealer(
        sizeof(audio_track_cblk_t) + frameCount * mFrameSize);
    sp<IMemory> cblkMemory = heap->allocate(...);

    // 5. 返回 RecordHandle + 共享内存 fd
    sp<RecordHandle> handle = new RecordHandle(recordTrack);
    output.cblk = cblkMemory;
    return handle;
}

RecordThread 采集机制

这是录音的核心"引擎",理解它能帮你解释很多录音延迟和质量问题。

RecordThread 的线程循环

cpp 复制代码
// frameworks/av/services/audioflinger/Threads.cpp
bool AudioFlinger::RecordThread::threadLoop() {
    while (!exitPending()) {
        // 1. 等待有 Active RecordTrack
        {
            Mutex::Autolock _l(mLock);
            while (mActiveTracks.isEmpty()) {
                mWaitWorkCV.wait(mLock);
            }
        }

        // 2. 从 Audio HAL 读取一个周期的数据
        //    HAL 的周期大小(period_size)决定了录音的最小延迟
        //    典型值:44100Hz × 480帧 ≈ 10.9ms
        ssize_t bytesRead = mInput->stream->read(
            mRsmpInBuffer,    // 读入缓冲区
            mBufferSize       // HAL 期望的每次读取大小
        );

        if (bytesRead < 0) {
            // HAL 读取失败,可能是设备断开
            ALOGE("RecordThread: read returned error %zd", bytesRead);
            usleep(50000);    // 50ms 后重试
            continue;
        }

        // 3. 采样率转换(如果需要)
        //    App 请求 16kHz,硬件输出 48kHz → 需要 3:1 降采样
        if (mResampler != nullptr) {
            mResampler->resample(
                mRecordBufferConverter, &provider,
                mRsmpOutBuffer, mRsmpOutFrameCount);
        }

        // 4. 将数据分发给所有 Active RecordTrack
        for (auto& track : mActiveTracks) {
            if (track->isActive()) {
                copyRecordToClients(track, ...);
            }
        }
    }
}

copyRecordToClients() ------ 写入共享内存

cpp 复制代码
// frameworks/av/services/audioflinger/RecordTracks.cpp
void AudioFlinger::RecordThread::copyRecordToClients(
        const sp<RecordTrack>& activeTrack, ...) {

    // 获取 Ring Buffer 的可写位置
    AudioBufferProvider::Buffer buffer;
    status_t status = activeTrack->getNextBuffer(&buffer);

    if (status == OK && buffer.frameCount > 0) {
        // 直接写入共享内存(这就是"零拷贝"的体现)
        //   mRsmpOutBuffer 是从 HAL 读来的数据(或重采样后的数据)
        //   buffer.raw 直接指向 App 进程 mmap 的共享内存
        memcpy(buffer.raw, mRsmpOutBuffer, buffer.frameCount * mFrameSize);

        // 更新写指针(原子操作)
        activeTrack->releaseBuffer(&buffer);

        // 唤醒正在 read() 阻塞的 App
        activeTrack->mCblk->cv.signal();
    }
}

关键洞察 :RecordThread 直接把 HAL 数据 memcpy 到共享内存,然后 App 的 read() 再从共享内存拷贝到 App 的 Buffer。整个过程实际上有一次 memcpy (从共享内存到 App buffer),而 AudioTrack 的 write() 也是一次 memcpy(App buffer 到共享内存)。这与 AudioTrack 是完全对称的。


共享内存 Ring Buffer ------ 录音版

录音的 Ring Buffer 与播放的完全对称,只是生产者和消费者角色互换

角色 播放(AudioTrack) 录音(AudioRecord)
生产者 App(write() audioserver(RecordThread)
消费者 audioserver(PlaybackThread) App(read()
写指针操作者 App RecordThread
读指针操作者 AudioFlinger App
数据流向 App → Buffer → 扬声器 麦克风 → Buffer → App

read() 的内部实现

cpp 复制代码
// frameworks/av/media/libaudioclient/AudioRecord.cpp
ssize_t AudioRecord::read(void* buffer, size_t userSize, bool blocking) {

    // 1. 获取可读帧(如果没有数据,在此等待)
    Buffer audioBuffer;
    status_t err = obtainBuffer(&audioBuffer,
            blocking ? &ClientProxy::kForever : &ClientProxy::kNonBlocking);

    if (err == TIMED_OUT || err == WOULD_BLOCK) {
        // 非阻塞模式:暂时没有数据
        return 0;
    }

    if (err != NO_ERROR) {
        return ssize_t(err);
    }

    // 2. 从共享内存拷贝到 App 缓冲区
    size_t toRead = min(audioBuffer.size, userSize);
    memcpy(buffer, audioBuffer.raw, toRead);

    // 3. 更新读指针(原子操作,不需要持锁)
    releaseBuffer(&audioBuffer);

    return toRead;
}

阻塞 vs 非阻塞 read()

java 复制代码
// 阻塞模式(默认):没有数据时 read() 会等待
// 适合:录音保存文件、语音识别(每帧数据都必须处理)
short[] buffer = new short[bufferSize / 2];
int readCount = audioRecord.read(buffer, 0, buffer.length);
// readCount > 0:成功读取了 readCount 个 short
// readCount == AudioRecord.ERROR_INVALID_OPERATION:状态错误
// readCount == AudioRecord.ERROR_BAD_VALUE:参数错误

// 非阻塞模式(Android 6+):没有数据立即返回 0
// 适合:需要精确控制时序的场景(如实时DSP处理)
int readCount = audioRecord.read(buffer, 0, buffer.length,
        AudioRecord.READ_NON_BLOCKING);
if (readCount == 0) {
    // 暂时没有数据,可以做其他事情
}

精确时间戳获取

java 复制代码
// Android 7.0+ 支持获取录音时间戳,用于音视频同步
AudioTimestamp timestamp = new AudioTimestamp();
if (audioRecord.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC)
        == AudioRecord.SUCCESS) {
    // framePosition:已采集并交给 App 的帧数
    // nanoTime:对应的 CLOCK_MONOTONIC 时间戳
    long capturedUs = timestamp.framePosition * 1_000_000L / sampleRate;
    Log.d(TAG, "已采集: " + capturedUs / 1000 + " ms");
}

录音控制流程

状态机

scss 复制代码
STATE_UNINITIALIZED
        ↓ new AudioRecord().build() 成功
STATE_INITIALIZED
        ↓ startRecording()
RECORDSTATE_RECORDING
        ↓ stop()
RECORDSTATE_STOPPED
        (release() 可随时调用)

AudioRecord 的状态比 AudioTrack 简单很多------没有 pause(),只有 start/stop

startRecording() 的内部流程

cpp 复制代码
// Java → Native → Binder
// Native 层:
status_t AudioRecord::start(AudioSystem::sync_event_t event, audio_session_t triggerSession) {

    // 通知 AudioFlinger:把这个 RecordTrack 加入活跃列表
    status_t status = mAudioRecord->start(event, triggerSession);

    if (status == NO_ERROR) {
        mActive = true;
        // 启动回调线程(如果有回调)
        if (mAudioRecordThread != nullptr) {
            mAudioRecordThread->wakeup();
        }
    }
    return status;
}

在 AudioFlinger 端,RecordThread 的 threadLoop() 会检测到有 Active Track,立即开始从 HAL 读取数据。

正确的 stop() 和 release()

java 复制代码
// 错误做法:直接 release() 不 stop()
audioRecord.release();  // ❌ 可能导致 RecordThread 空指针

// 正确做法
if (audioRecord != null) {
    if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
        audioRecord.stop();
    }
    audioRecord.release();
    audioRecord = null;
}

为什么要先 stop() 再 release()? stop() 会通知 RecordThread 将这个 Track 从活跃列表中移除,然后等待当前正在进行的 HAL read 完成。直接 release() 会释放共享内存,但 RecordThread 可能还在往里写,导致写越界。


音频格式处理

采样率转换的开销

录音时的采样率转换比播放时更敏感,因为它在 RecordThread 的关键路径上:

cpp 复制代码
// frameworks/av/services/audioflinger/Threads.cpp
// RecordThread 创建时检查是否需要重采样
if (mSampleRate != mInput->stream->getSampleRate()) {
    // 需要创建重采样器
    //   AudioResampler::kDefaultQuality:用于一般场景
    //   AudioResampler::kHighQuality:用于专业录音场景
    mResampler = AudioResampler::create(
        AUDIO_FORMAT_PCM_16_BIT,
        mChannelCount,
        mSampleRate,
        AudioResampler::kDefaultQuality);
}

实际影响:在 Snapdragon 8 Gen 3 上测试:

  • 无重采样(App 请求 = 硬件输出采样率):RecordThread 每周期 CPU 占用约 0.3%
  • 需要重采样(16kHz → 48kHz):约 1.2%
  • 同时需要声道转换(mono ← stereo):约 1.5%

因此,尽量让 App 请求的采样率与硬件一致,避免重采样开销。

声道转换

java 复制代码
// 如果硬件只支持 STEREO 输入,但你请求 MONO
// AudioFlinger 的 RecordThread 会自动做声道混合(downmix)
// L+R → (L+R)/2

// 查询硬件支持的声道配置
AudioRecord.getMinBufferSize(
    48000,
    AudioFormat.CHANNEL_IN_STEREO,  // 先试 STEREO
    AudioFormat.ENCODING_PCM_16BIT);

// 如果返回 ERROR_BAD_VALUE,说明不支持,改用 MONO

实战案例

案例一:完整录音机实现

java 复制代码
public class VoiceRecorder {
    private AudioRecord mAudioRecord;
    private Thread mRecordThread;
    private volatile boolean mIsRecording = false;
    private FileOutputStream mFileOutputStream;

    private static final int SAMPLE_RATE = 16000;
    private static final int CHANNEL = AudioFormat.CHANNEL_IN_MONO;
    private static final int FORMAT = AudioFormat.ENCODING_PCM_16BIT;

    public void startRecording(String outputPath) throws IOException {
        int bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL, FORMAT);
        if (bufferSize <= 0) {
            throw new IllegalStateException("设备不支持录音参数");
        }

        mAudioRecord = new AudioRecord.Builder()
            .setAudioSource(MediaRecorder.AudioSource.VOICE_RECOGNITION)
            .setAudioFormat(new AudioFormat.Builder()
                .setSampleRate(SAMPLE_RATE)
                .setEncoding(FORMAT)
                .setChannelMask(CHANNEL)
                .build())
            .setBufferSizeInBytes(bufferSize * 2)
            .build();

        if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
            throw new IllegalStateException("AudioRecord 初始化失败,检查 RECORD_AUDIO 权限");
        }

        mFileOutputStream = new FileOutputStream(outputPath);
        mIsRecording = true;
        mAudioRecord.startRecording();

        mRecordThread = new Thread(() -> {
            byte[] buffer = new byte[bufferSize];
            android.os.Process.setThreadPriority(
                android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);  // 提高录音线程优先级

            while (mIsRecording) {
                int bytesRead = mAudioRecord.read(buffer, 0, buffer.length);
                if (bytesRead > 0) {
                    try {
                        mFileOutputStream.write(buffer, 0, bytesRead);
                    } catch (IOException e) {
                        Log.e(TAG, "写入文件失败", e);
                        mIsRecording = false;
                    }
                } else if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION) {
                    Log.e(TAG, "AudioRecord 状态错误");
                    break;
                }
            }
        }, "VoiceRecordThread");
        mRecordThread.start();
    }

    public void stopRecording() {
        mIsRecording = false;

        if (mAudioRecord != null) {
            mAudioRecord.stop();
            mAudioRecord.release();
            mAudioRecord = null;
        }

        try {
            if (mFileOutputStream != null) {
                mFileOutputStream.close();
                mFileOutputStream = null;
            }
        } catch (IOException e) {
            Log.e(TAG, "关闭文件失败", e);
        }
    }
}

案例二:实时音频处理(FFT 频谱分析)

java 复制代码
public class RealtimeAudioProcessor {
    private AudioRecord mAudioRecord;
    private volatile boolean mProcessing = false;

    // 处理回调(在独立线程中调用)
    public interface AudioProcessCallback {
        void onAudioData(short[] pcmData, int sampleCount);
    }

    public void startProcessing(AudioProcessCallback callback) {
        int sampleRate = 44100;
        int frameSize = 1024;  // FFT 的帧大小必须是 2 的幂
        int bufferSize = Math.max(
            AudioRecord.getMinBufferSize(sampleRate,
                AudioFormat.CHANNEL_IN_MONO,
                AudioFormat.ENCODING_PCM_16BIT),
            frameSize * 2  // 至少能放下 2 帧
        );

        mAudioRecord = new AudioRecord.Builder()
            .setAudioSource(MediaRecorder.AudioSource.MIC)
            .setAudioFormat(new AudioFormat.Builder()
                .setSampleRate(sampleRate)
                .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                .setChannelMask(AudioFormat.CHANNEL_IN_MONO)
                .build())
            .setBufferSizeInBytes(bufferSize)
            .build();

        mProcessing = true;
        mAudioRecord.startRecording();

        new Thread(() -> {
            short[] pcmBuffer = new short[frameSize];
            android.os.Process.setThreadPriority(
                android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);

            while (mProcessing) {
                // 精确读取 frameSize 个 short(阻塞直到收集满一帧)
                int totalRead = 0;
                while (totalRead < frameSize && mProcessing) {
                    int read = mAudioRecord.read(
                        pcmBuffer, totalRead, frameSize - totalRead);
                    if (read > 0) {
                        totalRead += read;
                    } else {
                        break;
                    }
                }

                if (totalRead == frameSize) {
                    callback.onAudioData(pcmBuffer, frameSize);
                }
            }
        }, "AudioProcessThread").start();
    }

    public void stopProcessing() {
        mProcessing = false;
        if (mAudioRecord != null) {
            mAudioRecord.stop();
            mAudioRecord.release();
            mAudioRecord = null;
        }
    }
}

// 使用示例
processor.startProcessing((pcmData, count) -> {
    // 在这里做 FFT、音量计算、语音活跃检测等
    double rms = calculateRMS(pcmData, count);
    float dB = (float) (20 * Math.log10(rms / 32768.0));
    Log.d(TAG, "当前音量: " + dB + " dBFS");
});

案例三:使用 AudioRecord.OnRecordPositionUpdateListener 精确同步

java 复制代码
// 通知位置更新回调,适合需要精确时序的场景
audioRecord.setPositionNotificationPeriod(441);  // 每 441 帧通知一次(10ms at 44100Hz)
audioRecord.setRecordPositionUpdateListener(new AudioRecord.OnRecordPositionUpdateListener() {
    @Override
    public void onMarkerReached(AudioRecord recorder) {
        // 到达 setNotificationMarkerPosition 设定的位置时回调
    }

    @Override
    public void onPeriodicNotification(AudioRecord recorder) {
        // 每 441 帧回调一次
        // 注意:这个回调是在 AudioTrackThread(Native线程)中调用,不是主线程
        int headPosition = recorder.getRecordPosition();
        Log.d(TAG, "录音位置: " + headPosition + " 帧");
    }
}, handler);

调试技巧

检查 RecordThread 状态

bash 复制代码
# 查看所有录音线程信息
adb shell dumpsys media.audio_flinger | grep -A 30 "Input thread"

# 典型输出:
# Input thread 0x7f3d400000 type 0:
#   Input: ...
#   1 RecordTracks, 1 active:
#     active   [  0] state:0x01 id:3  sessionId:xx  16000Hz PCM_16_BIT MONO
#               client: com.example.app (pid=12345)

# state:0x01 表示 ACTIVE(正在录音)
# 关注采样率和格式,确认是否需要重采样

录音 underrun 检测(overrun in recording = 溢出)

bash 复制代码
# 对于录音,风险是 Ring Buffer 被 RecordThread 写满(App 读太慢)
# 这叫 overrun(溢出),与播放的 underrun 相反
adb shell dumpsys media.audio_flinger | grep -i "overrun\|overflow"

PCM 数据 Dump 调试

java 复制代码
// 方法1:在 App 层 dump
private void dumpPcm(byte[] data, int size, FileOutputStream dump) {
    if (dump != null) {
        try { dump.write(data, 0, size); } catch (IOException ignored) {}
    }
}

// 方法2:通过 adb shell 使用系统录音工具(无需权限)
// 注意:需要 adb root
adb shell tinycap /data/local/tmp/test.pcm -D 0 -d 0 -c 2 -r 44100 -b 16 -p 1024 &
# -D: card, -d: device, -c: channels, -r: rate, -b: bits, -p: period_size
# 录几秒后 Ctrl+C 停止
adb pull /data/local/tmp/test.pcm ./
# 用 Audacity(选 Raw Data,格式 Signed 16-bit PCM,Stereo,44100Hz)打开

常见问题排查

问题:startRecording()read() 一直返回 0

java 复制代码
// 原因1:没有声明权限(AndroidManifest.xml 缺 RECORD_AUDIO)
// 原因2:运行时权限没有申请(Android 6+)
// 原因3:应用在后台被系统静音(Android 9+ 前台服务限制)

// 诊断:检查 AudioRecord 状态
Log.d(TAG, "State: " + audioRecord.getState());  // 应为 STATE_INITIALIZED
Log.d(TAG, "RecordState: " + audioRecord.getRecordingState());  // 应为 RECORDSTATE_RECORDING

// 检查权限
boolean hasPermission = ContextCompat.checkSelfPermission(this,
    Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED;
Log.d(TAG, "Has RECORD_AUDIO: " + hasPermission);

问题:录音有噪音/杂音(不是硬件问题)

java 复制代码
// 原因:录音线程优先级太低,导致 read() 调用不及时,Ring Buffer 发生 overrun
// 解决:提高线程优先级
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);

// 验证:查看 overrun 计数
adb shell dumpsys media.audio_flinger | grep "overrun"

问题:getMinBufferSize() 返回 ERROR_BAD_VALUE (-2)

java 复制代码
// 尝试备用参数
int[] sampleRates = {44100, 22050, 16000, 11025, 8000};
int[] channelConfigs = {AudioFormat.CHANNEL_IN_STEREO, AudioFormat.CHANNEL_IN_MONO};

for (int rate : sampleRates) {
    for (int channel : channelConfigs) {
        int size = AudioRecord.getMinBufferSize(rate, channel,
            AudioFormat.ENCODING_PCM_16BIT);
        if (size > 0) {
            Log.d(TAG, "支持: " + rate + "Hz " +
                (channel == AudioFormat.CHANNEL_IN_MONO ? "MONO" : "STEREO"));
        }
    }
}

总结

从这两篇文章(AudioTrack + AudioRecord)的对比中,我们可以看到 Android 音频子系统设计的精妙之处:完全对称的双向管道

对比项 播放(AudioTrack) 录音(AudioRecord)
创建入口 AudioFlinger::createTrack() AudioFlinger::createRecord()
工作线程 PlaybackThread RecordThread
HAL操作 output->write() input->read()
共享内存生产者 App write() RecordThread
共享内存消费者 AudioFlinger App read()
低延迟路径 FastMixer / FAST Track FastCapture(Android 7+)
权限要求 无需特殊权限 RECORD_AUDIO 必须

三个最重要的录音原则

  1. 录音线程要提优先级THREAD_PRIORITY_URGENT_AUDIO,否则容易 overrun 产生杂音
  2. 采样率匹配硬件:避免 RecordThread 做重采样,降低 CPU 开销和延迟
  3. stop() 后再 release():让 RecordThread 安全地从活跃列表移除,避免共享内存冲突

下一篇,我们深入 AudioFlinger 最复杂的部分------混音机制:当多个 App 同时播放音乐、游戏音效和提示音时,AudioFlinger 是如何把它们混合成一路信号的?

相关推荐
alexhilton5 小时前
Jetpack Compose中的富文本输入
android·kotlin·android jetpack
兄弟加油,别颓废了。5 小时前
ctf.show_web4
android
哔哔龙7 小时前
Android OpenCV 实战:图片轮廓提取与重叠轮廓合并处理
android·算法
tangweiguo030519878 小时前
Android SSE 流式接收:从手写到框架的进阶之路
android
大尚来也8 小时前
PHP 反序列化漏洞深度解析:从原理利用到 allowed_classes 防御实战
android·开发语言·php
sp42a9 小时前
通过 RootEncoder 进行安卓直播 RTSP 推流
android·推流·rtsp
SY.ZHOU9 小时前
移动端架构体系(一):组件化
android·ios·架构·系统架构
恋猫de小郭10 小时前
Android 17 新适配要求,各大权限进一步收紧,适配难度提升
android·前端·flutter
流星白龙11 小时前
【MySQL】9.MySQL内置函数
android·数据库·mysql