资讯详情

资讯详情

基于FFT与谐波校验的辨音识键系统:从频率精估到嵌入式实现

简介这份PDF是2023年全国大学生电子设计竞赛高职高专组K题「辨音识键奏乐系统」的完整赛题文档面向备赛的专科与本科参赛队及指导教师帮助读者快速掌握题目任务、技术指标与评分细则。资源包仅含1个PDF文件大小约233KB内容涵盖任务描述、基本要求与发挥部分、说明规则及评分标准完整呈现了以5个水杯为琴键、敲击棒与识别控制器构成的电子琴系统设计框架。文档详细列出自动演奏、敲击学习识别、注水后音高变化、打乱杯序识别等递进任务并给出设计报告中方案论证、敲击声特征分析、传感器与信号处理电路、测控程序设计等评分要点可作为方案论证与分工排期的直接依据。目前已有813人学习适合需要对照官方赛题梳理硬件选型、识别算法与实验步骤的参赛者参考。1. 从一只水杯的敲击声说起这套辨音识键方案到底要解决什么敲一下玻璃杯听个响这事谁都会。但要让一台控制器在 3 秒内、最多敲 3 次的约束下准确判断出你敲的是 8 只外观完全一样、水位各不相同的杯子中的哪一只还要同步发出对应音高的声音这就是 2023 年全国大学生电子设计竞赛高职高专组 K 题「辨音识键奏乐系统」的核心难点。它表面是个电子琴实质是一道声音特征提取 分类识别 实时发声的综合题难点不在发声而在“辨音”这两个字上。这套方案面向两类人正在准备电赛或类似声学识别课设的学生队伍以及需要一份可交付的嵌入式音频识别方案的从业者。整套系统由拾音、分析识别、显示、发声四部分组成硬件成本可控软件流程清晰但每一个环节都有必须提前想清楚的坑。下面按需求、设计、实现、避坑、验收的顺序把这份方案完整拆开。2. 需求拆解与技术选型为什么不能只靠“听个响”2.1 把题目要求翻译成可量化的技术指标题目文字很长但真正约束设计的只有几条硬指标先把它们抽出来后面所有选型都围绕这张表走。需求项量化指标对设计的直接约束识别响应时间单杯 3s 内敲击不超过 3 次单次敲击后必须在数百毫秒内出结果学习训练时间基本部分 ≤90s发挥部分 ≤120s训练流程要自动化不能靠人工逐杯调参水量区分度相邻杯水量差 ≥20ml频率分辨率要能分辨 20ml 带来的音高差杯子数量基本 5 只发挥 8 只分类器要支持 8 类且能识别“非目标杯”杯子外观完全相同、无标记、随机摆放不能靠视觉、不能靠位置只能靠声音发声同步敲击同时发出对应音高识别与发声要低延迟联动这张表里最容易被低估的是“水量差 ≥20ml”。180~220ml 的玻璃杯20ml 水带来的基频变化通常在几赫兹到十几赫兹量级具体取决于杯型和敲击位置。这意味着你的频率估计精度必须做到 1Hz 级别随便做个 FFT 取峰值是不够的。2.2 拾音方案麦克风 前置放大而不是压电片拾音有两条常见路线驻极体麦克风或 MEMS 麦克风做空气声拾取或者压电陶瓷片贴杯做振动拾取。压电片灵敏度高、抗环境噪声好但题目明确要求杯子上“不能有任何标记和附加物”压电片方案直接出局。所以只能走空气声路线。我一般会选模拟输出的驻极体麦克风模块配一级同相放大增益设在 20~40 倍可调。原因是敲击声是瞬态信号峰值大、持续时间短增益太低会淹没在底噪里太高又会削顶失真。实际调试时用示波器看敲击波形让峰值落在 ADC 量程的 60%~80% 区间最稳妥。采样率建议不低于 20kHz这样 8kHz 以内的杯声主频成分都能完整保留给后续频率估计留足余量。2.3 主控选型为什么倾向带 FPU 的 MCU 或轻量 DSP识别算法要做加窗、FFT、峰值搜索、特征比对如果主控是纯整数运算的低端单片机跑一次 1024 点 FFT 就要几十毫秒加上多次敲击取平均很容易逼近 3 秒的时间窗。通常的做法是选带硬件浮点单元和 DSP 指令的 MCU或者直接用一块轻量 DSP 芯片。这样 1024 点或 2048 点 FFT 能在几毫秒内完成留出时间做多次敲击的融合判决。发声部分可以独立处理主控识别出杯号后通过查表得到对应音高频率用 PWM 或专用音频 DAC 驱动扬声器。如果主控资源紧张发声交给一颗独立的语音/音频芯片主控只发一个杯号编码过去这样识别和发声互不抢占算力是血泪经验换来的分工方式。2.4 识别算法路线频谱峰值 谐波校验而不是机器学习很多队伍第一反应是上机器学习分类器但在这个场景里不划算。训练样本只有每杯几次敲击样本量太小模型容易过拟合而且比赛现场光照、噪声、敲击力度都在变模型泛化风险高。更稳的路线是基于物理特征的模板匹配每只杯子的敲击声有一个稳定的基频基频由杯体质量和水量共同决定水量越多基频越低。识别时提取基频再和训练阶段存下的每杯基频模板比对取最接近的一类。为了抗干扰还要加一层谐波校验真实杯声的频谱在基频的整数倍处有谐波峰而环境噪声比如说话声、桌椅碰撞的频谱结构杂乱。校验基频前三个谐波的能量占比能有效过滤掉误触发。这套方法计算量小、可解释性强答辩时也讲得清楚比“我训了个神经网络但说不清为什么准”要靠谱得多。3. 总体架构设计四个模块与一条数据流3.1 系统分层拾音层、处理层、决策层、输出层整套系统按职责切成四层层与层之间用明确的数据结构衔接这样调试时可以逐层隔离问题。拾音层麦克风 前置放大 抗混叠低通滤波输出模拟电压信号。处理层ADC 采样、触发检测、加窗、FFT、基频与谐波特征提取。决策层训练模式下的模板建立识别模式下的最近邻匹配与置信度判决。输出层杯号显示数码管或 OLED 音高发声PWM/DAC 功放 扬声器。数据流是单向的敲击 → 触发 → 采样 → 特征 → 判决 → 显示与发声。单向流的好处是每一层都能单独打点观测出问题时能快速定位是“没采到”还是“采到了但判错”。3.2 触发检测用短时能量门限抓住敲击瞬间敲击声是瞬态事件不能一直全速采样等它来。通常的做法是做一个短时能量滑动窗窗长 5~10ms当窗内能量超过自适应门限时判定为一次敲击开始录制一段 100~200ms 的音频用于分析。门限不能写死因为不同杯子的响度不同写死门限会导致小杯子漏检。自适应门限的思路是维护一个底噪估计门限设为底噪的若干倍底噪随环境缓慢更新。// 短时能量触发检测伪代码运行在主控中断或主循环中 #define WIN_LEN 128 // 5ms 25.6kHz 采样率 #define NOISE_ALPHA 0.001f // 底噪更新系数越小越稳但适应越慢 #define TRIG_RATIO 6.0f // 触发倍数实测 4~8 之间较稳 static float noise_floor 0.0f; static float energy_buf[WIN_LEN]; static int buf_idx 0; void audio_sample_isr(int16_t sample) { float x (float)sample; float e x * x; // 滑动窗能量用指数平均近似省去逐点求和 static float win_energy 0.0f; win_energy win_energy * 0.95f e * 0.05f; // 底噪缓慢跟踪只在非触发状态下更新 if (!is_triggered) { noise_floor noise_floor * (1.0f - NOISE_ALPHA) e * NOISE_ALPHA; } // 触发判决能量超过底噪的 TRIG_RATIO 倍 if (win_energy noise_floor * TRIG_RATIO !is_triggered) { is_triggered 1; record_start(); // 开始录制 150ms 分析窗 } }这段代码的关键参数是TRIG_RATIO和NOISE_ALPHA。TRIG_RATIO太小会误触发环境稍吵就疯狂进分析流程太大则小杯子敲击漏检。NOISE_ALPHA决定底噪跟踪速度比赛现场如果背景噪声变化快可以适当调大但调太大又会让底噪跟着敲击声一起涨导致后续敲击触发不了。我一般会在现场先用空敲几次观察触发情况再定这两个值。3.3 特征提取基频估计与谐波能量比录到 150ms 的分析窗后先加汉宁窗抑制频谱泄漏再做 2048 点 FFT。基频估计不用简单的“取最大峰”因为杯声的基频峰不一定是最高的有时二次谐波反而更强。更稳的做法是在 200Hz~4kHz 范围内找所有显著峰然后对每个候选峰检查其谐波结构选谐波最完整的那个作为基频。import numpy as np def extract_features(signal, fs25600, n_fft2048): 输入一段敲击音频输出基频与谐波能量比 window np.hanning(len(signal)) spectrum np.abs(np.fft.rfft(signal * window, nn_fft)) freqs np.fft.rfftfreq(n_fft, 1.0 / fs) # 只在 200Hz~4kHz 内找候选峰 band (freqs 200) (freqs 4000) band_spec spectrum[band] band_freqs freqs[band] # 找局部极大值作为候选基频 candidates [] for i in range(1, len(band_spec) - 1): if band_spec[i] band_spec[i-1] and band_spec[i] band_spec[i1]: if band_spec[i] 0.1 * band_spec.max(): candidates.append(band_freqs[i]) # 对每个候选检查谐波能量占比选最高的 best_f0, best_score 0.0, 0.0 for f0 in candidates: score 0.0 for h in range(1, 4): # 检查前三个谐波 target f0 * h idx np.argmin(np.abs(freqs - target)) score spectrum[idx] if score best_score: best_score, best_f0 score, f0 # 谐波能量比前三个谐波能量 / 全带能量用于置信度判决 harmonic_energy best_score total_energy band_spec.sum() 1e-9 harmonic_ratio harmonic_energy / total_energy return best_f0, harmonic_ration_fft取 2048 时25.6kHz 采样率下的频率分辨率约 12.5Hz单靠 FFT 峰值定位精度不够分辨 20ml 水量差。所以实际实现里还要在峰值附近做抛物线插值把频率估计精度提到 1~2Hz。harmonic_ratio是判决置信度低于某个阈值比如 0.3就认为这次敲击不可信丢弃重来避免把环境噪声当成杯子。3.4 训练与识别流程模板建立与最近邻判决训练阶段对每只杯子连续敲 3 次每次提取基频取中位数作为该杯的模板频率同时记录谐波能量比作为质量参考。识别阶段对一次敲击提取基频后计算它与所有模板频率的距离取最近的一类如果最近距离超过阈值或者谐波比过低判为“未知杯”对应发挥部分里敲到新增杯子显示 0 的要求。这里有个细节发挥部分要求打乱 8 只杯子后识别出原来的 1~5 号新增的 3 只显示 0。这意味着分类器不仅要能分 5 类还要能拒识。拒识阈值不能拍脑袋定通常的做法是在训练阶段顺便记录同一杯子多次敲击的频率波动范围把拒识阈值设为波动范围的若干倍这样阈值就随现场条件自适应了。4. 关键模块实现从采样到发声的落地细节4.1 采样与预处理抗混叠与去直流ADC 采样前必须有一级抗混叠低通滤波截止频率设在采样率的一半以下否则高频噪声会折叠进基频带直接污染频率估计。模拟前端通常用一个二阶有源低通截止 8kHz 左右配合 20kHz 以上的采样率混叠风险就很小了。采样进来后先做去直流敲击信号本身是交流的但麦克风偏置和放大电路会带来直流分量不去掉的话 FFT 的零频峰会淹没低频信息。去直流用简单的滑动平均减法即可窗长取一个基频周期的几倍既能去掉直流又不损伤低频成分。// 去直流减去滑动平均 #define DC_WIN 256 static int32_t dc_buf[DC_WIN]; static int32_t dc_sum 0; static int dc_idx 0; int16_t remove_dc(int16_t sample) { dc_sum - dc_buf[dc_idx]; dc_buf[dc_idx] sample; dc_sum sample; dc_idx (dc_idx 1) % DC_WIN; int32_t dc_mean dc_sum / DC_WIN; return (int16_t)(sample - dc_mean); }DC_WIN取 256 在 25.6kHz 下对应 10ms远大于杯声基频周期几百赫兹对应几毫秒所以不会把有用信号当直流减掉。这个模块看着简单但漏掉它导致低频判决全错的翻车案例不少。4.2 频率精估抛物线插值把分辨率拉上来前面说过 FFT 分辨率不够必须插值。抛物线插值利用峰值点和左右相邻点的幅度拟合一个抛物线顶点位置就是精估频率。def parabolic_interp(spectrum, peak_idx, freq_res): 对 FFT 峰值做抛物线插值返回精估频率 if peak_idx 0 or peak_idx len(spectrum) - 1: return peak_idx * freq_res y0 spectrum[peak_idx - 1] y1 spectrum[peak_idx] y2 spectrum[peak_idx 1] denom (y0 - 2 * y1 y2) if abs(denom) 1e-12: return peak_idx * freq_res delta 0.5 * (y0 - y2) / denom # 偏移量范围约 -0.5~0.5 return (peak_idx delta) * freq_resdelta的物理含义是真实峰值相对 FFT 格点的偏移比例乘以频率分辨率就得到修正量。这一步能把频率估计误差从 ±6Hz 压到 ±1Hz 量级是分辨 20ml 水量差的关键。注意denom接近零时要保护否则会除出天文数字。4.3 发声模块查表法生成音高识别出杯号后发声不需要复杂合成查表法最直接预先算好每个杯号对应音高频率的正弦波一个周期采样点识别后按该频率循环输出即可。如果主控有 PWM用 PWM 加低通滤波也能出正弦音如果有音频 DAC直接写采样点更干净。// 查表法发声每个杯号对应一个频率循环输出正弦表 #define SINE_TABLE_LEN 64 static const int16_t sine_table[SINE_TABLE_LEN] { /* 预生成正弦值 */ }; void play_tone(float freq, int duration_ms) { float phase_step freq * SINE_TABLE_LEN / SAMPLE_RATE; float phase 0.0f; int samples duration_ms * SAMPLE_RATE / 1000; for (int i 0; i samples; i) { int idx (int)phase % SINE_TABLE_LEN; dac_write(sine_table[idx]); phase phase_step; if (phase SINE_TABLE_LEN) phase - SINE_TABLE_LEN; } }phase_step决定音高SAMPLE_RATE要和 DAC 输出速率一致。查表长度 64 对音高精度足够因为相位累加是浮点的表长只影响波形平滑度不影响频率精度。发声和识别最好分时复用敲击瞬间先完成识别再发声避免发声的扬声器振动反过来被麦克风拾到造成误触发——这个声学耦合坑在实际调试里很常见。4.4 显示与状态机训练态、识别态、演奏态整个软件用一个状态机串起来上电进入待机按键触发自动演奏基本要求第 1 项进入训练态后逐杯敲击建模板训练完成切识别态敲击即显示杯号并发声发挥部分打乱杯子后重新训练再识别。状态机的好处是每个状态只关心自己的输入输出训练和识别的代码不会互相干扰。typedef enum { STATE_IDLE, STATE_AUTO_PLAY, STATE_TRAIN, STATE_RECOGNIZE } sys_state_t; void state_machine_tick(void) { switch (current_state) { case STATE_IDLE: if (key_auto_play()) current_state STATE_AUTO_PLAY; if (key_train()) current_state STATE_TRAIN; break; case STATE_TRAIN: if (trigger_detected()) { float f0 analyze_last_hit(); save_template(current_cup_index, f0); current_cup_index; if (current_cup_index cup_count) current_state STATE_RECOGNIZE; } break; case STATE_RECOGNIZE: if (trigger_detected()) { int cup match_template(analyze_last_hit()); display_cup(cup); play_tone(cup_freq[cup], 300); } break; default: break; } }状态切换的触发条件要防抖按键要消抖敲击触发要防止一次敲击被重复计数。训练态里每杯敲 3 次取中位数所以save_template内部还要维护一个每杯的采样缓冲凑够 3 次再算模板。5. 避坑与常见问题排查这些地方最容易翻车5.1 现象同一只杯子两次敲击识别结果不一致原因通常是敲击位置和力度不一致导致频谱变化或者分析窗起点没对齐截到了敲击的上升沿或衰减尾。解决方法是触发后延迟几毫秒再开始录分析窗确保窗内包含完整的衰减段同时训练时要求队员按比赛规定敲击杯子靠近自己一侧的上沿保持位置一致。识别时对连续 3 次敲击做多数投票单次抖动就不会直接反映到显示上。5.2 现象加了水之后识别率骤降原因是水量改变了基频但训练模板是在空杯或旧水量下建的模板和实际不匹配。这个坑在发挥部分特别致命因为水量是现场注入的。解决方法是严格按流程走注水完成后先训练再识别训练和识别之间不能换水。另外水量差 20ml 时基频变化小如果频率精估没做插值分辨率不够就会把相邻两杯判成同一类回头检查parabolic_interp是否生效。5.3 现象环境稍吵就疯狂误触发原因是触发门限写死或者底噪跟踪太慢。解决方法是把TRIG_RATIO适当调高同时加快底噪更新更稳的做法是加一层谐波校验触发后先算harmonic_ratio低于阈值直接丢弃不进识别流程。比赛现场如果有空调声、人声这一层过滤能省掉大量假阳性。5.4 现象发声时识别突然变差原因是扬声器振动通过桌面或空气耦合回麦克风被当成新的敲击。解决方法是识别和发声分时敲击后先完成识别和显示再启动发声发声期间屏蔽触发检测或者把触发门限临时抬高。如果结构上能把麦克风和扬声器隔开、加减振垫效果更好。5.5 现象训练时间超 90 秒原因是训练流程要人工逐杯确认或者每杯敲击次数太多。解决方法是训练全自动进入训练态后按顺序提示当前该敲哪只杯敲够 3 次自动切下一只全程不需要按键。5 只杯 × 3 次 × 每次几秒正常 30 秒内能完成留足余量应对现场紧张。6. 验收与答辩怎么证明这套系统真的能跑验收不是把系统跑一遍就完事而是要让评委在有限时间里看到每一项指标都被覆盖。我一般会准备一张对照表把题目要求和实测数据一一对应现场按表演示。验收项演示方法记录指标合格线自动演奏一键启动计时演奏时长≤15s空杯识别1/3/5 号杯训练后逐杯敲识别正确率、总用时全对≤90s注水识别2/4/5 号注 50/100/150ml 后训练识别正确率、用时全对≤90s五杯演奏注不同水量训练后敲击演奏发声与杯号对应音高正确八杯拒识打乱 8 杯敲原 1~5 号与新增杯杯号显示、0 显示原杯正确新增显示 0答辩时最容易被问的是“为什么用频谱峰值不用机器学习”和“20ml 水量差你怎么保证分辨”。前一个问题按第 2.4 节的物理特征路线回答强调样本少、可解释、现场稳定后一个问题直接现场演示拿两只水量差 20ml 的杯子各敲 3 次把精估频率打在屏幕上让评委看到频率差稳定大于估计误差。这比任何口头解释都有说服力。还有一个习惯值得坚持每次改动算法参数后都强制走一遍完整的训练-识别流程再记录结果不要只测单点。因为触发门限、底噪跟踪、模板匹配是联动的改一个参数可能让另一个环节的余量被吃掉。从那以后我每次调完参数都强制走一遍全流程回归宁可多花十分钟也不在答辩现场才发现某个组合条件下系统崩了。希望这套拆解能帮到你把辨音识键这道题从“听个响”做成一份拿得出手的完整方案。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →