Python使用PyGame模块播放midi音符(一)

乐理知识

先铺垫一下基础知识。

BPM

  介绍

BPM为每分钟节拍数,是全曲速度标记,为独立在曲谱外的速度标准,一般以一个四分音符为一拍,60BPM为一分钟演奏均匀60个四分音符(或等效的音符组合)。一般记一个四分音符为一拍,而后描述一拍即为在当前BPM下的一个四分音符。60BPM对应的曲目速度为一分钟均匀演奏60个四分音符(或等效音符组合),即一个四分音符(或等效音符组合)的时值应为1秒,而对应提供演奏者现实的演奏速度。

BPM(每分钟节拍数的单位)_百度百科 (baidu.com)

人话:每分钟四分音符个数

  bpm换算四分音符时长

计算某个BPM值对应的每隔多少秒一拍的公式是

60÷某个BPM值=对应的每隔多少秒一拍

'''
输入bpm,返回四分音符时长
bpm:每分钟节拍数
'''
def bpm_to_quarterNote_duration(bpm):
   return 60/bpm

 拍号

拍号是在乐谱中使用的符号,用分数的形式来标画。 

分母表示拍子的时值(用几分音符来当一拍),分母代表每一小节有多少拍子。如2/4拍表示以四分音符为一拍,每小节2拍(2个四分音符的时值)

 强弱规律

  介绍

根据每小节的拍子数,可得出一个小节内音符的强拍、次强拍、弱拍,如

  • 42拍是强/弱
  • 43拍是强/弱/弱
  • 44拍是强/弱/次强/弱

并不需要按照这个规律来处理小节中的每一个音,强弱拍只是最能体现出音乐的律动,让人捕捉到音乐开始的拍点,所以把第一拍弹强,符合大多数人的听觉经验和音乐实践。

这篇文章讲的很透彻:

音乐的强弱规律是什么?如何在钢琴演奏中应用强弱拍? - 知乎 (zhihu.com)

  每小节拍子数计算强弱规律

根据每小节拍子数计算每一拍内的强弱规律

'''
根据每小节拍数计算强弱规律
beats_per_measure:每小节拍数,如4表示每小节4拍
2拍:强弱规律为 [1, 0.95]
强弱
3拍:强弱规律为 [1, 0.9, 0.9]
强弱弱
4拍:强弱规律为 [1, 0.9, 0.95, 0.9]
强弱次强弱
5拍:强弱规律为 [1, 0.9, 0.9, 1, 0.9]
强弱弱强弱
6拍:强弱规律为 [1, 0.9, 0.9, 0.95, 0.9, 0.9]
强弱弱次强弱弱
7拍:强弱规律为 [1, 0.9, 0.9, 1, 0.9, 0.9, 1]
强弱弱强弱弱强
8拍:强弱规律为 [1, 0.9, 0.95, 0.9, 1, 0.9, 0.95, 0.9]
强弱次强弱 强弱次强弱
12拍:强弱规律为[1, 0.9, 0.9, 0.95, 0.9, 0.9, 1, 0.9, 0.9, 0.95, 0.9, 0.9]
强弱弱次强弱弱 强弱弱次强弱弱
16拍:强弱规律为[1, 0.9, 0.95, 0.9, 0.95, 0.9, 1, 0.9, 0.95, 0.9, 0.95, 0.9, 1, 0.9, 0.95, 0.9]
强弱次强弱次强弱 强弱次强弱次强弱 强弱次强弱
'''
def meter_to_rhythm(beats_per_measure=4):
    if   beats_per_measure == 2:
        return [1, 0.95]
    elif beats_per_measure == 3:
        return [1, 0.9, 0.9]
    elif beats_per_measure == 4:
        return [1, 0.9, 0.95, 0.9]
    elif beats_per_measure == 5:
        return [1, 0.9, 0.9, 1, 0.9]
    elif beats_per_measure == 6:
        return [1, 0.9, 0.9, 0.95, 0.9, 0.9]
    elif beats_per_measure == 7:
        return [1, 0.9, 0.9, 1, 0.9, 0.9, 1]
    elif beats_per_measure == 8:
        return [1, 0.9, 0.95, 0.9, 1, 0.9, 0.95, 0.9]
    elif beats_per_measure == 12:
        return [1, 0.9, 0.9, 0.95, 0.9, 0.9, 1, 0.9, 0.9, 0.95, 0.9, 0.9]
    elif beats_per_measure == 16:
        return [1, 0.9, 0.95, 0.9, 0.95, 0.9, 1, 0.9, 0.95, 0.9, 0.95, 0.9, 1, 0.9, 0.95, 0.9]
    else:
        return [1] * beats_per_measure

音符

 midi音符

MIDI标准中,音高被编为128个不同的编号,这些编号从0到127,分别表示C0~G10的128个音,其中60号对应中央C(也称为Do)通常被指定为钢琴等音乐器的基准音。

  简谱音符

数字简谱以可动唱名法为基础,用1、2、3、4、5、6、7代表音阶中的7个音级,休止以0表示。 上方加点表示升八度,下方加点表示降八度。右侧每加一条横线表示延长一拍,下方每加一条横线表示缩短半拍

   简谱音符转换midi音符码
