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 是如何把它们混合成一路信号的?

相关推荐
千里马学框架15 小时前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台16 小时前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone16 小时前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc17 小时前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo18 小时前
Kotlin 协程中的 Job 结构化并发与取消
android
sun00770019 小时前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼19 小时前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone20 小时前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen20 小时前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone20 小时前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui