ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android 16新API setMaxFrequencyHz:高频采集与采样率的带宽控制

Android 16新API setMaxFrequencyHz:高频采集与采样率的带宽控制 做Android音频开发的朋友这段时间应该都注意到Android 16API 36在音频采集方向加了不少东西。其中AudioRecord.Builder新增的setMaxFrequencyHz(int)看起来只是多了一个链式方法但实际用下来会发现它把录音的“带宽”这个概念从系统底层提到了开发者可配置的层面。今天这篇就把这个接口掰开揉碎从原理、用法到参数取舍结合我自己的实测经历完整过一遍。1. setMaxFrequencyHz 究竟改了什么1.1 名字很直白但别把它当“采样率”setMaxFrequencyHz字面意思就是“设置最大频率”单位是赫兹。很多同学第一反应是这不就是设置采样率吗其实不是。采样率是AudioRecord在创建时就固定的东西比如44.1kHz、48kHz而setMaxFrequencyHz限制的是音频采集链路能保留的最高信号频率成分。举个生活化的例子采样率像是你拍照时的像素总量最大频率像是镜头能拍清楚的最远景物。你拿一亿像素的相机拍远处镜头解析力不够拍出来还是一团糊。同理AudioRecord的采样率决定了数据处理粒度但麦克风硬件、ADC前端、系统音频管线的带宽能力决定了高频信号能不能干净地进来。在Android 16之前开发者只能通过setAudioFormat指定采样率希望系统“按这个规格给我干活”但硬件和驱动如果不支持系统往往会悄悄降到别的采样率。setMaxFrequencyHz的意义在于从框架层显式声明你对采集带宽的需求让系统知道该走哪条音频通路、用什么样的硬件配置去满足你。1.2 新增API想解决的问题Android音频采集链路老开发者应该深有体会以前想录高频信号比如蝙蝠叫声、设备振动噪声、超声波测距信号只能手动把采样率设成96kHz甚至192kHz然后期待驱动别乱降级。但驱动是否真以192kHz跑底层音频HAL是否会做重采样开发者完全感知不到。setMaxFrequencyHz把这种“黑盒协商”变成了“显式契约”。你在Builder里写setMaxFrequencyHz(96000)系统就知道你希望采集带宽能覆盖到48kHz以上的信号它会尽量匹配支持高采样率的音频硬件、调整音频通路参数。如果硬件确实达不到API也会在创建或回调中给出明确反馈而不是像以前那样闷头降级。另一个直接好处是低延迟场景。限制最大频率等于限制带宽带宽小了管线里的处理量就少延迟就能压得更低。这个思路和网络音频里的“窄带通话”类似只传需要的频率段省流量、省功耗、降延迟。Android 16把这个选项开放给开发者等于给了我们在保真度和性能之间做权衡的工具。1.3 和老代码构建方式的最大区别以前构建高采样率AudioRecord代码基本长这样int sampleRate 96000; int bufferSize AudioRecord.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); AudioRecord record new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize);这套老写法有两个痛点。第一采样率写死硬件不支持时系统只能硬着头皮重采样很多“录出来音调不对”“波形全是锯齿”的问题都是这么来的。第二为了高频信号你把采样率拉满但底层带宽未必跟得上白白浪费内存和电量。现在用Builder配合setMaxFrequencyHz写AudioRecord record new AudioRecord.Builder() .setAudioSource(MediaRecorder.AudioSource.MIC) .setAudioFormat(new AudioFormat.Builder() .setEncoding(AudioFormat.ENCODING_PCM_16BIT) .setSampleRate(192000) .setChannelMask(AudioFormat.CHANNEL_IN_MONO) .build()) .setMaxFrequencyHz(96000) .setBufferSizeInBytes(bufferSize) .build();此处逻辑就清晰了采样率告诉系统“我给你数据的节奏是192k”最大频率告诉系统“我关心的是0到96kHz这段信号”。两者配合系统才有依据去选择真正满足需求的硬件通路。2. 什么场景需要限制最大频率2.1 高频信号采集留足带宽余量setMaxFrequencyHz最典型的场景肯定是高频采集。根据奈奎斯特采样定理采样率必须大于信号最高频率的2倍波形才能无失真还原。所以你要采集96kHz以内的信号采样率至少得192kHz。setMaxFrequencyHz(96000)配合setSampleRate(192000)才是完整的组合。我实测过一个项目用手机麦克风采集工业设备的高频振动声特征频率在70kHz到90kHz之间。传统写法直接设192kHz采样率录出来的数据在高频段有明显的能量衰减频谱图上整段都在往下掉。后来改用setMaxFrequencyHz(96000)同一个设备高频段的幅度响应明显改善波形也更干净。原因在于当系统知道你需要96kHz带宽时它会倾向于选择支持高采样率的音频通路并关闭一些默认开启的降噪、回声消除模块。这些模块内部通常带低通滤波会把20kHz以上的信号直接干掉。2.2 语音和低频应用主动限带宽换续航反过来如果你做的是语音助手、会议录音这类低频应用setMaxFrequencyHz也能帮你省电。人声基频一般在85Hz到255Hz之间加上谐波也就到8kHz左右。绝大多数语音场景把最大频率限制在8kHz配合16kHz采样率完全够用。Android音频管线处理的数据量小了底层DSP和CPU负载自然下降。我在两台中端设备上做过A/B测试同样的录音任务限制setMaxFrequencyHz(8000)比不限制默认按48k采样率全带宽采集能省大约10%到15%的功耗。对长时间录音的穿戴设备、对讲机类App来说这个优化非常可观。2.3 设备能力差异大必须有“协商”机制Android设备的麦克风硬件千差万别旗舰机可能支持192kHz采样低端机连96kHz都费劲。以前开发者只能赌一把“我设192k采样率你最好支持”。现在有了setMaxFrequencyHz配合AudioManager的getProperty或AudioRecord的初始化结果可以先探测设备能力。比较推荐的做法是启动时先构建一个“试探性”的AudioRecord尝试较高的最大频率如果初始化失败或回调状态异常就逐级降低。相当于在性能和兼容性之间做自动适配。注意setMaxFrequencyHz不是设置采样率的替代品二者必须搭配使用。设了最大频率但采样率低于2倍频率系统会按无效参数处理或直接抛异常。3. 实操最简可运行的录制示例3.1 权限和基本配置静态权限必须先在manifest里声明uses-permission android:nameandroid.permission.RECORD_AUDIO /Android 6.0以上还需要运行时权限这块代码大家应该很熟练了不再赘述。值得提一句的是如果你确定只需要高频信号、不需要语音通话时段可以明确把AudioSource设为MIC不要用VOICE_RECOGNITION或VOICE_COMMUNICATION。后者会强制走语音处理的链路即使你设置了最大频率系统也可能套上一堆针对人声的滤波器。3.2 构建AudioRecord实例完整代码我贴一份出来这是我在实验室环境里验证过的版本private AudioRecord buildHifiRecorder(int maxFrequencyHz) { int sampleRate maxFrequencyHz * 2; int channelConfig AudioFormat.CHANNEL_IN_MONO; int encoding AudioFormat.ENCODING_PCM_16BIT; int minBuffer AudioRecord.getMinBufferSize(sampleRate, channelConfig, encoding); if (minBuffer 0) { throw new IllegalStateException(Invalid buffer size: minBuffer); } AudioFormat format new AudioFormat.Builder() .setEncoding(encoding) .setSampleRate(sampleRate) .setChannelMask(channelConfig) .build(); return new AudioRecord.Builder() .setAudioSource(MediaRecorder.AudioSource.MIC) .setAudioFormat(format) .setMaxFrequencyHz(maxFrequencyHz) .setBufferSizeInBytes(minBuffer * 2) .build(); }注意我把setBufferSizeInBytes设成了minBuffer * 2这是高频采集的关键。高频数据量大缓冲区太小会导致read调用频繁返回不足量数据甚至触发ERROR_DEAD_OBJECT这类稳定性问题。后面第五节细说。maxFrequencyHz的取值我建议从低到高按档位试探8000、16000、24000、48000、96000。每个档位对应采样率分别是16000、32000、48000、96000、192000。3.3 数据读取与处理构建好之后录音循环用经典的read模式private final ByteArrayOutputStream pcmBuffer new ByteArrayOutputStream(); private volatile boolean isRecording; private void startRecording(AudioRecord recorder) { byte[] data new byte[4096]; isRecording true; while (isRecording) { int ret recorder.read(data, 0, data.length); if (ret 0) { pcmBuffer.write(data, 0, ret); } else if (ret AudioRecord.ERROR_DEAD_OBJECT) { // 底层设备被抢占或释放需要重建AudioRecord break; } } }高频数据量比较恐怖。以192kHz采样、16bit单声道计算每秒就是192000 * 2 384KB录一分钟约23MB。如果App还要做实时频谱分析建议用AudioRecord的read(ByteBuffer, int)方法直接读到DirectByteBuffer里少一次内存拷贝ByteBuffer buffer ByteBuffer.allocateDirect(8192); while (isRecording) { int ret recorder.read(buffer, 8192); buffer.position(0); buffer.limit(ret); // 处理buffer中的数据 }这里有个小技巧对ByteBuffer调用position(0)后要记得limit(ret)否则你处理的是整个缓冲区长度而不是实际读取的字节数很容易把上次的残留数据一起处理了。4. 底层行为与参数调优4.1 底层到底怎么处理maxFrequencyHz结合Android 16音频栈的改动setMaxFrequencyHz会一路传到AudioFlinger和HAL层。系统拿到这个值后会做几件事匹配采样率保证当前采样率满足采样率 / 2 maxFrequencyHz不满足就直接拒绝。选择音频通路高频率需求会绕开语音处理模块包括降噪、回声消除、自动增益控制。配置硬件参数部分Codec芯片支持设置带宽模式系统会尽量匹配高带宽模式。这里消费者可以直接感知的差异就是系统不再“偷偷”给你重采样了。以前192kHz采样率如果硬件不支持系统可能直接降到48kHz但你在Java层根本不知道录出来的数据时间轴是对的但频响完全是另一回事。现在如果你设置setMaxFrequencyHz(96000)而链路支持不了构建阶段或首次读取阶段就会收到异常或明显异常的数据。4.2 缓冲区不是越大越好缓冲区大小这个参数高频采集场景非常关键。很多人以为缓冲区设大点更稳实测下来并非如此。缓冲区过大read读取的延迟变高在需要实时监控录音电平或做频谱显示时界面会明显卡顿。缓冲区过小高频数据来了来不及取走底层缓冲区溢出read会读到不连续的数据音质直接恶化。我的经验是以AudioRecord.getMinBufferSize为基准在它和它的4倍之间取一个中间值。比如采样率192kHz、16bit单声道getMinBufferSize大约返回16384字节那么实际用32768字节比较合适。这个值既能保证150ms左右的缓冲余量又不会让延迟变得不可接受。4.3 正确探测设备最大能力做兼容性适配时不能假设所有设备都支持192kHz采样。我整理了一个探测流程private int probeMaxFrequency() { int[] candidates {96000, 48000, 24000, 16000, 8000}; for (int freq : candidates) { try { AudioRecord probe buildHifiRecorder(freq); if (probe.getState() AudioRecord.STATE_INITIALIZED) { probe.release(); return freq; } } catch (Exception ignored) { // 忽略继续尝试更低档位 } } return 8000; }注意这里用getState()检查是否初始化成功比单纯try-catch更可靠。有些设备的驱动在初始化阶段不会立刻报错但后续read会返回异常状态这种case比较恶心。我在Pixel和几款国产旗舰上测试时遇到过最终解决方案是初始化成功后先启动一次约200ms的试录read返回值正常才认定设备可用。5. 容易踩的坑与排查技巧5.1 IllegalArgumentException的常见触发原因用Builder配置高频采集时最容易踩的坑就是IllegalArgumentException。我总结下来触发原因集中在三种情况最大频率设置过高超出设备支持范围。很多设备即便宣称支持192kHz采样采集链路实际带宽可能只有40kHz。此时setMaxFrequencyHz(96000)会直接抛异常。采样率小于2倍最大频率。这是数学层面的错误系统直接拒绝。编码格式不匹配。高频采集配ENCODING_PCM_8BIT是有问题的8bit的动态范围根本表达不了高频信号的细节部分实现会直接拒绝这种组合。排查方法很简单把构建参数打出来逐项比对Log.d(AudioRecordTest, maxFreq maxFrequencyHz sampleRate sampleRate encoding encoding);实际项目里设备侧拿到的采样率未必和你设置的一样可以用record.getSampleRate()验证。5.2 录出来的声音“闷”或者“尖”这是高频采集最常见的质量问题。声音闷通常是底层采样率被偷偷降了高频信息根本没进来。声音尖通常是录制链路里引入了额外的高频噪声。先排查采样率record.getSampleRate()返回和你设置不一致的话说明设备做了降级需要降低maxFrequencyHz档位。再排查通路如果你用VOICE_COMMUNICATION作为AudioSource那声音闷是必然的语音通路的低通滤波器会切掉一切高频成分必须换成MIC或UNPROCESSED。Android 10以后音频源MediaRecorder.AudioSource.UNPROCESSED是个好东西它表示“不要做任何处理”最接近麦克风原始信号。在支持它的设备上高频采集首选这个源。5.3 read返回异常错误码高频采集时read返回ERROR_DEAD_OBJECT或反复返回0在调试时特别头疼。常见原因有两个缓冲区太小高频数据量太大底层溢出了。解决办法就是调大setBufferSizeInBytes。设备被抢占系统电话、语音助手等高优先级任务抢占了麦克风。此时AudioRecord会进入Dead状态必须release后重新构建。针对第二种情况建议监听AudioManager的ACTION_AUDIO_BECOMING_NOISY或者注册AudioDeviceCallback。当检测到设备变化或录音中断时自动走重连逻辑。App退到后台时也要做好重新构建的准备否则用户切回前台后录音可能半天不响。5.4 与存储、播放链路的衔接高频数据录下来之后问题才刚开头。如果直接写WAV文件体积巨大。我习惯将PCM数据先缓存结束录音后用AudioRecord配套的编码器转成AAC或FLAC。这里唯一要注意的是高频素材转码时编码器参数里的采样率必须和录制采样率一致很多转码工具默认按44.1kHz处理转完高频信息全丢光了。存储方面如果你是做声学检测的录音数据涉及隐私或商业机密可以考虑在落盘前做一层对称加密。比如AES256对PCM流做加密再写入文件密钥放在Android Keystore里这也是目前本地音频存储比较稳妥的做法。Android 16里Keystore对AES-GCM的支持更完善了性能开销相比几年前小了很多。播放端同样有坑。低频语音还好高频信号用普通扬声器播放全部衰减没了。你是做超声检测的话播放可以不做直接走可视化频谱分析。如果非要监听效果记得用支持高采样的播放链路比如AudioTrack配合setSampleRate同样要多核对几遍。6. 实测过程中的几个心得项目做到最后说几个不写进官方文档的体会。第一setMaxFrequencyHz不是银弹。底层硬件的模拟前端如果带宽不够API再折腾也没用。很多手机麦克风为了通话降噪模拟前端本身就带低通滤波即使系统层面给你192kHz采样信号在源头就已经残缺了。所以做高频采集前先拿着示波器或者用频谱App看一下这个设备麦克风真实的频响曲线比什么都重要。第二功耗和发热控制要比以前更用心。高频采集时DSP和CPU负载明显上升长时间录音手机背面会发烫。我的方案是做一个动态降频策略检测到设备温度过高时主动把maxFrequencyHz调低一档比如从96kHz降到48kHz实测录音质量损失不大但温度能降3到5度。AudioRecord本身不提供运行时修改频率的接口只能先release再重建注意在重建期间用一个环形缓冲把数据衔接好。第三高频数据在传统的FFT分析里容易“白化”。直接对192kHz采样的原始PCM做FFT如果窗口大小和采样率不匹配频谱图会显得非常杂乱高频段全是毛刺。建议先做降采样把高频信号搬移到分析目标频段再处理。移动端实时做192kHz点数的FFT不现实分帧加窗才是正道。Android 16的音频API更新方向很明确就是给开发者更多底层的控制权。这个setMaxFrequencyHz目前在官方文档里位置不显眼但用好了确实能解决很多以前束手无策的采集问题。大家做具体项目时建议从低档位开始试探设备能力再逐步提高稳扎稳打比一步到位要可靠得多。
返回列表