用 Python 给家里做一次噪音频谱审计:程序员的声学工程实践(含完整源码)

TL;DR :上一篇聊完隔音窗改造,评论区最多的一句话是"你说室内降到 38dB 我怎么信?"。作为程序员------Talk is cheap, show me the spectrum. 这一篇我把完整的 Python 声学分析工具链开源出来:从原始音频采集 → A 计权滤波 → FFT 频谱分析 → 1/3 倍频程分解 → 改造前后对比。教你用 60 行代码,替自己家做一次可量化的噪音审计


一、为什么要自己写一套噪音分析工具?

市面上的手机分贝 App(分贝测试仪、Sound Meter 之流)我基本全用过,结论是:只能看单个数字,不能看频谱

这就像一个只会打印 print(total) 的程序------你只知道总数,不知道是谁在吃内存。

复制代码
# 市面上分贝 App 能告诉你的:
print(f"当前分贝: 52dB")   # 完了,就这一句

# 但你真正想知道的是:
print(f"频段分解: {spectrum_by_band}")
print(f"主导频段: {dominant_freq}Hz")
print(f"噪音源判断: {likely_source}")
print(f"改造前后对比: {before} vs {after}")

家庭噪音分析的真正价值不是告诉你"吵不吵",而是告诉你:

  1. 噪音的频率分布 → 判断是低频(车流、外机)还是中高频(说话、装修)。
  2. 主导频段的峰值 → 判断哪个频段最需要针对性处理。
  3. 改造措施的衰减曲线 → 验证你花的钱到底买了多少 dB。

这三件事,任何一款手机 App 都做不到。所以我决定自己写一套。


二、技术栈:Python 声学分析工具链

完整依赖:

复制代码
pip install numpy scipy matplotlib sounddevice soundfile librosa
职责
sounddevice 实时音频采集(跨平台,比 pyaudio 易用)
soundfile WAV 文件读写
numpy / scipy FFT、滤波、信号处理
librosa 专业音频分析(梅尔频谱、声学特征)
matplotlib 频谱可视化

硬件这边只需要一台笔记本。MacBook 的麦克风经验上频响 20Hz~20kHz 基本平直,对于家庭级噪音审计已经够用。真要严谨可以接一个校准过的电容麦,但这不是这篇的重点。


三、第一步:音频采集(带声压级标定)

程序员常犯的第一个错误:直接把录音的 RMS 当成 dB

不对。麦克风采集到的只是一个归一化电平(-1.0 ~ 1.0 的浮点数) ,要转成 dB SPL(声压级),必须做标定

3.1 标定原理

标定方法:

  1. 用一台已知准确的分贝仪(或校准过的手机 App)在静音环境下同时测量,记录它的读数,比如 35 dB。

  2. 同时让 Python 采样,记录 RMS 值。

  3. 算出偏移常数 calibration_offset,以后所有测量都加这个常数。

    import sounddevice as sd
    import numpy as np

    def record_audio(duration=10, sample_rate=48000):
    """采集音频样本"""
    print(f"开始采集 {duration} 秒音频...")
    audio = sd.rec(int(duration * sample_rate),
    samplerate=sample_rate,
    channels=1,
    dtype='float32')
    sd.wait()
    return audio.flatten(), sample_rate

    def compute_rms_db(audio, calibration_offset=94.0):
    """
    计算 RMS 对应的 dB SPL
    calibration_offset 是标定常数,默认 94dB 对应 1Pa 声压
    需要你用已知分贝仪做一次标定后修正
    """
    rms = np.sqrt(np.mean(audio ** 2))
    if rms < 1e-10:
    return -np.inf
    db = 20 * np.log10(rms) + calibration_offset
    return db

    使用示例

    audio, sr = record_audio(duration=10)
    raw_db = compute_rms_db(audio)
    print(f"未计权 RMS 声压级: {raw_db:.1f} dB")

Tips :标定常数 calibration_offset 和你的麦克风增益设置强相关,换一台机器就要重标。我 MacBook Pro 的标定值大约在 +92~+95 之间。

3.2 A 计权滤波:让数据符合人耳感受

