這是一個(gè)可以讓iOS小白用戶忧勿,直接根據(jù)鋼琴或者其他樂器的簡(jiǎn)譜叠殷,直接開發(fā)一個(gè)可以播放的簡(jiǎn)單教程肆良,底層使用CoreMIDI.framework來實(shí)現(xiàn)香罐,中層使用開源的MIKMIDI庫來實(shí)現(xiàn),上層將簡(jiǎn)譜設(shè)計(jì)成合理的數(shù)據(jù)結(jié)構(gòu)铭拧,將簡(jiǎn)譜數(shù)據(jù)進(jìn)行對(duì)象化管理赃蛛,業(yè)務(wù)方簡(jiǎn)單調(diào)用進(jìn)而直接上手使用。
一搀菩、理論篇
1呕臂、認(rèn)識(shí)鋼琴鍵盤和簡(jiǎn)譜的關(guān)系
2、認(rèn)識(shí)簡(jiǎn)譜和MIDI鍵的關(guān)系
其中:中央C(1/do) 對(duì)應(yīng)的是 MIDI的 60秕磷,依次左右推算得到
音名 | 簡(jiǎn)譜 | MIDI值 | 和上個(gè)MIDI差值 |
---|---|---|---|
中央C | 1 | 60 | 2 |
d1 | 2 | 62 | 2 |
e1 | 3 | 64 | 2 |
f1 | 4 | 65 | 1 |
g1 | 5 | 67 | 2 |
a1 | 6 | 69 | 2 |
b1 | 7 | 71 | 2 |
c2 | 1+ | 72 | 1 |
... | ... | ... | ... |
規(guī)律點(diǎn)就是 3 - 4 MIDI 差值為1诵闭,7 - 1的MIDI差值為1,其他都是2澎嚣。這個(gè)正是和鋼琴按鍵對(duì)應(yīng)上。
這里整理了較為完整的 唱名對(duì)應(yīng)的MIDI值的枚舉關(guān)系:
typedef NS_ENUM(NSInteger, HDModulatorMidiValue) {
HDModulatorMidiEmpty = -1000, // 空一拍
HDModulatorMidiLowLowDo = -24,
HDModulatorMidiLowLowRe = -22,
HDModulatorMidiLowLowMi = -20,
HDModulatorMidiLowLowFa = -19,
HDModulatorMidiLowLowSol = -17,
HDModulatorMidiLowLowLa = -15,
HDModulatorMidiLowLowSi = -13,
HDModulatorMidiLowDo = -12,
HDModulatorMidiLowRe = -10,
HDModulatorMidiLowMi = -8,
HDModulatorMidiLowFa = -7,
HDModulatorMidiLowSol = -5,
HDModulatorMidiLowLa = -3,
HDModulatorMidiLowSi = -1,
HDModulatorMidiDo = 0,
HDModulatorMidiRe = 2,
HDModulatorMidiMi = 4,
HDModulatorMidiFa = 5,
HDModulatorMidiSol = 7,
HDModulatorMidiLa = 9,
HDModulatorMidiSi = 11,
HDModulatorMidiHighDo = 12,
HDModulatorMidiHighRe = 14,
HDModulatorMidiHighMi = 16,
HDModulatorMidiHighFa = 17,
HDModulatorMidiHighSol = 19,
HDModulatorMidiHighLa = 21,
HDModulatorMidiHighSi = 23,
HDModulatorMidiHighHighDo = 24,
HDModulatorMidiHighHighRe = 26,
HDModulatorMidiHighHighMi = 28,
HDModulatorMidiHighHighFa = 29,
HDModulatorMidiHighHighSol = 31,
HDModulatorMidiHighHighLa = 33,
HDModulatorMidiHighHighSi = 35,
};
使用方式(播放 “do瘟芝、re易桃、mi”)
UInt8 note = 60 + HDModulatorMidiDo;
MIKMIDINoteOnCommand *noteOn = [MIKMIDINoteOnCommand noteOnCommandWithNote:note velocity:127 channel:1 timestamp:[NSDate date]];
[self.synthesizer handleMIDIMessages:@[noteOn]];
note = 60 + HDModulatorMidiRe;
noteOn = [MIKMIDINoteOnCommand noteOnCommandWithNote:note velocity:127 channel:1 timestamp:[NSDate date]];
[self.synthesizer handleMIDIMessages:@[noteOn]];
note = 60 + HDModulatorMidiMi;
noteOn = [MIKMIDINoteOnCommand noteOnCommandWithNote:note velocity:127 channel:1 timestamp:[NSDate date]];
[self.synthesizer handleMIDIMessages:@[noteOn]];
3、鋼琴簡(jiǎn)譜如何生成對(duì)應(yīng)MIDI值
首先看一下該簡(jiǎn)譜的基本信息:
C4/4:表示C調(diào)锌俱,4/4拍晤郑,即 “da da da da”
?:四分音符(全音符時(shí)值的四分之一拍)
105:表示一分鐘有105拍,也就是60/107=0.57秒為一拍
0 67 1` 5` :該行為主旋律贸宏,其中 0 是空拍造寝;67 為一拍;1` 是do的高音
0 0 0 0 :該行為副旋律或者伴奏吭练,表示四個(gè)空拍
??來自北京超哥的用心指導(dǎo)
所以這四拍對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)應(yīng)該是
字段解析如下:
interval:表示一拍的時(shí)間間隔
hitCount/allCount:4/4拍
modulators:
modulator:鋼琴簡(jiǎn)譜的值(主旋律)诫龙。1-7直接輸入1-7;“1`-7`”高音輸入“11-77”鲫咽; “1.-7.”低音輸入為 “-1 -7”
mmodulator:鋼琴簡(jiǎn)譜的值(主旋律)
# 如果一拍又有多個(gè)拍子签赃,則需要將該拍的多個(gè)小拍子添加再 modulators/mmodulators 中
modulators = ({
modulators = ({
modulator = 6;
}, {
modulator = 7;
});
mmodulators = ({
mmodulator = "-2";
}, {
mmodulator = "-6";
});
})
原生代碼對(duì)象如下:
/// 節(jié)拍數(shù)據(jù)對(duì)象
@interface HDModulatorItem : NSObject
/// 節(jié)拍的間隔時(shí)間
@property (nonatomic, assign) NSTimeInterval interval;
/// 節(jié)拍應(yīng)該的拍數(shù)
@property (nonatomic, assign) NSUInteger hitCount;
/// 節(jié)拍的總拍數(shù)谷异,和 beatHitCount 可以組合成 3/4、6/8 等節(jié)拍
@property (nonatomic, assign) NSUInteger allCount;
/// 鋼琴曲中的音階(主旋律)和 modulators 互斥
@property (nonatomic, assign) NSInteger modulator;
/// 鋼琴曲中的音階(副旋律/伴奏)和 minorModulators 互斥
@property (nonatomic, assign) NSInteger minorModulator;
/// 鋼琴曲中的音階(該拍子中又包含多個(gè)音階) (主旋律)
@property (nonatomic, copy) NSArray <HDModulatorItem *>*modulators;
/// 鋼琴曲中的音階(該拍子中又包含多個(gè)音階)(副旋律/伴奏)
@property (nonatomic, copy) NSArray <HDModulatorItem *>*minorModulators;
/// MIDI的音階(主旋律)
@property (nonatomic, assign) HDModulatorMidiValue midiModulator;
/// MIDI的音階(副旋律/伴奏)
@property (nonatomic, assign) HDModulatorMidiValue midiMinorModulator;
@end
二锦聊、實(shí)踐篇
1歹嘹、根據(jù)鋼琴簡(jiǎn)譜生存MIDI數(shù)據(jù)庫
這里沒有好的方案,只能根據(jù)簡(jiǎn)譜及上面的理論知識(shí)孔庭,一個(gè)個(gè)將MIDI數(shù)據(jù)輸入到PLIST文件中尺上,當(dāng)然比較高級(jí)的做法應(yīng)該可以通過拍照來自動(dòng)生成。技術(shù)難度應(yīng)該有圆到,容錯(cuò)機(jī)制等細(xì)節(jié)問題需要具體場(chǎng)景去實(shí)現(xiàn)解決尖昏。
2、MIDI數(shù)據(jù)轉(zhuǎn)成原生對(duì)象
基于已經(jīng)設(shè)計(jì)的數(shù)據(jù)結(jié)構(gòu)构资,生成鋼琴簡(jiǎn)譜的原生對(duì)象
/// 一首曲子的節(jié)拍及音階信息
@interface HDModulator : NSObject
/// 節(jié)拍的間隔時(shí)間
@property (nonatomic, assign) NSTimeInterval beatInterval;
/// 節(jié)拍應(yīng)該的拍數(shù)
@property (nonatomic, assign) NSUInteger beatHitCount;
/// 節(jié)拍的總拍數(shù)抽诉,和 beatHitCount 可以組合成 3/4、6/8 等節(jié)拍
@property (nonatomic, assign) NSUInteger beatAllCount;
/// 音階組合(數(shù)據(jù)結(jié)構(gòu)是樹形結(jié)構(gòu)吐绵,對(duì)應(yīng)的是plist的數(shù)據(jù)結(jié)構(gòu))
@property (nonatomic, copy) NSArray <HDModulatorItem *>*beatModulators;
/// 音階組合(數(shù)據(jù)結(jié)構(gòu)是隊(duì)列結(jié)構(gòu))(主旋律)
@property (nonatomic, copy) NSMutableArray <HDModulatorItem *>*combineModulators;
/// 音階組合(數(shù)據(jù)結(jié)構(gòu)是隊(duì)列結(jié)構(gòu))(副旋律/伴奏)
@property (nonatomic, copy) NSMutableArray <HDModulatorItem *>*combineMinorModulators;
通過MJExtension來轉(zhuǎn)換
+ (instancetype)loadPlist:(NSString *)plist {
NSString *filePath = [NSBundle.mainBundle pathForResource:plist ofType:@"plist"];
HDModulator *model = [HDModulator mj_objectWithFile:filePath];
return model;
}
3迹淌、對(duì)象數(shù)據(jù)處理
上面一部分將數(shù)據(jù)庫轉(zhuǎn)成了原生模型,但是每一拍的結(jié)構(gòu)是樹形結(jié)構(gòu)己单,這里需要將其打平成一個(gè)數(shù)組唉窃,并且需要留言每個(gè)拍子中多個(gè)小拍的時(shí)間
/// 音階組合(數(shù)據(jù)結(jié)構(gòu)是隊(duì)列結(jié)構(gòu))(主旋律)
- (NSMutableArray <HDModulatorItem *>*)combineModulators {
if (!_combineModulators) {
_combineModulators = [NSMutableArray array];
for (HDModulatorItem *item in self.beatModulators) {
item.interval = self.beatInterval;
[self addModulatorItem:item];
}
}
return _combineModulators;
}
- (void)addModulatorItem:(HDModulatorItem *)item {
if (item.modulators.count > 0) {
// 這里需要注意,一拍中又包含多個(gè)小拍纹笼,時(shí)間間隔需要處理
NSTimeInterval beatInterval = item.interval / item.modulators.count;
for (HDModulatorItem *mm in item.modulators) {
mm.interval = beatInterval;
[self addModulatorItem:mm];
}
}
else {
[_combineModulators addObject:item];
}
}
/// 音階組合(數(shù)據(jù)結(jié)構(gòu)是隊(duì)列結(jié)構(gòu))(副旋律/伴奏)
- (NSMutableArray <HDModulatorItem *>*)combineMinorModulators {
if (!_combineMinorModulators) {
_combineMinorModulators = [NSMutableArray array];
for (HDModulatorItem *item in self.beatModulators) {
item.interval = self.beatInterval;
[self addMinorModulatorItem:item];
}
}
return _combineMinorModulators;
}
- (void)addMinorModulatorItem:(HDModulatorItem *)item {
if (item.minorModulators.count > 0) {
// 這里需要注意纹份,一拍中又包含多個(gè)小拍,時(shí)間間隔需要處理
NSTimeInterval beatInterval = item.interval / item.minorModulators.count;
for (HDModulatorItem *mm in item.minorModulators) {
mm.interval = beatInterval;
[self addMinorModulatorItem:mm];
}
}
else {
[_combineMinorModulators addObject:item];
}
}
4廷痘、數(shù)據(jù)播放
這里直接是調(diào)用已經(jīng)封裝好的MIDI播放器進(jìn)行播放即可
_modulator = [HDModulator loadPlist:@"bjehp"];
- (void)playIndex:(NSInteger)index {
if (!_isPlay) {
return;
}
if (_modulator.combineModulators.count <= index) {
return;
}
HDModulatorItem *item = _modulator.combineModulators[index];
NSInteger midiModulator = item.midiModulator;
NSLog(@"七音階(主):%ld MIDI音階:%ld 下一拍時(shí)間間隔:%0.2f", item.modulator, midiModulator, item.interval);
if (midiModulator != -1000) {
UInt8 note = 60 + midiModulator;
MIKMIDINoteOnCommand *noteOn = [MIKMIDINoteOnCommand noteOnCommandWithNote:note velocity:127 channel:0 timestamp:[NSDate date]];
[self.synthesizer handleMIDIMessages:@[noteOn]];
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(item.interval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self playIndex:index+1];
});
}
- (void)playMinorIndex:(NSInteger)index {
if (!_isPlay) {
return;
}
if (_modulator.combineMinorModulators.count <= index) {
return;
}
HDModulatorItem *item = _modulator.combineMinorModulators[index];
NSInteger midiMinorModulator = item.midiMinorModulator;
NSLog(@"七音階(副):%ld MIDI音階:%ld 下一拍時(shí)間間隔:%0.2f", item.minorModulator, midiMinorModulator, item.interval);
if (midiMinorModulator != -1000) {
UInt8 note = 60 + midiMinorModulator;
MIKMIDINoteOnCommand *noteOn = [MIKMIDINoteOnCommand noteOnCommandWithNote:note velocity:127 channel:1 timestamp:[NSDate date]];
[self.synthesizer handleMIDIMessages:@[noteOn]];
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(item.interval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self playMinorIndex:index+1];
});
}
播放過程中的日志信息如下:
2022-03-16 17:43:39.241376+0800 七音階(主):0 MIDI音階:-1000 下一拍時(shí)間間隔:0.60
2022-03-16 17:43:39.241565+0800 七音階(副):0 MIDI音階:-1000 下一拍時(shí)間間隔:0.60
2022-03-16 17:43:39.896846+0800 七音階(主):6 MIDI音階:9 下一拍時(shí)間間隔:0.30
2022-03-16 17:43:39.897053+0800 七音階(副):0 MIDI音階:-1000 下一拍時(shí)間間隔:0.60
2022-03-16 17:43:40.224594+0800 七音階(主):7 MIDI音階:11 下一拍時(shí)間間隔:0.30
2022-03-16 17:43:40.545589+0800 七音階(副):0 MIDI音階:-1000 下一拍時(shí)間間隔:0.60
2022-03-16 17:43:40.545754+0800 七音階(主):11 MIDI音階:12 下一拍時(shí)間間隔:0.60
2022-03-16 17:43:41.195015+0800 七音階(副):0 MIDI音階:-1000 下一拍時(shí)間間隔:0.60
2022-03-16 17:43:41.195196+0800 七音階(主):55 MIDI音階:19 下一拍時(shí)間間隔:0.60
2022-03-16 17:43:41.845323+0800 七音階(副):-2 MIDI音階:-10 下一拍時(shí)間間隔:0.30
2022-03-16 17:43:41.845505+0800 七音階(主):44 MIDI音階:17 下一拍時(shí)間間隔:0.60
2022-03-16 17:43:42.170909+0800 七音階(副):-6 MIDI音階:-3 下一拍時(shí)間間隔:0.30
2022-03-16 17:43:42.495522+0800 七音階(主):0 MIDI音階:-1000 下一拍時(shí)間間隔:0.60
2022-03-16 17:43:42.495700+0800 七音階(副):2 MIDI音階:2 下一拍時(shí)間間隔:0.60
2022-03-16 17:43:43.145171+0800 七音階(主):0 MIDI音階:-1000 下一拍時(shí)間間隔:0.60
2022-03-16 17:43:43.145845+0800 七音階(副):3 MIDI音階:4 下一拍時(shí)間間隔:0.60
2022-03-16 17:43:43.795268+0800 七音階(主):0 MIDI音階:-1000 下一拍時(shí)間間隔:0.60
看看效果如何蔓涧?
https://www.bilibili.com/video/BV1Yq4y1e7G9/
以上的所有功能均已經(jīng)開源 開源地址