1. 项目概述:当调色板遇见声音

作为一名在嵌入式开发和创客领域摸爬滚打了十多年的老玩家,我见过太多“为技术而技术”的项目,它们功能强大,却总让人觉得少了点灵魂。直到我动手做了这个“基于CircuitPython的交互式调色板”,我才真正体会到,技术最有魅力的地方,是它能成为艺术表达的桥梁,创造出一种全新的、可触摸的感官体验。

这个项目的核心,简单来说,就是 把一个传统的木质调色板,变成一个能听、能看、能交互的智能装置 。它的灵感来源于最基础的绘画动作——调色。当你用手指触摸调色板上的不同颜色区域(比如红色、蓝色),它会播放一段预设的、代表该颜色的音乐片段。更有趣的是,当你先后触摸两种颜色,装置不仅会依次播放两段音乐,还会通过板载的NeoPixel LED灯,将这两种颜色进行“混合”,并展示出混合后的新色彩。最后,触摸黑色区域,一切归零,等待下一次创作。这不仅仅是把声音和颜色绑定,而是试图模拟一种通感(Synesthesia)体验,让色彩拥有旋律,让声音染上色调。

它非常适合以下几类朋友尝试: 对物理计算和交互艺术感兴趣的创客 ,想找一个既有硬件动手又有软件编程的综合项目; 从事STEAM教育的老师或家长 ,需要一个能直观展示传感器、编程和艺术结合的教学案例;以及 任何觉得单纯写代码或单纯做手工不过瘾,想玩点新花样 的爱好者。整个项目围绕 Adafruit CircuitPlayground Bluefruit(简称CPB) 这块功能强大的微控制器开发板展开,用 CircuitPython 这种对新手极其友好的语言编程,结合 激光切割 制作木质结构,并用一些简单的导电材料(如导电胶带、锡箔)来制作触摸传感器,技术门槛被降得很低,但最终呈现的效果却足够惊艳。

2. 核心硬件选型与设计思路拆解

2.1 为什么是Adafruit CircuitPlayground Bluefruit?

在开始动手前,选择核心控制器是第一步。市面上微控制器很多,比如经典的Arduino Uno、功能更强的ESP32,但我最终选择了Adafruit CircuitPlayground Bluefruit,这是经过深思熟虑的。

首先,它高度集成,开箱即用。 CPB在一块圆形的板子上,集成了我们项目所需的所有关键部件:10个可编程的RGB NeoPixel LED灯、一个声音播放器、多个电容触摸感应引脚(A1-A6, TX)、甚至还有蓝牙功能(本项目未使用)。这意味着我们不需要再额外购买和焊接LED灯带、音频解码模块或触摸传感器,大大简化了硬件连接,降低了出错概率,特别适合原型制作和教育场景。如果使用Arduino Uno,我们需要单独连接触摸传感器模块、LED灯带和音频模块,光是一堆杜邦线和面包板就够头疼了。

其次,CircuitPython生态的支持。 CPB是Adafruit主打CircuitPython的明星产品。CircuitPython是MicroPython的一个分支,其最大优势在于“所见即所得”的开发体验。你将板子通过USB连接到电脑,它会显示为一个名为 CIRCUITPY 的U盘,直接编辑里面的 code.py 文件,保存后代码立即自动运行。调试时,打印的信息会直接显示在串行终端,交互性极强。对于处理“触摸颜色-播放声音-混合灯光”这种多事件、带状态的项目逻辑,用Python来写比传统的Arduino C++要直观和快速得多。

最后,社区与资源丰富。 Adafruit提供了极其详尽的教程、库文件和示例代码。项目中用到的 adafruit_circuitplayground 库,已经封装好了控制LED、读取触摸、播放音频等所有函数,我们只需要调用几行代码就能实现复杂功能,可以把精力集中在交互逻辑和艺术设计上,而不是底层驱动。

注意: 市面上还有一款叫CircuitPlayground Express的板子,它与Bluefruit的主要区别在于没有蓝牙。对于本项目,两者完全通用,选择你手头有的或更容易购买的即可。

2.2 交互机制的设计:从触摸到声光

