简介

在 ESP32 上用 micropython 玩蜂鸣器时发现可以通过调整占空比发出不同音调,于是想能否让蜂鸣器“唱出”完整的一首歌。搜索发现了这篇博客CSDN 博客,巧妙的实现了基本的音调编码。在此基础上我扩展了高/中/低音以及节拍/连音的发声编码,本文介绍下对音调的编码原理。

PWM 调音基础原理

MicroPython 中的 PWM(Pulse Width Modulation,脉冲宽度调制)是一种调节信号的方法,它利用微处理器的数字输出来对模拟电路进行控制。PWM 技术通过控制脉冲的占空比(Duty Cycle)和频率来实现对信号的调节,这两个参数是调节 PWM 信号特性的关键。

占空比

占空比是脉冲的高电平时间与周期的比值,表示在一个周期内,高电平时间占整个周期的比例。占空比的范围在 0 到 1 之间,也可以用百分比来表示。例如,50%的占空比意味着高电平时间占整个周期的一半。在实际应用中,改变占空比可以改变信号的幅度,即高电平的电压大小。当占空比接近 0 时,高电平时间很短,信号的幅度很小;当占空比接近 1 时,高电平时间很长,信号的幅度很大。

频率

频率是脉冲的周期,即在一个单位时间内脉冲的个数。频率通常以赫兹(Hz)来表示,表示每秒钟的脉冲个数。例如,100Hz 的频率表示每秒钟有 100 个脉冲。改变频率可以改变信号的变化速度,即脉冲的间隔时间。频率越高,脉冲的间隔时间越短,变化速度越快。

和音调的关系

声响生理学复杂的原理不在这里解释,通过调试可以发现频率调整可以模拟不同的音调,而占空比的调整可以模拟发音长短。

调音

C调音符与频率对照表

上图是 C4 音乐国际频率表,#是过渡音本文先不做支持,主要看高/中/低音,后面的 Hz 数就是 PWM 的频率参数,一一对应做代码处理即可。

简谱学习

曲谱网找到了一张《敢问路在何方》简谱如下

敢问路在何方

高中低音各分 7 种数字音调,而上面的简谱里除了数字外还有各种音符,如何用程序代码来描述不同的音乐符号呢?要完成音符编码先要了解几个基本的音符含义。

基本音符学习

  1. 音区识别
  • 低音,数字下方有个小黑点表示低音
    低音

  • 中音,数字上下没有小黑点表示中音
    中音

  • 高音,数字上方有个小黑点表示高音
    高音

  1. 节拍
  • 小节,就是简谱里竖线隔开的一节
    小节

  • 节拍值,一小节里的音符声音持续比例
    上图的4 4 3 3便是四分音符表示,根据乐曲速度不同每个音符持续时间不同,简单理解就是将一小节均分 4 份就是每个音符持续时间

  1. 辅助音符
  • 减时线,数字下划线,就是音符带下划线的要特殊处理。一条下划线表示这个音符持续时间减半(八分音符)
    减时线

  • 延音线,数字后跟减号,音符后面跟着的减号,表示下个音符同上
    延音线

  • 附点,数字后跟黑点,表示延长前面音符时值的一半
    附点

  • 圆滑线,数字上的弧线,要唱奏圆滑,就是中间不要停顿的转音
    圆滑线

编码

简单曲谱编码

小星星
以上图《小星星》节选为例,这个简谱比较简单,每一小节只有单纯的 4 个音符,因此很容易编码。比如这样:

1
1155665-4433221-5544332-5544332-1155665-4433221

复杂曲谱编码

敢问路在何方
比如《敢问路在何方》简谱,里面包含了多种音乐符号,就没法用上面纯数字表示完整。

所以,拓展下数字加入其他符号试试?

定义音调频率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
tones = {
'1-': 262,
'2-': 294,
'3-': 330,
'4-': 349,
'5-': 392,
'6-': 440,
'7-': 494,
'1=': 523,
'2=': 587,
'3=': 659,
'4=': 698,
'5=': 784,
'6=': 880,
'7=': 988,
'1+': 1046,
'2+': 1175,
'3+': 1318,
'4+': 1397,
'5+': 1568,
'6+': 1760,
'7+': 1976,
'__': 0
}

这里给出我的一套编码格式,用-表示低音,=表示中音,+表示高音,__表示节拍过渡间的停顿。另外加入()标记圆滑线。

复杂如《敢问路在何方》曲谱里有八分音符存在,因此我将一节拍里的一个完整音阶用两个数字编码,如3=3=其实代表四分之一拍。

通过这套编码格式将《敢问路在何方》编码如下:

1
2
3
4
5
6
7
8
9
10
11
12
# 《路在何方》
melody = \
"(6-1=1=6-)(3=3=3=)2=(2=1=1=1=1=1=1=1=)(7-6-6-7-)(2=2=2=)3=(1=6-6-6-6-6-6-6-)" \
"(3=3=3=3=)(6=6=6=3=)(6=6=5=4=)(3=3=3=3=)(1=1=1=)2=(3=3=4=3=)(2=2=2=2=2=2=2=2=)" \
"(6-6-)(3=3=)(2=3=6-6-)(1=1=1=1=1=1=3=3=)(2=7-7-3=)(2=6-1=2=)(3=3=3=3=3=3=3=3=)" \
"(3=3=3=3=)(6=6=6=3=)(6=6=5=4=)(3=3=3=3=)(5=2=2=4=)(3=2=1=1=)(2=2=2=2=2=2=3=3=)" \
"(2=7-7-3=)(7-6-5-5-)(6-6-6-6-6-6-)(3=3=)(5=5=5=5=5=5=)(3=5=)(6=6=6=)1+(7=6=)(5=5=)" \
"(6=6=6=6=6=6=6=6=)(1+1+1+1+)(7=7=7=)6-(5=6=)(5=5=5=5=)(5=6=)(3=3=3=3=3=3=3=3=)" \
"(1+1+1+1+)(7=7=7=)6=(5=6=)(5=5=5=5=)(5=6=)(3=3=3=3=3=3=3=3=)(5-6-)1=(3=3=3=)1=" \
"(2=3=)(2=2=2=2=2=2=)(2=7-7-)3=(7-6-5-5-)(6-6-6-6-6-6-6-6-)(5-6-6-)1=(3=3=3=)1=" \
"(2=3=)(2=2=2=2=2=2=)(3=3=5=5=5=5=)(3=3=)(7=7=7=1+7=6=5=5=)(6=6=6=6=6=6=6=6=6=6=6=6=6=6=6=6=)" \
"(3=3=5=5=5=5=)(3=3=)(7=7=7=1+7=6=5=5=)(6=6=6=6=6=6=6=6=6=6=6=6=6=6=6=6=)"

剩下的就是通过 micropython 解析和驱动蜂鸣器发声了,核心就是调整占空比duty以及音节间的间隔时间

最后

完整的可运行 micropython 代码示例放这里供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
from machine import Pin, PWM
import time

# 定义音调频率
tones = {
'1-': 262,
'2-': 294,
'3-': 330,
'4-': 349,
'5-': 392,
'6-': 440,
'7-': 494,
'1=': 523,
'2=': 587,
'3=': 659,
'4=': 698,
'5=': 784,
'6=': 880,
'7=': 988,
'1+': 1046,
'2+': 1175,
'3+': 1318,
'4+': 1397,
'5+': 1568,
'6+': 1760,
'7+': 1976,
'__': 0
}

beeper = PWM(Pin(6, Pin.OUT), freq=1000, duty=0)

def play(beeper, melody, duty = 10):
i = 0
keep = False
while i < len(melody):
if melody[i] == '(':
i += 1
keep = True
continue
elif melody[i] == ')':
i += 1
keep = False

# 连音结束后稍微停顿下
beeper.duty(0)
time.sleep_ms(50)
continue

tone, level = melody[i], melody[i+1]
i += 2
freq = tones[tone+level]
if freq:
beeper.init(duty=duty, freq=freq)
else:
beeper.duty(0) # 空拍时静音

# 停顿一下 (四四拍每秒两个音,每个音节中间稍微停顿一下)
time.sleep_ms(200)
if not keep:
print(tone, level)
beeper.duty(0) # 设备占空比为0,即不上电
time.sleep_ms(50)

# 《路在何方》
melody = \
"(6-1=1=6-)(3=3=3=)2=(2=1=1=1=1=1=1=1=)(7-6-6-7-)(2=2=2=)3=(1=6-6-6-6-6-6-6-)" \
"(3=3=3=3=)(6=6=6=3=)(6=6=5=4=)(3=3=3=3=)(1=1=1=)2=(3=3=4=3=)(2=2=2=2=2=2=2=2=)" \
"(6-6-)(3=3=)(2=3=6-6-)(1=1=1=1=1=1=3=3=)(2=7-7-3=)(2=6-1=2=)(3=3=3=3=3=3=3=3=)" \
"(3=3=3=3=)(6=6=6=3=)(6=6=5=4=)(3=3=3=3=)(5=2=2=4=)(3=2=1=1=)(2=2=2=2=2=2=3=3=)" \
"(2=7-7-3=)(7-6-5-5-)(6-6-6-6-6-6-)(3=3=)(5=5=5=5=5=5=)(3=5=)(6=6=6=)1+(7=6=)(5=5=)" \
"(6=6=6=6=6=6=6=6=)(1+1+1+1+)(7=7=7=)6-(5=6=)(5=5=5=5=)(5=6=)(3=3=3=3=3=3=3=3=)" \
"(1+1+1+1+)(7=7=7=)6=(5=6=)(5=5=5=5=)(5=6=)(3=3=3=3=3=3=3=3=)(5-6-)1=(3=3=3=)1=" \
"(2=3=)(2=2=2=2=2=2=)(2=7-7-)3=(7-6-5-5-)(6-6-6-6-6-6-6-6-)(5-6-6-)1=(3=3=3=)1=" \
"(2=3=)(2=2=2=2=2=2=)(3=3=5=5=5=5=)(3=3=)(7=7=7=1+7=6=5=5=)(6=6=6=6=6=6=6=6=6=6=6=6=6=6=6=6=)" \
"(3=3=5=5=5=5=)(3=3=)(7=7=7=1+7=6=5=5=)(6=6=6=6=6=6=6=6=6=6=6=6=6=6=6=6=)"

play(beeper, melody)

# 《小星星》
# melody = "1=1=5=5=6=6=5=__4=4=3=3=2=2=1=__5=5=4=4=3=3=2=__5=5=4=4=3=3=2=__1=1=5=5=6=6=5=__4=4=3=3=2=2=1="
# play(beeper, melody)

beeper.deinit()

# refer https://blog.csdn.net/fatway/article/details/118859714
# refer https://www.qupu123.com/Mobile-view-id-288352.html

https://github.com/caftxx/melody-micropython