C++项目-MIDI电子琴软件设计
本文最后更新于 159 天前,如有失效或谬误请评论区留言。

设计思路

计划实现功能

  • 实现音符文件的读取:音符文件内存储音符的音调、音符的shortname(唯一标识符)、音符绑定的按键键码。
  • 实现配置文件的读取:配置文件内存储当前选择的MIDI输出设备和是否使用命令行模式。
  • 实现日志系统:支持INFO、WARN、SERIOUS、DUBUG四个级别的日志输出,并自动生成日志文件并存档。
  • 实现键盘映射管理器:实现键盘映射的便捷管理。
  • 实现演奏功能:实现基本的电子琴演奏功能,包括音量和音色调节。
  • 实现曲谱的录制和播放:实现曲谱的录制并保存为曲谱文件,曲谱文件的读取、曲谱的自动播放。
  • 实现命令行菜单,包括帮助、选择MIDI输出设备、开始演奏、录制曲谱、播放曲谱,打印键盘映射六个命令。
  • 实现GUI界面:包括主界面和演奏界面两个界面,以及演奏界面的按钮控件、琴键变灰动画。

软件整体实现思路

软件整体有两个模式:命令行模式和GUI模式,可在配置文件中选择使用的模式。
项目由Commands(命令)、Entities(实体)、KeyManager(键盘映射管理器),Logger(日志记录器)、主源文件(包含主函数,作为程序入口)构成。
命令部分源文件:

  • HelpCommand:帮助命令所在源文件,只用于命令行模式下帮助信息的打印。
  • MusicPlayCommand:曲谱演奏命令所在源文件,GUI模式和命令行模式共用,用于启动曲谱的演奏。
  • RecordCommand:曲谱录制命令所在源文件,GUI模式和命令行模式共用,用于启动曲谱的录制。
  • SelectCommand:MIDI输出设备选取命令所在源文件,仅用于命令行模式下MIDI输出设备的选择。
  • StartCommand:开启电子琴演奏命令所在源文件,GUI模式和命令行模式共用,用于开始电子琴弹奏。
    实体部分源文件:
  • MusicEntity:曲谱类,定义了曲谱名和储存音符的数据结构。
  • MusicNoteEntity:曲谱中的音符类,定义了音符标识符,音符操作时间等。
  • NoteEntity:音符类,定义了音高,音符shortname(唯一标识符),绑定的键盘键码

功能实现思路

(1)实现音符文件的读取:

定义函数”initNoteFile”,函数中先检查在软件的同级下有没有”notes”文件夹,若没有,则创建该文件夹,若有,则读取文件夹下的所有文件,使用文件输入流读取文件内的音高、唯一标识符、对应键码三个字段,若输入正确且无重复唯一标识符的其他音符,就调用NoteEntity类的构造函数构造相应音符对象并存储在一个map表中,并添加键盘映射。当文件夹下所有文件读取结束,则音符文件读取结束。
代码(有删减):

 if (!std::filesystem::exists("./notes")) {//创建音符文件夹
        std::filesystem::create_directory("./notes");
    }
    for(const auto &entry: std::filesystem::directory_iterator("./notes"))       {//读取文件夹内所有音符文件
        std::ifstream noteFile(entry.path());
        int noteNo;
        std::string shortName;
        int key;
        noteFile >> noteNo >> shortName >> key;
        if (noteFile.fail()) {
            Logger::serious("音符文件格式错误 按任意键退出程序");
        }
        if (noteMap.find(shortName) != noteMap.end()) {
            Logger::serious("音符文件" + shortName + "重复 按任意键退出程序");
        }
        noteMap.insert(std::pair(shortName, NoteEntity(noteNo, shortName, key)));
        keyManager.addMapping(key, shortName);
        }
}

(2)实现音符文件的读取:

函数”initCfgFile”,检查程序同级目录下是否有”config.cfg”文件,若没有,则创建该文件,并使用文件输出流添加默认值,若存在该文件,则循环使用getline逐行获取文件内容,然后使用string的find和substr方法解析文件,并把对应值存在相应的全局变量中。
核心代码:

std::ifstream cfgFile("./config.cfg");//打开文件输出流
std::string line;// 存储每一行
while (getline(cfgFile, line)) {// 逐行解析
    if (line.find("selectedMidiDev=") != std::string::npos) {
        selectedMidiDev = stoi(line.substr(line.find('=') + 1)) - 1;
    } else if (line.find("isCommandLineMode=") != std::string::npos) {
        isCommandLineMode = line.find("true") != std::string::npos;
    }
}

(3)实现日志系统:

定义静态类”Logger”,拥有四个静态公共方法,分别对应四个等级的日志输出。一个私有的枚举”logLevel”,对应四个日志等级,以及两个私有方法sentMessage、getCurrentTime。在四个静态公共方法中,调用sentMessage,向sentMessage方法传递日志等级和要输出的字符串,在sentMessage中拼接日志等级,和使用getCurrentTime获取的当前时间以及要输出的字符串为一个字符串。并根据现在是否处于控制台模式选择是否使用cout输出,最后将要输出的字符串通过文件输出流添加到日志文件。

(4)实现键盘映射管理器:

定义类”KeyManager”,拥有四个公共方法,分别为:”addMapping”(添加键盘映射),”getKeyNote”(根据键码获取音符标识符),”getNoteKey”(根据音符标识符获取相应键码),”commandMap”(打印出现有所有的映射关系)。拥有一个私有变量,为键码(int类型)和音符标识符(string类型)的map表,即通过addMapping进行对该map的”增”操作,通过其他三个方法实现对该map的”查”的操作。

(5)实现演奏功能:

函数”commandStart”,函数参数为选取的midi输出设备代码。该函数调用函数”initMidiOut”初始化MIDI输出设备,赋值全局变量MIDI输出设备的句柄,注册键盘钩子,进入消息循环。

①键盘钩子回调函数”KeyboardProc”:

监听键盘事件,调用函数”keyHandler”进行键盘处理,并调用下一个回调函数。

②键盘消息处理函数”keyHandler”:

拥有两个参数,分别为要处理按键的键码,和按键是被按下还是松开。该函数判断按下的是否是功能键(退出、调节音量、调节音色),若是,则执行对应功能的函数;若不是,则执行”noteKeyHandler”函数,处理音符按键。

③音符按键处理函数”noteKeyHandler”:

与”keyHandler”的参数一样。该函数从keyManager类的实例中取出键码对应音符,并取出相应音符对象,并向MIDI输出设备发送相应音符开启和音符关闭消息。
代码:

void noteKeyHandler(DWORD key, bool sign) {
    if (sign) {// sign为true则为键盘按下
        std::string noteName = keyManager.getKeyNote(static_cast(key));
        if (!noteName.empty() && !keyState[key]) {
            keyState[key] = true;
            DWORD noteOnMsg = 0x90 | nowChannel; // 音符开启消息
            noteOnMsg |= (noteMap[noteName].noteNo & 0x7F) << 8;  // 音符号
            noteOnMsg |= (nowVelocity & 0x7F) << 16; // 力度
            midiOutShortMsg(hMidiOut, noteOnMsg);
            if (isRecording) {
                //录制相关代码 在此省略
            }
            return;
        }
    } else {
        std::string noteName = keyManager.getKeyNote(static_cast(key)); // 获取音符名
        if (!noteName.empty()) {
            keyState[key] = false;
            DWORD noteOnMsg = 0x80 | nowChannel; // 0x90表示Note Off消息
            noteOnMsg |= (noteMap[noteName].noteNo & 0x7F) << 8;  // 音符号
            noteOnMsg |= (0 & 0x7F) << 16; // 力度
            midiOutShortMsg(hMidiOut, noteOnMsg);
            if (isRecording) {
              //录制相关代码 在此省略
            }
        }
        return;
    }
}

(6)实现曲谱的录制和播放:

①曲谱录制:

函数”commandRecord”,有一个参数为曲谱的名字。首先判断调用方是否来自GUI界面(GUI界面原来就有演奏线程,不需要另开线程,故在此特判),如果是,则不开启演奏线程,如果不是,开启一个演奏线程,通过C++11的特性之一chrono头文件中的steady_clock记录开始录制的时间,再标记全局变量”isRecording”为true,接下来等待演奏线程加入,演奏线程加入后,保存曲谱文件,曲谱录制成功。
在演奏线程中,有函数noteKeyHandler,在其中若全局变量”isRecording”为true,即正处于录制模式,就计算当前时间与录制开始时间的时间差,并把音符标识符,时间差,和键位是按下还是抬起输出到曲谱文件中。
代码:

if (isRecording) {
    std::chrono::steady_clock::time_point via = std::chrono::steady_clock::now(); //获取当前时间
    std::chrono::duration duration = std::chrono::duration_cast>(via -startRecordTime); // 计算时间差
    musicFile << noteName << " " << duration.count() << " " << true << std::endl; // 写入文件
}

②曲谱文件的读取:

曲谱文件的读取和音符文件读取相似,利用文件输入流获取数据,构造对象存入相应的数据结构内,在这里不多赘述。

③曲谱的播放:

函数”commandMusicPlay”,有一个参数,为播放的曲谱名,在全局的存储曲谱的数据结构中查询是否有该曲谱,若曲谱不存在则提示警告信息并退出,若存在,则从数据结构中取出曲谱信息,记录开始时间,进入播放循环。在播放循环中,程序遍历曲谱对象中的曲谱音符向量,每次循环记录一次当前时间与开始播放时间的时间差,若时间差大于等于对应音符的时间差,则调用演奏线程的方法”noteKeyHandler”处理对应音符,演奏出音符。

(7)实现命令行菜单:

进入死循环,每次输入一个字符串,若与命令匹配则调用相关函数,若输入exit则退出循环,程序结束。

(8)实现GUI界面:

函数”initGui”,首先绘制主界面,包括标题和背景图,然后调用函数”keyAndMouseHandler”处理键盘和鼠标信息

①函数”keyAndMouseHandler”:

进入死循环,循环获取信息ExMessange,监听鼠标左键按下消息,判断按下坐标是否在各按钮内,若在各按钮内则执行相应的函数;监听键盘按下消息,若按下ESC且不处于录制模式则关闭窗口退出程序(录制模式需要按下ESC返回正常演奏模式),在主界面按下任意键则开启绘制界面的绘制线程,并使其分离,开始演奏。

②函数”drawPianoGUI”:

绘制标题,状态栏,按钮栏,并调用绘制琴键函数”drawPianoKeys”

③函数”drawPianoKeys”:

绘制三排琴键,按照物理键盘排序,绘制按键名,通过keyManager的实例获取键位对应音符名,并绘制音符名,若按键的状态为被按下,则使琴键变灰

所使用的技术栈

windows.h

windows.h是windows操作系统提供的头文件之一,它包含了很多用于Windows应用程序开发的声明和定义,该头文件提供了对Windows API的访问,允许开发者编写使用Windows特定功能的程序

(1) 键盘钩子Keyboard Hook:

键盘钩子是一种系统级别的编程技术,用于监视和拦截键盘输入时间,这一技术允许程序在操作系统层面截获并处理用户的键盘输入。使用”SetWindowsHookEx”函数来安装一个键盘钩子,在参数中提供一个回调函数,该函数会在用户按下或释放键盘上的按键时被调用,我们就可以在回调函数中实现监听键盘输入的逻辑,处理特定的键盘输入。

mmeapi.h

Mmeapi.h是Windows多媒体扩展的一部分,它提供了一些常量、结构和函数,用于访问和控制多媒体设备。

(1) 函数midiOutOpen:

用于打开MIDI输出设备,参数包括指向MIDI设备句柄类型的指针,MIDI设备的设备标识符,回调函数的地址(可以设为NULL,表示不使用回调函数)等。如果函数执行成功,该函数会返回” MMSYSERR_NOERROR”,如果其执行失败,则会返回错误代码。

(2) 函数midiOutShortMsg:

用于向MIDI输出设备发送一条短消息,其参数包括MIDI输出设备的句柄,和要发送给MIDI输出设备的消息,该消息是一个32位值,格式为:”0x00SSSSCC”其中的”SSSS”为状态字节,”CC”为通道字节,他们定义了MIDI事件类型和通道。如果函数执行成功,函数会返回” MMSYSERR_NOERROR”, 如果其执行失败,则会返回错误代码。

(3) 函数midiOutClose:

用于关闭MIDI输出设备,其参数为MIDI输出设备的句柄。返回值则与上面两个函数一样。

(4) 函数midiOutGetNumDevs:

用于获取系统中可用的MIDI输出设备数量,它没有参数。返回值就是MIDI输出设备的数量

(5) 函数midiOutGetDevCaps:

用于获取指定的MIDI输出设备的能力信息。其参数为MIDI输出设备的标识符、一个指向”MIDIOUTCAPS”结构体的指针和该结构体的大小。其返回值与前三个函数一样。

(6) 结构MIDIOUTCAPS:

存储MIDI输出设备的能力信息,它包括:

①wMid:制造商ID,标识设备的制造商。

②wPid:产品ID,X用于表示设备的型号。

③vDriverVersion:驱动程序版本。

④szPname:设备名称。

⑤dwSupport:设备支持的功能标志,包括支持的最大通道,音符数等等。

时间库chrono

Chrono时间库是C++标准库中的头文件,在C++11被引入,他提供了强大的时间相关的功能。

(1)时间点:

std::chrono::time_point表示时间轴上的一个点,可以用来表示一个时刻。

(2)时间段:

std::chrono::duration 表示持续的一段时间,可以以不同的事件单位来表示,最精确为纳秒。

(3)时钟类:

chrono提供了三个时钟,分别是system_clock、steady_clock、high_resolution_clock。分别为系统时间,即系统左下角显示的日期时间,可以被用户手动调整;稳定的时钟,不能被用户手动调整,适合用于记录时间差等;高精度时钟,它和steady_clock作用相似,但精度可能更高。

EasyX图形库

EasyX是一个简单易用的图形库,用于Windows平台的图形界面的绘制和图形程序的开发,提供了一系列简单的API,使得用户能较方便的绘制窗口、绘制图形。

(1)函数initgraph:

用于初始化绘图环境,两个参数分别为窗口的长和宽,单位为像素。

(2)函数setfillcolor:

用于设置填充图形的颜色,其参数为EasyX提供的颜色常量或用函数RGB(a,b,c)表示的颜色。

(3)函数settextcolor:

用于设置字符的颜色,其参数与setfillcolor相同。

(4)函数settextstyle:

用于设置字符的样式,包括大小,字体,是否斜体,粗细等。

(5)函数outtextstyle:

用于在界面上显示字符,其参数包括字符左上角点的x,y坐标和要输出的字符串

(6)函数solidrectangle:

用于绘制实心矩形,其参数为矩形左上角和右上角点的坐标。

(7)函数loadimage:

用于加载图片,其参数包括一个IMAGE类型的指针和图片路径。

(8)函数putimage:

用于在界面上显示图片,其参数包括图片左上角的x,y坐标和IMAGE指针。

代码仓库

本项目仓库地址为https://gitee.com/ly1093322955/MIDIPiano

以上仅代表个人观点,如有不当之处,欢迎与我进行讨论
版权声明:除特殊说明,博客文章均为Mareep原创,依据CC BY-SA 4.0许可证进行授权,转载请附上出处链接及本声明。

评论

  1. 1916954944
    Windows Edge 129.0.0.0
    7 月前
    2024-10-10 11:12:09

    感谢带佬开源(bushi)

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