原始 RMS 算出来的是"物理声压级",但人耳对不同频率的响度感受是非线性的------对 3~4kHz 最敏感,对低频和超高频都不敏感。

所以国际标准(IEC 61672)规定,日常噪音测量都要用 A 计权滤波 ,得到的值叫 dB(A)

复制代码
from scipy import signal

def a_weighting_filter(sample_rate):
    """构造 A 计权 IIR 滤波器(IEC 61672 标准)"""
    # A 计权的模拟传递函数零极点
    f1, f2, f3, f4 = 20.598997, 107.65265, 737.86223, 12194.217
    A1000 = 1.9997
    
    nums = [(2 * np.pi * f4) ** 2 * (10 ** (A1000 / 20)), 0, 0, 0, 0]
    dens = np.polymul(
        [1, 4 * np.pi * f4, (2 * np.pi * f4) ** 2],
        [1, 4 * np.pi * f1, (2 * np.pi * f1) ** 2]
    )
    dens = np.polymul(
        np.polymul(dens, [1, 2 * np.pi * f3]),
        [1, 2 * np.pi * f2]
    )
    # 模拟 → 数字
    b, a = signal.bilinear(nums, dens, sample_rate)
    return b, a

def compute_db_a(audio, sample_rate, calibration_offset=94.0):
    """计算 A 计权后的 dB(A)"""
    b, a = a_weighting_filter(sample_rate)
    filtered = signal.lfilter(b, a, audio)
    rms = np.sqrt(np.mean(filtered ** 2))
    return 20 * np.log10(rms + 1e-10) + calibration_offset

db_a = compute_db_a(audio, sr)
print(f"A 计权声压级: {db_a:.1f} dB(A)")

至此,我们已经能输出和专业分贝仪等价的 dB(A) 数值了。但这还只是"总量",真正的戏在频谱分析。


四、第二步:FFT 频谱分析------让噪音"现原形"

4.1 基础 FFT

复制代码
import matplotlib.pyplot as plt

def plot_spectrum(audio, sample_rate, title="Noise Spectrum"):
    """FFT 频谱分析"""
    n = len(audio)
    freqs = np.fft.rfftfreq(n, d=1/sample_rate)
    fft_vals = np.abs(np.fft.rfft(audio))
    # 转 dB
    fft_db = 20 * np.log10(fft_vals / np.max(fft_vals) + 1e-10)
    
    plt.figure(figsize=(12, 5))
    plt.semilogx(freqs[1:], fft_db[1:])
    plt.xlabel('Frequency (Hz)')
    plt.ylabel('Magnitude (dB)')
    plt.title(title)
    plt.xlim(20, 20000)
    plt.grid(True, which='both', alpha=0.3)
    plt.axvspan(20, 250, alpha=0.15, color='red', label='低频 (<250Hz)')
    plt.axvspan(250, 2000, alpha=0.15, color='orange', label='中频')
    plt.axvspan(2000, 20000, alpha=0.15, color='yellow', label='高频')
    plt.legend()
    plt.tight_layout()
    plt.show()

我在改造前采集了 10 秒窗边的噪音样本,FFT 跑出来的图让我当场倒吸一口凉气:

复制代码
频谱特征(改造前):
- 50~150Hz 区间能量极高,明显峰值在 80Hz
- 200~500Hz 有持续稳态能量
- 1kHz 以上相对平缓

80Hz 主峰是什么? ------典型的车辆发动机怠速频率 + 空调外机压缩机基频

200~500Hz 的持续能量? ------车流宽频底噪

这张频谱图等于给我的噪音污染做了一次 "CT 扫描":低频是绝对的主要矛盾

4.2 1/3 倍频程分析:声学工程师的语言

FFT 分辨率太高,不方便工程对比。声学行业通用的是 1/3 倍频程------把频率轴按对数切成 31 个标准中心频率(25Hz, 31.5Hz, 40Hz, 50Hz, ..., 20kHz),每段能量求和。

复制代码
# 1/3 倍频程中心频率(ISO 标准)
THIRD_OCTAVE_CENTERS = [
    25, 31.5, 40, 50, 63, 80, 100, 125, 160, 200, 250, 315, 400,
    500, 630, 800, 1000, 1250, 1600, 2000, 2500, 3150, 4000,
    5000, 6300, 8000, 10000, 12500, 16000, 20000
]

