C++ 使用MIDI库演奏《晴天》
那些在MIDI库里徘徊的十六分音符
终究没能拼成告白的主歌
我把周杰伦的《晴天》写成C++的类
在每个midiEvent里埋藏故事的小黄花
在每个midiEvent里埋藏故事的小黄花
调试器的断点比初恋更漫长
而青春不过是一串未导出的cmake工程文件
而青春不过是一串未导出的cmake工程文件
在堆栈溢出的夜晚
终将明白
有些旋律永远停在#pragma once的注释里
有些人永远停在未定义的引用里
终将明白
有些旋律永远停在#pragma once的注释里
有些人永远停在未定义的引用里
或许你我的心跳终归运行在不同的时钟频率
却愿始终记得如何编译出一场永不落幕的晴天
却愿始终记得如何编译出一场永不落幕的晴天
--题记
就像在题记里说的一样,这是一个从未导出成功的工程文件。
所以如果你也想听听,可以在PowerShell里运行以下指令:
所以如果你也想听听,可以在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,不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~