Funpack3-5Teensy MIDI 音乐播放器
该项目使用了Teensy 4.1,实现了USB乐器的设计,它的主要功能为:本项目基于Teensy 4.1开发板打造USB乐器,能实现手动输入与自动播放。它借助MIDI协议播放预定义曲目,通过M5UnitSynth模块转换MIDI信号为音频输出,MelodyPlayer类管理曲目播放,具备非阻塞播放等功能,且扩展性良好。。
标签
Funpack活动
Teensy 4.1
genvex
更新2025-01-15
华南农业大学
119


 Teensy MIDI音乐播放器

 

Teensy 4.1是一款基于NXPi.MX RT1062高性能微控制器的开发板,主频可达600 MHz。拥有丰富的I/O接口,扩展出多达42个数字I/O引脚、18个模拟输入引脚和多种通信接口(如SPII2CUART等)。Teensy 4.1支持USB主机和设备模式,并配备了SD卡插槽和额外RAMROM扩展焊盘,适合于需要高处理速度和实时性能的项目,如音频处理、图像分析以及机器人控制等应用。其兼容Arduino开发环境,方便开发者快速上手。

 

重要参考资源:

https://www.pjrc.com/store/teensy41.html  板卡使用入门介绍

https://www.eetree.cn/platform/3384      硬禾活动主页

https://www.pjrc.com/teensy/gui/         Teensy音频库的音频系统设计工具

Teensy音频库的音频系统设计工具,这是一个图形化工具,允许用户轻松绘制处理16位、44.1 kHz流式音频的系统,同时运行Arduino草图。该工具支持多种输入输出选项,并能生成代码以在Arduino编辑器中实现用户设计的系统。



 任务选择:

任务2:自行连接排针和按键(或滑动电阻器等),实现一个USB乐器,可以实现手动输入和自动播放

 

 

项目内容:

这个项目是能够通过MIDI协议播放预定义的曲目。项目中使用了M5UnitSynth库来控制MIDI播放器,MelodyPlayer类来播放曲目,以及ADKey类来处理按键输入。在主程序中,通过读取按键输入来切换曲目,如果当前曲目播放完毕,则释放内存并播放下一个曲目。

 1.硬件与依赖

M5UnitSynth:这是一个MIDI合成器模块,负责将MIDI信号转换为音频输出。项目中使用M5UnitSynth对象synth来初始化和管理MIDI合成器。

ADKey:这是一个模拟按键模块,用于检测用户的按键操作。项目中通过ADKey对象key来读取按键状态,并根据按键操作切换曲目。

 2.软件架构

MelodyPlayer:这是项目的核心类,负责管理曲目的播放。它接收MIDI合成器对象、曲目的音符序列、音符数量、曲目速度等参数,并提供非阻塞的播放功能。通过playNonBlocking()方法,曲目可以在后台播放,而主程序可以继续执行其他任务。

TrackInfo结构体:这是一个包含曲目信息的结构体,包括曲目名称、音符序列、音符数量和曲目速度等。项目中通过trackList数组来存储多个曲目的信息。

 3.功能实现

曲目切换:loop()函数中,项目不断检测按键状态。当检测到按键按下时,它会切换到下一个曲目。如果当前曲目播放完毕,项目会自动切换到下一个曲目,直到所有曲目播放完毕。

非阻塞播放:通过playNonBlocking()方法,项目实现了非阻塞的曲目播放。这意味着在曲目播放的同时,程序可以继续执行其他任务,如检测按键状态。

 4.内存管理

项目中使用了动态内存分配来创建和销毁MelodyPlayer对象。每次切换曲目或曲目播放完毕时,项目会释放当前曲目的内存,并重新分配内存以加载新的曲目。这种设计确保了内存的有效利用,避免了内存泄漏。

 

编程环境为:VSCODE-pio


 

一、 synth模块的工作原理

  synth模块是项目中的核心组件之一,负责将MIDI信号转换为音频输出。它基于M5UnitSynth类实现,通过MIDI协议与Teensy 4.1设备进行通信。synth模块通过MIDI协议与M5Stack设备进行通信,将MIDI信号转换为音频输出。它的工作原理包括初始化串口通信、设置乐器音色、发送MIDI消息和控制音符的播放。通过与MelodyPlayer类的配合,synth模块实现了非阻塞的曲目播放功能。

 1. MIDI协议简介