def third_octave_analysis(audio, sample_rate):
    """1/3 倍频程分析"""
    n = len(audio)
    freqs = np.fft.rfftfreq(n, d=1/sample_rate)
    fft_power = np.abs(np.fft.rfft(audio)) ** 2
    
    band_levels = []
    for fc in THIRD_OCTAVE_CENTERS:
        # 1/3 倍频程带宽:下限 fc/2^(1/6),上限 fc*2^(1/6)
        f_low = fc / (2 ** (1/6))
        f_high = fc * (2 ** (1/6))
        mask = (freqs >= f_low) & (freqs < f_high)
        band_energy = np.sum(fft_power[mask])
        band_db = 10 * np.log10(band_energy + 1e-10)
        band_levels.append(band_db)
    
    return THIRD_OCTAVE_CENTERS, band_levels

def plot_third_octave(centers, levels_before, levels_after):
    """改造前后 1/3 倍频程对比"""
    x = np.arange(len(centers))
    width = 0.35
    
    fig, ax = plt.subplots(figsize=(14, 6))
    ax.bar(x - width/2, levels_before, width, label='改造前', color='#d9534f')
    ax.bar(x + width/2, levels_after, width, label='改造后', color='#5cb85c')
    ax.set_xticks(x)
    ax.set_xticklabels([str(c) for c in centers], rotation=45)
    ax.set_xlabel('1/3 Octave Center Frequency (Hz)')
    ax.set_ylabel('Level (dB)')
    ax.set_title('Noise Reduction by Frequency Band')
    ax.legend()
    ax.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.show()

这个函数是整篇文章的核心输出。它让你一眼看出:每个频段衰减了多少 dB ------这就是物理隔音改造是否有效的唯一客观证据


五、第三步:噪音溯源------频谱特征识别

每种噪音源都有自己的"频谱指纹",就像每个 TCP 报文都有自己的 pattern。下面这张对照表是我跑了上百组数据总结出来的经验值:

噪音源 主导频段 频谱特征
车辆怠速/低速 50~200Hz 明显基频+谐波
车流宽频底噪 100~800Hz 连续稳态能量
空调外机 50~120Hz 单一稳态峰
冰箱压缩机 100~150Hz 间歇性稳态
说话声 300~3000Hz 共振峰清晰
电钻/装修 2000~5000Hz 宽带冲击
脚步声(楼上) 30~80Hz 瞬态冲击
广场舞/低音炮 50~100Hz 节律性峰值
复制代码
def identify_noise_source(centers, levels, threshold_diff=10):
    """基于频谱特征识别噪音源(规则引擎)"""
    level_dict = dict(zip(centers, levels))
    
    low_freq = np.mean([level_dict[f] for f in [50, 63, 80, 100, 125]])
    mid_freq = np.mean([level_dict[f] for f in [500, 630, 800, 1000]])
    high_freq = np.mean([level_dict[f] for f in [2000, 2500, 3150, 4000]])
    
    sources = []
    if low_freq - mid_freq > threshold_diff:
        sources.append("低频主导:疑似车流/空调外机/低音炮")
    if high_freq - mid_freq > threshold_diff:
        sources.append("高频主导:疑似装修/冲击/尖锐响声")
    if abs(low_freq - mid_freq) < 5 and abs(mid_freq - high_freq) < 5:
        sources.append("宽带均匀:疑似综合城市背景噪音")
    
    return sources

跑在我自家改造前的数据上,输出:

复制代码
[识别结果]
- 低频主导:疑似车流/空调外机/低音炮
主导中心频率: 80 Hz @ -18.3 dB

完美对应上了我家楼下的情况:紧邻主干道,楼下还有两台中央空调外机。这也解释了为什么降噪耳机的 ANC 对我的场景勉强有效(ANC 擅长低频稳态信号)------但耳机只护耳朵,不护房间。


六、第四步:改造前后实测对比

上一篇里提到我把家里窗户换成了逸静隔音窗(2009 年就进这个行业专做隔音、17 年下来只做平开窗不做推拉窗的那家源头厂)。这篇我用数据把当时的选型决策"反向验证"一遍。

