这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » Let'sDo第2期任务-《贪吃蛇》-游戏背景音乐(个性化)

共6条 1/1 1 跳转至

Let'sDo第2期任务-《贪吃蛇》-游戏背景音乐(个性化)

菜鸟
2024-08-17 17:25:38     打赏

midiplayer_map2.png

本帖相关内容链接:

Let'sDo第2期任务-《贪吃蛇》-开箱贴(美图)

Let'sDo第2期任务-《贪吃蛇》-过程帖

Let'sDo第2期任务-《贪吃蛇》-成果帖-详解


MidiPlayer 音频播放逻辑.png

在游戏进行的过程中还有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;:


音乐序列的编码信息,其中包含了音符(如 B5C#6E6 等)、时长。因为我们不需要音符的时长信息,我们的时长是通过节拍统一分配的,我们只要音符的顺序列表。 去除音频持续的时间,整理得到了我们想要的音频序列:

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_file.png

这一套的播放逻辑非常巧妙有趣,用到容器来实现midi音符解码,是一套midi音乐播放的可行解决方案 ,还可以继续开发同时播放不同音频的功能。

       播放逻辑代码在成果贴提供。 


高工
2024-08-18 00:42:25     打赏
2楼

感谢分享


专家
2024-08-18 00:52:10     打赏
3楼

感谢分享


专家
2024-08-18 01:04:33     打赏
4楼

感谢分享


院士
2024-08-18 16:04:58     打赏
5楼

这个还能这么玩啊


专家
2024-08-22 08:57:35     打赏
6楼

感谢分享


共6条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]