MIDIMusical Instrument Digital Interface)是一种用于电子乐器、计算机和其他设备之间通信的标准协议。它不直接传输音频信号,而是传输控制信息,如音符的开关、音量、音色等。MIDI协议的主要优势是数据量小,适合在资源有限的嵌入式系统中使用。

 2. M5UnitSynth模块的功能

M5UnitSynth模块是一个MIDI合成器,负责接收MIDI信号并将其转换为音频输出。它的主要功能包括:

-音符播放:根据接收到的MIDI音符信息,生成相应的音频信号。

-乐器设置:支持设置不同的乐器音色,如钢琴、吉他、口琴等。

-音量控制:可以调整音符的音量大小。

-音高控制:可以调整音符的音高。

 3. synth模块的初始化

在项目的setup()函数中,synth模块通过以下步骤进行初始化:

-串口通信:synth.begin(&Serial1, UNIT_SYNTH_BAUD)初始化MIDI合成器,并指定使用Serial1串口进行通信。UNIT_SYNTH_BAUD是串口的波特率,通常为31250,这是MIDI协议的标准波特率。

Serial1对应的引脚是RX7TX8,如果使用外置发声MIDI设备就接在这两个引脚上,同时注意方向,接反了也是不出声音的。

如果是电脑软件就要改为正常Serial输出。


-乐器设置:synth.setInstrument(0, 1, Harmonica)设置MIDI通道0的乐器为口琴。setInstrument方法的参数包括MIDI通道、音色库和乐器编号。

image.png


void setup()
{
  Serial.begin(9600);
  Serial.println("working");
  key.begin();                            // 初始化按键值
  synth.begin(&Serial1, UNIT_SYNTH_BAUD); // midi播放器的输入要接到设备的输出引脚上(TX),搞了一个晚上都没响,第二天误打误撞才响了,对应Serial1插在pin1,第三个管脚。
  synth.setInstrument(0, 1, Harmonica);   // 设置乐器
  // 初始化第一个曲目
  const TrackInfo &track = trackList[currentTrackIndex];
  currentTrack = new MelodyPlayer(synth, track.melody, track.noteCount, track.tempo, false);
  Serial.printf("%d,%d\n", track.noteCount, track.tempo);
  currentTrack->reset();
  Serial.println("Start playing...");
}

 4.音符的播放

synth模块通过playNote()方法播放音符。该方法的参数包括音符编号、音量和持续时间。音符编号对应MIDI协议中的音符值,范围从0127,其中60代表中央C。音量范围也是0127,表示音符的响度。持续时间表示音符播放的时间长度。

 5. MIDI消息的发送

synth模块通过串口发送MIDI消息来控制音符的播放。MIDI消息包括以下几种类型:

- Note On:表示开始播放一个音符。消息包括音符编号和音量。

- Note Off:表示停止播放一个音符。消息包括音符编号和音量(通常为0)。

- Program Change:表示改变乐器音色。消息包括音色编号。

 6..MelodyPlayer类的配合

synth模块与MelodyPlayer类紧密配合,实现曲目的播放。MelodyPlayer类负责管理曲目的音符序列和播放逻辑,而synth模块负责将音符序列转换为音频输出。通过playNonBlocking()方法,MelodyPlayer类可以非阻塞地播放曲目,而synth模块则负责实时生成音频信号。

 

  void loop()
{
  key.readADC();
  if (key.getKey() == 0)
  {
    currentTrackIndex++;
    if (currentTrackIndex >= trackList.size())
    {
      currentTrackIndex = 0;
    }
    delete currentTrack; // 释放内存
    currentTrack = nullptr;
    const TrackInfo &track = trackList[currentTrackIndex];
    Serial.println(track.name);
    currentTrack = new MelodyPlayer(synth, track.melody, track.noteCount, track.tempo, false);
  };
  // key.playMIDINote();// 同时播放按键声音。


  currentTrack->playNonBlocking();


#if 1 // 如果当前曲目播放完毕
  if (currentTrack->isFinished())
  {
    delete currentTrack;  // 释放内存
    currentTrack = nullptr;
    // 检查是否还有未播放的曲目
    if (currentTrackIndex < trackList.size() - 1)
    {
      currentTrackIndex++;
      const TrackInfo &track = trackList[currentTrackIndex];
      Serial.println(track.name);
      currentTrack = new MelodyPlayer(synth, track.melody, track.noteCount, track.tempo, false);
    }
    else
    {
      currentTrack = nullptr; // 所有曲目播放完毕
      Serial.println("playlist is empty");
    }
  }
#endif
}

 

 二、非阻塞播放的实现方式