这个项目的交互逻辑是整个设计的灵魂,它决定了用户体验是否流畅和有趣。我设计的核心状态机(虽然代码中可能没明说,但逻辑如此)大致分为三个状态:

  1. 待机选择状态: 系统启动后,NeoPixel灯可能以呼吸灯效果待机。用户触摸第一个颜色区域(如红色)。CPB通过电容触摸引脚检测到触摸事件。
  2. 第一颜色激活状态: 板载LED灯亮起对应的颜色(如红色),同时通过板载扬声器或音频口播放 red.wav 文件。系统记录下当前选择的颜色值(RGB)。
  3. 混合与播放状态: 等待第一段音乐播放完毕后,系统等待用户触摸第二个颜色区域。触摸后,播放对应的第二段音乐(如 blue.wav ),同时,在第二段音乐播放时或播放后,LED灯的颜色从第一个颜色平滑过渡(或直接显示)到两种颜色的混合色。这里的“混合”是数字意义上的,即对两个颜色的RGB值进行加权平均,例如 混合色 = (红色RGB * 0.5 + 蓝色RGB * 0.5) ,从而产生紫色。

为什么选择电容触摸,而不是按钮? 因为我们要还原“触摸颜料”的质感。电容触摸更自然,没有机械按键的“咔哒”声和物理行程,轻轻一碰就有反应,艺术交互感更强。CPB的多个触摸引脚允许我们为每个颜色分配独立的输入。

声音与颜色匹配的考量: 这不是随机的。在准备阶段,你需要为每个颜色精心挑选或制作一段短小的音乐片段(WAV格式)。例如,红色可能搭配一段激昂的小提琴旋律,蓝色搭配一段悠扬的钢琴曲。混合时,两段音乐先后播放,虽然受限于单声道扬声器无法真正混音,但通过先后顺序,也能在时间上形成“对话”或“融合”的听觉意象。如果使用更高端的音频输出设备,理论上可以实现实时混音,但这超出了CPB板载能力,作为一个简洁的交互原型,当前设计已足够有表现力。

“重置”功能的设计: 黑色区域被设计为重置按钮。这符合直觉——在调色板上,黑色常用来加深或覆盖。触摸黑色,所有LED灯熄灭,系统状态清零,回到初始待机状态,为下一次交互做好准备。这是一个至关重要的设计,它赋予了装置完整的交互闭环。

3. 材料准备与硬件制作详解

3.1 材料清单与工具

在开始切割和焊接之前,请准备好以下所有材料,避免中途停工。

核心控制器与电子部分:

  • Adafruit CircuitPlayground Bluefruit 主板 x1
  • 3xAAA电池盒(带开关)x1 (用于便携供电)
  • 微型扬声器(可选,如果使用板载扬声器则不需)或 3.5mm耳机/外放音箱
  • 细导线(如AWG30硅胶线)若干,或作为替代的 导电铜箔胶带 厨房锡箔纸
  • 绝缘胶带(如电工胶布)

结构制作部分:

  • 激光切割文件: 一个调色板形状的SVG或DXF文件。你可以在开源设计网站(如Thingiverse)搜索“palette silhouette”,或直接用矢量绘图软件(Inkscape、Illustrator)自行绘制。关键是要在“颜料格”和板子安装位置预留好穿线孔。
  • 板材: 1/8英寸(约3mm)厚的椴木板或亚克力板。椴木质感温润,易于激光切割且边缘光滑,是首选。
  • “颜料”片: 彩色卡纸或彩色丙烯酸薄片,用于贴在调色板的每个格子上,作为可视化的颜色提示。
  • 白胶或双面胶,用于固定彩色“颜料”片。

工具:

  • 激光切割机(或委托加工服务)
  • 电钻或手捻钻(钻头直径约2mm)
  • 小型螺丝刀套装
  • 剪刀、美工刀
  • 万用表(用于检查电路通断,非常推荐)
  • 电脑,并安装好Mu Editor或其他支持CircuitPython的代码编辑器。