'''
将简谱音符转换为midi音符码
solfeggio:简谱音符,如1, 2, 3, 4, 5, 6, 7, 1#, 2#, 3#, 4#, 5#, 6#, 7#
前面加点表示升八度,后面加点表示降八度,如.1, .2, .3, .4, .5, .6, .7, .1#, .2#, .3#, .4#, .5#, .6#, .7#
'''
def solfeggio_to_midi(solfeggio):
    if (type(solfeggio) == int): #如果输入的是midi码,则直接返回
        return solfeggio
    notes_dict = {
        '1': 60,
        '1#': 61,
        '2b': 61,
        '2': 62,
        '2#': 63,
        '3b': 63,
        '3': 64,
        '4': 65,
        '4#': 66,
        '5b': 66,
        '5': 67,
        '5#': 68,
        '6b': 68,
        '6': 69,
        '7b': 70,
        '7': 71
    }
    octave_shift = 0
    if solfeggio.endswith('.'):
        octave_shift -= solfeggio.count('.')
        solfeggio = solfeggio.rstrip('.')
    elif solfeggio.startswith('.'):
        octave_shift += solfeggio.count('.')
        solfeggio = solfeggio.lstrip('.')
    if solfeggio not in notes_dict:
        raise ValueError('Invalid solfeggio note')
    return notes_dict[solfeggio] + 12*octave_shift

  五线谱音符

以四分音符为一拍:

   五线谱音符转换midi音符码

五线谱音符转换为midi音符码需安装music21库

'''
将五线谱音符转换为midi音符码
note:音符名,如C4, A#5
'''
def staffNote_to_midi(note):
    return music21.note.Note(note).pitch.midi
  midi音符码转换五线谱音符

midi音符码转换为五线谱音符同样使用music21库

'''
将midi音符码转换为五线谱音符
midi:音符的midi码,如60, 69
'''
def midi_to_staffNote(midi):
    return music21.note.Note(midi).nameWithOctave

五线谱、简谱音符对照表

在音符(包括简谱音符、五线谱音符)右侧加点表示延长这个音的一半,如"1."时值是1.5拍 

曲调

  介绍

曲调决定了音高的升降,如

C--不变

C# / Db--升1个半音

Cb / B--降一个半音

  曲调计算音高升降

输入曲调和音符名,返回根据曲调调整后的音高

'''
输入曲调和音符名,返回根据曲调调整后的音高
tune:曲调,如C, Eb
note:音符名,用midi码表示,如60, 69
'''
def tune_to_semitones(self, note, tune='C'):
    tunes_dict = {
        'C': 0,
        'C#': 1,
        'Db': 1,
        'D': 2,
        'Eb': 3,
        'E': 4,
        'F': 5,
        'F#': 6,
        'Gb': 6,
        'G': 7,
        'Ab': -4,
        'A': -3,
        'Bb': -2,
        'B': -1,
        'Cb': -1
    }
    if tune not in tunes_dict:
        raise ValueError('Invalid tune')
    return note + tunes_dict[tune]

 时值

  介绍

音符时值,也称为音符值或音值,在乐谱中用来表达各音符之间的相对持续时间。一个完全音符等于两个二分音符;等于四个四分音符,八个八分音符;十六个十六分音符,三十二个三十二分音符。这只是音符时值的比例。

人话:音符持续时间

音符时值_百度百科 (baidu.com)

  计算音符时长


输入时长(单位:四分音符),返回音符时长(单位:秒)

'''
输入时长(单位:四分音符),返回音符时长(单位:秒)
duration:音符时长(以一个四分音符时长为1)
quaterNote_duration:四分音符时长(秒)
default_duration:默认音符时长(单位:四分音符),当duration为None时,使用默认音符时长
'''
def count_duration(duration, quarterNote_duration, default_noteDuration=1):
    if duration is None:
        duration = default_noteDuration
    return quarterNote_duration * duration

 力度

  介绍

力度就是音的强弱程度。在MIDI中,力度值用0~127表示,数字越大力度越大。

力度(音乐名词)_百度百科 (baidu.com)

  力度标记

力度标记通常采用意大利语的音乐术语。作曲家在乐谱上标有详细的力度标记,从最弱的到最强,通常可分为十几个层次,每一个层次的力度都是一个相对值。piano,是弱的意思,缩写为P,P越多就越弱,最多可有5个P,那就是极弱极弱。forte,是强的意思,缩写为f,f越多就越强,假如乐谱上标有五个f,那就是相当强,演奏者必须竭尽全力地演奏。除了这些记号以外,还有很象是数学中的小于号和大于号的渐强和渐弱记号,以及突强突弱记号等等。

   力度标记转换midi力度值

根据基础力度、强弱规律、力度倍数计算力度仅支持fff~ppp

'''
根据基础力度、强弱规律、力度倍数计算力度
base_dynamics:基本力度,默认为16
rhythm:强弱规律,列表
rhythm_count:计数器,用于循环强弱规律
dynamics_multi:力度倍数
'''
def count_dynamics(dynamics, rhythm, base_dynamics=16):
    global rhythm_count
    if type(dynamics) == int: #如果输入的是力度值,则直接返回
        return dynamics
    if   dynamics == 'fff':
        dynamics_mul = 8
    elif dynamics == 'ff':
        dynamics_mul = 7
    elif dynamics == 'f':
        dynamics_mul = 6
    elif dynamics == 'mf':
        dynamics_mul = 5
    elif dynamics == 'mp':
        dynamics_mul = 4
    elif dynamics == 'p':
        dynamics_mul = 3
    elif dynamics == 'pp':
        dynamics_mul = 2
    elif dynamics == 'ppp':
        dynamics_mul = 1
    else:
        dynamics_mul = 8
    dynamics = int((base_dynamics*rhythm[rhythm_count]*dynamics_mul) - 1)

    self.rhythm_count += 1
    if rhythm_count >= len(rhythm):
        rhythm_count = 0
    return dynamics


 

 音色

  介绍

