前言
本程序为本人大一阶段,也是人生中创建的首个实现一定功能,且有成品化趋向的程序。在实现过程中仍然存在不少问题亟待解决,会在后记中进行相关说明。本文的撰写意在对完成的项目进行整理与存档,也特别感谢mareep学长为本程序设计提供的帮助(甚至本博客诞生的idea也源自mareep),以及冉哥对本项目的大力支持和debug。
预期实现功能
- 音符演奏:通过键盘按下或者按钮按下的方式进行单次的音符触发,从而触发对应行为,进行音符的弹奏。
此处的实现亮点是实现同时处理多个音符的按下并共同发声以及键盘按下和 UI界面按钮同步更新。
- 切换音色:利用MIDI库中自带的钢琴音色进行演奏音色的切换。
-
切换调性和音域:实现键盘控制区域高八度,低八度,大小调切换等音乐制作与演奏中常需使用的功能。
-
界面编辑:利用Vs-Qt框架进行GUI界面的开发制作。
程序设计架构
工程的文件系统
- UI文件:Qt框架中的界面渲染文件,保存并渲染界面制作效果
源文件:main.cpp用于存储主函数,piano_gui对ui界面的函数进行具体实现且对窗口控件进行相应创新,piano_relevant_ability项目用于存储具体发声功能以及围绕核心功能的相应实现。
存储文件:用于存储UI界面中可能需要的图片和文字内容。
头文件:存储需要使用的全局变量和功能函数声明
GUI的UML类图实现
以上是大致的程序运行逻辑,注意控件的创建在应用之后,这一点在后续的制作中尤为重要。
核心功能实现思路
音色切换
实现思路
简单到难以想象,就是根据HIMIDIOUT的参考文件音色表重设发送的DWORD字符串中控制音色的数值即可。
此处附本程序使用到的音色清单和对应参数
音色名称 音色MIDI代码 Acoustic Grand Piano 大钢琴(声学钢琴) 0 Bright Acoustic Piano 明亮的钢琴 1 Electric Grand Piano 电钢琴 2 Honky-tonk Piano 酒吧钢琴 3 Rhodes Piano 柔和的电钢琴 4 Chorused Piano 加合唱效果的电钢琴 5 Harpsichord 羽管键琴(拨弦古钢琴) 6 Clavichord 科拉维科特琴(击弦古钢琴) 7
核心代码
切换方式非常简单,因此只以切换某一个音色为例
void ChangeSound00()//切换音色到大钢琴
{
midiOutShortMsg(handle, 0xC0 | 0 << 8 | 1);
}
调性和八度切换
实现思路
此功能本质上是改变按键的映射,本程序实现的思路是键位固定,故改变映射值即可实现预期功能。
核心代码
- 初始化映射所有音高,初始化按键谱
//枚举音阶表->声音表单 enum Scale { Rest = 0, C8 = 108, B7 = 107, A7s = 106, A7 = 105, G7s = 104, G7 = 103, F7s = 102, F7 = 101, E7 = 100, D7s = 99, D7 = 98, C7s = 97, C7 = 96, B6 = 95, A6s = 94, A6 = 93, G6s = 92, G6 = 91, F6s = 90, F6 = 89, E6 = 88, D6s = 87, D6 = 86, C6s = 85, C6 = 84, B5 = 83, A5s = 82, A5 = 81, G5s = 80, G5 = 79, F5s = 78, F5 = 77, E5 = 76, D5s = 75, D5 = 74, C5s = 73, C5 = 72, B4 = 71, A4s = 70, A4 = 69, G4s = 68, G4 = 67, F4s = 66, F4 = 65, E4 = 64, D4s = 63, D4 = 62, C4s = 61, C4 = 60, B3 = 59, A3s = 58, A3 = 57, G3s = 56, G3 = 55, F3s = 54, F3 = 53, E3 = 52, D3s = 51, D3 = 50, C3s = 49, C3 = 48, B2 = 47, A2s = 46, A2 = 45, G2s = 44, G2 = 43, F2s = 42, F2 = 41, E2 = 40, D2s = 39, D2 = 38, C2s = 37, C2 = 36, B1 = 35, A1s = 34, A1 = 33, G1s = 32, G1 = 31, F1s = 30, F1 = 29, E1 = 28, D1s = 27, D1 = 26, C1s = 25, C1 = 24, B0 = 23, A0s = 22, A0 = 21 };
根据需求更新全局变量->按键映射频谱
所有的更新逻辑都是一样的,设计时使用的一个重设一遍的
黑奴写法,可以有所改进。
void highermaj(){
v =
{
{'Z',C4},{'X',D4},{'C',E4},{'V',F4},{'B',G4},{'N',A4},{'M',B4},
{'A',C5},{'S',D5},{'D',E5},{'F',F5},{'G',G5},{'H',A5},{'J',B5},
{'Q',C6},{'W',D6},{'E',E6},{'R',F6},{'T',G6},{'Y',A6},{'U',B6},
};
}
//高八度处理,本质上就是把所有按键映射值改一下,简单,黑奴化,但是有效。
MIDI演奏功能
考核要求利用windows.h的midi功能进行演奏和实现的方式,实际的商用midi程序本质上使用合成器进行波形合成与模拟,此处只供学习使用。
实现思路
定义midi线程并开启,把对应音高映射到相应的按键(两次枚举),在发生时见监听键盘按键的按下/松开状态以验证此时是否应当连续发声,并支持多个键按下时同步响应。
核心代码
- 开启线程进入监听
int piano() {
midiOutOpen(&handle, 0, 0, 0, CALLBACK_NULL);//开启线程并启动监听
for (const auto& pair : v)//对每个按键进行实时监听,记录按键值与按键状态
{
keys.emplace_back(pair.first);
}
hKeyboardHook =SetWindowsHookEx;//后略,初始化键盘钩子供使用,并在初始化失败时进行报错。
MSG msg;
while (GetMessage(&msg, nullptr, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
....//窗口响应并且进入适当循环
}
}
- 安装键盘钩子以监听按键动作(本质上是一种多线程技术)
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode >= 0) { const KBDLLHOOKSTRUCT* kbdStruct = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam); char keyChar = MapVirtualKey(kbdStruct->vkCode, MAPVK_VK_TO_CHAR); bool keyFound = false; for (auto& key : keys) { if (key.key == keyChar) { keyFound = true;//检测到按键改变记录的按键值并优先发送 if (wParam == WM_KEYDOWN) { if (!key.isActive) { // 防止重复发送 Note On key.isActive = true; playNote(v[key.key], key.isActive); } } else if (wParam == WM_KEYUP) { if (key.isActive) { // 防止重复发送 Note Off key.isActive = false; playNote(v[key.key], key.isActive); } } break; // 一旦找到匹配的键,就退出循环 } } return CallNextHookEx(nullptr, nCode, wParam, lParam);//返回并等待下一次回调响应 } }
- 演奏功能(利用windows.h函数回传的值发送到midi句柄中实现发声,其中key<<8决定了发送的音高)
void playNote(int key, bool isActive) {
if (isActive)
{
midiOutShortMsg(handle, (nowVelocity << 16) | (key << 8) | 0x90 | 1);
}
else
{
midiOutShortMsg(handle, (0x007f << 16) | (key << 8) | 1<<7 | 1);
}
}
GUI界面的实现
实现思路
首先创建窗口界面,插入一张(从水果里的钢琴合成器中截取的)设计好的图片到应用界面,在主函数中创建好对应窗口,随后把对应控件初始化封装为成员函数,在构造函数中调用,并链接按钮和对应槽完成设计。
核心代码
Piano_playing::Piano_playing(QWidget* parent) :QWidget(parent)//构造函数实现
{
QCoreApplication::setQuitLockEnabled(true);
ui.setupUi(this);
BuildTheKeyBoard();
ScaleChanging();
SoundChanging();
VelocityChanging();
Refreshed();
KeyUpOrDown();
}
顾名思义,除了初始化ui之外,分别创建了键盘按钮,大小调切换按钮,音色切换按你牛,力度改变滑块条,更新按键,对应实现属于Qt相关技术栈,不做过多赘述。
四.使用的技术栈
windows.h
对于应用开发十分重要的一个库(
一个oier基本用不到一点的库),可以实现多样化的windows系统下功能。
键盘钩子技术
hook(钩子)是一种特殊的消息处理机制,它可以监视系统或者进程中的各种事件消息,截获发往目标窗口的消息并进行处理。所以说,我们可以在系统中自定义钩子,用来监视系统中特定事件的发生,完成特定功能,如屏幕取词,监视日志,截获键盘、鼠标输入等等。
在本程序中使用的钩子技术为键盘钩子,其作用是拦截键盘信息并对其做预设处理,本程序中用于读取键盘按键的状态并进行对应发声操作。
hook(钩子)就是一个Windows消息的拦截机制,可以拦截单个进程的消息(线程钩子),也可以拦截所有进程的消息(系统钩子),本程序中使用的便是系统级别的键盘钩子技术。
mmeapi.h
- windows系统中用于管理音频相关功能的拓展库,在本程序中主要负责midi功能.
- 本程序使用的函数主要为为midiOutShortMsg,midiOutOpen,midiOutClose(可以点击查看);官方参考文档有详细的说明。
- 关于MIDI句柄和DWORD的midi对应信息相关内容可以参考CSDN有关文章(可以点击查看)。
Qt图形开发应用
Qt 是一个1991年由Qt Company开发的跨平台C++图形用户界面,应用程序开发框架,可构建高性能的桌面、移动及Web应用程序,也可用于开发非GUI程序,比如控制台]工具和服务器。Qt是面向对象的框架,使用宏等技术,Qt很容易扩展,并且允许真正的组件编程。
QPushButton组件
QPushButton 是 Qt 框架中的一个按钮控件,常用于创建可点击的按钮。用户可以通过单击按钮触发相应的事件或操作。它支持文本、图标的显示,并可自定义样式,广泛应用于各种桌面应用程序的用户界面中。
QWidget窗口界面
QWidget 是 Qt 所有界面控件的基类,几乎所有的窗口和控件都是直接或间接继承自 QWidget。它提供了基本的窗口功能,如事件处理、绘图、布局管理等,是构建图形用户界面(GUI)的核心组件。
QHBoxLayout布局
QHBoxLayout 是 Qt 中的一种水平布局管理器,用于将控件按水平方向依次排列。它可以自动调整控件的大小和位置,确保界面在窗口大小变化时保持良好的布局效果,适合用于需要横向排列组件的场景。
QTreeWidget树状表
QTreeWidget 是 Qt 提供的树形控件,用于显示层次化的数据结构。每个节点可以包含子节点,用户可以展开或折叠这些节点,方便查看和管理复杂的数据结构,广泛用于文件管理器、项目管理等应用中。
QSlider滑块
QSlider 是 Qt 中的滑动条控件,允许用户通过拖动滑块来选择数值范围内的某个值。它可以水平或垂直显示,常用于调节音量、亮度、进度条等场景,提供直观的交互方式。
windeployqt打包工具
windeployqt 是 Qt 提供的一个打包工具,主要用于将 Qt 应用程序及其所需的动态链接库(DLL)文件打包到一起,方便在 Windows 平台上分发和运行。它可以自动检测并复制依赖项,大大简化了部署流程。
亟待解决的问题
- 嵌入的图片可能会在难以理解的情况下被选中(
大雾),而且ui中只使用了一部分空间,没有把所有的图片中UI完整实现。(如下图,实际上很多按键和功能都是没有实现的)
– 这个程序的退出必须严格使用Quit按键,(
-
不能实现曲谱的录制和批处理,只能进行单次的演奏。
-
全局变量的管理不够细分化,整体架构稍显混乱。
-
将信号作为独立函数链接值connect,这略微有悖于Qt框架信号和槽封装机制。
学习一下!
牛啊