C++使用MIDI庫(kù)實(shí)現(xiàn)演奏晴天
前言
那些在MIDI庫(kù)里徘徊的十六分音符
終究沒(méi)能拼成告白的主歌
我把周杰倫的《晴天》寫(xiě)成C++的類
在每個(gè)midiEvent里埋藏故事的小黃花
調(diào)試器的斷點(diǎn)比初戀更漫長(zhǎng)
而青春不過(guò)是一串未導(dǎo)出的cmake工程文件
在堆棧溢出的夜晚
終將明白
有些旋律永遠(yuǎn)停在#pragma once的注釋里
有些人永遠(yuǎn)停在未定義的引用里
或許你我的心跳終歸運(yùn)行在不同的時(shí)鐘頻率
卻愿始終記得如何編譯出一場(chǎng)永不落幕的晴天
--題記
就像在題記里說(shuō)的一樣,這是一個(gè)從未導(dǎo)出成功的工程文件。
所以如果你也想聽(tīng)聽(tīng),可以在PowerShell里運(yùn)行以下指令:
git clone https://github.com/TwilightLemon/SunnyDays cd SunnyDays mkdir build cd build cmake .. -G "MinGW Makefiles" mingw32-make ./SunnyDays.exe
沒(méi)環(huán)境?巧了,她也如是說(shuō)。
幸運(yùn)的話能得到以下效果:

下面來(lái)簡(jiǎn)單講講如何使用C++和MIDI庫(kù)作曲吧。
開(kāi)始工作
1. 引入MIDI庫(kù)和相關(guān)控制類
在CMakeLists.txt中:
target_link_libraries(SunnyDays winmm)
在MIDIHelper.h中:
#include <windows.h> #pragma comment(lib,"winmm.lib")
定義Scale(音階), Instrument(樂(lè)器, 僅包括部分)等枚舉。我把Drum單獨(dú)提了出來(lái)。
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
};一些基礎(chǔ)方法,包括初始化/關(guān)閉設(shè)備、設(shè)置參數(shù)、播放單個(gè)音符和播放和弦等。
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);
}
//播放單個(gè)音符,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. 初始化和結(jié)束
先在頭文件中定義一個(gè)全局MIDI句柄:
extern HMIDIOUT hMidiOut;
在入口處初始化MIDI設(shè)備并在結(jié)束時(shí)關(guān)閉:
HMIDIOUT hMidiOut;
int main() {
initDevice();
//...
closeDevice();
return 0;
}初始化MIDI設(shè)備之后,為每一個(gè)樂(lè)器分配一個(gè)通道channel(0~15,通常9分配給打擊類樂(lè)器,例如鼓組),控制音量volume,然后就可以開(kāi)始演奏了。
自制簡(jiǎn)易樂(lè)譜
以Voice.cpp為例,定義一個(gè)數(shù)組為頻譜,控制停頓和音符,遍歷數(shù)組播放:
namespace SunnyDays{
int channelVoice=1;
void playVoice(int note, int velocity){
PlayNote(hMidiOut, channelVoice, note, velocity);
}
void voice(){
Sleep(13100);//等待前奏
int sleep = 390;
int data[] =
{
//故事的小黃花
-90,
300,M5,M5,M1,M1,_,M2,M3,_,
//從出生那年就飄著
-90,
M5,M5,M1,M1,0,M2,M3,300,M2,M1,_,
//童年的蕩秋千
-90,
300,M5,M5,M1,M1,_,M2,M3,_,
//隨記憶一直晃到現(xiàn)在
-90,
M3,_,500,M2,M3,M4,M3,M2,M4,M3,700,M2,700,_,
//...
}
for (auto i : data) {
if(i==-30){logTime("Enter chorus");continue;}//調(diào)試用
if(i==-90){NextLyric(); continue;}
if (i == 0) { sleep = 180; continue; }
//...
if (i == _) {
Sleep(390);
continue;
}
playVoice(i, 80);
Sleep(sleep);
}
}
}打個(gè)鼓:
namespace SunnyDays{
int channelBassDrum=9;
void playDrum(int note, int velocity, int duration){
PlayNote(hMidiOut, channelBassDrum, note, velocity);
if(duration>0) {
Sleep(duration);
PlayNote(hMidiOut, channelBassDrum, note, 0);
}
}
void bassDrum(){
Sleep(11260);
cout<<"Drum Bass Start!"<<endl;
playDrum(SnareDrum,100,180);
playDrum(SnareDrum,100,210);
playDrum(BassDrum, 100, 210);
playDrum(SnareDrum,100,190);
playDrum(BassDrum, 100, 210);
playDrum(SnareDrum,100,200);
playDrum(SnareDrum,100,200);
playDrum(OpenHiHat,100,-1);
Sleep(200);
//...
}
}簡(jiǎn)易副歌和弦,是從B站一位up主那里學(xué)的(已經(jīng)忘記是哪位了qwq):
namespace SunnyDays {
int channelChord=2;
void chordLevel(int level,int sleep,int repeat=2,int vel=70){
repeat--;
int down=8;
if(level==1){
//一級(jí)和弦 加右指
playChord(hMidiOut, channelChord, M1, M3, M5, L1, vel);
while(repeat--) {
Sleep(sleep);
playChord(hMidiOut, channelChord, M1, M3, M5, vel - down);
}
}else if(level==3){
//三級(jí)和弦 加右指
playChord(hMidiOut, channelChord, M3, M5, M7, L3, vel);
while(repeat--) {
Sleep(sleep);
playChord(hMidiOut, channelChord, M3, M5, M7, vel - down);
}
}
//...
}
void chord(){
Sleep(63724);
int sleep=740;
int data[]={
//刮風(fēng)這天 我試過(guò)握著你手
1,4,
6,4,
//但偏偏 雨漸漸
4,2,
5,2,
//大到我看你不見(jiàn)
1,4,
//還有多久 我才能
3,4,
//↑ 在你身邊
6,4,
//↓ 等到放晴的那天
4,4,
//↑ 也許我會(huì)比較好一點(diǎn)
5,4,
//..
}
int count=sizeof(data)/sizeof(int);
for(int i=0;i<count;i+=2){
cout<<"chord "<<data[i]<<" x"<<data[i+1]<<endl;
chordLevel(data[i],sleep,data[i+1]);
Sleep(sleep);
}
//...
}
}合成演奏
我用了一個(gè)笨蛋方法,用多線程單獨(dú)控制每一個(gè)通道,然后在主線程中調(diào)用:
int main(){
//...
initDevice();
//設(shè)置音量
setVolume(channelChord,80);
setVolume(channelMainLine,80);
setVolume(channelVoice,120);
setVolume(channelBassDrum,80);
//設(shè)置樂(lè)器(特定音色)
setInstrument(channelChord,ElectricPiano1);
setInstrument(channelMainLine,ElectricPiano1);
system("pause");//按下回車,就開(kāi)始啦
beginLogger();
thread t0(voice);
thread t1(mainLine);
thread t2(bassDrum);
thread t3(chord);
t0.join();
t1.join();
t2.join();
t3.join();
closeDevice();
//...
}(最后疊個(gè)甲,俺不懂音樂(lè)制作,更不會(huì)什么C++)