音色又称为音品。为什么音色不同?是由于不同的振动总是可组合成为不同的声音。每一种乐器、不同的人的声带,以及其它所有的能振动的物体都能够发出各有特色的不同的声音,不同的发声体由于其材料、结构不同,则发出声音的音色也不同。

  midi中的各种音色

在MIDI中,一共有128种不同的音色(不包括打击乐器),用0~127中的整数表示

   乐器名转换midi乐器id

将乐器名转换为midi乐器id,支持中英文 

MIDI 128种音色码表_midi音色表_ruyulin的博客-CSDN博客

需安装re库

'''
将乐器名转换为midi乐器id
支持中文、英文、数字
范围:0-127
返回:第一个值为midi乐器id,第二个值为列表中对应乐器名称
midi音色列表:https://www.360docs.net/doc/632018825.html
            https://blog.csdn.net/ruyulin/article/details/84103186
'''
def instruments(self, instrument):
    #定义一个列表,用于存储乐器名称
    instruments = [
        "Acoustic Grand Piano 大钢琴",
        "Bright Acoustic Piano 亮音钢琴",
        "Electric Grand Piano 大电钢琴",
        "Honky-tonk Piano 酒吧钢琴",
        "Electric Piano 1 电钢琴1",
        "Electric Piano 2 电钢琴2",
        "Harpsichord 大键琴",
        "Clavinet 电翼琴",
        "Celesta 钢片琴",
        "Glockenspiel 钟琴",
        "Musical box 音乐盒",
        "Vibraphone 颤音琴",
        "Marimba 马林巴琴",
        "Xylophone 木琴",
        "Tubular Bell 管钟",
        "Dulcimer 洋琴",
        "Drawbar Organ 音栓风琴",
        "Percussive Organ 敲击风琴",
        "Rock Organ 摇滚风琴",
        "Church organ 教堂管风琴",
        "Reed organ 簧风琴",
        "Accordion 手风琴",
        "Harmonica 口琴",
        "Tango Accordion 探戈手风琴",
        "Acoustic Guitar nylon 木吉他 尼龙弦",
        "Acoustic Guitar steel 木吉他 钢弦",
        "Electric Guitar jazz 电吉他 爵士",
        "Electric Guitar clean 电吉他 清音",
        "Electric Guitar muted 电吉他 闷音",
        "Overdriven Guitar 电吉他 驱动音效",
        "Distortion Guitar 电吉他 失真音效",
        "Guitar harmonics 吉他泛音",
        "Acoustic Bass 贝斯",
        "Electric Bass finger 电贝斯 指奏",
        "Electric Bass pick 电贝斯 拨奏",
        "Fretless Bass 无品贝斯",
        "Slap Bass 1 捶鈎贝斯",
        "Slap Bass 2 捶鈎贝斯",
        "Synth Bass 1 合成贝斯1",
        "Synth Bass 2 合成贝斯2",
        "Violin 小提琴",
        "Viola 中提琴",
        "Cello 大提琴",
        "Contrabass 低音大提琴",
        "Tremolo Strings 颤弓弦乐",
        "Pizzicato Strings 弹拨弦乐",
        "Orchestral Harp 竖琴",
        "Timpani 定音鼓",
        "String Ensemble 1 弦乐合奏1",
        "String Ensemble 2 弦乐合奏2",
        "Synth Strings 1 合成弦乐1",
        "Synth Strings 2 合成弦乐2",
        "Voice Aahs 人声“啊”",
        "Voice Oohs 人声“喔”",
        "Synth Voice 合成人声",
        "Orchestra Hit 交响打击乐",
        "Trumpet 小号",
        "Trombone 长号",
        "Tuba 大号 吐巴号、低音号",
        "Muted Trumpet 闷音小号",
        "French horn 法国号 圆号",
        "Brass Section 铜管乐",
        "Synth Brass 1 合成铜管1",
        "Synth Brass 2 合成铜管2",
        "Soprano Sax 高音萨克斯风",
        "Alto Sax 中音萨克斯风",
        "Tenor Sax 次中音萨克斯风",
        "Baritone Sax 上低音萨克斯风",
        "Oboe 双簧管",
        "English Horn 英国管",
        "Bassoon 巴松管",
        "Clarinet 单簧管",
        "Piccolo 短笛",
        "Flute 长笛",
        "Recorder 直笛",
        "Pan Flute 排箫",
        "Blown Bottle 吹瓶",
        "Shakuhachi 日本尺八",
        "Whistle 口哨",
        "Ocarina 奥卡雷那",
        "Lead 1 (square) 合成主音1(方波)",
        "Lead 2 (sawtooth) 合成主音2(锯齿波)",
        "Lead 3 (calliope) 合成主音3(汽笛音)",
        "Lead 4 (chiff) 合成主音4(吹管音)",
        "Lead 5 (charang) 合成主音5(电吉他合音)",
        "Lead 6 (voice) 合成主音6(人声主音)",
        "Lead 7 (fifths) 合成主音7(五度音)",
        "Lead 8 (bass + lead) 合成主音8(贝司主音)",
        "Pad 1 (new age) 合成音色1(新世纪)",
        "Pad 2 (warm) 合成音色2(温暖)",
        "Pad 3 (polysynth) 合成音色3(多音合成器)",
        "Pad 4 (choir) 合成音色4(合唱团)",
        "Pad 5 (bowed) 合成音色5(拉弦音色)",
        "Pad 6 (metallic) 合成音色6(金属音色)",
        "Pad 7 (halo) 合成音色7(光环音色)",
        "Pad 8 (sweep) 合成音色8(扫掠音色)",
        "FX 1 (rain) 特殊音色1(下雨声)",
        "FX 2 (soundtrack) 特殊音色2(电影音效)",
        "FX 3 (crystal) 特殊音色3(水晶音色)",
        "FX 4 (atmosphere) 特殊音色4(氛围音色)",
        "FX 5 (brightness) 特殊音色5(明亮音色)",
        "FX 6 (goblins) 特殊音色6(鬼怪音色)",
        "FX 7 (echoes) 特殊音色7(回音音色)",
        "FX 8 (sci-fi) 特殊音色8(科幻音色)",
        "Sitar 西塔尔",
        "Banjo 班卓琴",
        "Shamisen 三味线",
        "Koto 十三弦琴",
        "Kalimba 卡林巴",
        "Bagpipe 风笛",
        "Fiddle 古提琴",
        "Shanai 善艾管",
        "Tinkle Bell 叮当铃",
        "Agogo 音乐杯",
        "Steel Drums 钢鼓",
        "Woodblock 木鱼",
        "Taiko Drum 太鼓",
        "Melodic Tom 旋律定音筒鼓",
        "Synth Drum 合成鼓",
        "Reverse Cymbal 反向钹",
        "Guitar Fret Noise 吉他品格噪音",
        "Breath Noise 呼吸噪音",
        "Seashore 海岸",
        "Bird Tweet 鸟叫",
        "Telephone Ring 电话铃声",
        "Helicopter 直升机",
        "Applause 鼓掌",
        "Gunshot 枪声"
    ]
    if type(instrument) == int: #如果输入的是midi乐器id,则不变
        if instrument > 127 or instrument < 0:
            #乐器id超出范围,抛出异常
            raise ValueError(f'Invalid instrument: {instrument}')
        return instrument, instruments[instrument]
    #将乐器名称转换为小写,并去除空格和标点符号
    instrument = re.sub(r'[^\w\s]', '', instrument.lower().replace(" ", ""))
    for i in instruments: #遍历乐器名称列表
        #转换为小写,并去除空格和标点符号
        _i = re.sub(r'[^\w\s]', '', i.lower().replace(" ", ""))
        #如果乐器名称在列表中,则返回对应的midi乐器id
        if instrument in _i:
            return instruments.index(i), i
    #不存在的乐器名称,抛出异常
    raise ValueError(f'Invalid instrument: {instrument}')

 识谱

  五线谱

  简谱 

 

