尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android实时声级计开发:从AudioRecord到A计权分贝的完整实践

Android实时声级计开发:从AudioRecord到A计权分贝的完整实践 简介面向城市轨道交通噪声测量场景的Android声级计项目集成A计权算法与实时显示功能可完成音频录制与播放、时域信号图绘制、等效声压级计算及测量结果可视化适合作为移动端环境噪声监测课程的课设或毕业设计参考。压缩包共985个文件以工程配置与构建产物为主包含json、flat、xml、dex、class、java、jar和apk等类型覆盖Gradle配置、中间缓存、项目源码与可安装安装包整体约15.76MB。已有1245人浏览学习。项目完整保留Android Studio工程目录与源码结构可直接导入开发环境查看A计权滤波与实时绘图的具体实现附带apk便于快速验证运行效果对学习数字信号处理、噪声评价和移动端可视化开发均有实操参考价值。该软件工作稳定、性能良好、使用方便经校验定标后可满足一般测量需要可用于地铁运维环境噪声测量与控制场景。 第一次在手机上调通A计权声级计的时候我盯着屏幕上跳动的分贝数字看了足足五分钟。不是因为数字多惊艳而是那种“把麦克风采到的PCM数据一路算成人耳感知的dB值”的完整链路终于跑通实在让人上瘾。这个项目用Android Studio做前端采集和实时显示核心算法放在A计权上——也就是让人眼看分贝值时数值更贴近人耳的真实听觉感受。它能做的很简单打开App实时告诉你当前环境大概多少分贝并把最近一两秒的声压波形画出来。如果你正在学Android开发又对信号处理感兴趣这个项目是个特别好的交叉练习。它不依赖什么高深框架核心就三件事用AudioRecord拿麦克风数据、用FFT做频域A计权加权、再用自定义View和Handler刷新UI。三个模块拆开看都不复杂串起来就是一个能装进口袋的声级计。下面我把整个项目的设计思路、算法原理、代码实现、踩坑记录全部摊开讲新手可以直接抄作业有基础的朋友也可以重点看算法和调校部分。1. 项目思路与整体设计1.1 声级计这个需求到底在解决什么问题声级计的本质是“把声音的物理强度映射成一个人能读懂的数值”。麦克风采集到的是随时间变化的声压波形单位是Pa但这个值直接展示出来没有任何意义——人耳对声音强弱的主观感受不是线性的。所以标准做法是先对一段时间的波形求RMS值再换算成dB然后在这个基础上做频率加权修正。其中A计权是最常用的修正曲线它模拟人耳在较低音量下对中频敏感、对低频迟钝的特性。同样的声压级1000Hz的声音听起来比100Hz响很多A计权会把这个差异体现在最终读数上。市面上大家常说的“环境噪声xx分贝”绝大多数就是指A计权声级标准依据是IEC 61672。这个项目把“A计权算法”作为核心关键词就是因为不单纯测物理声压而是要测“听起来有多响”。1.2 为什么选Android平台抛开“手里正好有手机”这个现实原因Android做声级计有几个天然优势麦克风硬件是现成的不需要额外接线Android Studio的开发链路成熟采集、计算、UI一套走完很快最重要的是手机作为移动设备可以随时随地进行噪声测试这是传统声级计做不到的便携性。当然要说清楚手机声级计无法替代专业仪器因为手机麦克风的频响曲线并非完全平直也没有经过标准声学校准。但作为学习信号处理、了解声学测量的工具它完全够用而且可以帮你把“采样率”“FFT”“窗函数”“RMS”这些抽象概念全部落到真实场景里。这也是我推荐这个项目的原因它不只是一个App更像一个移动信号处理实验台。1.3 技术路线AudioRecord采集 FFT加权 自定义View显示整体方案我拆成三条线各管一摊采集线用AudioRecord以44100Hz采样率、16bit精度、单声道读取PCM数据按固定帧长比如1024点切块。计算线对每帧数据加窗后做FFT把频谱幅值按A计权频响曲线加权再逆推回时域能量得到A计权分贝值。显示线UI层用一个后台线程池做计算通过主线程Handler每200毫秒更新一次分贝大数字和波形图。之前我犹豫过到底用时域IIR滤波器还是FFT频域加权。两者都能实现A计权但FFT方案的优点是直观可控你可以在频域里看到每个频率点的贡献后续想加C计权、Z计权甚至画频谱图都很方便而且调试起来容易定位问题。最终我选了FFT这条路后面第2章会详细展开。2. A计权算法的核心原理与工程化方案2.1 A计权到底在修正什么先看一条典型的A计权频响曲线特性在1kHz附近增益接近0dB往下到100Hz大概衰减约19dB到20Hz直接衰减约50dB往上到3kHz左右有小幅抬升过了6kHz又开始衰减。也就是说A计权相当于在声压测量路径上串了一个带通性质的滤波器滤掉人耳不敏感的频率成分。工程上A计权增益可以用标准公式计算对数形式的近似公式是A(f) 20log10(R_A(f)) 2.00其中R_A(f)是关于频率f的有理函数。实Now际写代码时不用纠结这个公式的推导过程直接把频率点代进去就能得到该频点的增益系数。要注意公式里的2.00dB修正是A计权标准定义的一部分漏了它最终读数会整体偏低。2.2 三种实现路径的取舍实现A计权有三种常见路径我对比后选了第三种方案原理优点缺点时域IIR滤波器串联多级Biquad滤波器逼近A计权曲线延迟低、计算量小系数设计繁琐调参不直观较难扩展到其他计权频域逐点加权对帧数据做FFT直接按频率响应乘增益直观、可扩展、容易调试计算量略高需要处理窗函数和频谱泄漏近似经验公式避开FFT仅用总体RMS加一个固定修正值最快误差大不同频段噪声下读数偏差明显不推荐需要说明一下第三种方案在很多简化教程里出现过但实际用起来会翻车。比如同一台空调的嗡嗡声低频为主和键盘敲击声中高频为主用总体RMS加固定修正值算出来的读数会严重失真因为不同噪声的频谱分布完全不同。既然项目名字里明确写了A计权算法就不该用这种糊弄过去的近似。2.3 FFT频域加权的具体计算流程我在项目里实际跑的步骤如下从AudioRecord里读取一帧PCM数据长度是FFT点数N我取的1024。对时域数据加Hann窗减少频谱泄漏。做实数FFT得到N/21个复数频点每个频点对应频率为 i * sampleRate / N。逐频点计算A计权增益gain_i用公式 10^(A(f)/20) 转成线性幅值系数。对每个频点的FFT幅值乘以gain_i再求所有频点的能量和。用能量和求RMS值再换算成dB公式是 20log10(rms 极小值)避免对数零值报错。加上校准偏移量CALIBRATION_OFFSET得到最终显示分贝值。关于第6步这里有一个工程简化理论上分贝值应该以标准声压20μPa为参考但手机麦克风的灵敏度、前置放大增益、数字满量程对应的声压都是未知的。与其硬套标准参考值不如先把数字域的信号当作满量程0dBFS体系来算得到一个“数字相对分贝”再通过校准偏移量把它拉到接近真实声压级。这个偏移量可以结合标准声源校准或者先和参考App对比取一个经验值。3. Android端实时采集与显示实现3.1 音频采集的关键配置与踩坑点AudioRecord是Android底层音频采集的核心类配置参数直接影响数据质量。我使用的配置是采样率44100Hz、单声道、PCM 16bit。这个组合兼容性最好几乎覆盖所有真机。bufferSize不能随便定要用AudioRecord.getMinBufferSize()查询系统建议值再乘2作为实际buffer。乘2的目的是留出余量防止底层数据堆积导致read()丢帧。另外采集线程必须常驻独立Thread不能跑在主线程read()是阻塞调用放在主线程会直接把UI卡死。代码骨架如下AudioRecord audioRecord new AudioRecord( MediaRecorder.AudioSource.MIC, sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize); audioRecord.startRecording(); short[] buffer new short[bufferSize / 2]; while (isRunning) { int read audioRecord.read(buffer, 0, buffer.length); if (read 0) { // 这里把read个采样点交给算法线程 processFrame(buffer, read); } } audioRecord.stop(); audioRecord.release();这里有个容易踩的坑AudioSource.MIC在某些手机上默认开启了自动增益控制AGC和噪声抑制这会让分贝读数忽大忽小一会像“呼吸声都被放大”一会又像“世界突然安静了”。如果你的目标机型支持可以试试AudioSource.UNPROCESSED未经处理的原始流或者AudioSource.VOICE_RECOGNITION部分机型会绕过AGC。实在不行就接受MIC的默认行为但要明白读数会有波动来源不是算法问题而是底层音频处理在做手脚。3.2 实时刷新策略线程模型与UI更新节奏实时显示不等于每一帧都要刷新UI。一帧1024点的FFT在44100Hz采样率下只对应约23毫秒的音频如果每23毫秒刷新一次UI屏幕会闪到怀疑人生而且电池和CPU都吃不消。我的做法是采集线程持续跑算法线程把每帧的dB值算出后塞进一个循环数组UI层每隔200毫秒从数组里取最新值刷新一次。200毫秒对应每秒5帧肉眼看起来数字丝滑流畅CPU占用也不高。如果再配合波形图绘制波形数据也按这个节奏更新不追逐帧渲染。刷新代码用Handler.postDelayed就能实现没必要上RxJava或者协程Flow这个场景最简单可靠的方案反而是最稳的private final Handler uiHandler new Handler(Looper.getMainLooper()); private final Runnable refreshTask new Runnable() { Override public void run() { float db analyzer.getLatestDb(); tvDb.setText(String.format(Locale.US, %.1f dB, db)); waveView.updateHistory(analyzer.getDbHistory()); uiHandler.postDelayed(this, 200); } };注意不要在refreshTask里做任何耗时计算它只负责取值和绘制。FFT和A计权计算都在后台线程完成线程之间用volatile变量或加锁的容器传递结果避免并发问题。3.3 显示模块分贝数字 波形图显示层我做了两个核心组件一个大号数字TextView显示当前分贝值一个自定义View画历史波形。数字部分没什么好说的关键是波形View。我在自定义View里维护一个环形缓冲数组存最近60个dB值onDraw时用Path把这些点连成折线。再根据当前dB值切换颜色低于50dB绿色50到70橙色高于70红色。这种视觉分层很重要用户扫一眼颜色就能判断环境状态不用盯着数字解读。代码如下Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (history null || history.length 0) return; Path path new Path(); float stepX getWidth() / (float) history.length; for (int i 0; i history.length; i) { float y getHeight() - (history[i] - minDb) / (maxDb - minDb) * getHeight(); if (i 0) path.moveTo(0, y); else path.lineTo(i * stepX, y); } canvas.drawPath(path, paint); }波形图不要画原始PCM采样点一是数据量太大、绘制成本高二是视觉上全是毛刺看不清趋势。画dB历史曲线反而最能反映环境声压的变化趋势比如有人说话时波形会明显凸起安静时拉平非常有辨识度。4. 完整实操从环境准备到跑通全流程4.1 Android Studio环境准备与中文语言包如果你刚接触Android Studio我建议先把环境弄顺手。从官网下载稳定版安装后首次启动会要求配置SDK路径默认设置即可。英文界面不习惯的话可以通过Settings里的Plugins搜索“Chinese Language Pack”安装后重启就是中文界面操作成本很低对新手比较友好。需要注意这个语言包只是汉化IDE不影响项目代码和Gradle脚本。打开Android Studio后新建一个Empty Views Activity项目语言选Java或Kotlin都可以。我用的是Java因为AudioRecord和FFT相关的示例代码Java生态里最全省去转译的麻烦。minSdk设为24即可覆盖绝大多数Android 7.0以上设备compileSdk用当前SDK Manager里最新的稳定版本。4.2 工程搭建与依赖引入FFT部分我直接用JTransforms这个开源库省得手写FFT还要调bug。在build.gradle的dependencies里加上implementation com.github.wendykierp:JTransforms:3.1如果你不想额外引库也可以自己写一个64点基2的FFT但工程上没必要重复造轮子JTransforms稳定、性能好够用。清单文件里要声明录音权限uses-permission android:nameandroid.permission.RECORD_AUDIO /注意Android 6.0以上不仅要声明权限还要在运行时动态申请。我在MainActivity的onCreate里调用ActivityCompat.requestPermissions并处理回调拒绝的话就提示用户去设置里开权限。4.3 核心代码落地与联调下面我把最核心的A计权加权计算代码贴出来。这是整个项目的灵魂建议逐行看仔细public class AWeightingAnalyzer { private final int sampleRate; private final int fftSize; private final DoubleFFT_1D fft; private final double[] window; private final double[] frame; private final double[] gains; public AWeightingAnalyzer(int sampleRate, int fftSize) { this.sampleRate sampleRate; this.fftSize fftSize; this.fft new DoubleFFT_1D(fftSize); this.window new double[fftSize]; this.frame new double[fftSize * 2]; this.gains new double[fftSize / 2 1]; // 预计算窗函数和A计权增益 for (int i 0; i fftSize; i) { window[i] 0.5 * (1 - Math.cos(2 * Math.PI * i / (fftSize - 1))); } for (int i 0; i fftSize / 2; i) { double freq (double) i * sampleRate / fftSize; gains[i] linearAWeightingGain(freq); } } private double linearAWeightingGain(double freq) { double f2 freq * freq; double f20 20.6 * 20.6; double f107 107.7 * 107.7; double f737 737.9 * 737.9; double f12200 12200.0 * 12200.0; double ra (f12200 * f2 * f2) / ((f2 f20) * Math.sqrt((f2 f107) * (f2 f737)) * (f2 f12200)); double db 20 * Math.log10(ra) 2.00; return Math.pow(10, db / 20); } public float process(short[] pcm, int length) { // 短数组搬入双倍长度数组前置填充0 Arrays.fill(frame, 0); for (int i 0; i length i fftSize; i) { frame[i] pcm[i] / 32768.0 * window[i]; } fft.realForwardFull(frame); double energy 0; for (int i 0; i fftSize / 2; i) { double re frame[2 * i]; double im frame[2 * i 1]; double mag Math.sqrt(re * re im * im); double weighted mag * gains[i] / fftSize; energy weighted * weighted; } double rms Math.sqrt(energy / (fftSize / 2)); double db 20 * Math.log10(rms 1e-10); return (float) (db CALIBRATION_OFFSET); } }这里的CALIBRATION_OFFSET是校准偏移量我先不写死具体值实际联调时再调。采集线程每拿到一帧数据就调用process()拿到float型dB值后就交给UI线程刷新。把代码跑通后你会看到一个能实时跳动的分贝数这时候再回头调偏移量把数值校准到合理范围。5. 常见问题与排查技巧记录这个项目看起来简单实际联调时踩坑点不少。我把最有代表性的问题整理成一张速查表都是实测中遇到过的现象可能原因排查与解决模拟器上始终显示静音或0dB模拟器没有真实麦克风换真机调试这是最直接的解决方案点击运行时闪退并提示SecurityException没有做运行时权限申请用ActivityCompat.requestPermissions动态申请处理授权回调分贝数字乱跳忽高忽低手机AGC自动增益在作怪尝试AudioSource.UNPROCESSED或VOICE_RECOGNITION对比读数数值一直偏低或偏高固定幅度缺少校准偏移量调整CALIBRATION_OFFSET和标准声压计App对比UI卡顿、点击无响应在UI线程做了FFT或音频读取把采集和计算全部放到后台线程主线程只做取值绘制某些型号手机read()返回0或报错采样率或编码格式不兼容检查getMinBufferSize返回值必要时降到22050Hz兼容除了表格里的问题我再分享两个实操中容易忽略的细节。第一真机调试时插上USB线经常会有电流底噪耦合进麦克风导致安静环境下读数偏高。我试过拔掉充电线后数字能降好几dB所以测量时要保持手机脱离充电器。第二FFT帧长不是越大越好1024点已经能覆盖40Hz以上的频率分辨率继续加大点数虽然频率分辨率更高但实时性会变差帧与帧之间的时间窗也变长反而会让数值刷新变“钝”。还有一个关于波形图的问题如果你发现波形曲线锯齿感特别重正常因为200毫秒只更新5帧曲线本身就是阶梯状的。想要更平滑可以在绘图时做一次简单的滑动平均把最近3帧数据求平均再画线视觉上会柔和很多这属于观感优化不改变数值准确性。6. 这个项目的后续扩展空间与我的建议6.1 计权切换与统计声级A计权不是终点顺着这个架构可以很自然地做扩展。我在代码里把gains数组预计算成系数表这意味着切换C计权、Z计权只需要换一套增益公式算法骨架完全复用。C计权更接近“响不响”的物理感知Z计权就是不加权的原始声压级三个模式放在一个下拉框里切换体验感会很完整。更进一步可以做Leq等效连续声级统计也就是在一段时间内对能量取平均这是环境噪声评估里的核心指标。实现上也不复杂每计算出一帧dB值就把对应的线性能量累加到一个总和变量每秒或者每分钟结算一次平均值。这个功能加上去项目就从“炫技Demo”变成了真正能用的噪声统计工具。6.2 校准与可信度问题关于准确度一定要管理好预期。手机声级计受限于麦克风硬件和没有标准声源绝对精度比不上专业仪器但这不代表它没有价值。我的使用经验是把手机放在固定位置、固定朝向测量结果的相对变化趋势非常可信能准确反映环境噪声的升降但不要指望它和隔壁实验室的BK声级计对到小数点后一位。如果你想提高可信度可以找一个安静环境用手机和专业App同时采集一分钟计算两者的平均差值把这个差值设为校准偏移量。我做过一次同一台手机在50dB到80dB区间内和参考App的差值基本稳定在4dB以内作为日常参考完全够用。最后说点实际的体会。这个项目做下来我对“分贝”这个概念的理解从公式层面上升到了直觉层面现在听到空调声、键盘声、窗外的车流声脑子里会自动估算个大概分贝数然后用App验证误差经常在3dB以内。这种感知迁移是纯理论很难训练出来的。所以如果你也想做一个能拿得出手、又能学到真东西的Android项目这个声级计值得一试。代码量不大但数据采集、算法、UI、权限、线程调度全打通了做完它会发现以后看其他需要实时处理的App项目思路都会清晰很多。本文还有配套的精品资源点击获取
返回列表