3.2 激光切割与结构组装

  1. 文件准备与切割: 将调色板的矢量文件导入激光切割软件(如LightBurn)。确保设计包含:a) 调色板外轮廓;b) 多个圆形或椭圆形的“颜料格”凹槽;c) 在调色板手柄或背面设计一个用于固定CPB主板的位置(可以是镂空圆孔,刚好卡住主板);d) 在每个“颜料格”底部和CPB固定位之间,设计细小的导线槽或预留穿线孔。设置好激光功率和速度后,进行切割。切割完成后,轻轻取下部件,用砂纸打磨边缘毛刺。

  2. 钻孔与走线规划: 这是硬件连接中最需要耐心和规划的一步。根据你的设计,在每个“颜料格”的底部中心钻一个小孔,用于将背面的导线引到正面。在CPB安装位置附近,也需要钻几个孔,用于将来自各颜料格的导线汇聚到此。 我的经验是,先不要急着粘合或固定,把所有部件摆在一起,用笔标记好钻孔位置和走线路径,确保路径最短、最整洁,且不会互相干扰。

  3. 制作触摸传感器(关键步骤): 这是实现触摸交互的核心。我们利用CPB的电容触摸功能,需要将每个“颜料格”变成一个触摸电极。

    • 方法A(使用导线): 剪一段适当长度的细导线。一端焊接或缠绕固定在CPB的某个触摸引脚上(例如A1)。另一端穿过调色板背面的孔,到达正面的“颜料格”。在颜料格背面,将导线末端盘绕成一个小圆盘(面积稍大有助于增加触摸灵敏度),并用胶带将其平整地固定在格子的底部。然后,将彩色的“颜料”纸片贴在格子正面,覆盖住导线盘。 确保颜料纸片与背面的导线盘有良好的物理接触 ,可以用一点导电胶(如银浆)点在接触处,或者确保粘贴时压紧。
    • 方法B(使用导电胶带/锡箔,我的选择): 这是我实际采用的方法,因为更灵活。剪一小条导电铜箔胶带,从CPB的触摸引脚(如A2)开始粘贴,沿着调色板背面规划的走线槽,一直贴到目标“颜料格”背面的孔处。将胶带穿过孔,在正面格子内也贴上一小片,同样盘绕一下以增加面积。然后贴上彩色纸片。 如果铜箔胶带不够长或需要连接,可以用锡箔纸搓成条,两端用导电胶带紧密包裹连接,效果一样好。 用万用表通断档检查,从CPB引脚到彩色纸片表面,电阻应接近0欧姆。

    为每个颜色格子重复以上步骤,连接到CPB不同的触摸引脚(如红->A1, 蓝->A2, 黄->A3, 白->A4, 黑->A6)。 GND(地线)也需要引出一根公共线,但触摸感应不需要回路,GND主要是为可能的LED或音频提供参考地。

  4. 固定CPB与连接电源/音频: 将CPB主板用尼龙柱或强力双面胶固定在预留位置。将电池盒的红线(正极)连接到CPB的 3.3V VOUT 引脚,黑线(负极)连接到 GND 引脚。如果你使用外接扬声器,将扬声器的信号线连接到CPB的 A0 引脚(这是默认的模拟音频输出),另一根线连接到 GND 。如果使用板载扬声器,则跳过这一步,但注意板载扬声器音量较小。

  5. 绝缘与整理: 用绝缘胶带将所有暴露的导线、焊点、导电胶带非连接部分仔细包裹起来,防止短路。特别是不同颜色的触摸电极之间,必须确保绝缘,否则会发生误触发。最后,可以将电池盒用胶带固定在调色板背面。一个整洁的背面是专业项目的标志。

4. CircuitPython代码深度解析与编程

硬件搭建好后,灵魂在于代码。下面我将逐段解析核心代码逻辑,并提供优化建议。

4.1 开发环境搭建与库安装

  1. 刷写CircuitPython固件: 访问Adafruit官网,找到CircuitPlayground Bluefruit的页面,下载最新的CircuitPython UF2固件文件。按住CPB上的“复位”按钮,同时用USB线连接电脑,直到它出现一个名为 CPLAYBTBOOT 的U盘。将下载的 .uf2 文件拖入该U盘,它会自动刷写并重启,之后会出现一个名为 CIRCUITPY 的新U盘。
  2. 安装必要的库: 访问Adafruit的CircuitPython库包页面,下载最新的库包。将其解压,找到我们需要的两个库文件: adafruit_circuitplayground neopixel.mpy (如果库包里有)。将它们复制到 CIRCUITPY U盘里的 lib 文件夹中(如果没有就新建一个)。
  3. 选择编辑器: 强烈推荐使用 Mu Editor 。它专为CircuitPython设计,内置了串行监视器和代码检查功能。打开Mu,它会自动识别到 CIRCUITPY 盘,并允许你直接编辑 code.py