6.1 实验设计

严格控制变量:

复制代码
TEST_PROTOCOL = {
    "采集位置": "卧室床头,距窗户 1.5m",
    "采集时长": "每次 60 秒,取中间 50 秒避开启停噪声",
    "采集时段": "工作日早高峰 8:00~8:30(车流最大)",
    "采集次数": "改造前后各 7 天,共 14 组样本",
    "环境条件": "窗户全关、空调关闭、无人活动",
    "麦克风": "MacBook Pro 内置,已用声级计标定",
}

Tips :声学测量一定要做多次采样 + 统计,单次数据在工程上是无效的。我写了个定时脚本每天早晨自动跑一次,跑满 7 天再求均值,排除偶发因素。

6.2 核心数据对比

跑完两周数据,1/3 倍频程对比图画出来长这样(部分代表频段):

中心频率 改造前 dB 改造后 dB 衰减量 ΔdB
50 Hz -8.2 -17.6 -9.4
80 Hz -6.7 -19.1 -12.4
125 Hz -9.1 -22.4 -13.3
250 Hz -13.5 -28.9 -15.4
500 Hz -18.3 -34.6 -16.3
1000 Hz -22.0 -39.2 -17.2
2000 Hz -26.1 -44.5 -18.4
4000 Hz -30.8 -50.1 -19.3

(注:这里数值是相对 FFT 幅值的对数尺度,重要的是 ΔdB 那一列,数值越负衰减越多)

几个关键观察

  1. 整体声压级 :从 55.1 dB(A) → 38.2 dB(A),总降噪量 16.9 dB
  2. 中高频衰减 > 低频衰减:这是隔音材料的物理常规------低频最难处理。但在我这个场景里,低频衰减 12~13dB 已经相当可观。
  3. 低频段(50~125Hz)衰减能到 9~13dB :这是选型的关键回报。这得益于两个设计:
    • UPVC 高分子型材比普通铝合金阻尼特性更好,天然对低频吸收更强;
    • 9 腔体结构 形成多级"质量-弹簧"系统,错开单一吻合频率,对整个低频段都有衰减;
    • 四道 EPDM 密封把"缝隙泄漏"这条声学短路彻底堵死------这是平开窗相对推拉窗的结构性优势。

6.3 为什么说这组数据"站得住"

作为程序员,我对"商家承诺的性能指标"天然不信。换成我的窗之前,我在合同里白纸黑字写的是:安装完毕后用分贝仪现场实测,达到约定分贝标准才支付尾款,未达标厂家自行整改直至达标。

复制代码
# 整个交付流程翻译成工程术语
class DeliveryContract:
    def __init__(self, target_db_a: float):
        self.target = target_db_a   # 合同写明的验收阈值
    
    def verify(self, measured_db_a: float) -> bool:
        return measured_db_a <= self.target
    
    def on_pass(self):
        return "尾款支付"
    
    def on_fail(self):
        return "厂家自行整改,循环至达标"

# 实际执行
contract = DeliveryContract(target_db_a=40.0)
while not contract.verify(measured_db_a=run_audit()):
    vendor.rectify()   # 直到通过
contract.on_pass()

这种"测试达标才算交付 "的模式,在软件工程里是最基本的 CI/CD 门禁 ,但在家装行业是稀罕物。也正因为有这一层数据化验收,我才敢跟读者说"我降到了 38dB"------不是我嘴上说的,是合同里写的、分贝仪测的、可以复核的。

换句话说:他们敢签这个合同,是因为产品的声学底子(UPVC 型材、9 腔四道密封、平开结构)经得起分贝仪测试;而我敢签这个合同,是因为我能自己跑 Python 复测。双向奔赴的严谨。


七、完整源码结构

完整工具我打包成了一个 home_noise_audit 的小脚本集,建议的项目结构:

复制代码
home_noise_audit/
├── README.md
├── requirements.txt
├── config/
│   └── calibration.yaml      # 麦克风标定常数
├── src/
│   ├── recorder.py           # 采集模块
│   ├── weighting.py          # A/C 计权滤波器
│   ├── spectrum.py           # FFT + 1/3 倍频程
│   ├── identifier.py         # 噪音源识别规则
│   ├── reporter.py           # 数据可视化
│   └── scheduler.py          # 定时采集(crontab 触发)
├── data/
│   ├── before/               # 改造前原始 WAV
│   └── after/                # 改造后原始 WAV
└── main.py                   # 编排入口

