iOS番外-搞点音乐玩玩

这是一个可以让iOS小白用户,直接根据钢琴或者其他乐器的简谱,直接开发一个可以播放的简单教程,底层使用CoreMIDI.framework来实现,中层使用开源的MIKMIDI库来实现,上层将简谱设计成合理的数据结构,将简谱数据进行对象化管理,业务方简单调用进而直接上手使用。

一、理论篇

1、认识钢琴键盘和简谱的关系

piano.png

2、认识简谱和MIDI键的关系

其中:中央C(1/do) 对应的是 MIDI的 60,依次左右推算得到

音名 简谱 MIDI值 和上个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
... ... ... ...

规律点就是 3 - 4 MIDI 差值为1,7 - 1的MIDI差值为1,其他都是2。这个正是和钢琴按键对应上。

这里整理了较为完整的 唱名对应的MIDI值的枚举关系:

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、钢琴简谱如何生成对应MIDI值

bjehp.jpeg

首先看一下该简谱的基本信息:

C4/4:表示C调,4/4拍,即 “da da da da”
♩:四分音符(全音符时值的四分之一拍)
105:表示一分钟有105拍,也就是60/107=0.57秒为一拍

0 67 1` 5`  :该行为主旋律,其中 0 是空拍;67 为一拍;1` 是do的高音
0 0  0  0       :该行为副旋律或者伴奏,表示四个空拍
beat.jpg

👆来自北京超哥的用心指导

所以这四拍对应的数据结构应该是

bjehp_data.jpg

字段解析如下:

interval:表示一拍的时间间隔
hitCount/allCount:4/4拍
modulators:
    modulator:钢琴简谱的值(主旋律)。1-7直接输入1-7;“1`-7`”高音输入“11-77”; “1.-7.”低音输入为 “-1  -7”
    mmodulator:钢琴简谱的值(主旋律)
    
# 如果一拍又有多个拍子,则需要将该拍的多个小拍子添加再 modulators/mmodulators 中
modulators = ({
    modulators = ({
        modulator = 6;  
        }, {
        modulator = 7;
    });
    mmodulators = ({
        mmodulator = "-2";
        }, {
        mmodulator = "-6";
    });
})

原生代码对象如下:

/// 节拍数据对象
@interface HDModulatorItem : NSObject
  
/// 节拍的间隔时间
@property (nonatomic, assign) NSTimeInterval interval;

/// 节拍应该的拍数
@property (nonatomic, assign) NSUInteger hitCount;

/// 节拍的总拍数,和 beatHitCount 可以组合成 3/4、6/8 等节拍
@property (nonatomic, assign) NSUInteger allCount;

/// 钢琴曲中的音阶(主旋律)和 modulators 互斥
@property (nonatomic, assign) NSInteger modulator;

/// 钢琴曲中的音阶(副旋律/伴奏)和 minorModulators 互斥
@property (nonatomic, assign) NSInteger minorModulator;

/// 钢琴曲中的音阶(该拍子中又包含多个音阶) (主旋律)
@property (nonatomic, copy) NSArray <HDModulatorItem *>*modulators;

/// 钢琴曲中的音阶(该拍子中又包含多个音阶)(副旋律/伴奏)
@property (nonatomic, copy) NSArray <HDModulatorItem *>*minorModulators;

/// MIDI的音阶(主旋律)
@property (nonatomic, assign) HDModulatorMidiValue midiModulator;

/// MIDI的音阶(副旋律/伴奏)
@property (nonatomic, assign) HDModulatorMidiValue midiMinorModulator;

@end

二、实践篇

1、根据钢琴简谱生存MIDI数据库

这里没有好的方案,只能根据简谱及上面的理论知识,一个个将MIDI数据输入到PLIST文件中,当然比较高级的做法应该可以通过拍照来自动生成。技术难度应该有,容错机制等细节问题需要具体场景去实现解决。

2、MIDI数据转成原生对象

基于已经设计的数据结构,生成钢琴简谱的原生对象

/// 一首曲子的节拍及音阶信息
@interface HDModulator : NSObject

/// 节拍的间隔时间
@property (nonatomic, assign) NSTimeInterval beatInterval;

/// 节拍应该的拍数
@property (nonatomic, assign) NSUInteger beatHitCount;

/// 节拍的总拍数,和 beatHitCount 可以组合成 3/4、6/8 等节拍
@property (nonatomic, assign) NSUInteger beatAllCount;

/// 音阶组合(数据结构是树形结构,对应的是plist的数据结构)
@property (nonatomic, copy) NSArray <HDModulatorItem *>*beatModulators;

/// 音阶组合(数据结构是队列结构)(主旋律)
@property (nonatomic, copy) NSMutableArray <HDModulatorItem *>*combineModulators;

/// 音阶组合(数据结构是队列结构)(副旋律/伴奏)
@property (nonatomic, copy) NSMutableArray <HDModulatorItem *>*combineMinorModulators;

通过MJExtension来转换

+ (instancetype)loadPlist:(NSString *)plist {
    NSString *filePath = [NSBundle.mainBundle pathForResource:plist ofType:@"plist"];
    HDModulator *model = [HDModulator mj_objectWithFile:filePath];
    return model;
}

3、对象数据处理

上面一部分将数据库转成了原生模型,但是每一拍的结构是树形结构,这里需要将其打平成一个数组,并且需要留言每个拍子中多个小拍的时间

/// 音阶组合(数据结构是队列结构)(主旋律)
- (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) {
            // 这里需要注意,一拍中又包含多个小拍,时间间隔需要处理
        NSTimeInterval beatInterval = item.interval / item.modulators.count;
        for (HDModulatorItem *mm in item.modulators) {
            mm.interval = beatInterval;
            [self addModulatorItem:mm];
        }
    }
    else {
        [_combineModulators addObject:item];
    }
}

/// 音阶组合(数据结构是队列结构)(副旋律/伴奏)
- (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) {
        // 这里需要注意,一拍中又包含多个小拍,时间间隔需要处理
        NSTimeInterval beatInterval = item.interval / item.minorModulators.count;
        for (HDModulatorItem *mm in item.minorModulators) {
            mm.interval = beatInterval;
            [self addMinorModulatorItem:mm];
        }
    }
    else {
        [_combineMinorModulators addObject:item];
    }
}

4、数据播放

这里直接是调用已经封装好的MIDI播放器进行播放即可

_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 下一拍时间间隔:%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 下一拍时间间隔:%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 下一拍时间间隔:0.60
2022-03-16 17:43:39.241565+0800 七音阶(副):0 MIDI音阶:-1000 下一拍时间间隔:0.60
2022-03-16 17:43:39.896846+0800 七音阶(主):6 MIDI音阶:9 下一拍时间间隔:0.30
2022-03-16 17:43:39.897053+0800 七音阶(副):0 MIDI音阶:-1000 下一拍时间间隔:0.60
2022-03-16 17:43:40.224594+0800 七音阶(主):7 MIDI音阶:11 下一拍时间间隔:0.30
2022-03-16 17:43:40.545589+0800 七音阶(副):0 MIDI音阶:-1000 下一拍时间间隔:0.60
2022-03-16 17:43:40.545754+0800 七音阶(主):11 MIDI音阶:12 下一拍时间间隔:0.60
2022-03-16 17:43:41.195015+0800 七音阶(副):0 MIDI音阶:-1000 下一拍时间间隔:0.60
2022-03-16 17:43:41.195196+0800 七音阶(主):55 MIDI音阶:19 下一拍时间间隔:0.60
2022-03-16 17:43:41.845323+0800 七音阶(副):-2 MIDI音阶:-10 下一拍时间间隔:0.30
2022-03-16 17:43:41.845505+0800 七音阶(主):44 MIDI音阶:17 下一拍时间间隔:0.60
2022-03-16 17:43:42.170909+0800 七音阶(副):-6 MIDI音阶:-3 下一拍时间间隔:0.30
2022-03-16 17:43:42.495522+0800 七音阶(主):0 MIDI音阶:-1000 下一拍时间间隔:0.60
2022-03-16 17:43:42.495700+0800 七音阶(副):2 MIDI音阶:2 下一拍时间间隔:0.60
2022-03-16 17:43:43.145171+0800 七音阶(主):0 MIDI音阶:-1000 下一拍时间间隔:0.60
2022-03-16 17:43:43.145845+0800 七音阶(副):3 MIDI音阶:4 下一拍时间间隔:0.60
2022-03-16 17:43:43.795268+0800 七音阶(主):0 MIDI音阶:-1000 下一拍时间间隔:0.60

看看效果如何?

https://www.bilibili.com/video/BV1Yq4y1e7G9/

以上的所有功能均已经开源 开源地址

三、参考资料

MIDI播放库

数据转模型库

Typora插入视频

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容