4.2 核心代码实现与逻辑剖析

以下是一个增强版的 code.py 示例,包含了详细的注释和更健壮的逻辑。

# 导入必要的库
import time
import board
import neopixel
from adafruit_circuitplayground import cp
import audiocore
import audioio

# 初始化常量
NUM_PIXELS = 10  # CPB上有10个NeoPixel
BRIGHTNESS = 0.2 # 亮度设置,避免太刺眼
# 定义颜色 (R, G, B)
COLOR_RED = (255, 0, 0)
COLOR_GREEN = (0, 255, 0)
COLOR_BLUE = (0, 0, 255)
COLOR_YELLOW = (255, 255, 0)
COLOR_WHITE = (255, 255, 255)
COLOR_BLACK = (0, 0, 0) # 用于重置
COLOR_OFF = (0, 0, 0)

# 将颜色和触摸引脚、音频文件映射起来
# 键:颜色名称, 值:一个字典,包含触摸引脚、RGB值、音频文件名
COLOR_CONFIG = {
    "red": {"pin": cp.touch_A1, "rgb": COLOR_RED, "audio": "red.wav"},
    "blue": {"pin": cp.touch_A2, "rgb": COLOR_BLUE, "audio": "blue.wav"},
    "yellow": {"pin": cp.touch_A3, "rgb": COLOR_YELLOW, "audio": "yellow.wav"},
    "white": {"pin": cp.touch_A4, "rgb": COLOR_WHITE, "audio": "white.wav"},
    "black": {"pin": cp.touch_A6, "rgb": COLOR_BLACK, "audio": None}, # 黑色没有声音,用于重置
}

# 初始化NeoPixel
pixels = neopixel.NeoPixel(board.NEOPIXEL, NUM_PIXELS, brightness=BRIGHTNESS, auto_write=False)

# 状态变量
first_color = None  # 记录第一个触摸的颜色名称
second_color = None # 记录第二个触摸的颜色名称
is_playing = False # 标志是否正在播放音频,防止打断

def play_audio(filename):
    """播放指定的WAV音频文件"""
    global is_playing
    try:
        with open(filename, "rb") as wave_file:
            wave = audiocore.WaveFile(wave_file)
            audio = audioio.AudioOut(board.SPEAKER) # 使用板载扬声器
            # 如果使用外接扬声器,改为 audioio.AudioOut(board.A0)
            audio.play(wave)
            is_playing = True
            while audio.playing:
                time.sleep(0.01) # 等待播放完成,期间可以检测其他触摸吗?这里会阻塞。
            is_playing = False
    except OSError:
        print("找不到音频文件:", filename)
        cp.play_tone(440, 0.5) # 播放一个错误提示音

def mix_colors(rgb1, rgb2):
    """简单地将两种RGB颜色等比例混合"""
    if rgb1 is None or rgb2 is None:
        return COLOR_OFF
    r = (rgb1[0] + rgb2[0]) // 2
    g = (rgb1[1] + rgb2[1]) // 2
    b = (rgb1[2] + rgb2[2]) // 2
    return (r, g, b)

def set_all_pixels(color):
    """将所有NeoPixel设置为同一颜色"""
    pixels.fill(color)
    pixels.show()

def reset_state():
    """重置所有状态和灯光"""
    global first_color, second_color
    first_color = None
    second_color = None
    set_all_pixels(COLOR_OFF)
    print("状态已重置")

# 开机灯光自检
for i in range(NUM_PIXELS):
    pixels[i] = COLOR_WHITE
    pixels.show()
    time.sleep(0.05)
set_all_pixels(COLOR_OFF)
print("交互式调色板已启动!触摸颜色开始。")

