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 [点我收藏+] ...
随机推荐
- 【FAQ】HarmonyOS SDK 闭源开放能力 —Push Kit(6)
1.问题描述: 推送通知到手机,怎么配置拉起应用指定的页面? 解决方案: 1.如果点击通知栏打开默认Ability的话, actionType可以设置为0, 同时可以在.clickAction.dat ...
- Vue.js vuex
1.前言 Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式,它解决了vue中不同组件之间状态共享的问题. 通俗的说,它就是一个带响应式的全局变量管理,它数据的改变会触发相关页面/组件的更 ...
- 黑客玩具入门——3、NMAP入门
1.Nmap基础 Nmap是主机扫描工具,它的图形化界面是Zebmap,分布式框架是Dnmap,Nmap可以完成诸如:主机探测.端口扫描.版本检测.系统监测等.并且支持探测脚本的编写. Nmap在实际 ...
- uniapp 坑 - sslVerify不支撑离线打包
uniapp 打包为Android的apk时,由于适用https和自签证书,离线打包不支撑sslVerify,导致出现Trust anchor for certification path not f ...
- Visual Studio C++ 汇编 混合编程
Visual Studio C++ 汇编 混合编程 实验要求 请用汇编语言编写实现GCD递推公式的子程序,对入口和出口参数形式不做要求,但需要用 C 语言函数来获取输入.调用汇编递推子程序,并且用 C ...
- GienTech动态|入选软件和信息技术服务竞争力百强;参展世界计算大会、网络安全博览会
---- GienTech动态 ---- 中电金信参展广东省网络安全博览会.世界计算机大会 近期,中电金信跟随中国电子参展2023年广东省网络安全博览会(下简称"博览会&qu ...
- Qt编写地图综合应用30-世界地图
一.前言 世界地图也属于区域地图的一种,最开始做项目的时候只有国内的地图需求,后面有个客户他们的产品是面向国际市场,所以在大屏展示的时候,必须展示一张世界地图,所有箭头动态流向中国,当然这些都要离线使 ...
- Qt编写地图综合应用6-百度在线地图
一.前言 百度在线地图的应用老早就做过,后面经过不断的完善才到今天的这个程序,除了基本的可以载入地图并设置一些相关的属性以外,还增加了各种js函数直接异步加载数据比如动态添加点.矩形.圆形.行政区划等 ...
- Qt开发经验小技巧146-150
Qt中自带的很多控件,其实都是由一堆基础控件(QLabel.QPushButton等)组成的,比如日历面板 QCalendarWidget 就是 QToolButton+QSpinBox+QTable ...
- 使用Visual Studio Code进行前端开发并运行前端页面时提示:npm WARN Local package.json exists, but node_modules missing, did you mean to install?
使用Visual Studio Code进行前端开发并运行前端页面时提示如下错误: npm WARN Local package.json exists, but node_modules missi ...