C++ 使用MIDI库演奏《晴天》

那些在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,不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

相关推荐
萌の鱼2 小时前
leetcode 2466. 统计构造好字符串的方案数
数据结构·c++·算法·leetcode
bbqz0074 小时前
浅说 c++20 cppcoro (三)
c++·c++20·协程·coroutine·co_await·co_yield·cppcoro·co_return
ChoSeitaku5 小时前
NO.13十六届蓝桥杯备战|条件操作符|三目操作符|逻辑操作符|!|&&|||(C++)
c++·职场和发展·蓝桥杯
ByteDreamer6 小时前
C/C++内存管理
开发语言·c++
ox00806 小时前
C++ 设计模式-桥接模式
c++·设计模式·桥接模式
上元星如雨7 小时前
详解C++的存储区
java·开发语言·c++
ox00807 小时前
C++ 设计模式-原型模式
c++·设计模式·原型模式
tamak9 小时前
c/c++蓝桥杯经典编程题100道(21)背包问题
c语言·c++·蓝桥杯
c-c-developer9 小时前
C++ Primer 条件语句
c++