# 主循环
while True:
    # 如果正在播放音频,则跳过触摸检测,防止打断(这是一个简化处理)
    # 更复杂的实现可以用中断或状态机,但这里为了清晰,先做阻塞式播放。
    # 因此,在play_audio函数播放期间,无法检测触摸。

    for color_name, config in COLOR_CONFIG.items():
        touch_pin = config["pin"]
        if touch_pin: # 如果该颜色配置了触摸引脚
            if touch_pin.value: # 检测到触摸
                print(f"触摸到: {color_name}")
                time.sleep(0.2) # 简单防抖

                if color_name == "black":
                    # 触摸黑色,执行重置
                    reset_state()
                    continue # 跳过本次循环的后续处理

                if not is_playing:
                    if first_color is None:
                        # 选择第一个颜色
                        first_color = color_name
                        set_all_pixels(config["rgb"])
                        if config["audio"]:
                            play_audio(config["audio"])
                        print(f"第一颜色设置为: {first_color}")
                    elif second_color is None and color_name != first_color:
                        # 选择第二个颜色(且与第一个不同)
                        second_color = color_name
                        # 播放第二个颜色的声音
                        if config["audio"]:
                            play_audio(config["audio"])
                        # 混合颜色并显示
                        rgb1 = COLOR_CONFIG[first_color]["rgb"]
                        rgb2 = COLOR_CONFIG[second_color]["rgb"]
                        mixed_color = mix_colors(rgb1, rgb2)
                        set_all_pixels(mixed_color)
                        print(f"第二颜色设置为: {second_color}, 混合色为: {mixed_color}")
                        # 混合完成后,可以等待一段时间或等待重置
                        # 这里我们什么也不做,等待用户触摸黑色重置

代码逻辑解读与优化点:

  1. 状态管理: 使用 first_color second_color 两个变量来记录用户的选择顺序,这是实现“先A后B”混合逻辑的关键。
  2. 音频播放阻塞问题: 上述代码中 play_audio 函数在播放时会用 while audio.playing: 循环阻塞主程序。这意味着在播放声音的几秒内,装置无法响应新的触摸。 这是一个明显的体验瑕疵。
    • 优化方案: 引入一个简单的状态机和非阻塞播放。我们可以设置一个 current_audio 对象,在主循环中检查 if current_audio and not current_audio.playing: 来判断是否播放完毕,而不是用循环等待。这样在播放期间,主循环依然可以运行,可以检测到“黑色”重置触摸,实现立即中断播放。
  3. 触摸防抖: 代码中使用了 time.sleep(0.2) 进行简单防抖。对于电容触摸,这是必要的,因为手指接触瞬间信号可能不稳定。更高级的做法是使用软件滤波,比如连续多次读取引脚值为高才判定为有效触摸。
  4. 颜色混合算法: mix_colors 函数采用了最简单的平均值混合。你可以尝试更复杂的混合模式,比如基于HSL色彩空间的混合,或者模拟颜料混合的减色法(但这需要复杂的转换),这能为项目增加更多的艺术探索空间。
  5. 错误处理: 代码中包含了对音频文件丢失的基本错误处理(播放提示音)。在实际使用中,还应考虑触摸引脚初始化失败等情况,增加健壮性。

4.3 音频文件制作要点

项目要求声音文件是 22050 Hz采样率、16位、单声道的WAV文件 。这是为了适配CPB有限的处理能力和内存。

  • 如何制作: 你可以使用免费音频编辑软件如 Audacity
    1. 录制或导入一段音乐。
    2. 如果它是立体声,点击菜单栏 轨道 -> 立体声音轨转换为单声道
    3. 点击左下角,将项目采样率改为 22050 Hz
    4. 截取你需要的片段(建议每段5-10秒,不要太长)。
    5. 点击 文件 -> 导出 -> 导出为WAV ,在格式选项中选择 WAV (Microsoft) signed 16-bit PCM
  • 命名与存放: 将处理好的文件,如 red.wav blue.wav 等,直接复制到 CIRCUITPY U盘的根目录下。确保代码中 COLOR_CONFIG 字典里 audio 键指定的文件名与盘里的文件完全一致(包括大小写)。

5. 调试、优化与创意扩展

5.1 常见问题与排查技巧

在制作过程中,你几乎一定会遇到以下问题。别担心,这是学习的一部分。

