简介:这份PDF是2023年全国大学生电子设计竞赛高职高专组K题「辨音识键奏乐系统」的完整赛题文档,面向备赛的专科与本科参赛队及指导教师,帮助读者快速掌握题目任务、技术指标与评分细则。文档完整给出基本要求与发挥部分:一键自动演奏短曲、空杯与注水杯的敲击学习识别、5杯注水演奏乐曲、8杯打乱后识别原1~5号杯并显示杯号等,并附水杯规格、敲击棒敲击区、3秒内最多敲击3次等测试规则。评分标准细化为设计报告20分、基本要求50分、发挥部分50分,共120分,其中敲击声特征分析、水量对敲击声影响分析、传感器与信号处理电路设计等条目可直接指导方案论证与报告撰写。资源包仅含1个PDF文件,约233KB,轻量易读,已有813人学习。读者可据此明确硬件选型、识别算法与实验步骤,是赛前研读题目、规划分工与自查得分的实用依据。
1. 敲杯子就能弹出音阶:一份被低估的声学识别赛题拆解
五个玻璃杯排成一排,拿实心棒敲一下,控制器不光要认出你敲的是第几个杯子,还得立刻发出对应音高的声音——这不是玩具,是 2023 年电赛高职高专组 K 题「辨音识键奏乐系统」的核心任务。很多人第一眼觉得这题简单:敲击声识别嘛,做个 FFT 找峰值频率不就完了。真上手才发现,空杯和装了 150ml 水的杯子,敲击频谱差异远比想象中微妙,而且题目还要求现场学习训练、打乱顺序后仍能认出来。这份赛题 PDF 把任务、要求、评分标准写得相当细,适合准备声学信号处理、嵌入式识别类项目的同学拿来当实战蓝本,也适合想练「传感器+算法+人机交互」完整链路的从业者拆着看。
2. 从敲击声到杯号:识别链路的四个技术决策点
2.1 为什么不能只靠固定阈值判杯号
题目最狠的地方在于「现场学习训练」这个约束。基本要求第(2)项让你先敲 1、3、5 号空杯做训练,然后逐个敲击显示杯号;第(3)项又换成 2、4、5 号杯分别装 50、100、150ml 水再训练。这意味着系统不能预置一套固定频率表,必须在每次测试开始时采集样本、建立当前这批杯子的特征库。发挥部分更绝:8 个杯子打乱排序,还要从里面认出原来的 1~5 号,敲到新增杯子显示 0。
固定阈值方案在这里会翻车,原因很直接:玻璃杯的共振频率受水量、杯壁厚度、敲击位置、敲击力度共同影响。同一批「外观相同」的杯子,个体差异就足以让空杯基频偏移几十赫兹。更别说注水后频率整体下移,50ml 和 100ml 之间的差距可能只有十几赫兹。所以识别策略必须是「先学后认」——训练阶段提取每个杯子的声学指纹,识别阶段做最近邻匹配。
常见做法是提取敲击声的频谱峰值序列,取前 3~5 个显著峰作为特征向量,训练时每个杯子敲 3 次取平均,识别时算欧氏距离或余弦相似度,取最小距离对应的杯号。这个方案在 3 秒内敲 3 次的规则下完全够用,计算量也小,单片机跑得动。
2.2 拾音电路与采样参数怎么定
敲击声是典型的瞬态信号,持续时间短、能量集中在低频段。玻璃杯的基频大致落在 500Hz~4kHz 区间,水量越多频率越低。采样率按奈奎斯特准则取 8kHz 以上就够,但为了留余量、方便做峰值检测,我一般会设到 16kHz 或 20kHz。采样位数 12 位足够,8 位在弱敲击时信噪比不够。
拾音前端建议用驻极体麦克风加一级运放,增益调到敲击时 ADC 读数接近满量程的 70%~80%,留出余量防止削顶。这里有个容易被忽略的点:敲击瞬间的声压峰值很高,增益调太大直接饱和,频谱全是谐波失真,峰值检测就废了。稳妥的做法是加一个自动增益或者软件限幅,训练阶段先敲一次看峰值,动态调增益。
触发检测用能量阈值法:连续采样,算短时能量,超过阈值且持续超过 2ms 就认为是一次有效敲击,开始录 100~200ms 的数据做 FFT。窗口长度取 1024 点(16kHz 采样约 64ms)比较合适,太短频率分辨率不够,太长会把环境噪声带进来。
import numpy as np def detect_hit(signal, fs=16000, threshold_ratio=0.3, min_duration_ms=2): """能量阈值触发检测,返回敲击起始索引""" frame_len = int(fs * 0.005) # 5ms 一帧 energy = np.array([ np.sum(signal[i:i+frame_len]**2) for i in range(0, len(signal)-frame_len, frame_len) ]) peak = np.max(energy) if peak < 1e-6: return None thresh = peak * threshold_ratio min_frames = max(1, int(min_duration_ms / 5)) for i in range(len(energy) - min_frames): if all(energy[i:i+min_frames] > thresh): return i * frame_len return None这段代码做的是粗触发,threshold_ratio控制灵敏度,设 0.3 意味着能量超过峰值的 30% 才触发,能过滤掉环境底噪。min_duration_ms防止单点毛刺误触发。实际部署时这个函数跑在中断里,检测到触发后切到采集模式录 1024 点做后续处理。
2.3 特征提取:峰值序列比整段频谱更稳
直接拿 1024 点 FFT 的幅度谱做模板匹配,问题是数据维度太高、对敲击力度敏感。更稳的做法是只取前几个显著峰的频率值。具体流程:加汉宁窗抑制频谱泄漏,做 FFT,取幅度谱,找局部极大值,按幅度排序取前 4 个峰,记录频率和相对幅度比。
def extract_features(signal, fs=16000, n_peaks=4): """提取敲击声的峰值频率特征""" windowed = signal * np.hanning(len(signal)) spectrum = np.abs(np.fft.rfft(windowed)) freqs = np.fft.rfftfreq(len(signal), 1/fs) # 只在 300Hz~5kHz 有效频段找峰 valid = (freqs > 300) & (freqs < 5000) spec_valid = spectrum[valid] freq_valid = freqs[valid] # 找局部极大值 peaks = [] for i in range(1, len(spec_valid)-1): if spec_valid[i] > spec_valid[i-1] and spec_valid[i] > spec_valid[i+1]: peaks.append((freq_valid[i], spec_valid[i])) peaks.sort(key=lambda x: x[1], reverse=True) top = peaks[:n_peaks] # 归一化幅度,消除敲击力度影响 max_amp = top[0][1] if top else 1.0 return np.array([p[0] for p in top]), np.array([p[1]/max_amp for p in top])n_peaks=4是经验值,玻璃杯敲击声通常前 3~4 个峰最稳定,再多就进入噪声区了。频率范围卡在 300Hz~5kHz 是因为低于 300Hz 是桌面振动和环境低频噪声,高于 5kHz 能量衰减快、信噪比差。幅度归一化这一步很关键,它让特征对「敲得重还是轻」不敏感,只反映频率结构。
2.4 训练与识别的匹配逻辑
训练阶段每个杯子敲 3 次(题目允许 3 秒内敲 3 次),每次提取特征向量,3 次取平均存为该杯号的模板。识别阶段敲一次,提取特征,和所有模板算距离,取最小。距离度量用频率的加权欧氏距离,权重按峰值幅度给,主峰权重高。
def match_cup(features, templates): """features: (freqs, amps), templates: {cup_id: (freqs, amps)}""" freqs, amps = features best_id, best_dist = None, float('inf') for cup_id, (t_freqs, t_amps) in templates.items(): n = min(len(freqs), len(t_freqs)) if n == 0: continue # 频率差按幅度加权 diff = (freqs[:n] - t_freqs[:n]) * amps[:n] dist = np.sqrt(np.sum(diff**2)) if dist < best_dist: best_dist, best_id = dist, cup_id return best_id, best_dist这里有个边界要处理:发挥部分第(2)项要求敲到新增杯子显示 0。新增杯子的水量是 30、100、150ml,和原有 5 个杯子的水量分布可能重叠,光靠最近邻会误判成某个已知杯号。解决办法是设一个距离阈值,最小距离超过阈值就判为「未知杯」,显示 0。阈值在训练阶段用类内距离的 2~3 倍来标定,训练时记录每个杯子 3 次敲击之间的最大距离,取全局最大值乘个系数当门限。
3. 从赛题要求到可运行系统:分阶段落地步骤
3.1 硬件搭建与信号链验证
先别急着写识别算法,把信号链跑通是第一步。硬件清单不复杂:驻极体麦克风模块、运放(如 NE5532 或轨到轨的 MCP6002)、单片机(STM32F103 或同级别带 12 位 ADC 的型号)、OLED 或数码管显示、蜂鸣器或 DAC 发声。麦克风输出接运放同相放大,偏置到 ADC 量程中点,输出接 ADC 引脚。
验证信号链的方法:写个最简单的程序,ADC 连续采样,通过串口把波形打到电脑上看。敲一下杯子,看波形是不是一个快速衰减的振荡包络。如果波形削顶,调小增益;如果幅度太小淹没在噪声里,调大增益。这一步花半小时能省后面几小时的调试。
发声部分按题目要求「音高对应」,可以用 PWM 驱动蜂鸣器,改变 PWM 频率来变调。五个杯号映射到五个音高,比如 do re mi fa sol 对应 262、294、330、349、392Hz。自动演奏乐曲就是把音符序列和节拍存成数组,定时切换 PWM 频率。
3.2 训练模式的交互设计
题目对时间卡得很死:基本要求 90 秒内完成训练加逐个识别,发挥部分 120 秒。交互流程必须极简。我的设计是:上电后按一个键进入训练模式,屏幕提示「敲 1 号杯」,选手敲 3 次,系统自动采集 3 次取平均,然后提示「敲 2 号杯」,依次到 5 号。训练完成后自动进入识别模式,敲哪个显示哪个。
关键细节:每次敲击后要有明确反馈,比如蜂鸣器短响一声或者屏幕闪一下,让选手知道这次采集有效。如果能量太低没触发,屏幕提示「重敲」,避免选手以为采集成功实际没录到。这个反馈机制在赛场上能救命,血泪经验是有人训练时敲太轻,系统静默没采集,后面识别全乱。
训练数据存哪里?5 个杯子各 4 个峰值频率加 4 个幅度,一共 40 个 float,片内 Flash 或 EEPROM 都够。不用掉电保存,每次测试重新训练,所以放 RAM 里也行,但建议放 Flash 防止意外复位丢数据。
3.3 识别模式的实时性保障
识别模式的主循环:等触发 → 录 1024 点 → FFT → 提特征 → 匹配 → 显示杯号 + 发声。这一串在 STM32F103 上跑,FFT 用 CMSIS-DSP 库的arm_rfft_fast_f32,1024 点大概 1~2ms,特征提取和匹配都是几十微秒级,整体延迟远小于人耳感知阈值。
要注意的是触发后的采集不能被打断。用 DMA 加 ADC 连续采样,触发后开 DMA 传 1024 点到缓冲区,传完中断里置标志位,主循环处理。别在中断里做 FFT,会阻塞其他中断。显示刷新和发声可以放在匹配完成后异步做,不占关键路径。
提示:FFT 之前一定要加窗。不加窗的话,敲击声的瞬态特性会导致严重的频谱泄漏,峰值频率偏移能到几十赫兹,直接毁掉匹配精度。汉宁窗是性价比最高的选择,计算量小、旁瓣衰减够用。
3.4 发挥部分的打乱排序处理
发挥部分第(2)项是整题最难的:8 个杯子打乱,要认出原来的 1~5 号,新增的 3 个显示 0。这里的逻辑是训练阶段只训练 5 个目标杯(按杯底编号),识别阶段对每个被敲的杯子做匹配,距离小于阈值就显示对应编号,大于阈值显示 0。
坑在于新增杯子的水量(30、100、150ml)可能和 5 个目标杯的水量接近,导致特征向量距离很近。缓解手段有两个:一是训练时多敲几次(题目允许 3 次),取平均降噪;二是匹配时不仅看频率距离,还看峰值幅度的相对关系,因为不同水量下高次峰的能量分布会有差异。实测下来,单靠频率前 4 峰,30ml 和空杯的区分度大概在 20~30Hz,阈值设 15Hz 左右能分开大部分情况,但边界样本还是会翻车,所以训练质量比算法花哨更重要。
4. 避坑与排查:五条赛场血泪记录
4.1 敲击位置偏移导致识别率骤降
现象:训练时识别正常,正式测试换人敲或者敲的位置偏了,杯号乱跳。原因:玻璃杯不同位置的振动模态不同,靠近杯口上沿和杯身中部的频谱峰值分布有差异,题目规定敲「靠近操作队员一侧的上沿」,但人手很难每次都精准。解决:训练时故意在允许范围内换几个角度敲,让模板覆盖位置变化;特征提取时只取最低的几个峰(基频和前两个谐波),高次峰对位置太敏感,直接丢弃。
4.2 环境噪声触发误识别
现象:没人敲杯子,屏幕自己跳数字。原因:赛场环境有空调声、说话声、桌子振动,能量阈值设太低就会误触发。解决:触发条件加双重判据——能量超过阈值且过零率在敲击声的典型范围内(敲击声是衰减振荡,过零率中等;说话声过零率偏高,低频噪声偏低)。另外触发后录到的数据先算一下频谱平坦度,太平坦的判为噪声丢弃。
4.3 水量差异小于分辨率导致无法区分
现象:发挥部分两个杯子水量差 20ml,识别时互相串。原因:20ml 水量差对应的基频偏移可能只有 5~10Hz,而 1024 点 FFT 在 16kHz 采样下的频率分辨率是 15.6Hz,根本分不开。解决:要么提高采样点数到 2048(分辨率 7.8Hz),要么用峰值插值——对峰值附近做抛物线拟合,把频率估计精度提到亚 bin 级。后者计算量小,推荐。
def interpolate_peak(spectrum, idx): """抛物线插值细化峰值频率""" if idx <= 0 or idx >= len(spectrum)-1: return float(idx) a, b, c = spectrum[idx-1], spectrum[idx], spectrum[idx+1] denom = a - 2*b + c if abs(denom) < 1e-12: return float(idx) delta = 0.5 * (a - c) / denom return idx + delta插值后频率精度能到 1~2Hz,20ml 的水量差就够分了。delta是相对 bin 的偏移量,范围在 -0.5 到 0.5 之间,加到原始索引上就是细化后的峰值位置。
4.4 训练超时:交互流程太啰嗦
现象:90 秒内没完成 5 个杯子的训练加识别。原因:每个杯子敲 3 次,每次之间等提示、等反馈,加上选手犹豫,时间全耗在流程上。解决:训练阶段每个杯子只要求敲 2 次(规则允许 3 次以内),系统自动连续采集,敲完第 2 次立刻切下一个杯号,不等确认。识别阶段敲一次就出结果,不重复采集。整个流程压缩到 60 秒以内,留 30 秒余量给意外。
4.5 发声与识别抢资源导致卡顿
现象:敲击后显示杯号正常,但发声延迟明显或者音调不对。原因:PWM 发声和 ADC 采集共用定时器,或者发声阻塞了主循环。解决:发声用独立定时器输出 PWM,匹配完成后只改频率寄存器,不阻塞;显示刷新用 DMA 或低优先级任务。关键路径(采集→FFT→匹配)里不碰任何外设初始化,全部预配置好。
5. 进阶技巧:用倍频程分析替代裸 FFT 提升鲁棒性
前面讲的峰值频率匹配在实验室环境够用,但赛场环境复杂,敲击力度、位置、背景噪声都在变。想让识别更稳,可以把特征从「绝对频率」换成「倍频程能量分布」——把 300Hz~5kHz 分成若干个 1/3 倍频程带,算每个带的能量占比,得到一个 8~10 维的归一化向量。这个特征对整体频率偏移不敏感(比如杯子个体差异导致基频整体偏了 20Hz),但对水量变化引起的频谱重心移动很敏感,正好契合这道题的需求。
具体做法:FFT 后按倍频程边界累加能量,边界频率用标准 1/3 倍频程中心频率(315、400、500、630、800、1000、1250、1600、2000、2500、3150、4000Hz),每个带内能量除以总能量做归一化。训练时存这个向量,识别时算余弦相似度。实测下来,对 20ml 水量差的区分度比裸峰值法高出一截,而且对敲击力度的鲁棒性更好。
| 特征方案 | 维度 | 对力度敏感度 | 对位置敏感度 | 20ml 水量区分度 |
|---|---|---|---|---|
| 前 4 峰值频率 | 4 | 低(已归一化) | 中 | 勉强 |
| 前 4 峰值+插值 | 4 | 低 | 中 | 可用 |
| 1/3 倍频程能量 | 12 | 很低 | 低 | 好 |
| 整段频谱模板 | 512 | 高 | 高 | 好但易过拟合 |
代价是计算量稍大,但 12 个带的能量累加在 1024 点 FFT 结果上做,额外开销可以忽略。我一般会两套特征都算,峰值法做主判据、倍频程做置信度校验,两者一致才输出杯号,不一致就提示重敲。这个双保险在时间紧张的赛场上能减少误判。
从那以后我每次做声学识别类项目,都强制先把「训练-识别」闭环在桌面上跑通再上硬件,而且一定留一个「重敲」的退路——识别系统再准也有边界样本,给用户一个低成本的重试机会,比追求单次 100% 准确率实在得多。希望这份拆解帮到你。
本文还有配套的精品资源,点击获取