Teensy MIDI音乐播放器
Teensy 4.1是一款基于NXP的i.MX RT1062高性能微控制器的开发板,主频可达600 MHz。拥有丰富的I/O接口,扩展出多达42个数字I/O引脚、18个模拟输入引脚和多种通信接口(如SPI、I2C、UART等)。Teensy 4.1支持USB主机和设备模式,并配备了SD卡插槽和额外RAM和ROM扩展焊盘,适合于需要高处理速度和实时性能的项目,如音频处理、图像分析以及机器人控制等应用。其兼容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协议简介
MIDI(Musical 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对应的引脚是RX7,TX8,如果使用外置发声MIDI设备就接在这两个引脚上,同时注意方向,接反了也是不出声音的。
如果是电脑软件就要改为正常Serial输出。
-乐器设置:synth.setInstrument(0, 1, Harmonica)设置MIDI通道0的乐器为口琴。setInstrument方法的参数包括MIDI通道、音色库和乐器编号。
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协议中的音符值,范围从0到127,其中60代表中央C。音量范围也是0到127,表示音符的响度。持续时间表示音符播放的时间长度。
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 MHz,I/O接口丰富,支持USB主设备模式,有SD卡插槽及扩展焊盘,满足多样需求。
2.功能丰富:能实现手动输入和自动播放,通过MIDI协议播放预定义曲目,可手动切换曲目,曲目播放完毕后自动切换。
3.非阻塞播放:MelodyPlayer类的playNonBlocking()方法实现非阻塞播放,播放同时程序能执行其他任务,提升运行效率。
4. 扩展性强:修改trackList数组可轻松增减曲目,MelodyPlayer类设计便于调整播放逻辑。