前言
有一天突然发现电脑上键这么多,刚好可以用来弹琴!
这个是我很早之前就有的一个想法,终于准备着手做。
一开始打算用c++做,在网上搜了一下c++怎么调用电脑的扬声器模块,发现比较难搞;
于是转而考虑使用python,发现好像还蛮简单的。
我的思路是,先找到do,re,mi,fa,so,la,xi音调对应的音频,然后根据输入的不同按键来播放不同的音频文件就可以啦。
那么第一步,先找音频~
苦苦寻找
唉,不好找啊:(
去百度网盘康康!
。。。
再去网易云
怎么办呐QAQ
终于。。。!
找到了!!!
csdn上好多
下载链接,一键直达
这么多资源,是不是有人做过?!
下载了音频文件,接下来开始写代码
用python实现
首先找了下,用playsound库可以实现播放wav文件
详细用法见playsound官方文档
以及一篇中文的博客
如何利用Python播放和录制声音
两行代码,播放刚才下好的文件,好用
1 | from playsound import playsound |
再用键盘控制
python中捕获键盘事件的方法有很多
我用的是pygame里的方法
下面是代码
实现的是按下z键播放一个钢琴按键声
1 | from playsound import playsound |
有几个小的坑:
- 导入播放声音的库必须得这样写
from playsound import playsound
不能直接写import playsound
键盘事件需要创建窗口才会有用
即必须包含screen = pygame.display.set_mode((600, 400))
pygame.display.set_caption(‘pygame event’)必须有
pygame.init()
否则会报错pygame.error: video system not initialized
这样就实现了基本的按键控制钢琴,但是有个不完美的地方(应该叫体验极差)
每个录音文件后面都有一段不短的无声
但必须要等待这个文件播放完毕才会进行下一步的操作
造成这个钢琴很不跟手啊。。。
第一个键按完半天,才能按下一个
真实的钢琴声音应该是一个音未落,另一个音就能弹出来
于是考虑,使用多线程
这样可以按完一个键,不用等文件播放完,就能播放下一个键,甚至可以多个键一起按,更符合真实钢琴的亚子
那么再去找找怎么实现Python多线程。。。
参考几篇博客
Python 多线程操作 <–这篇特别棒
python之多线程
多线程:廖雪峰的官方网站
看了很多文章,发现python的多线程不能并行处理多个任务,因为python解释器在执行代码时,有一个GIL锁,这个锁的作用是保证同一时刻只有一个线程在工作,哭了
哭完发现,还可以使用多进程
又是几篇好文章
多进程:廖雪峰的官方网站
第 10 章 python进程与多进程
经过很长时间的学习和尝试,用python自带的multiprocessing库实现了可以先按一个键,再马上按下一个键,代码如下:
实现的是用两个进程运行控制键盘播放录音的程序:
当按下z时,播放第60个音阶
当按下x时,播放第20个音阶
无需等待
1 | from playsound import playsound |
但是仍然存在两个问题:
问题一
pygame获取键盘事件必须要创建一个窗口,否则会报错
于是当使用多进程时,就需要打开多个窗口
这样只有鼠标选中窗口1时,按下该进程所对应的按键才会有用
即选中哪个窗口才会执行哪个窗口对应的进程
ps:
{
点住窗口一
按下z键
“噔”的一声
瞬间再点住窗口二
按下x键
“Duang”的一声
。。。
}问题二
有的音乐是几个相同的音阶连在一起的,比如mi mi mire mi ,do re do la so
那这个程序还是会按下第一个mi等很长时间才可以按第二个
解决思路:
对于第一个问题,可以尝试一下换用其他的获取键盘事件的方法;
对于第二个问题,可以尝试批量把录音文件剪短;
尝试其他方法获得键盘事件
这三个都可以读取键盘事件,可是无一例外的都需要一个GUI窗口
我想可能想要获取键盘事件必须要有一个window才行
因为电脑同时有很多进程在工作,有很多窗口例如浏览器、word文档、IDE,这些窗口都需要获得键盘事件。如果不选中某一个确定的窗口,计算机无法知道当前的键盘事件哪个进程调用的。
也将会出现一些奇奇怪怪的事情,比如你打开着跟你妈聊天的QQ界面,同时在浏览器上输入了可以描述的东西,按下enter键,嗖的一下,消息就到了你妈眼里。。。
于是放弃了这个方案
把音频文件剪短
之前做过一点音频的处理,用的软件叫Cool Edit Pro,还蛮好用的
因为有88个文件,得批量处理
先批量导入音频文件
从音频的波形图可以看出,钢琴声后面很大一部分都是很微弱甚至没有声音,剪之
经过裁剪,发现对于高音效果很好,像上面的低音如果剪断会在结束的时候很突兀,从有声一瞬间变到无声,有一声小小的突变“砰~”
那该怎么办呢???
黔驴技穷的我问了问带佬们,果然有了新的方法
用pygame里的一个方法
1 | pygame.mixer.init() |
啥问题都没有
按一下响一下
完事了,去您妈的多进程,去您妈的playsound,pygame牛批!
pygame.mixer.music模块的一些链接
Pygame详解(十四):music 模块
[BUG]pygame.mixer.music.play
最终代码
1 | import pygame |
可以开心地弹琴啦!
追加一些问题记录
问题:pygame.key.get_pressed()不工作,一开始用的这个方法,困扰了很久
在stack overflow找到了答案
原因及解决方法:The problem is that you don’t process pygame’s event queue. You should simple call pygame.event.pump() at the end of your loop and then your code works fine。(在循环的最后面加一句pygame.event.pump)还有一个问题,pygame虽然好用,但仍有瑕疵,在用pygame.mixer.music播放音乐时,连续按五六下按键,还是会出现停顿,要等一会才能继续按,可能是音乐播放的任务是有上限的
我想了一个办法,用一个list存放最近5次的播放记录,每次有新的键盘事件产生时,关闭除最近5次记录外的所有正在播放的进程。
试了下
果然解决了问题!!!
代码如下
def stop_too_early(tone_now):这个函数关闭了当前按键的五个之前的所有播放进程1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73import pygame
def window_init():
pygame.init()
pygame.mixer.init()
screen = pygame.display.set_mode((1200, 600))
pygame.display.set_caption('keyboardpiano')
# init pygame
window_init()
# load tunes
tone = []
for i in range(1, 27):
name_str = 'tone (' + '%d' % (i*3) + ').wav'
print(name_str)
tone.append(pygame.mixer.Sound(name_str))
# save keys
key = [pygame.K_q, pygame.K_a, pygame.K_z, pygame.K_w, pygame.K_s, pygame.K_x, pygame.K_e, pygame.K_d, pygame.K_c, pygame.K_r, pygame.K_f, pygame.K_v, pygame.K_t, pygame.K_g, pygame.K_b, pygame.K_y, pygame.K_h, pygame.K_n, pygame.K_u, pygame.K_j, pygame.K_m, pygame.K_i, pygame.K_k, pygame.K_o, pygame.K_l, pygame.K_p]
# save play history
play_history = []
# stop early tune, incase play jam
def stop_too_early(tone_now):
if len(play_history) < 5:
play_history.append(tone_now)
else:
play_history.pop(0)
play_history.append(tone_now)
for t in tone:
if len(play_history) < 5:
break
else:
if t == tone_now:
continue
elif t == play_history[0]:
continue
elif t == play_history[1]:
continue
elif t == play_history[2]:
continue
elif t == play_history[3]:
continue
else:
print('stop')
t.stop()
# use event.type == pygame.KEYDOWN to get keyboard input
def k_control():
while True:
# print('true')
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
elif event.type == pygame.KEYDOWN:
print(event.key)
for e in key:
if e == event.key:
tone[key.index(e)].play()
stop_too_early(tone[key.index(e)])
break
pygame.display.update()
def main():
k_control()
if __name__ == '__main__':
main()ps:这里的代码没有专门性能优化,只是简单地实现了功能,有什么问题可以留言讨论哈~