前两篇我们实现了python捕获键盘动作和用python播放midi音乐。本篇我们把按键和音乐播放结合在一起,做一个极简电子琴。在pycharm里新建一个python文件,然后把以下代码贴到文件里。先执行看看效果,然后我们再逐步拆解每句话的功能。import timeimport midoNoteMap = { '1' : 60, '2' : 62, '3' : 64, '4' : 65, '5' : 67, '6' : 69, '7' : 70, '8' : 72}melody = "1 2 3 3 2 1 1 3 2 1 2"Instruments = { "大钢琴" : 0, "八音盒" : 10, "管风琴" : 19, "吉他" : 24, "贝斯" : 32, "小提琴" : 40, "萨克斯": 65, "长笛": 73,}print("MIDI player start.")with mido.open_output() as output: print(f"begin to play {melody}") output.send(mido.Message('program_change', program = Instruments["吉他"])) for char in melody.split(): if char in NoteMap: midi_note = NoteMap[char] output.send(mido.Message('note_on', note=midi_note, velocity=100)) time.sleep(0.5) output.send(mido.Message('note_off', note=midi_note, velocity=100)) time.sleep(0.05)print("stop playing.")
- import time 因为我们需要暂停功能,调用time.sleep使用。
- import mido 这是python的midi库,如果你是新来的读者,请参考上一篇解释过,还有个后台库需要安装,仅仅安装这一个库是不响的。
- NoteMap 定义了一个字典,python的字典格式使用{}大括号定义,并且里面有key : value的“键值对”,我们可以很简单的通过key键(我觉得翻译成钥匙可能更好理解)找到对应的值。我们用1~8几个字符,对应的是midi协议定义的琴键上中央C开始的,哆来咪发丝拉缇哆。
- melody定义了一个字符串,这是要演奏的乐曲,我们这里找了小星星奏鸣曲里面一小段做演示。
- Instruments 定义了另一个字典,同样记住用{}花括号来定义,里面保存了乐器的名字和midi协议指定的乐器种类编号。我们在这里找了几个常见的乐器列在这里。因为python没有传统意义上的结构体,所以简单的数据存储经常使用字典。
- with mido.open_output() as output: 使用优雅的方式打开mido设备,打开以后这个设备名称叫做output,以方便使用。with经常伴随打开类的动作,它的好处是自动会回收资源。不用专门的语句去close。:符号代表开始,后面带缩进的语句都是在with打开设备的前提下执行的。
- output.send(mido.Message('program_change', program = Instruments['吉他'])) 这句话说的是给midi设备发送一个消息,内容是更换乐器,乐器的类型是字典里吉他对应的数字24.
- for char in melody.split(): 发起了一个循环,把melody乐曲字符串用空格给分隔开,得到了一个列表,遍历(一个一个的全部过一遍)这个列表,每次把当前一个音符给char。:冒号后面带缩进的语句都是跟着for做循环执行的语句。
- if char in NoteMap: 为了不发送垃圾信息给midi,所以首先判断一下,本次循环拿到的char 乐曲音符,是不是在我们一开始定义的NoteMap字典里。如果再字典里就用,如果不在字典里就丢弃。:冒号后面的语句是if判断在NoteMap字典的音符情况下,执行的语句。
- midi_note = MoteMap[char] 这里是通过char音符,取出来字典里对应的midi音符数字。放到midi_node变量里。因为我们后面要用好几次,所以先查字典存下来,以后就不用查字典了。
- output.send(mido.Message('Note_on', note = midi_note, velocity = 100)) 给midi设备发送一个信息,这个信息命令是开启音符,也就是开始演奏,音符音调是note = midi_note决定的。 velocity是演奏强度,midi协议是用速度来定义的演奏强度, 取值0~127。
- time.sleep(0.5)开始演奏总要延迟一会再关掉,所以,这里要顶一会,时间是0.5秒。
- output.send(mido.Message('Note_off', note= midi_note, velocity = 100)) 这句话是给midi设备发送一个停止信息,停止演奏音符。
- time.sleep(0.05) 为了让两个音符之间有一点停顿,这里等了0.05秒。


