
本帖相关内容链接:

在游戏进行的过程中还有midi音乐的伴奏。游戏开始自动启动播放,A键暂定播放,B键可切换音乐。
播放音乐关键函数实现了按照指定的节奏播放音乐,并支持暂停、切歌和循环播放的功能。 实现非阻塞的播放功能(非阻塞的意思是可以一边上班一边摸鱼),函数会记录当前时间,用于检查是否需要切换到新的音调。最主要的功能是对midi音频数据序列进行解码。
播放音乐关键函数
#include "MidiPlayer.h"
#include <Arduino.h>
std::map<String, int> tones = {
{"C0", 16}, {"C#0", 17}, {"D0", 18}, {"D#0", 19}, {"E0", 21}, {"F0", 22}, {"F#0", 23}, {"G0", 24}, {"G#0", 26}, {"A0", 28}, {"A#0", 29}, {"B0", 31}, {"C1", 33}, {"C#1", 35}, {"D1", 37}, {"D#1", 39}, {"E1", 41}, {"F1", 44}, {"F#1", 46}, {"G1", 49}, {"G#1", 52}, {"D2", 73}, {"D#2", 78}, {"E2", 82}, {"F2", 87}, {"F#2", 92}, {"G2", 98}, {"G#2", 104}, {"A2", 110}, {"A#2", 117}, {"B2", 123}, {"C3", 131}, {"C#3", 139}, {"D3", 147}, {"D#3", 156}, {"E3", 165}, {"F3", 175}, {"F#3", 185}, {"G3", 196}, {"G#3", 208}, {"A3", 220}, {"A#3", 233}, {"B3", 247}, {"C4", 262}, {"C#4", 277}, {"D4", 294}, {"D#4", 311}, {"E4", 330}, {"F4", 349}, {"F#4", 370}, {"G4", 392}, {"G#4", 415}, {"A4", 440}, {"A#4", 466}, {"B4", 494}, {"C5", 523}, {"C#5", 554}, {"D5", 587}, {"D#5", 622}, {"E5", 659}, {"F5", 698}, {"F#5", 740}, {"G5", 784}, {"G#5", 831}, {"A5", 880}, {"A#5", 932}, {"B5", 988}, {"C6", 1047}, {"C#6", 1109}, {"D6", 1175}, {"D#6", 1245}, {"E6", 1319}, {"F6", 1397}, {"F#6", 1480}, {"G6", 1568}, {"G#6", 1661}, {"A6", 1760}, {"A#6", 1865}, {"B6", 1976}, {"C7", 2093}, {"C#7", 2217}, {"D7", 2349}, {"D#7", 2489}, {"E7", 2637}, {"F7", 2794}, {"F#7", 2960}, {"G7", 3136}, {"G#7", 3322}, {"A7", 3520}, {"A#7", 3729}, {"B7", 3951}, {"C8", 4186}, {"C#8", 4435}, {"D8", 4699}, {"D#8", 4978}, {"E8", 5274}, {"F8", 5588}, {"F#8", 5920}, {"G8", 6272}, {"G#8", 6645}, {"A8", 7040}, {"A#8", 7459}, {"B8", 7902}, {"C9", 8372}, {"C#9", 8870}, {"D9", 9397}, {"D#9", 9956}, {"E9", 10548}, {"F9", 11175}, {"F#9", 11840}, {"G9", 12544}, {"G#9", 13290}, {"A9", 14080}, {"A#9", 14917}, {"B9", 15804}};
MidiPlayer::MidiPlayer()
{
// this->pinNumber = BUZZER_PIN;
}
MidiPlayer::~MidiPlayer()
{
}
// uint8_t MidiPlayer::pinNumber = 5;
uint8_t MidiPlayer::musicIndex = 0;
void MidiPlayer::init()
{
pinMode(this->pinNumber, OUTPUT);
this->paused = 0; // control by the start btn at gamekit,it will not play the song automatically.
// speaker.begin(); // confrim the buzzer is working.
}
// static pinNmber = 5;
void MidiPlayer::playNote(const char *note) // extract the freq from the note string.
{
static int lastnote = 0;
int freq = 0;
// freq = tones["A#5"]; // confirm that the key_value works
// 访问指定音符的频率
String _note = String(note);
if (tones.count(_note) > 0)
{
freq = tones[_note];
}
#if 0
if (!strcmp(note, "#"))
{ // means note == "#" return 0 ,and then !0==1 ,take effected. empty or silent note.
freq = 0;
}
else
{
for (int i = 0; i < 115; ++i)
{ // 115 is the numbers of the exist note items.
if (!strcmp(note, tones[i].note))
{ // search the right note in the dict.
freq = tones[i].freq; // set the its freq.
break; // leave and wait for next run.
}
}
}
#endif
if (freq != lastnote)
{ // the lastnote is 0 at first time.
lastnote = freq; // lastnote updated here,
// tone(this->pinNumber, freq); // when the frequence is changed, then play the new tone.
// speaker.tone(freq);
tone(this->pinNumber, freq); // used for pico
// Serial.print(freq);
}
}
void MidiPlayer::setSong(const char *song)
{
this->song = song;
this->p = this->song;
this->stopped = 0;
}
void MidiPlayer::play(int tempo, uint8_t looping, const char *song) // the key function here.
{
static unsigned long music_tick = millis(); // caculate the time.
if (song && this->song != song) // the most import change here.
{
this->setSong(song);
}
if (this->paused)
{ // check if stop by the control
return;
}
if (millis() - music_tick >= tempo) // 结束的时候 超过了 1节拍的时长。
{ // where dose the tempo come from. how low will it play. the tempo is fixed, by the song.
// if (this->p && *(this->p))
if (this->p)
{
char note[4];
sscanf(this->p, "%[^,]", note); // difficult point here.
// Serial.println(note);
this->playNote(note);
this->p = strchr(this->p, ','); // the pointer arrives at the next","
if (this->p)
{ // not empty(still has "," in the song list) ,will keep going to next tone.
++(this->p); // next char of the ",",the real freq index string.
}
}
else // outer judgement.
{
if (looping)
{ // repeat the song or not !
this->p = this->song;
}
else
{
this->stopped = 1; // move on till the end of the song
// noTone(this->pinNumber);
noTone(this->pinNumber);
Serial.println("End");
}
}
music_tick = millis();
}
}
uint8_t MidiPlayer::isStop()
{
return this->stopped;
}
uint8_t MidiPlayer::pause()
{
this->paused = 1;
this->playNote("#");
return 1;
}
uint8_t MidiPlayer::resume()
{
this->paused = 0;
return 1;
// }
}
void MidiPlayer::setMusicIndex()
{
musicIndex = (musicIndex + 1) % 4;
}
void MidiPlayer::musicupdate()
{
// static uint8_t musicIndex = 0; // change the music vaule by changing song.
// model.play(100, 1, my_people_my_country);
// 必须放在循环里面不然也是不出声音,以为是自己会一直播放,具体使用个多任务播放即可。至此,完成了一个midi解码库。
switch (musicIndex)
{
case 0:
play(80, 1, my_people_my_country);
break;
case 1:
play(120, 1, noname);
break;
case 2:
play(120, 1, turkish_march);
break;
case 3:
play(120, 1, demo_song);
break;
}
}
void MidiPlayer::deinit()
{
// this->pause();
this->paused = 1;
// speaker.mute();
noTone(this->pinNumber);
// and del a timer.
};
(1)Midi 曲目的数据格式:
const char *my_people_my_country = "#,#,A#5,A#5,A#5,A#5,C6,C6,C6,...#”
每个音符使用逗号进行分隔,接下来,函数会根据设定的节拍时间(音调的播放持续时间)判断是否到达了一个节拍点。如果到达了节拍点,函数会解析字符串中的音符,并播放对应的音符。
(2)音符快速识别
音符的识别使用了map容器来快速根据音符字符串查询到对应的音符频率数值,具体实现方式查看源码。
std::map<String, int> tones = {
{"C0", 16}, {"C#0", 17}, {"D0", 18},,,,{"A#9", 14917}, {"B9", 15804}};
解析字符串中的音符的逻辑是函数会移动指针到下一个逗号的位置,以准备播放下一个音符。如果还有逗号存在,函数会继续播放下一个音符, 如再未检测到逗号,表明播放结束,歌曲播放结束后会检测是否循环播放。
音频素材制作
可以网页:www.onlinesequencer.net 制作音频素材。以下是以“生日歌”的midi音频为例,介绍制作音频素材的方法。


