本帖相关内容链接:
在游戏进行的过程中还有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音乐播放的可行解决方案 ,还可以继续开发同时播放不同音频的功能。
播放逻辑代码在成果贴提供。