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库演奏《晴天》

下面来简单讲讲如何使用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++😿)

 

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

  本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名TwilightLemon,不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

发表评论

评论已关闭。

相关文章