【音乐课】认识五线谱 (qq.com)

 滑音、颤音

模块使用

用到的模块: pygame.midi、re、music21、time、threading

 安装

pip install pygame
pip install music21

  导入

#导入pygame.midi模块,用于播放midi音乐
from  pygame.midi import *
import pygame.midi
import re #导入re模块,用于正则表达式
import music21 #导入music21模块,用于音乐分析
from time import sleep #导入sleep函数,用于等待一定时间
from threading import Thread #导入Thread类,用于多线程播放音乐

 pygame.midi

#导入pygame.midi模块,用于播放midi音乐
from  pygame.midi import *
import pygame.midi

#初始化
pygame.midi.init() #初始化pygame.midi
player = Output(device_id) #设置输出设备,默认设备id为0
player.set_instrument(self.instrument_id, channel) #设置乐器,0~127

#播放音符
player.note_on(note, dynamics_value) #播放音符
sleep(duration) #等待一定时长
player.note_off(note, dynamics_value) #停止播放音符

#关闭
player.close()
pygame.midi.quit()

 PyGame Python上的MIDI库|极客笔记

 threading

from threading import Thread

t = Thread(name="print", target=print, args=('ab', '\n', '1')) #新线程
t.start() #启动线程
t.join() #阻塞当前上下文环境的线程,直到调用此方法的线程终止或到达指定的timeout(可选参数)

python的threading模块_python threading模块_Ernestjackson的博客-CSDN博客

 time

from time import sleep

print("a")
sleep(2) #延时2秒
print("b")

python的time库详解_python time_蕾峰的博客-CSDN博客

正式开干

三个类:MusicConvertPrintMusicInfoMidiMusic

注意:MidiMusic类需要继承另外两个类,并且三个类的方法、变量需统一命名

 MusicConvert

转换音乐数据

先将第一部分写的函数整合成一个类,并在cvt_init函数调用时根据传入的音乐信息设置基础力度、默认音符时长、乐器、曲调、拍号、强弱规律、八分音符时长等:

