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 [点我收藏+] ...
随机推荐
- Nuxt.js 应用中的 render:response 事件钩子
title: Nuxt.js 应用中的 render:response 事件钩子 date: 2024/11/29 updated: 2024/11/29 author: cmdragon excer ...
- VTK 视角的旋转、平移、缩放
在CAD/CAM软件中,都需要旋转.平移和缩放视角,来观察操作图形.由于VTK定义的交互的类型不是很适用,所有通过定义一套自己的交互方式. 在下面代码中,鼠标左键平移,滚轮缩放,右键旋转. 先定义一个 ...
- MYSQL8以上修改初始root密码的方法
很遗憾的告诉你,你网上查到的各种改my.enf ,各种update,在新版MYSQL中,统统失效. 解决方法,如下: 1. [root@yisu-5f735cb14d716 ~]# service m ...
- COS 数据工作流 + Serverless云函数自定义处理能力发布!
01 背景 在工业4.0的浪潮下,智能和数据与物理世界结合越加紧密,多元化.灵活.高效的数据处理能力成为各行各业的热点需求. 虽然COS已经预置电商.文创.教育.社交.安防等行业需要的基础数据处理 ...
- 人工智能应用的“繁花时代”,各大企业何以破局AI模型挑战
AI技术的崛起,为各行业发展带来巨大变革和超强的创新潜力.然而,各大企业在拥抱AI的进程中并非一路坦途,"繁花盛开"的背后隐藏着AI模型生产与管理环节的诸多痛点. 先来看看部分 ...
- 【Python】【Flask】【字符串索引】计算人民币与美元的相互计算
目录 简介 Python Code 导包 设置首页 计算的接口 问题0:设置请求方式 问题1:关于接收数据可能存在的问题 问题2:返回结果 启动 完整代码 HTML Code 问题分析 分析:获取下拉 ...
- 一个GLSL Shader的格式化算法(LALR解析器)
一个GLSL Shader的格式化算法(LALR解析器) 在进行OpenGL程序开发时,我需要自行解析`string`类型的Shader代码,抽取出里面的某些变量名和subroutine名. 由于找不 ...
- Qt编写地图综合应用29-迁徙图
一.前言 迁徙图和上一篇的闪烁点图类似,也需要提供三个基本要素:城市名称集合.城市经纬度集合.中心点城市经纬度,至于城市点的颜色和迁徙流向箭头的颜色,都可以单独设置,echart对每个类对象都竭尽全力 ...
- spark (三) hadoop上传文件并运行spark
目录 1. 上传文件到hdfs 2. 运行wordCount 2.1 spark-shell运行 2.2 spark-submit运行example(stand-alone) 2.3 spark-su ...
- 基于AI底座的数智油气田参考架构
基于AI底座的数智油气田参考架构 Architecture for Intelligent & Digital Oilfileds Based-on AI 王权 2024.12.29 ...