import timeimport midoimport osimport keyboardKeyMap = { 'i' : 55, # low so 'o' : 57, # low la 'p' : 59, # low qi 'a' : 60,# DO middle C 's' : 62,# Re 'd' : 64, 'f' : 65, 'g' : 67, 'h' : 69, 'j' : 71, 'k' : 72}if not mido.get_output_names(): print("not found midi device.") os._exit(0)highlight_note = {} #record act notedef onKeyPressed(event): if event.name == 'esc': print("stop playing.") os._exit(0) elif event.name in KeyMap and event.name not in highlight_note: midi_note = KeyMap[event.name] highlight_note[event.name] = midi_note output.send(mido.Message('note_on', note = midi_note, velocity = 100, channel = 0)) elif event.name == 'space' and 'space' not in highlight_note: highlight_note[event.name] = 36 output.send(mido.Message('note_on', note = 36, velocity = 120, channel = 9))def onKeyReleased(event): if event.name in highlight_note: midi_note = highlight_note[event.name] output.send(mido.Message('note_off', note=midi_note, velocity=100, channel=9)) del highlight_note[event.name]os.system('cls' if os.name == 'nt' else 'clear')print("ready to play midi.")with mido.open_output() as output: keyboard.on_press(onKeyPressed, suppress=True) keyboard.on_release(onKeyReleased) keyboard.wait()print("stop playing.")
如果要加入键盘响应,当用户长期按下一个按键后,我们并不能源源不断的给midi设备发送无限个noet_no命令。我们更希望用户抬起按键后才给midi设备发送note_off命令。但是这里有几个细节问题先分析清楚。1、电脑键盘在用户按下一个按键后,会不停地发送这个键盘的字符,不信你可以随便打开一个文本编辑工具,长按下键盘上某个按键,看看电脑表现就明白了。 可是我们实现电子琴的时候,并不能无限制给midi设备发送相同的note_on, 这就需要每当我们按下按键后,就把这个按键标记(记录)出来,如果下次按键命令和上一次标记(记录)的按键一致,那就不要重复发送midi信息。2、添加键盘按键释放的动作检测,只有在按键释放的时候才发送停止note_off消息。3、去掉上一个程序的sleep保护,因为我们是手弹奏的,加上sleep会让演奏非常不跟手。引入四个库,time用作sleep,mido用来播放音乐,os调用系统的命令,keyboard是键盘响应功能。KeyMap定义了一个字典, 定义键盘上的中间一排按键和钢琴从中央C开始白键所对应的midi音符的编号。为了演奏曲调更丰富一点,我们还多定义了c调的5. 6. 7. 三个低音分别对应着键盘i o pif not mido.get_output_names():这里是做了一个程序保护,如果电脑没有midi设备,那么用mido.get_output_names是拿不到的,这样的话,就执行os._exit()退出程序。highlight_note = {} 这里定义了一个字典,如果哪个键被按下,就把这个键保存到highlight_note里面,之所以定义了一个字典,而不是字符串,主要是考虑演奏者可能同时按下多个按键的情况,我们都要记录下来。def onKeyPressed(event ) : 这里定义个函数,当监测到键盘按键被按下就执行这个函数。 :冒号后面的做进的语句都是这个函数内部的语句。输入参数event会是当前按键被按下的那个事件的描述。elif event.name in KeyMap and event.name not in highlight_note: midi_note = KeyMap[event.name] highlight_note[event.name] = midi_note output.sent(mido.Message('note_on', note = midi_note, velocity = 100, channel = 0))如果按键名字在keyMap字典里,(如果没在字典里那就是非法字符,我们不做响应), 并且按键名字没有在highlight_note里面,因为hightlight_note里面是正在使用的按键,只有还没放在highlight里面的才能给midi发送信息。如果都满足条件,那么给midi发送note_on信息,具体解释可以看上一段程序更清楚。 多说一句的是,这时候按键因为已经播放了,也是正在被用的状态,所以要把这个按键名字放到highlight_note里面做标记。即highlight_note[event.name] = midi_noteelif event.name == 'space' and 'space' not in highlight_note: highlight_note[event.name] = 36 output.send(mido.Message('note_on', note = 36, velocity = 120, channel = 9))这里判断如果按键是空格,并且空格键没有被放到highlight_note里面的话,那么就执行下面的逻辑,首先把空格放到highlight_note里面做标记记录。然后给midi设备发送消息, note_on, 音符是36,速度(强度)120,通道9. 9号通道被midi专门定义打击乐,36是鼓的声音。midi设备定义通道从0~31,有些老旧设备是0~16,其中9通道是专门打击乐通道。其他的都是演奏旋律的通道。os.system('cls' if os.name == 'nt' else 'clear') 这里用os模块调用系统命令行命令清屏。因为windows的清屏是cls,而mac和linux清屏是clear,所以要根据系统名称选择。 我们用了python的三元表达式。如果系统名称是nt,即win, 那么就用cls,如果是其他,那么就执行clear。python的三元表达式跟很多其他语言格式不一样: if event.name in highlight_note: midi_note = highlight_note[event.name] output.send(mido.Message('note_off', note=midi_note, velocity=100, channel=9)) del highlight_note[event.name]
定义一个函数,用来响应按键释放,:后面缩进的语句都是函数内部的语句,参数event是传入的按键事件的描述。首先我们要看一下当前释放的按键是不是在highlight_note里面标记过的,如果是标记过的,说明midi正在演奏这个按键对应的音符,所以要发送停止,如果不在highlight里面那就是误触发,不做响应动作。如果满足条件后,首先查字典,把按键对应的音符的数字取出来放到midi_note里面,然后用output.send发送停止midi播放的信息。再然后,del命令从highlighte_note里删掉刚刚停止的按键标记。with mido.open_output as output: keyboard.on_press(onKeyPressed, suppress = True) keyboard.on_release(onKeyReleased)这里是首先用with开启midi设备,用output命名。 然后把键盘的按下on_press动作关联到onKeyPressed函数上,supporess=True意思是只有这个程序接收键盘按键,不再分享给其他程序,这样情况下,你即使最小化窗口,焦点不在当前窗口,也会捕获按键动作。同理on_release动作和函数onKeyReleased关联上。keyboard.wait()是让整个事件驱动轮子转起来。他内置多线程的循环,我们不用不关心细节。这样就把整个功能实现了。在这个程序里,我们做了一个小技巧,用highlight_note用来标记保存正在播放的按键音符,这样就不会给一个相同的音符发送重复的信息了。另外多次使用字典的数据结构。 同时还用了if elfi这样的多条件分支结构来整理不同分支逻辑的语句。