'''
转换音乐数据
转换音符名、midi码、简谱音符
分析曲调、bpm、拍号
计算音符时长、力度
'''
class MusicConvert:
    def cvt_init(self, tune='C', bpm=120, signature=[4,4], instrument="piano", default_noteDuration=1, base_dynamics=16):
        self.rhythm_count = 0 #计数器,用于循环强弱规律
        self.base_dynamics = base_dynamics #设置基础力度
        self.default_noteDuration = default_noteDuration #设置默认音符时长
        self.instrument_id, self.instrument_name = self.instruments(instrument) #将乐器名转换为midi乐器id
        self.tune = tune #设置曲调
        self.signature = signature #设置拍号
        self.rhythm = self.meter_to_rhythm(signature[0]) #设置强弱规律
        self.quarterNote_duration = self.bpm_to_quarterNote_duration(bpm) #设置八分音符时长

    '''
    将乐器名转换为midi乐器id
    支持中文、英文、数字
    范围:0-127
    返回:第一个值为midi乐器id,第二个值为列表中对应乐器名称
    midi音色列表:https://www.360docs.net/doc/632018825.html
                https://blog.csdn.net/ruyulin/article/details/84103186
    '''
    def instruments(self, instrument):
        #定义一个列表,用于存储乐器名称
        instruments = [
            "Acoustic Grand Piano 大钢琴 声学钢琴",
            "Bright Acoustic Piano 亮音钢琴",
            "Electric Grand Piano 大电钢琴",
            "Honky-tonk Piano 酒吧钢琴",
            "Electric Piano 1 电钢琴1",
            "Electric Piano 2 电钢琴2",
            "Harpsichord 大键琴",
            "Clavinet 电翼琴",
            "Celesta 钢片琴",
            "Glockenspiel 钟琴",
            "Musical box 音乐盒",
            "Vibraphone 颤音琴",
            "Marimba 马林巴琴",
            "Xylophone 木琴",
            "Tubular Bell 管钟",
            "Dulcimer 洋琴",
            "Drawbar Organ 音栓风琴",
            "Percussive Organ 敲击风琴",
            "Rock Organ 摇滚风琴",
            "Church organ 教堂管风琴",
            "Reed organ 簧风琴",
            "Accordion 手风琴",
            "Harmonica 口琴",
            "Tango Accordion 探戈手风琴",
            "Acoustic Guitar nylon 木吉他 尼龙弦",
            "Acoustic Guitar steel 木吉他 钢弦",
            "Electric Guitar jazz 电吉他 爵士",
            "Electric Guitar clean 电吉他 清音",
            "Electric Guitar muted 电吉他 闷音",
            "Overdriven Guitar 电吉他 驱动音效",
            "Distortion Guitar 电吉他 失真音效",
            "Guitar harmonics 吉他泛音",
            "Acoustic Bass 贝斯 木贝斯 声学贝斯",
            "Electric Bass finger 电贝斯 指奏",
            "Electric Bass pick 电贝斯 拨奏",
            "Fretless Bass 无品贝斯",
            "Slap Bass 1 掌击贝斯1",
            "Slap Bass 2 掌击贝斯2",
            "Synth Bass 1 合成贝斯1",
            "Synth Bass 2 合成贝斯2",
            "Violin 小提琴",
            "Viola 中提琴",
            "Cello 大提琴",
            "Contrabass 低音大提琴",
            "Tremolo Strings 颤弓弦乐",
            "Pizzicato Strings 弹拨弦乐",
            "Orchestral Harp 竖琴",
            "Timpani 定音鼓",
            "String Ensemble 1 弦乐合奏1",
            "String Ensemble 2 弦乐合奏2",
            "Synth Strings 1 合成弦乐1",
            "Synth Strings 2 合成弦乐2",
            "Voice Aahs 人声“啊”",
            "Voice Oohs 人声“喔”",
            "Synth Voice 合成人声",
            "Orchestra Hit 交响打击乐",
            "Trumpet 小号",
            "Trombone 长号",
            "Tuba 大号 吐巴号、低音号",
            "Muted Trumpet 闷音小号",
            "French horn 法国号 圆号",
            "Brass Section 铜管乐",
            "Synth Brass 1 合成铜管1",
            "Synth Brass 2 合成铜管2",
            "Soprano Sax 高音萨克斯风",
            "Alto Sax 中音萨克斯风",
            "Tenor Sax 次中音萨克斯风",
            "Baritone Sax 上低音萨克斯风",
            "Oboe 双簧管",
            "English Horn 英国管",
            "Bassoon 巴松管 大管",
            "Clarinet 单簧管 黑管",
            "Piccolo 短笛",
            "Flute 长笛",
            "Recorder 直笛",
            "Pan Flute 排箫",
            "Blown Bottle 吹瓶",
            "Shakuhachi 日本尺八",
            "Whistle 口哨",
            "Ocarina 奥卡雷那",
            "Lead 1 square 合成主音1 方波",
            "Lead 2 sawtooth 合成主音2 锯齿波",
            "Lead 3 calliope 合成主音3 汽笛音",
            "Lead 4 chiff 合成主音4 吹管音",
            "Lead 5 charang 合成主音5 电吉他合音",
            "Lead 6 voice 合成主音6 人声主音",
            "Lead 7 fifths 合成主音7 五度音",
            "Lead 8 bass + lead 合成主音8 贝司主音",
            "Pad 1 new age 合成音色1 新世纪",
            "Pad 2 warm) 合成音色2 温暖",
            "Pad 3 polysynth 合成音色3 多音合成器",
            "Pad 4 choir 合成音色4 合唱团",
            "Pad 5 bowed 合成音色5 拉弦音色",
            "Pad 6 metallic 合成音色6 金属音色",
            "Pad 7 halo 合成音色7 光环音色",
            "Pad 8 sweep 合成音色8 扫掠音色",
            "FX 1 rain 特殊音色1 下雨声",
            "FX 2 soundtrack 特殊音色2 电影音效",
            "FX 3 crystal 特殊音色3 水晶音色",
            "FX 4 atmosphere 特殊音色4 氛围音色",
            "FX 5 brightness 特殊音色5 明亮音色",
            "FX 6 goblins 特殊音色6 鬼怪音色",
            "FX 7 echoes 特殊音色7 回音音色",
            "FX 8 sci-fi 特殊音色8 科幻音色",
            "Sitar 西塔尔",
            "Banjo 班卓琴",
            "Shamisen 三味线",
            "Koto 十三弦琴",
            "Kalimba 卡林巴",
            "Bagpipe 风笛",
            "Fiddle 古提琴",
            "Shanai 善艾管",
            "Tinkle Bell 叮当铃",
            "Agogo 音乐杯",
            "Steel Drums 钢鼓",
            "Woodblock 木鱼",
            "Taiko Drum 太鼓",
            "Melodic Tom 旋律定音筒鼓",
            "Synth Drum 合成鼓",
            "Reverse Cymbal 反向钹",
            "Guitar Fret Noise 吉他品格噪音",
            "Breath Noise 呼吸噪音",
            "Seashore 海岸",
            "Bird Tweet 鸟叫",
            "Telephone Ring 电话铃",
            "Helicopter 直升机",
            "Applause 鼓掌",
            "Gunshot 枪声"
        ]
        if type(instrument) == int: #如果输入的是midi乐器id,则不变
            if instrument > 127 or instrument < 0:
                #乐器id超出范围,抛出异常
                raise ValueError(f'Invalid instrument: {instrument}')
            return instrument, instruments[instrument]
        #将乐器名称转换为小写,并去除空格和标点符号
        instrument = re.sub(r'[^\w\s]', '', instrument.lower().replace(" ", ""))
        for i in instruments: #遍历乐器名称列表
            #转换为小写,并去除空格和标点符号
            _i = re.sub(r'[^\w\s]', '', i.lower().replace(" ", ""))
            #如果乐器名称在列表中,则返回对应的midi乐器id
            if instrument in _i:
                return instruments.index(i), i
        #不存在的乐器名称,抛出异常
        raise ValueError(f'Invalid instrument: {instrument}')

    '''
    将简谱音符转换为midi音符码
    solfeggio:简谱音符,如1, 2, 3, 4, 5, 6, 7, 1#, 2#, 3#, 4#, 5#, 6#, 7#
    前面加点表示升八度,后面加点表示降八度,如.1, .2, .3, .4, .5, .6, .7, .1#, .2#, .3#, .4#, .5#, .6#, .7#
    '''
    def solfeggio_to_midi(self, solfeggio):
        if (type(solfeggio) == int): #如果输入的是midi码,则直接返回
            return solfeggio
        notes_dict = {
            '1': 60,
            '1#': 61,
            '2b': 61,
            '2': 62,
            '2#': 63,
            '3b': 63,
            '3': 64,
            '4': 65,
            '4#': 66,
            '5b': 66,
            '5': 67,
            '5#': 68,
            '6b': 68,
            '6': 69,
            '7b': 70,
            '7': 71
        }
        octave_shift = 0
        if solfeggio.endswith('.'):
            octave_shift -= solfeggio.count('.')
            solfeggio = solfeggio.rstrip('.')
        elif solfeggio.startswith('.'):
            octave_shift += solfeggio.count('.')
            solfeggio = solfeggio.lstrip('.')
        if solfeggio not in notes_dict:
            raise ValueError('Invalid solfeggio note')
        return notes_dict[solfeggio] + 12*octave_shift

    '''
    将五线谱音符转换为midi音符码
    note:音符名,如C4, A#5
    '''
    def staffNote_to_midi(self, note):
        return music21.note.Note(note).pitch.midi

    '''
    将midi音符码转换为五线谱音符
    midi:音符的midi码,如60, 69
    '''
    def midi_to_staffNote(self, midi):
        return music21.note.Note(midi).nameWithOctave

    '''
    将音符名转换为midi音符码,并根据曲调调整音高
    note:音符名,如C4, A#5
    '''
    def note_to_midi(self, note, tune='C'):
        if type(note) == int: #如果输入的是midi码,则不变
            pass
        elif note[0].isalpha(): #如果输入的是音符名,则转换为midi码
            note = self.staffNote_to_midi(note)
        elif note[0].isdigit() or note[0] == '.': #如果输入的是简谱音符,则转换为midi码
            note = self.solfeggio_to_midi(note)
        note = self.tune_to_semitones(note, tune) #根据曲调调整音高
        return note

    '''
    输入bpm,返回四分音符时长
    bpm:每分钟节拍数
    '''
    def bpm_to_quarterNote_duration(self, bpm):
        return 60/bpm

    '''
    根据每小节拍数计算强弱规律
    beats_per_measure:每小节拍数,如4表示每小节4拍
    2拍:强弱规律为 [1, 0.95]
    强弱
    3拍:强弱规律为 [1, 0.9, 0.9]
    强弱弱
    4拍:强弱规律为 [1, 0.9, 0.95, 0.9]
    强弱次强弱
    5拍:强弱规律为 [1, 0.9, 0.9, 1, 0.9]
    强弱弱强弱
    6拍:强弱规律为 [1, 0.9, 0.9, 0.95, 0.9, 0.9]
    强弱弱次强弱弱
    7拍:强弱规律为 [1, 0.9, 0.9, 1, 0.9, 0.9, 1]
    强弱弱强弱弱强
    8拍:强弱规律为 [1, 0.9, 0.95, 0.9, 1, 0.9, 0.95, 0.9]
    强弱次强弱 强弱次强弱
    12拍:强弱规律为[1, 0.9, 0.9, 0.95, 0.9, 0.9, 1, 0.9, 0.9, 0.95, 0.9, 0.9]
    强弱弱次强弱弱 强弱弱次强弱弱
    16拍:强弱规律为[1, 0.9, 0.95, 0.9, 0.95, 0.9, 1, 0.9, 0.95, 0.9, 0.95, 0.9, 1, 0.9, 0.95, 0.9]
    强弱次强弱次强弱 强弱次强弱次强弱 强弱次强弱
    '''
    def meter_to_rhythm(self, beats_per_measure=4):
        if   beats_per_measure == 2:
            return [1, 0.95]
        elif beats_per_measure == 3:
            return [1, 0.9, 0.9]
        elif beats_per_measure == 4:
            return [1, 0.9, 0.95, 0.9]
        elif beats_per_measure == 5:
            return [1, 0.9, 0.9, 1, 0.9]
        elif beats_per_measure == 6:
            return [1, 0.9, 0.9, 0.95, 0.9, 0.9]
        elif beats_per_measure == 7:
            return [1, 0.9, 0.9, 1, 0.9, 0.9, 1]
        elif beats_per_measure == 8:
            return [1, 0.9, 0.95, 0.9, 1, 0.9, 0.95, 0.9]
        elif beats_per_measure == 12:
            return [1, 0.9, 0.9, 0.95, 0.9, 0.9, 1, 0.9, 0.9, 0.95, 0.9, 0.9]
        elif beats_per_measure == 16:
            return [1, 0.9, 0.95, 0.9, 0.95, 0.9, 1, 0.9, 0.95, 0.9, 0.95, 0.9, 1, 0.9, 0.95, 0.9]
        else:
            return [1] * beats_per_measure

    '''
    根据基础力度、强弱规律、力度倍数计算力度
    base_dynamics:基本力度,默认为16
    rhythm:强弱规律,列表
    rhythm_count:计数器,用于循环强弱规律
    dynamics_multi:力度倍数
    '''
    def count_dynamics(self, dynamics):
        if type(dynamics) == int: #如果输入的是力度值,则直接返回
            return dynamics
        if   dynamics == 'fff':
            dynamics_mul = 8
        elif dynamics == 'ff':
            dynamics_mul = 7
        elif dynamics == 'f':
            dynamics_mul = 6
        elif dynamics == 'mf':
            dynamics_mul = 5
        elif dynamics == 'mp':
            dynamics_mul = 4
        elif dynamics == 'p':
            dynamics_mul = 3
        elif dynamics == 'pp':
            dynamics_mul = 2
        elif dynamics == 'ppp':
            dynamics_mul = 1
        else:
            dynamics_mul = 8
        dynamics = int((self.base_dynamics*self.rhythm[self.rhythm_count]*dynamics_mul) - 1)
        self.rhythm_count += 1
        if self.rhythm_count >= len(self.rhythm):
            self.rhythm_count = 0
        return dynamics

    '''
    输入时长(单位:四分音符),返回音符时长(单位:秒)
    duration:音符时长(以一个四分音符时长为1)
    quaterNote_duration:四分音符时长(秒)
    default_duration:默认音符时长(单位:四分音符),当duration为None时,使用默认音符时长
    '''
    def count_duration(self, duration):
        if duration is None:
            duration = self.default_noteDuration
        return self.quarterNote_duration * duration

    '''
    输入曲调和音符名,返回根据曲调调整后的音高
    tune:曲调,如C, Eb
    note:音符名,用midi码表示,如60, 69
    '''
    def tune_to_semitones(self, note, tune='C'):
        tunes_dict = {
            'C': 0,
            'C#': 1,
            'Db': 1,
            'D': 2,
            'Eb': 3,
            'E': 4,
            'F': 5,
            'F#': 6,
            'Gb': 6,
            'G': 7,
            'Ab': -4,
            'A': -3,
            'Bb': -2,
            'B': -1,
            'Cb': -1
        }
        if tune not in tunes_dict:
            raise ValueError('Invalid tune')
        return note + tunes_dict[tune]

