C++ 使用MIDI库演奏《晴天》
在每个midiEvent里埋藏故事的小黄花
而青春不过是一串未导出的cmake工程文件
终将明白
有些旋律永远停在#pragma once的注释里
有些人永远停在未定义的引用里
却愿始终记得如何编译出一场永不落幕的晴天
所以如果你也想听听,可以在PowerShell里运行以下指令:
git clone https://github.com/TwilightLemon/SunnyDays
cd SunnyDays
mkdir build
cd build
cmake .. -G "MinGW Makefiles"
mingw32-make
./SunnyDays.exe
没环境?巧了,她也如是说。
幸运的话能得到以下效果:

下面来简单讲讲如何使用C++和MIDI库作曲吧。
一、开始工作
1. 引入MIDI库和相关控制类
在CMakeLists.txt中:
target_link_libraries(SunnyDays winmm)
在MIDIHelper.h中:
#include <windows.h>
#pragma comment(lib,"winmm.lib")
定义Scale(音阶), Instrument(乐器, 仅包括部分)等枚举。我把Drum单独提了出来。
enum Scale
{
X1 = 36, X2 = 38, X3 = 40, X4 = 41, X5 = 43, X6 = 45, X7 = 47,
L1 = 48, L2 = 50, L3 = 52, L4 = 53, L5 = 55, L6 = 57, L7 = 59,
M1 = 60, M2 = 62, M3 = 64, M4 = 65, M5 = 67, M6 = 69, M7 = 71,
H1 = 72, H2 = 74, H3 = 76, H4 = 77, H5 = 79, H6 = 81, H7 = 83,
LOW_SPEED = 500, MIDDLE_SPEED = 400, HIGH_SPEED = 300,
_ = 0XFF
};
enum Drum{
BassDrum = 36, SnareDrum = 38, ClosedHiHat = 42, OpenHiHat = 46
};
enum Instrument{
AcousticGrandPiano = 0, BrightAcousticPiano = 1,
ElectricGrandPiano = 2, HonkyTonkPiano = 3,
ElectricPiano1 = 4, ElectricPiano2 = 5
};
一些基础方法,包括初始化/关闭设备、设置参数、播放单个音符和播放和弦等。
void initDevice();
void closeDevice();
void setInstrument(int channel, int instrument);
void setVolume(int channel, int volume); void PlayNote(HMIDIOUT handle, UINT channel, UINT note, UINT velocity); void playChord(HMIDIOUT handle, UINT channel, UINT note1, UINT note2, UINT note3, UINT note4, UINT velocity); void playChord(HMIDIOUT handle, UINT channel, UINT note1, UINT note2, UINT note3, UINT velocity);
在MIDIHelper.cpp中:
void initDevice(){
midiOutOpen(&hMidiOut, 0, 0, 0, CALLBACK_NULL);
}
void closeDevice(){
midiOutClose(hMidiOut);
}
void setInstrument(int channel,int instrument){
if (channel > 15 || instrument > 127) return;
DWORD message = 0xC0 | channel | (instrument << 8);
midiOutShortMsg(hMidiOut, message);
}
void setVolume(int channel,int volume){
if (channel > 15 || volume > 127) return;
DWORD message = 0xB0 | channel | (7 << 8) | (volume << 16);
midiOutShortMsg(hMidiOut, message);
}
//播放单个音符,note是音符,velocity是力度
void PlayNote(HMIDIOUT handle, UINT channel, UINT note, UINT velocity) {
if (channel > 15 || note > 127 || velocity > 127) return;
DWORD message = 0x90 | channel | (note << 8) | (velocity << 16);
midiOutShortMsg(handle, message);
}
//四指和弦
void playChord(HMIDIOUT handle, UINT channel, UINT note1, UINT note2, UINT note3, UINT note4, UINT velocity){
if (channel > 15 || note1 > 127 || note2 > 127 || note3 > 127 || note4 > 127 || velocity > 127) return;
DWORD message1 = 0x90 | channel | (note1 << 8) | (velocity << 16);
DWORD message2 = 0x90 | channel | (note2 << 8) | (velocity << 16);
DWORD message3 = 0x90 | channel | (note3 << 8) | (velocity << 16);
DWORD message4 = 0x90 | channel | (note4 << 8) | (velocity << 16);
midiOutShortMsg(handle, message1);
midiOutShortMsg(handle, message2);
midiOutShortMsg(handle, message3);
midiOutShortMsg(handle, message4);
}
//三指和弦
void playChord(HMIDIOUT handle, UINT channel, UINT note1, UINT note2, UINT note3, UINT velocity) {
if (channel > 15 || note1 > 127 || note2 > 127 || note3 > 127 || velocity > 127) return;
DWORD message1 = 0x90 | channel | (note1 << 8) | (velocity << 16);
DWORD message2 = 0x90 | channel | (note2 << 8) | (velocity << 16);
DWORD message3 = 0x90 | channel | (note3 << 8) | (velocity << 16);
midiOutShortMsg(handle, message1);
midiOutShortMsg(handle, message2);
midiOutShortMsg(handle, message3);
}
2. 初始化和结束
先在头文件中定义一个全局MIDI句柄:
extern HMIDIOUT hMidiOut;
在入口处初始化MIDI设备并在结束时关闭:
HMIDIOUT hMidiOut;
int main() {
initDevice();
//...
closeDevice();
return 0;
}
初始化MIDI设备之后,为每一个乐器分配一个通道channel(0~15,通常9分配给打击类乐器,例如鼓组),控制音量volume,然后就可以开始演奏了。
二、自制简易乐谱
以Voice.cpp为例,定义一个数组为频谱,控制停顿和音符,遍历数组播放:
1 namespace SunnyDays{
2 int channelVoice=1;
3 void playVoice(int note, int velocity){
4 PlayNote(hMidiOut, channelVoice, note, velocity);
5 }
6 void voice(){
7 Sleep(13100);//等待前奏
8 int sleep = 390;
9 int data[] =
10 {
11 //故事的小黄花
12 -90,
13 300,M5,M5,M1,M1,_,M2,M3,_,
14 //从出生那年就飘着
15 -90,
16 M5,M5,M1,M1,0,M2,M3,300,M2,M1,_,
17 //童年的荡秋千
18 -90,
19 300,M5,M5,M1,M1,_,M2,M3,_,
20 //随记忆一直晃到现在
21 -90,
22 M3,_,500,M2,M3,M4,M3,M2,M4,M3,700,M2,700,_,
23 //...
24 }
25 for (auto i : data) {
26 if(i==-30){logTime("Enter chorus");continue;}//调试用
27 if(i==-90){NextLyric(); continue;}
28 if (i == 0) { sleep = 180; continue; }
29 //...
30 if (i == _) {
31 Sleep(390);
32 continue;
33 }
34
35 playVoice(i, 80);
36 Sleep(sleep);
37 }
38 }
39 }
打个鼓:
1 namespace SunnyDays{
2 int channelBassDrum=9;
3
4 void playDrum(int note, int velocity, int duration){
5 PlayNote(hMidiOut, channelBassDrum, note, velocity);
6 if(duration>0) {
7 Sleep(duration);
8 PlayNote(hMidiOut, channelBassDrum, note, 0);
9 }
10 }
11
12 void bassDrum(){
13 Sleep(11260);
14 cout<<"Drum Bass Start!"<<endl;
15 playDrum(SnareDrum,100,180);
16 playDrum(SnareDrum,100,210);
17 playDrum(BassDrum, 100, 210);
18 playDrum(SnareDrum,100,190);
19 playDrum(BassDrum, 100, 210);
20 playDrum(SnareDrum,100,200);
21 playDrum(SnareDrum,100,200);
22 playDrum(OpenHiHat,100,-1);
23 Sleep(200);
24 //...
25 }
26 }
简易副歌和弦,是从B站一位up主那里学的(已经忘记是哪位了qwq):
1 namespace SunnyDays {
2 int channelChord=2;
3 void chordLevel(int level,int sleep,int repeat=2,int vel=70){
4 repeat--;
5 int down=8;
6 if(level==1){
7 //一级和弦 加右指
8 playChord(hMidiOut, channelChord, M1, M3, M5, L1, vel);
9 while(repeat--) {
10 Sleep(sleep);
11 playChord(hMidiOut, channelChord, M1, M3, M5, vel - down);
12 }
13 }else if(level==3){
14 //三级和弦 加右指
15 playChord(hMidiOut, channelChord, M3, M5, M7, L3, vel);
16 while(repeat--) {
17 Sleep(sleep);
18 playChord(hMidiOut, channelChord, M3, M5, M7, vel - down);
19 }
20 }
21 //...
22 }
23 void chord(){
24 Sleep(63724);
25 int sleep=740;
26 int data[]={
27 //刮风这天 我试过握着你手
28 1,4,
29 6,4,
30 //但偏偏 雨渐渐
31 4,2,
32 5,2,
33 //大到我看你不见
34 1,4,
35 //还有多久 我才能
36 3,4,
37 //↑ 在你身边
38 6,4,
39 //↓ 等到放晴的那天
40 4,4,
41 //↑ 也许我会比较好一点
42 5,4,
43 //..
44 }
45 int count=sizeof(data)/sizeof(int);
46 for(int i=0;i<count;i+=2){
47 cout<<"chord "<<data[i]<<" x"<<data[i+1]<<endl;
48 chordLevel(data[i],sleep,data[i+1]);
49 Sleep(sleep);
50 }
51 //...
52 }
53 }
三、合成演奏
我用了一个笨蛋方法,用多线程单独控制每一个通道,然后在主线程中调用:
1 int main(){
2 //...
3 initDevice();
4 //设置音量
5 setVolume(channelChord,80);
6 setVolume(channelMainLine,80);
7 setVolume(channelVoice,120);
8 setVolume(channelBassDrum,80);
9
10 //设置乐器(特定音色)
11 setInstrument(channelChord,ElectricPiano1);
12 setInstrument(channelMainLine,ElectricPiano1);
13
14
15 system("pause");//按下回车,就开始啦
16 beginLogger();
17
18
19 thread t0(voice);
20 thread t1(mainLine);
21 thread t2(bassDrum);
22 thread t3(chord);
23 t0.join();
24 t1.join();
25 t2.join();
26 t3.join();
27
28 closeDevice();
29 //...
30 }
(最后叠个甲,俺不懂音乐制作,更不会什么C++)

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名TwilightLemon,不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。
C++ 使用MIDI库演奏《晴天》的更多相关文章
- [转]MIDI常识20条
原文链接:http://www.midifan.com/modulearticle-detailview-488.htm Keyboard杂志老资格编辑Jim Aikin在纪念MIDI诞生20的时候发 ...
- iOS开发--开源库
图像: 1.图片浏览控件MWPhotoBrowser 实现了一个照片浏览器类似 iOS 自带的相册应用,可显示来自手机的图片或者是网络图片,可自动从网络下载图片并进行缓存.可对图片进行缩 ...
- 一个第三方Dart库导致的编译错误!
今天学习flutter过程中,突然程序不能运行了,无论是命令行,抑或Android Studio,还是Idea都是出现同样错误,如下: Running .5s Launching lib\main.d ...
- 开源 iOS 项目分类索引大全 - 待整理
开源 iOS 项目分类索引大全 GitHub 上大概600个开源 iOS 项目的分类和介绍,对于你挑选和使用开源项目应该有帮助 系统基础库 Category/Util sstoolkit 一套Cate ...
- 史上最全的常用iOS的第三方框架
文章来源:http://blog.csdn.net/sky_2016/article/details/45502921 图像: 1.图片浏览控件MWPhotoBrowser 实现了一个照片 ...
- 常用iOS的第三方框架
图像:1.图片浏览控件MWPhotoBrowser 实现了一个照片浏览器类似 iOS 自带的相册应用,可显示来自手机的图片或者是网络图片,可自动从网络下载图片并进行缓存.可对图片进行缩放等 ...
- 开源 iOS 项目分类索引大全
GitHub 上大概600个开源 iOS 项目的分类和介绍,对于你挑选和使用开源项目应该有帮助 系统基础库 Category/Util sstoolkit 一套Category类型的库,附带很多自定义 ...
- iOS开发-常用第三方开源框架介绍(你了解的ios只是冰山一角)--(转)
图像: 1.图片浏览控件MWPhotoBrowser 实现了一个照片浏览器类似 iOS 自带的相册应用,可显示来自手机的图片或者是网络图片,可自动从网络下载图片并进行缓存.可对图片进行缩放等操作. 下 ...
- iOS - 常用iOS的第三方框架
图像:1.图片浏览控件MWPhotoBrowser 实现了一个照片浏览器类似 iOS 自带的相册应用,可显示来自手机的图片或者是网络图片,可自动从网络下载图片并进行缓存.可对图片进行缩放等 ...
- IOS-常用第三方开源框架介绍
iOS开发-常用第三方开源框架介绍(你了解的ios只是冰山一角) 时间:2015-05-06 16:43:34 阅读:533 评论:0 收藏:0 [点我收藏+] ...
随机推荐
- 【异或运算】codeforces 1153 B. Dima and a Bad XOR
前言 异或运算:是一种在二进制数系统中使用的逻辑运算.它的基本规则是对两个二进制位进行比较,如果这两个位不同,则结果为 \(1\):如果相同,则结果为 \(0\). 异或运算的规则 \(0\) XOR ...
- openEuler欧拉使用sshpass不输入密码远程登录其他服务器
ssh登陆不能在命令行中指定密码,sshpass 的出现则解决了这一问题.用 -p 参数指定明文密码,然后直接登录远程服务器,它支持密码从命令行.文件.环境变量中读取. 操作步骤: 一.关闭防火墙 ...
- 使用 ASP.NET Core 5 Web API 创建可发现的 HTTP API
使用 ASP.NET Core 5 Web API 创建可发现的 HTTP API https://devblogs.microsoft.com/aspnet/creating-discoverabl ...
- 实用干货分享(4)- 分布式金融PaaS容器化部署实战
编辑 一.学习链接 http://www.itmuch.com/docker/00-docker-lession-index/ 二.安装步骤 sudo yum install -y yum-ut ...
- [Java]多个参数的非空判断,不要再使用多个if挨个判断了!(多参数非空判断技巧)
先上示例代码: if (StringUtils.isAnyBlank(form, to, subject, content)) { log.error("发送人,接收人,主题,内容均不可为空 ...
- 【Mybatis】学习笔记01:连接数据库,实现增删改
需要数据库SQL的请跳转到文末 哔哩哔哩 萌狼蓝天 [转载资料][尚硅谷][MyBatis]2022版Mybatis配套MD文档 [Mybatis]学习笔记01:连接数据库,实现增删改 [Mybati ...
- Spring注解之-@ConditionalOnExpression表达式
@ConditionalOnExpression("'true") 当括号中的内容为true时,使用该注解的类被实例化,支持语法如下: @ConditionalOnExpressi ...
- Qt编写物联网管理平台39-报警联动
一.前言 本系统支持报警联动,就是某个探测器报警后,再去下发命令,通知下面的继电器警号,一般是通过串口发送,由于现场会利用现有的串口线路比如485总线,所以本系统需要做特殊处理,就是公用485通信总线 ...
- Qt项目升级到Qt6经验总结
1 直观总结 增加了很多轮子,同时原有模块拆分的也更细致,估计为了方便拓展个管理. 把一些过度封装的东西移除了(比如同样的功能有多个函数),保证了只有一个函数执行该功能. 把一些Qt5中兼容Qt4的方 ...
- NET6使用AutoFac依赖注入(仓储模式)
第一次使用autofac,然后net6最新长期支持的,就想着在net6的基础上使用autofac,我对依赖注入理解很差,一知半解的搞了好久.好在有了一点点的头绪,记录下省的以后忘记(突然发现自己以前用 ...