iOS番外-搞點(diǎn)音樂玩玩

這是一個(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)系

piano.png

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值

bjehp.jpeg

首先看一下該簡(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è)空拍
beat.jpg

??來自北京超哥的用心指導(dǎo)

所以這四拍對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)應(yīng)該是

bjehp_data.jpg

字段解析如下:

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)開源 開源地址

三、參考資料

MIDI播放庫

數(shù)據(jù)轉(zhuǎn)模型庫

Typora插入視頻

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末笋额,一起剝皮案震驚了整個(gè)濱河市元暴,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌兄猩,老刑警劉巖茉盏,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異枢冤,居然都是意外死亡鸠姨,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門淹真,熙熙樓的掌柜王于貴愁眉苦臉地迎上來讶迁,“玉大人,你說我怎么就攤上這事趟咆√泶桑” “怎么了梅屉?”我有些...
    開封第一講書人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長鳞贷。 經(jīng)常有香客問我坯汤,道長,這世上最難降的妖魔是什么搀愧? 我笑而不...
    開封第一講書人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任惰聂,我火速辦了婚禮,結(jié)果婚禮上咱筛,老公的妹妹穿的比我還像新娘搓幌。我一直安慰自己,他們只是感情好迅箩,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開白布溉愁。 她就那樣靜靜地躺著,像睡著了一般饲趋。 火紅的嫁衣襯著肌膚如雪拐揭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評(píng)論 1 285
  • 那天奕塑,我揣著相機(jī)與錄音堂污,去河邊找鬼。 笑死龄砰,一個(gè)胖子當(dāng)著我的面吹牛盟猖,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播换棚,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼式镐,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了圃泡?” 一聲冷哼從身側(cè)響起碟案,我...
    開封第一講書人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎颇蜡,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體辆亏,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡风秤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了扮叨。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片缤弦。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖彻磁,靈堂內(nèi)的尸體忽然破棺而出碍沐,到底是詐尸還是另有隱情狸捅,我是刑警寧澤,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布累提,位于F島的核電站尘喝,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏斋陪。R本人自食惡果不足惜朽褪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望无虚。 院中可真熱鬧缔赠,春花似錦、人聲如沸友题。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽度宦。三九已至踢匣,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間斗埂,已是汗流浹背符糊。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留呛凶,地道東北人男娄。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像漾稀,于是被迫代替她去往敵國和親模闲。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容