ESP32本地声音事件检测:用FFT与状态机识别蜂鸣报警

发布时间:2026/11/3 6:20:09

ESP32本地声音事件检测:用FFT与状态机识别蜂鸣报警 上个月半夜出差回来刚把钥匙插进锁孔门没开楼道里的声控灯倒是先亮了。我一回头发现门上那个报警蜂鸣器正在“哔——”地叫是出门时门没关严触发了防撬报警。问题是我人在外面压根没听见。那一刻我特别想要一个能“听懂”蜂鸣声、并把事件推送到手机的小装置。于是就有了这个叫buzz的项目。它不是一个门锁也不是一个摄像头而是一个专门侦测“特定蜂鸣声/报警音”的本地声音事件检测器。整个项目从硬件选型、信号处理到阈值调校前后折腾了将近三周。这篇文章就把整个过程拆开写清楚包括我踩过的坑和最后稳定运行的状态机逻辑。适合正在做嵌入式声音识别、智能家居事件传感或者单纯想用ESP32做点带信号处理项目的朋友参考。1. 为什么我要自己做 buzz而不是买现成产品1.1 一个被忽略的感知维度做智能家居的人通常会接一堆传感器门磁、红外、摄像头、烟雾报警器。这些设备都有一个共同点——它们是“物理量”检测靠的是通断、电平变化或者画面差分。但声音这个维度经常被忽略尤其是“特定声音事件”的识别门槛比读一个GPIO高太多。可现实生活里大量关键信息恰恰是通过声音传递的门锁报警、烟雾报警器、燃气报警、婴儿哭声、烧水壶鸣笛。这些声音的共同特征是频段固定、节奏重复、持续时间短则一两秒长则几十秒。它们和语音识别完全是两码事——不需要理解语义只需要判断“是不是那种声”。而这件事用一块几十块钱的开发板加一个麦克风完全能搞定。那为什么不直接买成品市面上的声音报警器基本都是分贝触发看到的是“声强”判断不了“声种”。楼下装修电钻声也能把它激活然后你的手机一夜之间被垃圾通知塞满。而云端的音频识别服务每个月有免费额度限制更麻烦的是它会把我家里的原始声音上传到别人服务器。我这个人对隐私比较敏感宁可自己用本地算法做也不想为了听一个蜂鸣声把整屋子声音交给云端。1.2 项目边界先说清楚buzz 并不是要做一个通用声音识别引擎它只解决一件事在本地持续监听一旦识别到预定类型的蜂鸣声或报警音就触发通知。它需要满足几个硬指标实时性从声音出现到触发通知延迟控制在 500ms 以内误报率每天误触不得超过 1 次否则就不值得用了功耗能长期插电运行不发热不死机隐私所有音频数据只在本地处理不上云不存储原始录音。抱着这几个目标我开始选型。2. 拾音链路与硬件选型笔记2.1 麦克风模拟驻极体还是数字 I2S这是最初踩的第一个坑。我手头有几个现成的 MAX9814 模拟麦克风放大模块接 ADC 就能用看起来很简单。实际跑起来发现ESP32 的 ADC 在采集音频时有两个毛病一是噪声偏大底噪里总有周期性的电源纹波二是 ADC 的采样率虽然能配置但精度和稳定性比不上专用音频 ADC。后来我换成INMP441一颗数字 I2S 接口的 MEMS 麦克风。它的好处是数字输出直接怼到 ESP32 的 I2S 外设不经过模拟 ADC信噪比高而且模块本身很便宜十块钱左右。以下是三个常见选择的对比麦克风方案接口类型优点缺点适合场景MAX9814 模拟模块模拟/ADC便宜、上手快、增益可调底噪大、易受电源干扰实验教学、语音响度检测INMP441 数字模块I2S抗干扰强、信噪比好、采样率高模块方向有讲究正反面容易焊错声音特征识别、需要可靠采样的项目MSM261S4030H0I2S PDM更小更便宜、低功耗需要 PDM 转 PCM 代码稍复杂低功耗电池设备我最后选了 INMP441理由就一条省心。I2S 采样在 ESP32 上是标准外设配置好就能稳定拿数据不用跟 ADC 里那些奇奇怪怪的衰减系数搏斗。注意INMP441 模块有正反面焊的时候认准金手指那一面装反了采到的就是一坨噪声。2.2 ESP32 的 I2S 采样参数配置核心采样参数是这样的16kHz 采样率、16bit 位深、单声道。为什么选 16kHz 而不是 44.1kHz因为我要识别的门锁报警蜂鸣声主频一般在 2kHz 到 4kHz 之间16kHz 采样率能覆盖到 8kHz 的理论上限完全够用更高的采样率意味着每秒要处理的样本数翻倍FFT 计算量、内存占用都跟着上去对识别率没有实质帮助。在 ESP-IDF 里I2S 的驱动配置可以从老的i2s_driver_install迁移到新的i2s_std驱动。以下是一段可用的初始化代码使用 ESP-IDF 5.x#include driver/i2s_std.h void i2s_init(void) { i2s_chan_config_t chan_cfg I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER); i2s_std_config_t std_cfg { .clk_cfg I2S_STD_CLK_DEFAULT_CONFIG(16000), .slot_cfg I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO), .gpio_cfg { .mclk I2S_GPIO_UNUSED, .bclk GPIO_NUM_4, .ws GPIO_NUM_5, .dout I2S_GPIO_UNUSED, .din GPIO_NUM_18, }, }; i2s_new_channel(chan_cfg, rx_chan, NULL); i2s_channel_init_std_mode(rx_chan, std_cfg); i2s_channel_enable(rx_chan); }注意几个细节INMP441 需要dout接 ESP32 的din就是麦克风数据输出到主控输入ws是左右声道选择脚单声道模式仍然要接mclk可以不接INMP441 内部有时钟恢复。如果跑起来一点声音都采不到先用示波器或逻辑分析仪查bclk和ws有没有波形大概率是引脚映射写错了。2.3 组装时的两个易错点第一个是供电。INMP441 的供电范围是 1.6V 到 3.6V直接接 ESP32 的 3.3V 输出没问题。但注意不要让麦克风模块靠近 ESP32 的天线区WiFi 发射瞬间的射频干扰会在 I2S 数据线上耦合出杂音我实测下来把模块挪到离天线 3cm 以上底噪立刻降了一个量级。第二个是排线。I2S 的bclk、ws、data三根线最好等长、粗一点不要用那种细长的杜邦线飞线。我刚开始为了省事用了 20cm 的杜邦线采样数据里出现了大量间歇性毛刺一度以为是麦克风坏了。换成一排短杜邦线之后问题消失。如果你要固定安装建议直接把麦克风模块焊到转接板上或者用排针直连。3. 信号特征提取从一路波形到几个可用数字有了稳定的音频流下一步是处理数据。一开始我以为最简单的方法是“声音大了就报警”也就是用 RMS 音量做阈值。实测发现根本不靠谱楼上的一声关门、楼下汽车喇叭音量都能超过门锁蜂鸣声。音量只能说明“响不响”说明不了“是什么在响”。3.1 为什么要引入 FFT蜂鸣器和普通噪声最大的区别在于能量集中。门锁报警那种“哔——”声听起来是单一音调能量几乎都集中在某个频点上而关门声、说话声、电视声能量是散布在很宽的频带里的。所以只要把时域波形变成频域看“能量尖峰在哪个频率”就能把蜂鸣声和其他声音分开。这里用到的工具就是 FFT快速傅里叶变换。它能把一段 512 个采样点的时间窗口拆成 256 个频率分量。16kHz 采样率、512 点 FFT 的频率分辨率是 16000 / 512 31.25Hz也就是说能区分相隔 31Hz 左右的频点对蜂鸣声识别来说足够精细。3.2 三个核心特征量我在 buzz 里提取了三组特征分别对应“响不响”“在哪个频段响”“响得连不连续”短时能量STE一个窗口内所有采样点平方和的均值直观反映这段时间里的声音强度。它的作用相当于前置闸门只有能量高于阈值才进入下一步分析省掉大量无谓的 FFT 计算。主频峰值Dominant FrequencyFFT 结果里幅度最大的频率分量。蜂鸣声的主频是稳定的比如某款门锁在 3.2kHz而噪声的主频是跳来跳去的。判断“主频是否落在目标频段”是过滤误报最关键的一步。过零率ZCR一个窗口内信号穿越零点的次数。纯音在固定频率下过零率非常稳定语音和噪声的过零率则很高且变化剧烈。它用来区分连续音和碎声对防误报帮助很大。这三个量组合起来就能对一帧声音做出判断。三个特征的关系可以用一个粗浅的类比短时能量是“有没有人说话”主频峰值是“说话的是男是女”过零率是“在唱歌还是吵架”。单独看哪个都不够合在一起才有判别力。3.3 特征计算的代码实现我用的是 Arduino 环境配合arduinoFFT库代码结构很直观。每次 I2S 读到 512 个样本填充到vReal数组然后执行 FFT最后从结果中提取峰值。#include arduinoFFT.h #define SAMPLES 512 #define SAMPLING_FREQ 16000 double vReal[SAMPLES]; double vImag[SAMPLES]; arduinoFFT FFT arduinoFFT(vReal, vImag, SAMPLES, SAMPLING_FREQ); void compute_features() { // vReal 已经填充好 PCM 数据vImag 清零 FFT.Windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD); FFT.Compute(FFT_FORWARD); FFT.ComplexToMagnitude(); // 找 200Hz 到 8000Hz 之间的峰值 double max_mag 0; uint16_t max_index 0; for (uint16_t i 4; i SAMPLES / 2; i) { if (vReal[i] max_mag) { max_mag vReal[i]; max_index i; } } double dominant_freq (double)max_index * SAMPLING_FREQ / SAMPLES; // 顺便计算过零率这里用原始时域数据代码略 }这段代码跑一遍大约耗时 8ms 到 15ms完全跟得上实时流。我设置的是每 20ms 取一次窗口、每 5 个窗口做一次特征计算这样既不漏掉最短约 200ms 的短蜂鸣又把 CPU 占用控制在合理范围。4. 事件识别状态机与阈值标定特征算出来之后最核心的问题变成了怎么判断这些特征意味着“一次报警”如果单帧能量超了就发通知一定被误报淹死。一个很短的关门声可能能量峰值达到了但不能算事件一次真正的蜂鸣报警则通常持续 300ms 以上、甚至连续好几秒。4.1 用状态机滤掉瞬时噪声我设计了一个四状态的状态机IDLE→CONFIRMING→ALERT→COOLDOWN。这个状态机和网络用的有限状态机没什么区别但它解决的问题非常具体——把“瞬时响声”和“持续报警”分离。每次特征分析得到三个布尔量is_loud短时能量超阈值、in_band主频落在目标频带、is_tone_like过零率在合理范围。只有三个条件同时满足状态才从IDLE切到CONFIRMING。在CONFIRMING状态里如果连续 5 帧约 100ms都满足条件状态更新为ALERT此时才真正触发推送如果中途任何一帧不满足状态立刻回落IDLE。ALERT状态下系统会持续跟踪声音是否还在响。直到连续 30 帧不满足触发条件说明报警声已经停了进入COOLDOWN冷却 60 秒内不重复通知。这个设计的用意是防止同一次报警重复轰炸手机——很多商业产品忽略了这一点一次报警能推 20 条消息。下表是我最终确定的状态跳转规则当前状态触发条件下一状态IDLE单帧三条件全满足CONFIRMINGCONFIRMING连续 5 帧满足约100msALERTCONFIRMING任一帧不满足IDLEALERT连续 30 帧不满足COOLDOWNALERT仍满足ALERT保持COOLDOWN计时满 60 秒IDLE这套状态机有个好处确认窗口和松弛窗口的长度都能独立调节。想要更灵敏就缩短确认窗口想要更稳定就加长确认窗口不需要动任何特征阈值。4.2 阈值标定的详细过程阈值不能一拍脑袋定。我的做法很笨但很有效把门锁报警器搬到桌面上录了 10 次正常报警音作为正样本再把日常环境声电视、谈话、窗外车声录了 30 分钟作为负样本。用一段脚本统计这两类样本的特征分布然后取中间值作为初始阈值。以我的环境为例本底噪声的短时能量大约是 30平时说话在 200 到 800而门锁蜂鸣声稳定在 5000 到 9000所以我设的energy_threshold 1500留出的余量足够规避日常说话。主频范围设定在2800Hz - 3600Hz因为该门锁报警器实测主频是 3.2kHz加上温漂和供电波动的影响给频段留了一点余量。这里特别提醒一点主频范围宁可窄一点也不要宽。一开始我设的是 2kHz 到 5kHz结果手机铃声、微波炉“叮”都中招了误报率惨不忍睹。后来收到 1kHz 带宽误报率下降了一个数量级而目标蜂鸣声一次都没漏。窄频段可能会有漏报风险解决办法是先用频谱仪或 FFT 准确测出目标声音的主频再根据该值上下浮动 10% 到 15% 设定范围。4.3 一次完整的事件识别过程实跑效果很直观。我先模拟了三个场景播放录音里的门锁报警声、用手机播放一首流行歌、正常聊天。系统串口日志输出如下[frame 234] STE7200, dom3215Hz, ZCR42 - IDLE - CONFIRMING [frame 235] STE6900, dom3190Hz, ZCR40 - CONFIRMING [frame 236] STE7100, dom3220Hz, ZCR41 - CONFIRMING [frame 237] STE6800, dom3180Hz, ZCR39 - CONFIRMING [frame 238] STE7000, dom3205Hz, ZCR40 - ALERT: push notification看到这里可能有人问为什么STE、dom、ZCR每个窗口都在波动而不是恒定值因为蜂鸣声本身会有轻微的幅度抖动而且采样窗口起始位置不一样算出来的瞬时值自然有起伏。只要这些值都在阈值范围内、主频不跳出目标频段状态机就能稳定判断。相反放歌的时候主频不断跳变一会儿 500Hz 一会儿 3kHz基本无法连续 5 帧都落在指定频段里。5. 联动、提醒与日常可靠性的几个细节5.1 联动通知的具体实现识别到报警事件之后要怎么办我在标题里最看重的就是这一层“感知后动作”。buzz 用 ESP32 的 WiFi 连接家里路由器报警时通过 MQTT 推送到本地的 Home Assistant再由 Home Assistant 统一发通知到手机、启动家里的卧室灯闪烁。这样做的好处是通知策略完全由自己掌控不上任何第三方云。关键代码如下void send_alert(void) { char topic[64] home/buzz/alert; char payload[128]; snprintf(payload, sizeof(payload), {\event\:\buzzer\,\duration_ms\:%lu}, alert_duration_ms); mqtt_publish(topic, payload); }如果家里没有 Home Assistant也可以用 ESP32 直接发 HTTP POST 到 Server酱、PushDeer 等服务商的接口手机就能收到推送。但我个人推荐走 MQTT 到本地家庭网关原因只有一个通知链路和声音识别链路一样不依赖外部网络断网时也不至于完全失聪。5.2 供电与长时间运行注意buzz 适合找一个不碍事的角落长期插电运行。ESP32 配 INMP441整板平均功耗在 70mA 到 100mA 左右5V 供电随便一个旧手机充电头就能带动。不要用没有隔离的劣质充电头我遇到过一次电源开关瞬间电压尖峰直接把 ESP32 的 I2S 外设搞到死机重启才恢复。如果打算长期跑建议在 5V 输入和 3.3V LDO 之间并一颗 470uF 电解电容能吸收掉相当一部分开机冲击。6. 误报整治我把这三类假警报逐个消灭了这是我整个项目里花费时间最多、也最能体现“真实感”的部分。阈值设定完毕第一版能跑通之后挂着跑了三天收集到的误报有 20 多次每天七八次根本没法用。我把它当作排障问题来处理一类一类地消灭。6.1 第一类电视和音乐里的高频啸叫电影特效里的高频音效、演唱会直播的掌声和尖叫声都会短时间压过阈值甚至主频落在目标频段内。我的对策是把确认窗口加长从连续 3 帧改成连续 8 帧确认时间从约 60ms 拉长到 160ms。电视啸叫往往一闪而过很难撑满 160ms于是被状态机滤掉了。这个改动几乎没有影响真正的门锁报警——它的蜂鸣声本来就会持续响 3 秒以上。6.2 第二类相同频段的家电噪声空气净化器的电机声、老式空调压缩机的嗡嗡声虽然频率不常落在 3.2kHz但它们的谐波分量偶尔会撞进来。我的办法是增加主频稳定性判断连续 10 帧的主频最大值和最小值之差不超过 150Hz才认为是稳定音源。家电的噪声主频会漂移蜂鸣器则稳如泰山。这个判断加进去之后此类误报彻底清零。6.3 第三类同一声源重复触发一次门磁报警持续 15 秒我用 60 秒冷却期挡掉了大部分重复通知。但有一次报警停了之后 5 分钟又响了一次实际上是门锁在第二次防撬尝试。这类情况属于真实新事件不应该被冷却期挡住。目前的做法是记录最近 10 分钟内的历史报警次数超过 3 次就只发一条“多次触发”的汇总状态。反正对用户来说知道门有事就够了不需要知道它具体闹了几次。经过这三轮调整现在 buzz 的误报率稳定在一周 0 到 1 次而门锁报警的识别率在 50 次真实验证里做到了 100%。这个结果让我觉得不靠机器学习、不做云端上传用经典信号处理也能做到实用级别的声音事件检测。7. 我给同样想做声音识别的人留几句体己话这套方案不止能识别门锁蜂鸣声。换一个目标频段和阈值它就能变成烧水壶哨音检测器、烟雾报警器远程转发器、甚至婴儿哭声提醒器。核心代码框架完全不用动只需要重新录制一遍目标声音的正负样本来标定阈值。这也是我把它命名为 buzz 的初衷——学会识别“那一声响”就等于给智能家居装上了一只耳朵。如果你也准备做类似的本地声音事件检测我最想提醒的不是 FFT 也不是阈值——而是别贪多。一次只锁定一种声音把它的主频、时长、节奏特征摸清楚再考虑扩展。通用声音识别是一个深不见底的坑而特定声音检测是几天就能看到成果的捷径。先把一个应用跑稳你会在调参的过程中积累出远超资料的直觉。
延伸阅读

