做Android音频采集绕不开AudioRecord。提到采样率大部分人的条件反射就是setSampleRate(48000)然后该干嘛干嘛。但Android 16这个版本在AudioRecord.Builder上多了一个setMaxFrequencyHz说实话我第一次看到这个API也有点愣——频率采样率这俩不是一个东西吗这个系列写到一百九十八篇今天就拿它开刀。setMaxFrequencyHz的用途一句话可以概括给底层音频采集的采样时钟频率设一个上限。它不强制你用某个具体采样率而是告诉系统“你的采样率最高别超过我这个值具体用哪个你根据硬件情况自己看着办”。写这篇的起因是我在帮一个做声学监测的项目排查功耗问题时发现他们录环境音明明只处理48kHz的数据但底层设备却以192kHz开启录音通道中间白白做了一层重采样CPU和电量都在悄悄流失。当时Android 16还没推出setMaxFrequencyHz我只能通过setSampleRate(48000)硬绑。现在这个API出现正好补上了这个场景的空缺——如果你做录音App、语音助手、VAD检测、声学监测这类应用或者想给现有录音管线省电降负载这篇值得花10分钟看完。1. 这个API到底在限制什么1.1 从一次“采样率事故”说起先说一个我实际踩过的场景。某台测试机自带高规格音频Codec录音硬件最高能开到192kHz。我用最朴素的方式创建AudioRecordAudioRecord record new AudioRecord.Builder() .setAudioSource(MediaRecorder.AudioSource.MIC) .setAudioFormat(new AudioFormat.Builder() .setSampleRate(48000) .setEncoding(AudioFormat.ENCODING_PCM_16BIT) .setChannelMask(AudioFormat.CHANNEL_IN_MONO) .build()) .setBufferSizeInBytes(bufferSize) .build();表面上看我要求了48000的采样率系统也确实通过getSampleRate()返回了48000。但用底层工具抓HAL层的PCM配置时发现录音设备实际上是以192kHz打开的然后由AudioFlinger在中间做了重采样降到48000再喂给我的应用。问题在哪如果音频源本身是语音识别这类对高频不敏感的场景那192kHz的采集重采样就是纯浪费——ADC以高频率工作功耗涨了总线带宽占了CPU还得多算一轮转换。这就是setMaxFrequencyHz诞生的背景把“目标采样率”和“底层硬件频率上限”两个概念分开。在Android 16之前setSampleRate承担了过多职责它既当目标值又当协商下限导致系统为了“保证能匹配上目标值”宁愿开启高采样率再降下来。而现在你可以只限制上限把具体选择权交还给系统。1.2 “最大频率”限的到底是什么很多人会把setMaxFrequencyHz和setSampleRate搞混。这里把概念彻底理一遍采样率应用层拿到的PCM数据速率单位Hz。比如48000表示每秒采集48000个样本点。采样频率底层ADC实际工作的时钟频率单位也是Hz。setMaxFrequencyHz限制底层打开录音设备时的最大采样频率是HAL协商上限不是输出给应用的目标值。官方文档里有一句关键描述默认值是0表示“不设上限”此时系统可以选用设备支持的任何频率通常是硬件能开的最高值。一旦设置了非0值系统在打开录音流时会参考这个上限来挑选最合适的采样率组合。用生活化类比就是你去物业报修水管以前只说“我要用水”物业直接把总闸开到最大结果水压过大你家水管嗡嗡响。现在你可以说“水压别超过3公斤”物业就会在不超过3公斤的前提下根据楼层高度和管径选一个合适的压力给你供水。1.3 三个典型应用场景场景一语音识别与通话降噪。这类Pipeline通常只吃16kHz或48kHz的PCM数据。如果不设上限在支持高采样率的设备上底层可能以96kHz或192kHz采集再做重采样。加了setMaxFrequencyHz(48000)之后底层协商会直接往48000靠省掉一层无谓的重采样。场景二省电降载。ADC在高采样率模式下功耗提升明显特别是需要长时间录音的应用比如环境噪声监测、录音笔、车载音频事件检测。设置一个符合业务需求的上限对延长续航有实际帮助。我实测过一台设备连续录音半小时不设上限时底层采样率为192kHz微掉电明显更快设了48kHz上限后同样时间差距肉眼可见。场景三老设备与特殊外设兼容。市面上仍有一部分老款USB麦克风、车载蓝牙模块在高采样率下工作不稳定甚至会出现爆音或断流。通过限制最大频率可以减少采样率协商失败的概率。这点在做外设兼容测试时特别有用——不需要为每个设备写特判只要把上限压低到稳定区间就行。2. setMaxFrequencyHz与setSampleRate的正确配合2.1 参数语义对比为了直观理解我整理了一个对比表方法语义典型使用场景setSampleRate(int)应用层目标采样率系统尽量匹配精确值算法固定要求48kHz/16kHz比如定点DSP模型setMaxFrequencyHz(int)底层采样频率协商上限不指定精确值希望硬件自适应只想限制最高频率都不设置系统自动选择可能使用硬件最高采样率需要最大限度保真、对功耗不敏感这里有个容易踩坑的认知setSampleRate和setMaxFrequencyHz并不冲突前者解决“我要什么”后者解决“我最多能承受什么”。两者都设置是完全合理的而且推荐在需要固定采样率的场景下都写上。2.2 推荐的两种写法写法A只设上限让系统自己选。如果你对采样率不是强敏感只希望别超过某个值可以只调用setMaxFrequencyHz。if (Build.VERSION.SDK_INT 36) { AudioRecord record new AudioRecord.Builder() .setAudioSource(MediaRecorder.AudioSource.MIC) .setAudioFormat(new AudioFormat.Builder() .setEncoding(AudioFormat.ENCODING_PCM_16BIT) .setChannelMask(AudioFormat.CHANNEL_IN_MONO) .build()) .setMaxFrequencyHz(48000) .build(); int actual record.getSampleRate(); }这种写法的好处是系统会根据设备能力自动选择最合适的采样率可能是44100也可能是48000。但缺点也明显——如果设备原生采样率是22050系统可能会选22050而你的算法后期需要48kHz数据还得自己做重采样。所以写法A更适合“高级玩家”。写法B精确值加上限防守型配置。这也是我最常用的方式int sampleRate 48000; int minBuffer AudioRecord.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); AudioRecord record 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(Math.max(minBuffer, sampleRate * 2 * 2)) .setMaxFrequencyHz(sampleRate) .build();这里把setMaxFrequencyHz设置成与目标采样率相同的值等于告诉系统“我只要48000你也别给我开更高了。”这种配置在支持高采样率的设备上能有效阻止底层以192kHz采集再降采样同时又能确保应用拿到的就是精确的48000数据不引入额外的重采样。2.3 什么时候千万不要用它注意一个反向坑setMaxFrequencyHz设置得过低会导致高频信息被丢弃。如果你在做动物叫声识别、工业超声监测、音频设备质检这类需要高频分量的应用把上限设成48000等于自杀硬件192kHz的能力被白白浪费。另一个场景是录像同步收音。相机录制高帧率视频时音频管线需要保持在48000或更高如果业务上把setMaxFrequencyHz设成了16000录出来的声音会发闷高频全部丢失。我在做音视频录制同步时通常只对AudioRecord设置setSampleRate(48000)而不去调用setMaxFrequencyHz就是为了避免误伤除非功耗真的压不住了。3. 完整用法实例从权限申请到数据验证3.1 权限与SDK版本判断setMaxFrequencyHz是Android 16API 36新增的方法需要保证两点一是compileSdk至少为36否则IDE直接编译报错二是运行设备的系统要高于Android 15否则运行时会抛NoSuchMethodError。录音权限方面需要RECORD_AUDIO权限运行时动态申请不能少。这里给出一个兼容旧版本的创建函数private AudioRecord createAudioRecord(int sampleRate) { if (Build.VERSION.SDK_INT 36) { // 老版本走传统构造函数 int minBuffer AudioRecord.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); return new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, Math.max(minBuffer, sampleRate * 2 * 2)); } int minBuffer AudioRecord.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); return 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(Math.max(minBuffer, sampleRate * 2 * 2)) .setMaxFrequencyHz(sampleRate) .build(); }注意setBufferSizeInBytes这个值不能太小否则录音线程会因为buffer不足而丢帧。我习惯在getMinBufferSize的基础上再乘个系数对一般应用Math.max(minBuffer, sampleRate * 2 * 2)已经够用即约200ms的缓冲。3.2 初始化AudioRecord的完整代码下面是完整初始化加入状态检查和startAudioRecord record createAudioRecord(48000); if (record.getState() ! AudioRecord.STATE_INITIALIZED) { throw new IllegalStateException(AudioRecord init failed); } record.startRecording();创建后必须检查状态。setMaxFrequencyHz并不会改变STATE_INITIALIZED的判断逻辑但如果底层协商失败比如参数组合非法getState()可能返回STATE_UNINITIALIZED。这里建议在debug时打印record.getSampleRate()确认实际协商结果。3.3 读取、停止与释放录音循环常规操作关键点是read的参数和超时处理byte[] buffer new byte[4096]; while (isRecording) { int read record.read(buffer, 0, buffer.length); if (read 0) { // 这里可以把PCM数据交给音频处理线程 onPcmData(buffer, read); } }停止时必须先stop()再release()顺序反了偶尔会导致底层音频服务句柄泄漏。另外read在设备切换或采样率重协商时可能返回负值比如ERROR_DEAD_OBJECT如果出现这个值正确的做法是释放当前实例并重新创建——这个后面常见问题里详细说。3.4 用getSampleRate验证上限是否生效空口说白话没有用教大家一个实验方法在同一台支持高采样率的设备上分别创建两个实例一个带setMaxFrequencyHz(48000)一个不带然后打印getSampleRate()和getRoutedDevice()的原生采样率。private void logRecordInfo(String tag, AudioRecord record) { AudioDeviceInfo info record.getRoutedDevice(); Log.d(tag, sampleRate record.getSampleRate() , deviceSampleRate (info null ? unknown : String.valueOf(info.getSampleRate())) , product (info null ? unknown : info.getProductName())); record.release(); }实测下来在没有设置上限的设备上getSampleRate()可能返回192000或96000在设置了上限的设备上则稳定返回48000。说明系统协商时确实参考了这个值。如果两者返回一致说明这台设备原生只支持到48000那你加不加这个API都一样不必纠结。用Python核对频域数据也是一个好办法。把录制的PCM数据转成WAV文件在电脑上做频谱分析能够直观看到高频分量是否被保留import wave import numpy as np with wave.open(record_with_max48k.wav, rb) as wav: data np.frombuffer(wav.readframes(wav.getnframes()), dtypenp.int16) fs wav.getframerate() spectrum np.fft.rfft(data) freqs np.fft.rfftfreq(len(data), 1 / fs) peak_freq freqs[np.argmax(np.abs(spectrum))] print(fpeak frequency: {peak_freq:.1f} Hz)如果你采集的信号源含有较高频分量比如口哨或金属敲击声setMaxFrequencyHz(48000)的设备录出来的频谱在20kHz以上会迅速衰减而没设上限的设备则能保留更多高频能量。这一点在声学监测类项目里是重要的验证手段。4. 实战中的问题与排查技巧4.1 低版本设备直接崩溃现象在Android 15或更低的设备上调用setMaxFrequencyHz运行时报NoSuchMethodError。原因方法在低版本API中不存在ART虚拟机找不到实现。处理像我前面给的代码一样用Build.VERSION.SDK_INT 36做判断。还有一个办法是使用AndroidX库的兼容封装层但目前在录音这一块并没有官方兼容库所以还是手动分支最稳。4.2 参数非法导致异常现象构造AudioRecord时抛出IllegalArgumentException日志显示频率值不合法。原因setMaxFrequencyHz接收的频率值不在系统允许范围内。我遇到过负值和超过硬件最大能力值的情况——前者纯粹是业务代码传错参数后者出现在某些设备上硬件的最大采样频率并没有文档公开设置过高反而触发参数校验失败。处理设置前先查AudioDeviceInfo.getSampleRate()等接口获取设备能力区间宁可保守一点也不要拍脑袋写一个过大的值。同时整个创建过程包在try-catch里捕获异常后回退到不设setMaxFrequencyHz的传统路径。4.3 蓝牙设备连接时上限不生效现象连接蓝牙耳机后无论setMaxFrequencyHz设为多少实际采样率始终是16000Hz甚至更低。原因蓝牙传输通道尤其是HFP/SCO链路在录音方向有固定的采样率限制通常是16kHz。这与setMaxFrequencyHz的协商范围无关——底层HAL在这个模式下根本没有选择余地。顺便说一句蓝牙耳机所谓的低功耗高保真音频传输主要是播放方向的技术录音方向受SCO链路约束学通信的朋友应该都懂。处理这不是API设计缺陷别想着用setMaxFrequencyHz去撬动蓝牙耳机录音采样率。如果你需要高采样率的录音质量建议检查当前路由设备类型如果是蓝牙设备提示用户切换到有线麦克风或者接受16kHz并做对应算法适配。4.4 高频信息“凭空消失”的坑现象录制某个音频事件时高频部分听起来明显缺失频谱分析发现18kHz以上能量几乎为零。原因这就是“上限设太低”的经典症状。项目初期为了省电把setMaxFrequencyHz设成了16000后期做音频分析时需要采集更高频的信号忘记修改结果一整天录的数据全部报废。处理设计音频管线时把“业务需要的最大频率”和“功耗控制的限制频率”拆成两个配置项单独管理。上线前跑一条全频段扫频测试比如播放20Hz到20kHz的扫频信号对比频谱就能发现是否被削波。4.5 动态路由切换后的采样率重协商现象录音过程中拔掉耳机或切换外设read返回负值线程退出。原因路由切换后底层采样率会重新协商。如果在高采样率下创建的AudioRecord切换到一个低采样率设备数据链路会重建旧的句柄可能失效。处理监听AudioManager.ACTION_AUDIO_BECOMING_NOISY等广播或注册AudioDeviceCallback在设备变化时暂停录音、释放实例、按新的路由重建AudioRecord。重建时记得再次应用setMaxFrequencyHz设置因为不同设备支持的能力不同直接复用旧的Builder参数可能会再次协商失败。4.6 验证工具推荐排查这类问题我常用的工具是dumpsys media.audio_flinger可以看到当前音频流策略的采样率协商结果adb shell dumpsys media.audio_flinger | grep -A 10 AudioRecord输出里能看到录音流的采样率、通道、格式等关键信息对比自己设置的setMaxFrequencyHz就能快速判断是否生效。这个命令比反复打log高效得多。我在实际项目里已经把setMaxFrequencyHz当作一个功耗开关来用。语音唤醒、VAD、通话降噪这些Pipeline通常只吃48kHz把上限锁死在48000之后设备连续录音时的发热和耗电都有明显改善。反倒是那些需要高频分析的场景比如动物叫声识别、工业超声监听千万别为了省电把上限砍得太低——高频信息一旦在采集侧被丢掉了后面算法再怎么补救都补不回来。建议你拿到新设备时先跑一遍上面那个getSampleRate对比实验摸清自己设备的行为模式再决定这个API在你的业务里设多少合适。这个API在Android 16上还算新后续版本大概率还会继续打磨但“给底层音频协商设上限”这个思路短时间内是不会过时的。