前言
这是"拾颜播放器"项目学习记录的第一部分,用蜂鸣器,播放不同风格的音乐。
以前也玩过蜂鸣器,基本用法知道,比如小星星。但这次系统整理了一下,发现有很多细节之前没注意到,比如音符频率其实可以计算、不同音阶有不同情绪、和弦怎么编配等。这篇笔记把学习过程中觉得有用的内容都记录下来。
项目用的硬件:
组件说明
| ESP32-S3 Reverse TFT Feather | 主控板,240MHz 双核 |
| DFRobot DFR0032 | 数字蜂鸣器模块 |
| TCS3200 颜色传感器 | 检测颜色用 |
| Littelfuse 59001 | 接近传感器,检测物体靠近 |
引脚连接:
BUZZER_PIN = board.D5


最开始的代码很简单:
import time import board import pwmio buzzer = pwmio.PWMOut(board.D5, duty_cycle=0, frequency=440) buzzer.duty_cycle = 32768 # 50% 音量 time.sleep(0.5) buzzer.duty_cycle = 0 # 关闭
注意:duty_cycle 范围是 0-65535,32768 就是 50% 音量。
其实不用死记硬背所有频率,记住一个公式就行:
频率 = 440 × 2^((n-69)/12)
其中 n 是 MIDI 音符编号。中央 C (C4) 的编号是 60。
def midi_to_freq(midi_note):
"""MIDI音符编号转频率"""
return 440 * (2 ** ((midi_note - 69) / 12))# 计算几个常用音
print(f"C4 (60): {midi_to_freq(60):.2f} Hz") # 261.63 Hz
print(f"A4 (69): {midi_to_freq(69):.2f} Hz") # 440.00 H
zprint(f"C5 (72): {midi_to_freq(72):.2f} Hz") # 523.25 Hz下面是 test_buzzer.py 里直接用到的频率,整理成了表格:
音符频率音符频率
| C4 | 261.63 | C5 | 523.25 |
| D4 | 293.66 | D5 | 587.33 |
| E4 | 329.63 | E5 | 659.25 |
| F4 | 349.23 | F5 | 698.46 |
| G4 | 392.00 | G5 | 783.99 |
| A4 | 440.00 | A5 | 880.00 |
| B4 | 493.88 | B5 | 987.77 |
高音区:
音符频率音符频率
| C6 | 1046.50 | B6 | 1975.53 |
| E6 | 1318.51 | C7 | 2093.00 |
| G6 | 1567.98 | E7 | 2637.02 |
| A6 | 1760.00 | E8 | 5274.04 |
相邻两个半音,频率比是 2^(1/12) ≈ 1.0595。
# 计算升号音符
def semitone_up(freq):
return freq * 1.0595def semitone_down(freq):
return freq / 1.0595# 从 C4 计算 C#4
print(f"C4: {NOTE_C4:.2f}")
print(f"C#4: {semitone_up(NOTE_C4):.2f}") # 277.18把播放逻辑封装起来,代码更整洁:
class BuzzerTest:
def __init__(self):
self.buzzer = None
self.enabled = False
def setup(self):
"""初始化蜂鸣器"""
try:
self.buzzer = pwmio.PWMOut(board.D5, duty_cycle=0, frequency=440)
self.enabled = True
print("✓ 蜂鸣器初始化成功")
return True
except Exception as e:
print(f"✗ 初始化失败: {e}")
return False
def play_tone(self, frequency, duration):
"""播放单个音调"""
if not self.enabled or frequency <= 0:
time.sleep(duration)
return
self.buzzer.frequency = int(frequency)
duty_cycle = int((VOLUME / 100) * 65535)
self.buzzer.duty_cycle = duty_cycle
time.sleep(duration)
def play_melody(self, melody, gap=0.02):
"""播放旋律(可选间隔)"""
for freq, dur in melody:
self.play_tone(freq, dur)
time.sleep(gap) # 避免杂音的间隔为什么要封装:
统一管理 PWM 输出
方便调整音量
可以添加公共功能(间隔、音效等)
先测试八度音阶,确保能发出正确音高:
def test_scale(self): """测试:播放音阶""" scale = [ (NOTE_C4, 0.3), (NOTE_D4, 0.3), (NOTE_E4, 0.3), (NOTE_F4, 0.3), (NOTE_G4, 0.3), (NOTE_A4, 0.3), (NOTE_B4, 0.4), (NOTE_C5, 0.4) ] self.play_melody(scale)
听到"do re mi fa so la si do"就对了!
def test_melody(self): """测试:小星星""" melody = [ (NOTE_C4, 0.25), (NOTE_C4, 0.25), (NOTE_G4, 0.25), (NOTE_G4, 0.25), (NOTE_A4, 0.25), (NOTE_A4, 0.25), (NOTE_G4, 0.5), (NOTE_F4, 0.25), (NOTE_F4, 0.25), (NOTE_E4, 0.25), (NOTE_E4, 0.25), (NOTE_D4, 0.25), (NOTE_D4, 0.25), (NOTE_C4, 0.5), ] self.play_melody(melody)
数据结构:[(频率, 时长), ...],时长单位是秒。
def test_birthday(self): """生日歌""" birthday = [ (NOTE_C4, 0.25), (NOTE_C4, 0.25), (NOTE_D4, 0.5), (NOTE_C4, 0.25), (NOTE_F4, 0.5), (NOTE_E4, 0.75), (NOTE_C4, 0.25), (NOTE_C4, 0.25), (NOTE_D4, 0.5), (NOTE_C4, 0.25), (NOTE_G4, 0.5), (NOTE_F4, 0.75), (NOTE_C4, 0.25), (NOTE_C4, 0.25), (NOTE_C5, 0.5), (NOTE_A4, 0.25), (NOTE_F4, 0.25), (NOTE_E4, 0.25), (NOTE_D4, 0.25), (NOTE_AS4, 0.25), (NOTE_A4, 0.5), ] self.play_melody(birthday)
def test_jasmine(self): """茉莉花(简化版)""" jasmine = [ (NOTE_D4, 0.5), (NOTE_E4, 0.25), (NOTE_F4, 0.25), (NOTE_G4, 0.5), (NOTE_A4, 0.25), (NOTE_B4, 0.25), (NOTE_A4, 0.25), (NOTE_G4, 0.25), (NOTE_F4, 0.25), (NOTE_E4, 0.25), (NOTE_D4, 0.5), (NOTE_D4, 0.25), (NOTE_E4, 0.25), (NOTE_D4, 0.5), (NOTE_G4, 0.25), (NOTE_G4, 0.75), ] self.play_melody(jasmine)
这个很有名,就几个音:
def test_mario(self): """马里奥音效 - 顶蘑菇""" self.play_tone(NOTE_E5, 0.1) self.play_tone(NOTE_E5, 0.1) self.play_tone(0, 0.1) # 休止符 self.play_tone(NOTE_E5, 0.1) self.play_tone(0, 0.1) self.play_tone(NOTE_C5, 0.1) self.play_tone(NOTE_E5, 0.4)
技巧:频率设为 0 就是休止符。
def test_tetris(self): """俄罗斯方块主题曲""" tetris_theme = [ (NOTE_E5, 0.2), (NOTE_B4, 0.15), (NOTE_C5, 0.15), (NOTE_D5, 0.2), (NOTE_C5, 0.15), (NOTE_B4, 0.15), (NOTE_A4, 0.2), (NOTE_A4, 0.15), (NOTE_C5, 0.15), (NOTE_E5, 0.2), (NOTE_D5, 0.15), (NOTE_C5, 0.15), (NOTE_B4, 0.3), (NOTE_C5, 0.15), (NOTE_D5, 0.2), (NOTE_E5, 0.2), ] self.play_melody(tetris_theme)
这部分最好玩!
MIDI 风格的经典音效:
def test_midi_coin(self): """经典吃金币音效""" for _ in range(3): self.play_tone(NOTE_E5, 0.08) self.play_tone(NOTE_G5, 0.08) time.sleep(0.15)
两个音快速交替,非常经典!
分 8 个阶段,完整还原街机感觉:
def test_contra(self):
"""超级魂斗罗战斗音效 (8阶段)"""
stages = [
("开场急促", [
(NOTE_E5, 0.08), (NOTE_G5, 0.08), (NOTE_E6, 0.08), (NOTE_G5, 0.08),
]),
("主旋律A", [
(NOTE_E6, 0.12), (NOTE_G6, 0.12), (NOTE_E7, 0.12), (NOTE_G6, 0.12),
]),
("主旋律B", [
(NOTE_E6, 0.12), (NOTE_G6, 0.12), (NOTE_E7, 0.12), (NOTE_G6, 0.12),
]),
("紧张段", [
(NOTE_E6, 0.06), (NOTE_D6, 0.06), (NOTE_C6, 0.06), (NOTE_B5, 0.06),
]),
("爆发段", [
(NOTE_E5, 0.10), (NOTE_G5, 0.10), (NOTE_E6, 0.10), (NOTE_G6, 0.10),
]),
("下行冲刺", [
(NOTE_G7, 0.08), (NOTE_E7, 0.08), (NOTE_G6, 0.08), (NOTE_E6, 0.08),
]),
("紧张攀升", [
(NOTE_E5, 0.08), (NOTE_G5, 0.08), (NOTE_E6, 0.08), (NOTE_G6, 0.08),
]),
("终止高潮", [
(NOTE_E5, 0.15), (NOTE_G5, 0.15), (NOTE_E6, 0.15), (NOTE_E7, 0.3),
]),
]
for stage_name, melody in stages:
print(f" 播放: {stage_name}")
self.play_melody(melody)
time.sleep(0.2)def test_shoot(self): """射击音效""" # 频率快速下降,模拟"啾"的声音 for freq in range(880, 220, -40): self.play_tone(freq, 0.02)
def test_explosion(self): """爆炸音效 - 用噪声模拟""" # 蜂鸣器不好做真正的噪声,但可以做低频震动 for _ in range(10): self.play_tone(100, 0.05) self.play_tone(80, 0.05)
def test_powerup(self): """升级音效 - 连续上滑""" for freq in range(440, 880, 20): self.play_tone(freq, 0.03)
快速在两个频率之间切换:
def test_vibrato(self): """颤音效果""" for _ in range(3): self.play_tone(440, 0.05) self.play_tone(450, 0.05)
频率逐渐变化:
def play_tone_glide(self, start_freq, end_freq, duration): """滑音效果""" steps = 20 step_time = duration / steps for i in range(steps + 1): freq = start_freq + (end_freq - start_freq) * i / steps self.play_tone(freq, step_time) # 使用 self.play_tone_glide(440, 880, 0.3) # 上滑八度 self.play_tone_glide(880, 440, 0.3) # 下滑八度
8.3 渐强渐弱 (Crescendo/Diminuendo)
def play_tone_fade(self, frequency, duration, fade_in=True): """渐强/渐弱播放""" steps = 20 step_time = duration / steps for i in range(steps): factor = (i + 1) / steps if fade_in else (steps - i) / steps duty_cycle = int((VOLUME / 100) * 65535 * factor) self.buzzer.duty_cycle = duty_cycle self.play_tone(frequency, step_time)
def play_tone_vibrato_modulated(self, center_freq, duration): """带颤音的音调(正弦调制)""" import math steps = 100 step_time = duration / steps for i in range(steps): t = i * 0.1 # 颤音:±10Hz 调制 freq = center_freq + 10 * math.sin(t * 20) self.play_tone(freq, step_time)
def test_pwm_volume(self): """PWM 音量对比测试""" volume_levels = [5, 10, 20, 50, 80, 100] for vol in volume_levels: duty_cycle = int((vol / 100) * 65535) self.buzzer.duty_cycle = duty_cycle time.sleep(0.3)
明亮、快乐的感觉:
# C大调 MAJOR_SCALE = [NOTE_C4, NOTE_D4, NOTE_E4, NOTE_F4, NOTE_G4, NOTE_A4, NOTE_B4, NOTE_C5]
忧郁、悲伤的感觉:
# C小调(自然小调) MINOR_SCALE = [NOTE_C4, NOTE_D4, NOTE_D4_SHARP, NOTE_F4, NOTE_G4, NOTE_G4_SHARP, NOTE_A4_SHARP, NOTE_C5] # 计算变化音 NOTE_D4_SHARP = NOTE_D4 * 1.0595 # D# NOTE_G4_SHARP = NOTE_G4 * 1.0595 # G# NOTE_A4_SHARP = NOTE_A4 * 1.0595 # A#
很多流行歌和民族音乐用的音阶,只有5个音:
# C五声音阶 PENTATONIC = [NOTE_C4, NOTE_D4, NOTE_E4, NOTE_G4, NOTE_A4, NOTE_C5] # 特点:任意两个音组合都和谐,适合即兴 def test_pentatonic(self): """五声音阶测试""" self.play_melody([(freq, 0.3) for freq in PENTATONIC])
在五声音阶基础上加了"蓝调音符":
# C蓝调音阶 BLUES_SCALE = [NOTE_C4, NOTE_D4_SHARP, NOTE_F4, NOTE_G4, NOTE_G4_SHARP, NOTE_B4, NOTE_C5]
单个音叫音符,多个音同时发出来叫和弦。
# 三和弦(根音-三度-五度) CHORD_C = (NOTE_C4, NOTE_E4, NOTE_G4) # C大三和弦 CHORD_DM = (NOTE_D4, NOTE_F4, NOTE_A4) # D小三和弦 CHORD_EM = (NOTE_E4, NOTE_G4, NOTE_B4) # E小三和弦 CHORD_F = (NOTE_F4, NOTE_A4, NOTE_C5) # F大三和弦 CHORD_G = (NOTE_G4, NOTE_B4, NOTE_D5) # G大三和弦 CHORD_AM = (NOTE_A4, NOTE_C5, NOTE_E5) # A小三和弦
几个经典和弦进行:
# 卡农进行:C - G - Am - GCANON_PROGRESSION = [ CHORD_C, CHORD_G, CHORD_EM, CHORD_AM, CHORD_F, CHORD_C, CHORD_G, CHORD_F,] def test_chord_progression(self): """测试和弦进行(轮流播放)""" for chord in CANON_PROGRESSION: for freq in chord: self.play_tone(freq, 0.3)
把和弦的音依次弹出来:
def test_arppegio(self, chord, tempo=0.15): """琶音播放""" for freq in chord: self.play_tone(freq, tempo)
# 假设以四分音符为一拍,每分钟120拍 # 每拍时长 = 60 / 120 = 0.5 秒 TEMPO_120 = 0.25 # 四分音符时长(120 BPM) TEMPO_90 = 0.33 # 四分音符时长(90 BPM) TEMPO_60 = 0.5 # 四分音符时长(60 BPM) # 八分音符 = 四分音符的一半 EIGHTH_NOTE = TEMPO_120 / 2
def test_rhythm_patterns(self): """各种节奏型测试""" # 1. 平均节奏(行进感) marching = [(freq, 0.25) for freq in [NOTE_C4, NOTE_E4, NOTE_G4] * 2] # 2. 切分音(节奏感强) syncopation = [ (NOTE_C4, 0.2), (NOTE_E4, 0.1), (NOTE_G4, 0.2), (NOTE_G4, 0.1), (NOTE_E4, 0.2), (NOTE_C4, 0.2), ] # 3. 三连音(圆舞曲感) triplets = [] for _ in range(4): triplets.extend([(NOTE_C4, 0.08), (NOTE_E4, 0.08), (NOTE_G4, 0.08)]) # 4. 附点节奏(悠扬感) dotted = [ (NOTE_C4, 0.45), (NOTE_E4, 0.15), (NOTE_G4, 0.3), (NOTE_E4, 0.45), (NOTE_G4, 0.15), (NOTE_C4, 0.3), ]
这是"拾颜播放器"项目的核心功能:
# 颜色到情感的映射COLOR_MUSIC = {
"red": {
"name": "炽热红",
"tempo": 0.12, # 快节奏
"scale": "major", # 大调
"melody": [ # 热情洋溢的旋律
(NOTE_E5, 0.12), (NOTE_G5, 0.12), (NOTE_E6, 0.12), (NOTE_G5, 0.12),
(NOTE_E5, 0.12), (NOTE_G5, 0.12), (NOTE_E6, 0.24),
]
},
"blue": {
"name": "深海蓝",
"tempo": 0.35, # 慢节奏
"scale": "minor", # 小调
"melody": [ # 舒缓的旋律
(NOTE_A3, 0.35), (NOTE_C4, 0.35), (NOTE_E4, 0.35),
(NOTE_A3, 0.35), (NOTE_C4, 0.35), (NOTE_E4, 0.7),
]
},
"green": {
"name": "清新绿",
"tempo": 0.25,
"scale": "pentatonic",
"melody": [ # 五声音阶,清新感
(NOTE_E4, 0.25), (NOTE_G4, 0.25), (NOTE_A4, 0.25),
(NOTE_C5, 0.25), (NOTE_D5, 0.25), (NOTE_E5, 0.5),
]
},
"yellow": {
"name": "温暖黄",
"tempo": 0.3,
"scale": "major",
"melody": [ # 明亮温暖
(NOTE_C4, 0.3), (NOTE_E4, 0.3), (NOTE_G4, 0.3),
(NOTE_C5, 0.6),
]
},
"purple": {
"name": "神秘紫",
"tempo": 0.4,
"scale": "blues",
"melody": [ # 蓝调风格
(NOTE_A3, 0.2), (NOTE_C4, 0.2), (NOTE_D4_SHARP, 0.2),
(NOTE_F4, 0.2), (NOTE_G4, 0.2), (NOTE_A4, 0.8),
]
},}def play_color_music(self, color_name):
"""根据颜色播放音乐"""
if color_name not in COLOR_MUSIC:
return
config = COLOR_MUSIC[color_name]
print(f"播放: {config['name']}")
for freq, base_dur in config['melody']:
actual_dur = base_dur * config['tempo']
self.play_tone(freq, actual_dur)
time.sleep(0.02) # 间隔检查 VOLUME 设置,建议 50-80。如果还小,检查硬件连接。
在播放间隔加短暂静音:
def play_melody(self, melody): for freq, dur in melody: self.play_tone(freq, dur) time.sleep(0.02) # 短暂间隔,避免杂音
高频音使用较短的时长。
用固定的 tempo 变量控制,不要手写时长:
TEMPO = 0.25 # 四分音符melody = [ (NOTE_C4, TEMPO), # 一拍 (NOTE_E4, TEMPO/2), # 半拍 (NOTE_G4, TEMPO/2), # 半拍 (NOTE_C5, TEMPO*2), # 两拍]
把旋律数据放在单独的文件里:
# melodies.pyTETRIS_THEME = [ (NOTE_E5, 0.2), (NOTE_B4, 0.15), (NOTE_C5, 0.15), (NOTE_D5, 0.2), # ... 更多音符] # main.py from melodies import TETRIS_THEME self.play_melody(TETRIS_THEME)
通过 CircuitPython 的 PWM 模块,蜂鸣器可以实现:
基础功能:播放准确音高的音符
旋律演奏:演奏完整歌曲
游戏音效:还原经典 8-bit 音效
音效特效:颤音、滑音、渐强渐弱
情感表达:不同颜色对应不同音乐风格
关键要点:
记住标准音 A4=440Hz,可以用公式推导其他音
理解大调、小调、五声音阶的情绪区别
节奏比音高更重要
多听多练,培养乐感
十五、播放效果

视频展示:https://www.bilibili.com/video/BV1y1viBcE8v
在"拾颜播放器"项目中,蜂鸣器负责把颜色检测结果转换成音乐输出,配合屏幕显示和颜色传感器,完成了"颜色→情感→音乐"的完整交互体验。
我要赚赏金
