那些在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. 2023 CCPC 深圳

    2023 CCPC 深圳 D. Bot Brothers 有一棵 \(n\) 个点的树,\(m\) 个叶子,编号为 \(1∼m\).两人在树上博弈,均从根出发,轮流行动,每次走向一个当前所在节点的子节 ...

  2. DTL事务控制语言--sql事务

    DTL事务控制语言体格sql语句就是一个事务事务可以保证 一组sql语句要么都成功,要么都失败默认自动提交一可以关闭 set autocommit=0关闭自动提交最后 插入或者修改时 只有commit ...

  3. .NET 使用 ZXing.Net 生成二维码,并识别

    .NET 使用 ZXing.Net 生成二维码,并识别 前言 前面已经分享给很多创建二维码,条形码...等一系列的方式 各有优缺点,暂时不做评价.今天推荐ZXing.Net .也是比较全面的一种方式, ...

  4. pycharm选择conda虚拟环境出错:python的SDK无效

    检查项如下: 0.安装了python,并在系统环境变量中配置了python 0.5 正确配置了conda的系统环境变量 1.安装conda的文件夹又读写权限(不需要管理员模式运行也能进行读写) 直接在 ...

  5. Netty 中ChannelOption的含义以及使用的场景Netty 中ChannelOption的含义以及使用的场景

    一.概述 最近在写一个分布式服务框架,打算用netty框架做底层网络通信,关于netty的学习可以参考如下资料: http://blog.csdn.net/column/details/enjoyne ...

  6. Qt/C++编写跨平台的推流工具(支持win/linux/mac/嵌入式linux/安卓等)

    一.前言 跨平台的推流工具当属OBS最牛逼,功能也是最强大的,唯一的遗憾就是多路推流需要用到插件,而且CPU占用比较高,默认OBS的规则是将对应画布中的视频画面和设定的音频一起重新编码再推流,意味着肯 ...

  7. 概率论与数理统计教程(第二版,茆诗松,2010)PDF下载

    概率论与数理统计教程(第二版,茆诗松,2010)PDF下载:共享地址

  8. 开源即时通讯IM框架 MobileIMSDK v6.2 发布

    一.更新内容简介 本次更新为次要版本更新,进行了若干优化(更新历史详见:码云 Release Nodes).可能是市面上唯一同时支持 UDP+TCP+WebSocket 三种协议的同类开源IM框架. ...

  9. Matlab深度学习

    View image files Instructions are in the task pane to the left. Complete and submit each task one at ...

  10. 关于存入sessionStorage中boolean值拿出来为字符串

    上面是html部分,然后定义了变量 定义方法 在mounted中获取,然后刷新页面,打印类型为字符串 解决办法