简介
在 ESP32 上用 micropython 玩蜂鸣器时发现可以通过调整占空比发出不同音调,于是想能否让蜂鸣器“唱出”完整的一首歌。搜索发现了这篇博客CSDN 博客,巧妙的实现了基本的音调编码。在此基础上我扩展了高/中/低音以及节拍/连音的发声编码,本文介绍下对音调的编码原理。
PWM 调音基础原理
MicroPython 中的 PWM(Pulse Width Modulation,脉冲宽度调制)是一种调节信号的方法,它利用微处理器的数字输出来对模拟电路进行控制。PWM 技术通过控制脉冲的占空比(Duty Cycle)和频率来实现对信号的调节,这两个参数是调节 PWM 信号特性的关键。
占空比
占空比是脉冲的高电平时间与周期的比值,表示在一个周期内,高电平时间占整个周期的比例。占空比的范围在 0 到 1 之间,也可以用百分比来表示。例如,50%的占空比意味着高电平时间占整个周期的一半。在实际应用中,改变占空比可以改变信号的幅度,即高电平的电压大小。当占空比接近 0 时,高电平时间很短,信号的幅度很小;当占空比接近 1 时,高电平时间很长,信号的幅度很大。
频率
频率是脉冲的周期,即在一个单位时间内脉冲的个数。频率通常以赫兹(Hz)来表示,表示每秒钟的脉冲个数。例如,100Hz 的频率表示每秒钟有 100 个脉冲。改变频率可以改变信号的变化速度,即脉冲的间隔时间。频率越高,脉冲的间隔时间越短,变化速度越快。
和音调的关系
声响生理学复杂的原理不在这里解释,通过调试可以发现频率
调整可以模拟不同的音调,而占空比
的调整可以模拟发音长短。
调音

上图是 C4 音乐国际频率表,#
是过渡音本文先不做支持,主要看高/中/低音,后面的 Hz 数就是 PWM 的频率参数,一一对应做代码处理即可。
简谱学习
从曲谱网找到了一张《敢问路在何方》简谱如下

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

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

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

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

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

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

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

编码
简单曲谱编码

以上图《小星星》节选为例,这个简谱比较简单,每一小节只有单纯的 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) 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)
beeper.deinit()
|
https://github.com/caftxx/melody-micropython