进入编辑曲面状态后可拷贝得到的原始信息:
Online Sequencer:902213:0 B5 1 0;1 B5 1 0;2 C#6 1 0;4 B5 1 0;6 E6 1 0;8 D#6 1 0;11 B5 1 0;12 B5 1 0;13 C#6 1 0;15 B5 1 0;17 F#6 1 0;19 E6 1 0;22 B5 1 0;23 B5 1 0;24 B6 1 0;26 G#6 1 0;28 E6 1 0;29 E6 1 0;30 D#6 1 0;32 C#6 1 0;34 A6 1 0;35 A6 1 0;36 G#6 1 0;38 E6 1 0;40 F#6 1 0;42 E6 1 0;10 B5 1 43;:
音乐序列的编码信息,其中包含了音符(如 B5、C#6、E6 等)、时长。因为我们不需要音符的时长信息,我们的时长是通过节拍统一分配的,我们只要音符的顺序列表。 去除音频持续的时间,整理得到了我们想要的音频序列:
G5,G5,A5,G5,C6,B5,G5,G5,A5,G5,D6,C6,G5,G5,G6,E6,C6,B5,A5,F6,F6,E6,C6,D6,C6";
最终保存在cpp文件中供主程序条用。
G5,G5,A5,G5,C6,B5,G5,G5,A5,G5,D6,C6,G5,G5,G6,E6,C6,B5,A5,F6,F6,E6,C6,D6,C6";

这一套的播放逻辑非常巧妙有趣,用到容器来实现midi音符解码,是一套midi音乐播放的可行解决方案 ,还可以继续开发同时播放不同音频的功能。
播放逻辑代码在成果贴提供。
我要赚赏金
