那些在MIDI库里徘徊的十六分音符
终究没能拼成告白的主歌
 
我把周杰伦的《晴天》写成C++的类
在每个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库演奏《晴天》的更多相关文章

  1. [转]MIDI常识20条

    原文链接:http://www.midifan.com/modulearticle-detailview-488.htm Keyboard杂志老资格编辑Jim Aikin在纪念MIDI诞生20的时候发 ...

  2. iOS开发--开源库

    图像: 1.图片浏览控件MWPhotoBrowser        实现了一个照片浏览器类似 iOS 自带的相册应用,可显示来自手机的图片或者是网络图片,可自动从网络下载图片并进行缓存.可对图片进行缩 ...

  3. 一个第三方Dart库导致的编译错误!

    今天学习flutter过程中,突然程序不能运行了,无论是命令行,抑或Android Studio,还是Idea都是出现同样错误,如下: Running .5s Launching lib\main.d ...

  4. 开源 iOS 项目分类索引大全 - 待整理

    开源 iOS 项目分类索引大全 GitHub 上大概600个开源 iOS 项目的分类和介绍,对于你挑选和使用开源项目应该有帮助 系统基础库 Category/Util sstoolkit 一套Cate ...

  5. 史上最全的常用iOS的第三方框架

    文章来源:http://blog.csdn.net/sky_2016/article/details/45502921 图像: 1.图片浏览控件MWPhotoBrowser       实现了一个照片 ...

  6. 常用iOS的第三方框架

    图像:1.图片浏览控件MWPhotoBrowser       实现了一个照片浏览器类似 iOS 自带的相册应用,可显示来自手机的图片或者是网络图片,可自动从网络下载图片并进行缓存.可对图片进行缩放等 ...

  7. 开源 iOS 项目分类索引大全

    GitHub 上大概600个开源 iOS 项目的分类和介绍,对于你挑选和使用开源项目应该有帮助 系统基础库 Category/Util sstoolkit 一套Category类型的库,附带很多自定义 ...

  8. iOS开发-常用第三方开源框架介绍(你了解的ios只是冰山一角)--(转)

    图像: 1.图片浏览控件MWPhotoBrowser 实现了一个照片浏览器类似 iOS 自带的相册应用,可显示来自手机的图片或者是网络图片,可自动从网络下载图片并进行缓存.可对图片进行缩放等操作. 下 ...

  9. iOS - 常用iOS的第三方框架

    图像:1.图片浏览控件MWPhotoBrowser       实现了一个照片浏览器类似 iOS 自带的相册应用,可显示来自手机的图片或者是网络图片,可自动从网络下载图片并进行缓存.可对图片进行缩放等 ...

  10. IOS-常用第三方开源框架介绍

    iOS开发-常用第三方开源框架介绍(你了解的ios只是冰山一角) 时间:2015-05-06 16:43:34      阅读:533      评论:0      收藏:0      [点我收藏+] ...

随机推荐

  1. 【异或运算】codeforces 1153 B. Dima and a Bad XOR

    前言 异或运算:是一种在二进制数系统中使用的逻辑运算.它的基本规则是对两个二进制位进行比较,如果这两个位不同,则结果为 \(1\):如果相同,则结果为 \(0\). 异或运算的规则 \(0\) XOR ...

  2. openEuler欧拉使用sshpass不输入密码远程登录其他服务器

    ​​ssh登陆不能在命令行中指定密码,sshpass 的出现则解决了这一问题.用 -p 参数指定明文密码,然后直接登录远程服务器,它支持密码从命令行.文件.环境变量中读取. 操作步骤: 一.关闭防火墙 ...

  3. 使用 ASP.NET Core 5 Web API 创建可发现的 HTTP API

    使用 ASP.NET Core 5 Web API 创建可发现的 HTTP API https://devblogs.microsoft.com/aspnet/creating-discoverabl ...

  4. 实用干货分享(4)- 分布式金融PaaS容器化部署实战

    ​ ​编辑 一.学习链接 http://www.itmuch.com/docker/00-docker-lession-index/ 二.安装步骤 sudo yum install -y yum-ut ...

  5. [Java]多个参数的非空判断,不要再使用多个if挨个判断了!(多参数非空判断技巧)

    先上示例代码: if (StringUtils.isAnyBlank(form, to, subject, content)) { log.error("发送人,接收人,主题,内容均不可为空 ...

  6. 【Mybatis】学习笔记01:连接数据库,实现增删改

    需要数据库SQL的请跳转到文末 哔哩哔哩 萌狼蓝天 [转载资料][尚硅谷][MyBatis]2022版Mybatis配套MD文档 [Mybatis]学习笔记01:连接数据库,实现增删改 [Mybati ...

  7. Spring注解之-@ConditionalOnExpression表达式

    @ConditionalOnExpression("'true") 当括号中的内容为true时,使用该注解的类被实例化,支持语法如下: @ConditionalOnExpressi ...

  8. Qt编写物联网管理平台39-报警联动

    一.前言 本系统支持报警联动,就是某个探测器报警后,再去下发命令,通知下面的继电器警号,一般是通过串口发送,由于现场会利用现有的串口线路比如485总线,所以本系统需要做特殊处理,就是公用485通信总线 ...

  9. Qt项目升级到Qt6经验总结

    1 直观总结 增加了很多轮子,同时原有模块拆分的也更细致,估计为了方便拓展个管理. 把一些过度封装的东西移除了(比如同样的功能有多个函数),保证了只有一个函数执行该功能. 把一些Qt5中兼容Qt4的方 ...

  10. NET6使用AutoFac依赖注入(仓储模式)

    第一次使用autofac,然后net6最新长期支持的,就想着在net6的基础上使用autofac,我对依赖注入理解很差,一知半解的搞了好久.好在有了一点点的头绪,记录下省的以后忘记(突然发现自己以前用 ...