项目中,非阻塞播放通过MelodyPlayer类的playNonBlocking()方法实现。它通过 M5UnitSynth 对象播放 MIDI 音符,并支持循环播放和动态调整播放速度。通过使用 millis() 函数来跟踪时间,该类确保播放不会阻塞主循环,从而允许其他任务同时执行。


 1.非阻塞播放的基本原理

非阻塞播放的核心思想是将播放过程分解为多个小步骤,并在每次循环中只执行一个步骤,而不是一次性完成整个播放过程。这样,程序可以在每次循环中执行其他任务,而不会因为播放操作而阻塞。

 2. MelodyPlayer类的设计

MelodyPlayer类负责管理曲目的播放。它的主要成员包括:

 音符序列:存储曲目的音符信息。

 音符数量:记录曲目中音符的总数。

 当前播放位置:记录当前播放到哪个音符。

 播放速度:控制音符之间的时间间隔。

 3. playNonBlocking()方法的实现

playNonBlocking()方法是实现非阻塞播放的关键。它的工作原理如下:

 检查当前播放状态:方法首先检查当前是否正在播放曲目。如果曲目已经播放完毕,则直接返回。

 播放当前音符:如果当前有音符需要播放,方法会通过MIDI合成器播放该音符,并更新当前播放位置。

 控制播放间隔:根据曲目的速度(tempo),方法会计算下一个音符的播放时间,并设置一个定时器或延迟,以确保音符之间的间隔符合曲目的节奏。

 返回控制权:在播放完当前音符后,方法立即返回,允许主程序继续执行其他任务。

 4.与主循环的配合

loop()函数中,playNonBlocking()方法被反复调用。每次调用时,它只处理一个音符的播放,然后立即返回。这样,主程序可以在每次循环中执行其他任务,如检测按键状态、更新显示等。

 5.示例代码

以下是playNonBlocking()方法的简化实现:

 void MelodyPlayer::playNonBlocking()
{
    if (finished)
        return;
    unsigned long currentTime = millis();


    // 如果到达播放下一个音符的时间
    if (currentTime - lastNoteTime >= getNoteDuration(currentNote) * 0.9)
    {
        // 关闭当前音符
        synth.setNoteOff(0, melody[currentNote * 2], 127);


        // 移动到下一个音符
        currentNote++;


        // 如果还有音符,播放下一个音符
        if (currentNote < notes)
        {
            synth.setNoteOn(0, melody[currentNote * 2], 127);
            lastNoteTime = currentTime; // 更新最后一次播放的时间
        }
        else
        {
            // 如果是循环播放,重新开始
            if (loopPlayback)
            {
                currentNote = 0;
                synth.setNoteOn(0, melody[currentNote * 2], 127);
                lastNoteTime = currentTime;
            }
            else
            {
                currentNote = notes;
                finished = true; // 否则标记为播放结束
            }
        }
    }
}

总结:

项目基于Teensy 4.1开发板的USB乐器项目亮点突出,在硬件、功能、架构等方面具备优势,为音乐创作与应用提供便利。具体如下:

1.硬件优势:依托高性能Teensy 4.1开发板,主频达600 MHzI/O接口丰富,支持USB主设备模式,有SD卡插槽及扩展焊盘,满足多样需求。

2.功能丰富:能实现手动输入和自动播放,通过MIDI协议播放预定义曲目,可手动切换曲目,曲目播放完毕后自动切换。

3.非阻塞播放:MelodyPlayer类的playNonBlocking()方法实现非阻塞播放,播放同时程序能执行其他任务,提升运行效率。

4.  扩展性强:修改trackList数组可轻松增减曲目,MelodyPlayer类设计便于调整播放逻辑。

附件下载
midiplayer.zip
团队介绍
\
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号