PrintMusicInfo

调用prt_init函数设置音乐信息,之后在每次操作时打印音乐详细信息, 包括

  • 乐曲信息

  • 音符信息

  • 和弦信息

  • 休止符信息

  • 踏板信息

'''
打印音乐信息、音符信息、和弦信息、休止符信息、踏板信息
需要传入Midi类的对象、通道
与Midi类的对象的isPrintInfo属性配合使用
'''
class PrintMusicInfo:
    def prt_init(self, music, channel, isPrintInfo=True):
        if isPrintInfo:
            self.note_count = 1 #音符计数器,用于计算音符个数
            self.chord_count = 1 #和弦计数器,用于计算和弦个数
            self.rest_count = 1 #休止符计数器,用于计算休止符个数
            self.pedal_count = 1 #踏板计数器,用于计算踏板操作次数
            self.tenutoPedal_on = False #延音踏板状态
            self.softPedal_on = False #弱音踏板状态
            self.music = music #MidiMusic类的对象
            self.channel = channel #通道
            self.isPrintInfo = isPrintInfo #是否打印信息

    #打印音乐信息
    def print_musicInfo(self):
        if self.isPrintInfo:
            print('曲调:', self.music.tune)
            print('拍号:', self.music.signature)
            print('八分音符时长(秒):', self.music.quarterNote_duration)
            print('基础力度:', self.music.base_dynamics)
            print('强弱规律:', self.music.rhythm)
            print('通道:', self.music.channel)
            print('乐器名:', self.music.instrument_name, '乐器id:', self.music.instrument_id)
            print('设备id:', self.music.player.device_id)
            print('设备信息:', get_device_info(self.music.player.device_id))
            print('\n')

    #打印音符信息
    def print_noteInfo(self, note, duration, dynamics_mark, dynamics_value):
        if self.isPrintInfo:
            print('第', self.note_count, '个单音')
            print('音符名:', self.music.midi_to_staffNote(note), 'midi码:', self.music.note_to_midi(note))
            print('时长(秒):', duration, '时长(四分音符):', duration/self.music.quarterNote_duration)
            print('力度标记:', dynamics_mark, '力度值:', dynamics_value)
            print('\n')
            self.note_count += 1

    #打印和弦信息
    def print_chordInfo(self, *notes, duration, dynamics_mark, dynamics_value):
        if self.isPrintInfo:
            print(notes)
            print('第', self.chord_count, '个和弦')
            print('音符名:', end='')
            for note in notes:
                print(self.music.midi_to_staffNote(note), end=' ')
            print('\nmidi码:', end='')
            for note in notes:
                print(self.music.note_to_midi(note), end=' ')
            print('\n时长(秒):', duration, '时长(四分音符):', duration/self.music.quarterNote_duration)
            print('力度标记:', dynamics_mark, '力度值:', dynamics_value)
            print('\n')
            self.chord_count += 1

    #打印休止符信息
    def print_restInfo(self, duration):
        if self.isPrintInfo:
            print('第', self.rest_count, '个休止符')
            print('时长(秒):', duration, '时长(四分音符):', duration/self.music.quarterNote_duration)
            print('\n')
            self.rest_count += 1

    #打印踏板信息
    def print_pedalInfo(self):
        if self.isPrintInfo:
            print('第', self.pedal_count, '次踏板操作')
            if self.tenutoPedal_on:
                print('延音踏板:踩下')
            else:
                print('延音踏板:松开')
            if self.softPedal_on:
                print('弱音踏板:踩下')
            else:
                print('弱音踏板:松开')
            print('\n')
            self.pedal_count += 1

  MidiMusic