更多相关文章

2026/11/3 6:20:09

2024年TensorFlow全面解读:安装避坑、生态链路与PyTorch选型指南

2024年聊TensorFlow的处境,确实有点微妙。我经常看到群里讨论直接跳过安装步骤转投PyTorch,甚至有人断言TensorFlow已经凉了。但实际情况真的这么简单吗?作为一个从TensorFlow 1.x时代一路用过来的老用户,我必须说:热度…

2026/11/3 6:20:09

Univer深度解析:开源在线表格引擎的架构与落地实践

做在线表格开发的同行应该都有体会,这块骨头是真的难啃。从单元格编辑、公式计算、行列拖拽到协同光标、版本留痕,每一层都是深水区,自研一套能扛住生产环境的表格引擎,没有一两个季度下不来。但如果你正在做低代码平台、企业协作…

2026/11/3 6:20:09

CSP-S 2024四题解析:过程转结构的图论、贪心与DP建模

简介:面向信息学奥赛选手的2024年CSP-S第二轮上机真题PDF,适合备战CCF非专业级软件能力认证提高级、需要按正式赛题训练算法设计与代码实现的中学生及教练使用。压缩包内共1个PDF文件,约210KB,完整收录决斗、超速检测、染色、擂台…

2026/11/3 7:15:14