到此這篇關(guān)于C++使用MIDI庫(kù)實(shí)現(xiàn)演奏晴天的文章就介紹到這了,更多相關(guān)C++ MIDI演奏音樂(lè)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
OpenCV基于距離變換和分水嶺實(shí)現(xiàn)圖像分割
圖像分割是根據(jù)灰度、顏色、紋理和形狀等特征,把圖像分成若干個(gè)特定的、具有獨(dú)特性質(zhì)的區(qū)域。本文將基于距離變換和分水嶺實(shí)現(xiàn)圖像分割,需要的可以了解一下2022-09-09
C語(yǔ)言實(shí)現(xiàn)基于控制臺(tái)的電子時(shí)鐘
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)基于控制臺(tái)的電子時(shí)鐘,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05
VScode配置C語(yǔ)言環(huán)境完整版(親測(cè)可用)
這篇文章主要介紹了VScode配置C語(yǔ)言環(huán)境完整版,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-08-08
C++實(shí)現(xiàn)讀取圖片長(zhǎng)度和寬度
這篇文章主要介紹了C++實(shí)現(xiàn)讀取圖片長(zhǎng)度和寬度,本文直接給出實(shí)現(xiàn)代碼,需要的朋友可以參考下2015-04-04
詳解C語(yǔ)言中fseek函數(shù)和ftell函數(shù)的使用方法
這篇文章主要介紹了C語(yǔ)言中fseek函數(shù)和ftell函數(shù)的使用方法,兩個(gè)函數(shù)分別用于設(shè)置和返回文件指針stream的位置,需要的朋友可以參考下2016-03-03
OpenCV獲取圖像中直線上的數(shù)據(jù)具體流程
對(duì)圖像進(jìn)行處理時(shí),經(jīng)常會(huì)有這類需求:客戶想要提取出圖像中某條直線或者ROI區(qū)域內(nèi)的感興趣數(shù)據(jù),進(jìn)行重點(diǎn)關(guān)注,怎么操作呢,下面小編通過(guò)實(shí)例代碼介紹下OpenCV獲取圖像中直線上的數(shù)據(jù),一起看看吧2021-11-11
C語(yǔ)言實(shí)現(xiàn)輸出平均成績(jī)最高學(xué)生的信息
這篇文章主要介紹利用C語(yǔ)言實(shí)現(xiàn)輸出平均成績(jī)最高學(xué)生的信息,文章舉例說(shuō)明并附有詳細(xì)代碼,需要的朋友可以參考一下2021-10-10
C語(yǔ)言實(shí)現(xiàn)簡(jiǎn)單餐飲管理與點(diǎn)餐系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)簡(jiǎn)單餐飲管理與點(diǎn)餐系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05
C++?list模擬實(shí)現(xiàn)過(guò)程
list是基于雙向循環(huán)鏈表的順序容器,支持O(1)插入刪除,迭代器分類明確,insert不失效而erase導(dǎo)致當(dāng)前迭代器失效,與vector對(duì)比,list空間利用率低但靈活,適合頻繁增刪操作,通過(guò)迭代器封裝實(shí)現(xiàn)統(tǒng)一訪問(wèn)接口2025-09-09