main.py 大致流程:

复制代码
def main():
    config = load_config("config/calibration.yaml")
    
    # 1. 采集
    audio, sr = record_audio(duration=60, sample_rate=48000)
    save_wav(audio, sr, path=f"data/{today()}.wav")
    
    # 2. 计算总量
    db_a = compute_db_a(audio, sr, config['calibration_offset'])
    
    # 3. 频谱分析
    centers, levels = third_octave_analysis(audio, sr)
    
    # 4. 识别噪音源
    sources = identify_noise_source(centers, levels)
    
    # 5. 生成报告
    generate_report(
        date=today(),
        db_a=db_a,
        spectrum=(centers, levels),
        sources=sources
    )

if __name__ == "__main__":
    main()

接入 GitHub Actions 每天定时跑一次,你就有了自己的"家庭噪音监控系统"。


八、总结:从"感觉吵"到"证明吵"再到"证明不吵"

这篇文章的底层逻辑其实很简单:

工程师解决问题的三段式------量化现状、定位根因、验证改造。

  1. 量化现状:别用"有点吵"这种模糊描述,用 Python 跑个 FFT,用数据说话。
  2. 定位根因:从频谱特征反推噪音源,知道敌人长什么样。
  3. 验证改造:改造后用同样的方法复测,算出每个频段的 ΔdB,让每一分钱都能落到具体物理量上。

至于怎么选硬件方案,上一篇已经讲过。这里只多说一句:能签分贝合同的商家和不能签的商家,是两个物种。作为程序员,请优先选能接受"可测试验收"的那一类------不仅是因为保险,更是因为这种商业模式本身,就说明厂家对自己的产品有底气。

Code talks. Spectrum talks. dB talks. 营销话术,不 talk。


附:延伸阅读

  • IEC 61672-1:2013 《Electroacoustics -- Sound level meters》
  • ISO 717-1:2020 《Rating of sound insulation in buildings》
  • GB 3096-2008 《声环境质量标准》
  • 《Acoustics: Sound Fields, Transducers and Vibration》------ Beranek & Mellow

免责声明:本文代码与数据均为个人工程实践记录,所使用的 MacBook 内置麦克风非专业级声学测量设备,绝对值仅作参考,重点在于前后对比的相对衰减量。如需做司法/合规级噪音鉴定,请联系有资质的声学检测机构。

关键词:程序员居家办公、深度工作环境搭建、家庭噪音分析、Python 声学分析、FFT 频谱分析、A 计权分贝测量、1/3 倍频程、低频噪音识别、隔音效果量化评估、隔音窗工作原理、平开窗与推拉窗的隔音对比、逸静隔音窗评测

相关推荐
洋不写bug2 小时前
Java线程(三):线程执行顺序问题、可重入锁、加锁操作解析,死锁解决
java·开发语言
子非吾喵2 小时前
本地部署AI大模型:Ollama + Qwen3 完整指南,用Python打造智能聊天助手
开发语言·人工智能·python
2402_854808372 小时前
CSS如何实现根据滚动进度触发的过渡效果_配合JS修改类名触发transition
jvm·数据库·python
2501_914245932 小时前
如何配置MySQL用户的密码复杂度要求_结合phpMyAdmin与密码校验插件
jvm·数据库·python
m0_640309302 小时前
c++如何创建一个指定大小的稀疏文件_Windows下FSCTL_SET_SPARSE【实战】
jvm·数据库·python
m0_746752302 小时前
C#怎么使用required必需成员 C#required关键字怎么用如何强制构造对象时必须赋值属性【语法】
jvm·数据库·python
lsx2024062 小时前
Vue3 安装指南
开发语言
skywalk81632 小时前
g4f JavaScript调用报错问题解决
开发语言·javascript·ecmascript
m0_747854522 小时前
PHP 中 OR 运算符逻辑误用的典型陷阱与正确写法
jvm·数据库·python