DeepSeek 使用方法全解析:官方、本地与 API 三条路径选型指南

简介:这份PDF面向初次接触DeepSeek、希望快速上手国产大模型的普通用户与技术爱好者,系统梳理了官方网页与手机APP、Ollama本地部署、API加客户端三种使用路径,并对比DeepSeek V3与R1两款模型在功能全面性与推理、编程、数学求解能力上的差异…

2026/11/3 7:15:14

Android免ROOT取应用数据:权限边界与可行通道解析

标题里问的这件事,我先说结论:不 ROOT 取应用数据,技术上确实存在几条可行通道,但每一条能拿到的东西都远比想象中少。过去几年我帮人处理过不少类似需求——换机丢了聊天记录想找回、自己写的脚本要读取某个应用的通知、做数据分…

2026/11/3 7:15:14

Java类名作为形参与返回值:引用语义与参数对象设计

写业务代码时间久了会发现一件事:真正每天都在敲的,既不是多线程也不是反射,而是把一个对象塞进方法,再让方法吐出一个对象。形参位置写类名、返回值位置写类名,这看着像最基础的语法点,实际却决定了代码是…

2026/11/3 7:15:14

HashMap原理、线程安全问题与ConcurrentHashMap优化

1. HashMap核心原理与设计初衷HashMap作为Java集合框架中最常用的数据结构之一,其设计初衷是为了在单线程环境下提供高效的键值对存取操作。理解它的底层实现原理,远比死记硬背面试题答案更有价值。1.1 基础数据结构解析HashMap的底层实现经历了从JDK1.7…

2026/11/3 7:15:14

o系列常驻智能体:OpenAI的AI工作流编排基础设施解析

1. 项目概述:o系列常驻智能体不是“新模型”,而是OpenAI在构建AI工作流基础设施最近OpenAI官宣将在DevDay上发布o系列常驻智能体,消息一出,“o系列”“常驻智能体”“DevDay”这几个词迅速冲上技术圈热搜。但我要先泼一盆冷静水&a…

2026/11/3 7:10:13

从硬编码到配置化:提升代码维护性的实践方案

1. 代码维护的艺术:从硬编码到灵活配置在软件开发中,我们经常会遇到需要根据业务状态进行排序的场景。最近我在代码审查时就遇到了这样一个典型案例:开发者直接在代码中硬编码了状态值与排序顺序的映射关系。这种实现方式看似简单直接&#x…

2026/11/2 6:08:01

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/2 6:39:12

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/3 1:04:48

STM32实战:基于MAX14431数字隔离器的SPI隔离通信方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/3 1:04:48

工业SPI隔离通信设计:MAX14431BAEE+与STM32L4A6RG实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/3 1:04:48

STM32+NRF24L01无线抢答器:硬件设计、通信协议与低功耗实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 6:08:01

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/2 6:39:12

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