在构造函数内设置音乐信息,可调用play、playChord、rest、tenutoPedal、softPedal、close方法实现演奏midi音乐

详解:

  __init__

   作用

初始化类(创建对象时)

   参数
  • bpm:每分钟节拍数,默认为120

  • tune:曲调,如C, Eb,默认为C

  • signature:拍号,如[4,2],默认为[4,4]

  • instrument:乐器,可用中、英、数字,默认为钢琴(id=0)

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面是使用Pygame模块编写坦克大战游戏的一个示例代码: ```python import pygame import random # initialize pygame pygame.init() # set window size screen = pygame.display.set_mode((800, 600)) # set title pygame.display.set_caption("Tank War") # load tank image tank_img = pygame.image.load("tank.png") # set tank starting position tank_x = 400 tank_y = 500 # set enemy tank starting position enemy_x = random.randint(0, 800) enemy_y = random.randint(50, 150) # load enemy tank image enemy_img = pygame.image.load("enemy.png") # set bullet starting position bullet_x = 0 bullet_y = 0 # load bullet image bullet_img = pygame.image.load("bullet.png") # game loop running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # move tank keys = pygame.key.get_pressed() if keys[pygame.K_LEFT]: tank_x -= 5 if keys[pygame.K_RIGHT]: tank_x += 5 if keys[pygame.K_UP]: tank_y -= 5 if keys[pygame.K_DOWN]: tank_y += 5 # fire bullet if keys[pygame.K_SPACE]: bullet_x = tank_x + 20 bullet_y = tank_y - 20 # move bullet bullet_y -= 5 # check if bullet hit enemy if bullet_x >= enemy_x and bullet_x <= enemy_x + 40 and bullet_y >= enemy_y and bullet_y <= enemy_y + 40: enemy_x = random.randint(0, 800) enemy_y = random.randint(50, 150) bullet_x = 0 bullet_y = 0 # redraw screen screen.fill((0, 0, 0)) screen.blit(tank_img, (tank_x, tank_y)) screen.blit(enemy_img, (enemy_x, enemy_y)) screen.blit(bullet_img, (bullet_x, bullet_y)) pygame.display.update() # quit game pygame.quit() ``` 这个代码实现了一个简单的坦克大战游戏,其中包括操纵坦克移动和开火、敌方坦克随机生成和碰撞检测等功能
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值