试着在 ESP32 开发板上折腾用蜂鸣器播放音乐,简单来说需要将简谱上标记的音符转换成一系列(频率,时长)的序列,然后控制蜂鸣器发出指定频率、指定时长的声音。网上的一些教程在这一个步骤大都直接快进,在说明原理以后直接给出一段算好的音符序列数据。但实际上如果要手动进行这个转换,想必会比较麻烦,因此有了下面这个尝试。
文章目录
定义
每个音符使用音符数字 [1-7]
及后缀修饰符(^
、.
、-
、*
、/
、~
)来表示。
在无修饰符的情况下,一个音符数字表示一个四分音符。
结合简谱的调号、拍号,以及歌曲的速度(BPM),可以计算得到音符的频率和时长。
音符的频率
参考:https://github.com/lbernstone/Tone32/blob/master/src/pitches.h
音符的修饰符
/
:音符下方的横线,每个使当前音符时长减半^
:音符上方圆点,升高一个八度.
:音符下方圆点,降低一个八度-
:音符右侧横线,延长一个四分音符的时长~
:音符上方连音线,当前音符与下一个音符要连贯演奏*
:音符右下附点,一个附点延长原有时长的 0.5 倍,两个附点延长原有时长的 0.5 + 0.25 倍
解析、播放简谱字符串
// 发出指定频率的声音
void instrument_play(int freq);
// 停止
void instrument_stop();
// 延时(延时期间声音不停止)
void delay(int ms);
// music 乐谱字符串
// bpm 每分钟节拍数
// baseTone 调号 1=X
// divPerBeat 几分音符为一拍
static void play_music(const char *music, double bpm, char baseTone, int divPerBeat)
{
// 乐谱字符串读取偏移
int offset = 0;
// 一拍的时长
double beatDuration = 60000 / bpm;
// 一个四分音符的时长
int baseNoteDuration = beatDuration * divPerBeat / 4;
while (1) {
char toneChar = music[offset];
if (toneChar == '\0') {
break;
}
// 音高,音符数字 1 至 7
int tone = toneChar - '0';
// 音符上方或下方的圆点:音高升高或降低 octaveDelta 个八度
int octaveDelta = 0;
// 音符下方减时线数量,0=四分音符,1=八分音符,2=十六分音符
int div = 0;
// 音符右侧增时线数量
int multiplier = 1;
// 音符右侧附点数量
int dot = 0;
// 是否有连音线
bool connect = false;
// 是否仍有修饰符号需要读取
bool cont = true;
while (cont) {
char nextChar = music[++offset];
switch (nextChar) {
case '/': // 减时线
div++;
break;
case '^': // 音符升高八度
octaveDelta++;
break;
case '.': // 音符降低八度
octaveDelta--;
break;
case '-': // 增时线
multiplier++;
break;
case '~': // 连音线
connect = true;
break;
case '*': // 附点
dot++;
break;
case ' ': // 空格忽略
case '\n': // 换行忽略
break;
default:
cont = false;
break;
}
}
double noteDuration = baseNoteDuration;
// 计算 n 分音符时长
for (int i = 0; i < div; i++) {
noteDuration /= 2;
}
// 计算附点时长
double dotDuration = 0;
int dotDivider = 1;
for (int i = 0; i < dot; i++) {
dotDivider *= 2;
dotDuration += noteDuration / dotDivider;
}
noteDuration = noteDuration * multiplier + dotDuration;
int freq = tone == 0 ? 0 : getNoteFreq(baseTone, tone, 4 + octaveDelta);
if (connect) {
// 有连音线,音符之间无间隔
instrument_play(freq);
delay((int)(noteDuration));
instrument_stop();
} else {
// 无连音线,音符的 1/4 时长用作间隔
instrument_play(freq);
delay((int)(noteDuration * 0.75));
instrument_stop();
delay((int)(noteDuration * 0.25));
}
}
}
示例
歌曲是《外婆的澎湖湾》中的一段
play_music(
"3/.5/.5/.5/.6//.1/*6/.~5/."
"1/1/6/.6/.5.-"
"3/3/3/3/4/3/2//~1/*"
"2//2//2//2//2/3/2-"
"3/3/3/3//3//4/3/2//~1/*"
"6/.1/1/6/.5.-"
"3/3/3/3//3//4/3/2//~1/*"
"5//.5//.5//.5//.2//~1//7/.1-"
, 100, 'C', 4);
对应的简谱如下。
关于我造了个轮子这件事情
刚刚画简谱的时候发现了一个类似的实现,https://aigepu.com/ ,(也就是说我造了个轮子)。这个的实现比较完善,有 文档,也可以把符号化的曲谱转换成简谱 渲染 出来。