简介:面向城市轨道交通噪声测量场景的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/2+1个复数频点,每个频点对应频率为 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:name="android.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 校准与可信度问题
关于准确度,一定要管理好预期。手机声级计受限于麦克风硬件和没有标准声源,绝对精度比不上专业仪器,但这不代表它没有价值。我的使用经验是:把手机放在固定位置、固定朝向,测量结果的相对变化趋势非常可信,能准确反映环境噪声的升降;但不要指望它和隔壁实验室的B&K声级计对到小数点后一位。
如果你想提高可信度,可以找一个安静环境,用手机和专业App同时采集一分钟,计算两者的平均差值,把这个差值设为校准偏移量。我做过一次,同一台手机在50dB到80dB区间内,和参考App的差值基本稳定在4dB以内,作为日常参考完全够用。
最后说点实际的体会。这个项目做下来,我对“分贝”这个概念的理解从公式层面上升到了直觉层面:现在听到空调声、键盘声、窗外的车流声,脑子里会自动估算个大概分贝数,然后用App验证,误差经常在3dB以内。这种感知迁移是纯理论很难训练出来的。所以如果你也想做一个能拿得出手、又能学到真东西的Android项目,这个声级计值得一试。代码量不大,但数据采集、算法、UI、权限、线程调度全打通了,做完它会发现,以后看其他需要实时处理的App项目,思路都会清晰很多。
本文还有配套的精品资源,点击获取