问题现象 可能原因 排查与解决方法
触摸完全没有反应 1. 导线/导电胶带未连通。
2. 触摸引脚配置错误。
3. 代码未正确运行。
1. 用万用表! 测量从CPB触摸引脚到彩色纸片表面的电阻,应为导通(低电阻)。检查所有连接点。
2. 检查 COLOR_CONFIG 字典中 pin 的值是否与物理连接一致(A1接红色就对应 cp.touch_A1 )。
3. 通过Mu Editor的串行监视器查看打印信息,确认代码是否运行,触摸时是否有 触摸到: xxx 的打印。
触摸反应不灵敏或时灵时不灵 1. 触摸电极面积太小。
2. 导线过长或干扰。
3. 没有可靠的地参考。
1. 增大彩色纸片背面导电材料的面积,可以盘成紧密的螺旋形。
2. 尽量缩短导线长度,并让导线远离其他电源线。使用屏蔽线或双绞线可能改善。
3. 虽然电容触摸理论上不需要回路GND,但将CPB的GND引脚连接到一个大的公共接地平面(比如一块铝箔贴在木板背面)有时能显著提高稳定性。
LED灯不亮或颜色不对 1. NeoPixel初始化失败。
2. 颜色RGB值超出范围(0-255)。
3. pixels.show() 未被调用。
1. 确认 neopixel.mpy 库文件已正确放入 lib 文件夹。
2. 检查代码中的RGB元组,每个数值应在0-255之间。
3. NeoPixel在设置颜色后,必须调用 pixels.show() 才会实际更新。
没有声音或声音失真 1. 音频文件格式不正确。
2. 音量设置过低或硬件连接错误。
3. 板载扬声器驱动能力弱。
1. 严格按照要求:22050 Hz, 16-bit, 单声道WAV。 用Audacity等工具检查并转换。
2. 检查代码中使用的是 board.SPEAKER (板载)还是 board.A0 (外接)。外接扬声器正极接A0,负极接GND。
3. 板载扬声器音量小是通病,对于嘈杂环境,外接一个有源小音箱是更好的选择。
同时触摸两个颜色会错乱 1. 代码逻辑未处理“同时触摸”。
2. 触摸电极间绝缘不良,导致串扰。
1. 在主循环中,我们的代码是顺序检测每个颜色,理论上无法处理“同时”。这通常可以接受,因为用户很难真正同时触摸。如果需要,可以引入更复杂的触摸扫描逻辑。
2. 仔细检查绝缘! 确保不同颜色的导电部分没有通过胶水、木屑或潮湿环境形成意外连接。用万用表测量不同颜色电极间的电阻,应为无穷大。

5.2 项目优化与进阶玩法

当基础功能实现后,你可以尝试以下方向让项目更具个性:

  1. 视觉反馈升级: 目前是所有LED灯显示同一种颜色。你可以编程让LED灯呈现更动态的效果。例如,播放音乐时,让灯光随着声音的振幅跳动(需要分析音频);颜色混合时,让灯光像涟漪一样从一点扩散到全部,或者逐个像素地进行颜色过渡动画。
  2. 更复杂的交互逻辑: 实现“长按”功能——触摸并按住一个颜色超过2秒,可以进入该颜色的“调音台”模式,通过倾斜板子(使用CPB内置的加速度计)来实时改变该颜色对应声音的音调或播放速度。
  3. 无线控制与扩展: 利用CPB内置的蓝牙功能,开发一个手机App,可以远程为每个颜色分配新的音乐文件,或者自定义颜色混合的公式,甚至将混合出的新颜色保存下来。
  4. 材料与形式的艺术化: 放弃纸质“颜料”,改用半透明的彩色亚克力板,并在背面安装独立的迷你LED,触摸时该格子的亚克力板会被点亮,效果更佳。或者将整个装置封装在环氧树脂中,做成一个永恒的“声音调色板”艺术品。
  5. 教育场景扩展: 将这个项目作为一个教学平台。让学生们自己用Scratch或Python编写简单的旋律,导出为WAV文件,关联到颜色上。或者学习色彩理论,将RGB混合与CMY(青、品红、黄)颜料混合进行对比实验。

这个项目的魅力在于,它从一个简单的想法出发,融合了硬件制作、嵌入式编程和艺术设计。它没有标准答案,你的每一个设计选择和代码修改,都在定义属于你自己的“颜色与声音的混合艺术”。我最享受的时刻,不是它第一次成功运行的时候,而是看到不同背景的朋友们 interacting with it,每个人对颜色和声音的关联都有自己独特的感受和解读。这或许就是创客项目最动人的地方:你创造的不是一个冰冷的工具,而是一个引发共鸣和想象的触点。

更多推荐