ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

树莓派+LED灯带:打造带灯光引导的MIDI跟弹键盘

树莓派+LED灯带:打造带灯光引导的MIDI跟弹键盘 把一首歌的旋律变成琴键上的灯光指引让琴键在对应音符到来前一两秒自动亮起你要做的事情就是跟着那盏灯按下琴键——这就是我今天想聊的“跟弹键盘”Play-Along Keyboard。它既是一台练习辅助工具也是一个相当完整的软硬件结合项目涉及 MIDI 协议解析、按键输入检测、可寻址 LED 驱动、实时音频播放和调度算法。这篇文章会把我从零搭建这套系统的完整过程、踩过的坑和最终成型方案都写出来适合想把手上的 MIDI 键盘变成“带教学功能的智能琴”的玩家也适合正在寻找一个能同时练习电子、编程和音乐理解的综合实战项目的朋友。项目不依赖太昂贵的部件核心物料就是一块树莓派、一条灯带和一台普通 MIDI 键盘总成本可以控制在千元以内。1. 项目整体设计与思路拆解1.1 为什么做一把“跟弹键盘”先说我做这个项目之前的状态。我弹键盘属于半吊子水平能看谱慢慢弹但做不到拿到一首新歌就流畅演奏。传统练习方式是一个音一个音死磕指法不熟、和弦转换也慢练一会儿就容易烦躁。后来我发现一个效率很高的练习方式先听熟旋律然后看着“下一个音在哪”的提示直接上手弹让手指在短时间内重复正确动作肌肉记忆形成得特别快。这个“下一个音在哪”的提示如果用纸质谱你得做“读谱 → 映射到琴键位置 → 找键 → 按下去”这一整条链路但 LED 直接亮在琴键上等于把“找键”这一步彻底去掉了注意力可以全部放在节奏和手指动作上。所以我当时定下来的目标非常明确能加载任意 MIDI 文件并自动解析旋律与伴奏演奏时琴键上方对应的 LED 提前 1~2 秒亮起能同时播放伴奏音频形成“跟着伴奏弹旋律”的练习环境最好还能检测我弹对了没有事后给出反馈。这套需求拆开来看其实就是三个子系统的协作MIDI 文件的解析与时间轴调度、LED 指示灯的实时驱动、琴键按下事件的检测识别。一开始看会觉得有点复杂但把三个模块分开做每一个单独调试整个项目就清楚很多了。我后面所有的代码结构也都是按这个思路组织的每个模块一个文件主程序只负责把三块串起来。1.2 整体架构核心模块拆分整个系统我最后是这么组织的核心控制器是一块树莓派 4B2GB 版本足够跑一个 Python 主程序输入设备是一台 61 键的 USB MIDI 键盘通过 USB 线接入树莓派它同时承担“发声乐器和按键输入”两个角色灯光指示部分在琴键上方固定一条 WS2812B 可寻址灯带每个琴键对应一颗灯珠用树莓派的 GPIO 输出信号控制音频输出方面键盘自带音源伴奏和演奏声都通过同一副耳机或音箱出来。这里有一个非常关键的设计决定把“灯”和“键”分开处理。灯的亮灭由树莓派控制键盘发出声音由键盘自带音源完成两者独立工作、互不干扰。这样做的最大好处是就算 LED 调度出了点问题也不会影响发声调试时压力小很多。另一种做法是让树莓派实时合成所有声音但那样会引入音频延迟和 CPU 占用问题新手做起来会非常痛苦。我不推荐一开始就玩全软件合成先把跟弹功能跑起来后面想加再慢慢加。另外曲谱来源也是需要提前想清楚的。我用的全部是从网上免费下载的 MIDI 文件或者自己用音乐软件导出的 MIDI。MIDI 文件的好处是音符信息是结构化的不需要做任何音频识别解析出来就是干净的“时间 音高”对跟弹系统来说是再理想不过的输入格式。1.3 技术路线选型树莓派 vs PC vs 单片机动手之前我先纠结了一阵控制核心的选择。这个项目有三个主流方案我仔细做了对比方案优点缺点适合人群树莓派 Python生态完善写代码效率高GPIO 方便驱动灯带MIDI 和音频都能处理实时性一般极端情况下有毫秒级抖动大多数业余玩家强烈推荐PC 主机 显示器性能最强调试方便方便做可视化界面体积大LED 还需要 Arduino 转接接线复杂想做成桌面级练习站的玩家Arduino/ESP32 单片机实时性最好延迟极低解析 MIDI 文件困难存储空间小调试麻烦对单片机非常熟悉、追求极致的硬核玩家实测下来树莓派路线的开发效率最高。Python 生态里的 mido、python-rtmidi 和 rpi_ws281x 三个库正好覆盖 MIDI 解析、MIDI 通信和 LED 驱动不用自己造轮子。虽然 Python 的实时性确实不如 C但跟弹功能对延迟的容忍度其实很高——人按下琴键到听到声音之间有个二三十毫秒的延迟完全感觉不出来LED 提前量的误差在 50 毫秒以内也不影响使用。所以“树莓派 Python”就是我最终的选择后面所有内容都基于这条路线展开。2. 硬件准备与关键部件选型2.1 琴键部分直接买 MIDI 键盘还是自己焊“Build a Play-Along Keyboard”里的 Build大家理解的尺度不太一样。有人以为要从零做出一台物理钢琴那属于木工加电子加乐理的超级大工程普通爱好者基本不现实。我建议把“键盘”分成两个层面如果你已经有电钢琴或 MIDI 键盘直接用现成的你“Build”的是键盘外围的智能跟弹系统如果你连键盘都没有也可以从零焊一台迷你跟弹琴来练手但那就是另一个量级的项目了涉及按键矩阵、扫描电路、力度检测这些东西新手会被劝退。我采用的是现成 MIDI 键盘方案。选型时我关注四个点键数至少 49 键不然弹流行歌的旋律经常不够用必须有 USB MIDI 或五针 MIDI 接口这个年代基本都是 USB MIDI接树莓派很方便键盘要带耳机或音频输出口最好有自带音源省去电脑软音源的麻烦。我用的是一台二手的 61 键编曲键盘USB-MIDI 输出自带几百种音色耳机口直出整体非常省事。如果你手头只有不带音源的纯 MIDI 控制器也不是不行可以让树莓派跑一个软音源但延迟和配置会复杂一些我会在后面的问题章节里说明。2.2 LED 指示灯方案对比这是整个项目里最需要花心思的硬件部分。LED 要直观指示琴键我对比过几种安装方式在琴键上方一字排开灯珠、在琴键表面贴片灯、在琴键内部做导光柱、以及在琴键上方装亚克力导光条。方案视觉效果安装难度成本琴键上方灯珠一字排开能看出大致位置但不够直观低低琴键表面贴片 LED非常直观但影响弹奏手感中中琴键内部导光柱琴键直接发光效果最好高高琴键上方亚克力导光条兼顾直观与美观中中我最终用了亚克力导光条方案买一条一米的乳白色亚克力条固定在琴键后上方的挡板上把 WS2812B 灯带贴在亚克力条的端面灯珠的光线经过亚克力均匀散射出来形成一条横贯整台琴的光带。每一颗灯珠对应一个琴键的位置灯亮时能看到那个琴键上方有一段光带亮起。这样做不用改动琴体不会影响演奏手感装拆都方便。如果你不想折腾导光条直接灯珠一字排开其实也能用只是映射关系要靠自己脑补体验差一些。WS2812B 灯珠有一个必须注意的供电问题。一颗灯珠全亮时大约 60mA61 颗就是 3.66A这远远超过树莓派 GPIO 能承受的电流绝对不要直接从树莓派的 5V 引脚取电。正确做法是单独用一个 5V/5A 电源适配器给灯带供电树莓派只输出信号线并且灯带的地和树莓派的地要共地。我第一次就是偷懒直接接树莓派 5V结果灯带到第 30 颗左右开始明显变暗查下来才知道是供电压降后来换了独立电源才正常。2.3 音频输出与功放选择音频这一块相对简单但有一个容易忽略的点。我让键盘自带音源发声同时把伴奏通过键盘的音频输入口混进去这样耳机里听到的是“伴奏 键盘演奏声”的混合音非常自然。具体实现是树莓派播放伴奏音频用一根 3.5mm 公对公音频线把树莓派的耳机口接到键盘的音频输入口最终从键盘的耳机或音箱接口输出。这样做的好处是整个音频链路非常稳定全走硬件混音没有任何软件层面的延迟。相比之下如果让树莓派用软件合成器来同时播伴奏和键盘声音频线程稍微一忙就会出现爆音或者延迟体验会差很多。硬件混音还有一个额外好处伴奏音量和键盘演奏音量可以分别独立调节想突出哪一个都很方便。这个方案唯一的限制是你的键盘必须有音频输入口选购的时候留意一下。2.4 实测硬件清单与预算把我最后实际用到的硬件列个清单方便你照着准备部件型号/规格价格参考备注主控树莓派 4B 2GB约 350用 Zero 2W 也行但跑 Python 会慢一些键盘61 键带音源编曲键盘二手约 600关键要有 USB-MIDI 输出和音频输入口灯带WS2812B 60 灯/米1 米约 30长度按键盘宽度裁剪电源5V 5A 适配器约 30专供灯带使用导光条乳白亚克力条 1 米约 20用于均匀散射灯光音频线3.5mm 公对公约 10树莓派到键盘音频输入其他杜邦线、面包板、外壳等约 50按需购买总共加起来一千出头。如果你手上已经有键盘和树莓派那再花不到一百块就能搞定。这个成本相对于市售的智能电钢琴来说便宜了不止一个数量级而且曲库完全不受限制想弹什么 MIDI 就弹什么 MIDI。这也是自己动手做这类项目最大的吸引力。3. 核心软件逻辑从 MIDI 文件到灯亮3.1 解析 MIDI 文件的思路搞定了硬件接下来是软件。整个程序的核心是解析 MIDI 文件把它变成一条“什么时间、哪颗灯、亮多久”的事件序列。MIDI 文件本质上是一串消息的集合消息里带有相对时间戳delta time。最常见的消息类型是 note_on 和 note_offnote_on 表示某个音符开始note_off 表示结束。我用的解析库是 mido它会把音符消息按先后顺序流式返回把每个消息的 time 累加起来乘以当前速度对应的每秒 tick 数就能换算成绝对秒数。换算公式的核心是 tempoBPM和 ticks_per_beat这两个字段共同决定了时间轴。这里有一个容易踩的坑MIDI 文件可能在曲子的中途改变 tempo。流行歌曲的 MIDI 经常在间奏、尾奏或者自由速度段落插入速度变化如果只用文件头部的 BPM 去换算全部时间到后面会越来越偏。正确的做法是程序里维护一个当前 BPM 变量遇到 set_tempo 消息就更新当前值这样解析出来的时间轴才能一直准确。我自己第一次写解析时就是吃了这个亏后面会在问题章节里详细展开。3.2 关键算法音符时间轴与提前量解析完成后我得到的是一个按时间排序的事件列表。但跟弹不能“音符到了才亮灯”那等于没有任何提示。所以我在事件列表上做了一步预处理把所有 note_on 事件提前一个固定时间量比如提前 1.5 秒亮灯给演奏者一个准备时间。提前量的选择有讲究。太短的话新手反应不过来灯一亮就得立刻按手忙脚乱太长的话连续快节奏的音符会同时亮起一堆灯反而分不清要按哪个。我的建议是旋律密集的曲子用 1.2 到 1.5 秒慢歌可以用 2 秒。最好把它做成一个可调参数在程序里用一个变量控制。我实际使用时会根据曲子的难度临时调整后面还做了一个根据音符密度自动调节的版本会在调优章节提到。处理完提前量后还要做一件事把同时刻的多个事件按音高排序。因为 MIDI 文件里伴奏和旋律是混在一起的如果不排序灯光触发的顺序可能和听觉感受不一致虽然实际影响不大但排序后 LED 的亮起会更符合“从低音到高音”的视觉规律看起来更舒服。3.3 击键检测与正确性判断如果只是亮灯让人跟着按那还不算真正完整的“跟弹系统”。我还加了击键检测通过 python-rtmidi 读取键盘返回的 MIDI 消息和当前应该弹的音符做比对。具体逻辑是当 LED 提示了某个音符后程序进入“等待击键”状态如果在允许的时间窗口内我设的是灯亮后 1 秒内收到了对应的 note_on 消息就判定为命中否则视为漏弹。这里需要处理一个细节同一个音符可能在曲子中多次出现比如主歌里反复出现的某个音。如果只按音高匹配很容易把“本来该弹下一个 A4”误判成“提前弹了现在的 A4”。我的处理办法是给每个击键事件维护一个“是否已被命中”的状态标记新的击键消息只去匹配最近一个尚未被命中的同音高事件匹配成功后立即标记这样就能避免重复命中。这个功能对练习特别有用。它能把整首曲子的演奏过程记录下来弹完之后统计每一段的命中率然后针对不熟练的小节单独循环练习。相当于自己给自己做了一个带评分功能的跟弹练习器。我身边几个朋友用了这个功能之后都说练新歌的积极性明显提高了因为每一次练习都能看到具体的进步数据。3.4 代码骨架示例下面是我主程序的核心骨架去掉了一些与硬件强相关的细节保留了最关键的逻辑方便理解整体流程import time import mido class PlayAlongApp: def __init__(self, midi_path, lead_time1.5): self.lead_time lead_time self.events [] # [(trigger_time, note, on/off)] self.current_bpm 120 self._parse_midi(midi_path) self._apply_lead_time() def _parse_midi(self, midi_path): mid mido.MidiFile(midi_path) tick_accum 0 for msg in mid: tick_accum msg.time seconds mido.tick2second( tick_accum, mid.ticks_per_beat, self.current_bpm) if msg.type set_tempo: self.current_bpm mido.tempo2bpm(msg.tempo) elif msg.type note_on and msg.velocity 0: self.events.append([seconds, msg.note, on]) elif msg.type note_off or ( msg.type note_on and msg.velocity 0): self.events.append([seconds, msg.note, off]) self.events.sort(keylambda e: e[0]) def _apply_lead_time(self): for ev in self.events: if ev[2] on: ev[0] max(0.0, ev[0] - self.lead_time) def run(self, led_callback, key_callback): start time.monotonic() idx 0 while idx len(self.events): now time.monotonic() - start ev self.events[idx] if ev[0] now: led_callback(ev[1], ev[2]) idx 1 else: key_callback() # 顺带处理击键检测 time.sleep(0.005)这个循环里最关键的一点是统一用time.monotonic()作为时钟基准而不是time.time()。因为time.time()可能被系统时间调整影响导致灯和音乐越来越不同步。led_callback负责把音符号映射到对应的灯珠并点亮或熄灭key_callback里读取 MIDI 输入缓冲并做命中判断。两个回调都放在主线程里快速执行没有额外开线程这样避免了很多同步上的麻烦。4. 实操搭建完整复现流程4.1 第一步硬件接线与测试先做最基础的硬件准备。树莓派关机断电后把 WS2812B 灯带的电源线接到 5V 5A 适配器的正极地线接到树莓派和电源的 GND信号线接到树莓派的 GPIO 18BCM 编码。这里务必记得把灯带的地和树莓派的地连在一起否则信号线的电平参考不统一灯带会出现乱闪。接好线后先别急着写完整代码先用一个最简单的脚本测试灯带能不能逐颗点亮。先安装驱动库sudo apt install python3-pip sudo pip3 install rpi_ws281x然后运行下面的测试程序。如果灯带能按顺序跑一个彩虹效果说明硬件接线没问题可以进入下一步import time from rpi_ws281x import PixelStrip, Color LED_COUNT 61 LED_PIN 18 FREQ_HZ 800000 DMA 10 strip PixelStrip(LED_COUNT, LED_PIN, freq_hzFREQ_HZ, dmaDMA) strip.begin() for i in range(LED_COUNT): strip.setPixelColor(i, Color(255, 0, 0)) strip.show() time.sleep(0.05)第一次跑这个脚本如果发现灯带不亮或者闪烁先别急着怀疑代码。先用手摸一下灯带第一个灯珠附近的电源线是不是发烫然后用万用表量 5V 和 GND 之间的电压是否稳定在 4.8V 以上。大部分灯带问题其实都是供电问题不是代码问题。我见过一个朋友折腾了半天最后发现是电源适配器的插头松了。4.2 第二步环境搭建与驱动确认灯带测试通过后把 MIDI 键盘通过 USB 接到树莓派。在终端里执行aconnect -l或amidi -l能看到类似client 20: USB MIDI Keyboard的字样就说明系统识别到了设备。Python 端用 mido 和 python-rtmidi 两个库装好之后可以用下面这段代码确认 MIDI 输入是否正常工作import mido print(mido.get_input_names())如果这个列表是空的大概率是权限问题把当前用户加入audio组再重新登录试试。接下来设置系统音频输出为耳机口执行alsamixer检查音量把 3.5mm 音频线从树莓派接到键盘的音频输入口用手机放一首歌测试一下键盘的音箱能否正常出声。到这一步硬件和系统层面就全部打通了后面都是纯软件的事。4.3 第三步加载曲谱与联调接下来是软件联调。找一个你熟悉的 MIDI 文件最好是主旋律清晰、没有太多复杂和声的流行歌放到树莓派上。启动主程序前先做一步预检把解析出来的事件数量和最后一条事件的秒数打印出来看和实际歌曲时长是否一致。如果不一致基本就是 BPM 换算的问题重点检查有没有 set_tempo 消息被漏掉。第一次联调建议用最简单的模式只开灯不检测击键。把整首歌跑一遍观察灯带亮起的节奏是否和歌曲旋律对得上。这一步能快速暴露时间轴的偏差。我记得自己第一次联调时前奏部分灯是对得上的进到主歌之后开始慢半拍排查下来就是曲子中途有个速度变化
返回列表