前言

有一天突然发现电脑上键这么多,刚好可以用来弹琴!
这个是我很早之前就有的一个想法,终于准备着手做。
一开始打算用c++做,在网上搜了一下c++怎么调用电脑的扬声器模块,发现比较难搞;
于是转而考虑使用python,发现好像还蛮简单的。
我的思路是,先找到do,re,mi,fa,so,la,xi音调对应的音频,然后根据输入的不同按键来播放不同的音频文件就可以啦。

那么第一步,先找音频~

在这里插入图片描述
苦苦寻找
在这里插入图片描述
在这里插入图片描述
唉,不好找啊:(
在这里插入图片描述
去百度网盘康康!

在这里插入图片描述
。。。
再去网易云
在这里插入图片描述
怎么办呐QAQ
终于。。。!
在这里插入图片描述
找到了!!!
csdn上好多
下载链接,一键直达
这么多资源,是不是有人做过?!
在这里插入图片描述
下载了音频文件,接下来开始写代码

用python实现

首先找了下,用playsound库可以实现播放wav文件

详细用法见playsound官方文档
以及一篇中文的博客
如何利用Python播放和录制声音
两行代码,播放刚才下好的文件,好用

1
2
from playsound import playsound
playsound('tone (1).wav')

再用键盘控制

python中捕获键盘事件的方法有很多
我用的是pygame里的方法
下面是代码
实现的是按下z键播放一个钢琴按键声

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from playsound import playsound
import pygame

pygame.init()
screen = pygame.display.set_mode((600, 400))
pygame.display.set_caption('pygame event')
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_z:
playsound('tone (10).wav')
pygame.display.update()

有几个小的坑:

  • 导入播放声音的库必须得这样写

    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
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
from playsound import playsound
import pygame
from multiprocessing import Process

def window_init():
pygame.init()
screen = pygame.display.set_mode((600, 400))
pygame.display.set_caption('keyboardpiano')

def k_control(key_param):
window_init()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
elif event.type == pygame.KEYDOWN:
if event.key == key_param:
if key_param == pygame.K_z:
playsound('tone (60).wav')
elif key_param == pygame.K_x:
playsound('tone (20).wav')
pygame.display.update()

def main():
p1 = Process(target=k_control, args=(pygame.K_z,))
p2 = Process(target=k_control, args=(pygame.K_x,))
p1.start()
p2.start()
p1.join()
p2.join()

if __name__ == '__main__':
main()

但是仍然存在两个问题:

  • 问题一
    pygame获取键盘事件必须要创建一个窗口,否则会报错
    于是当使用多进程时,就需要打开多个窗口
    这样只有鼠标选中窗口1时,按下该进程所对应的按键才会有用
    即选中哪个窗口才会执行哪个窗口对应的进程
    ps:
    {
    点住窗口一
    按下z键
    “噔”的一声
    瞬间再点住窗口二
    按下x键
    “Duang”的一声
    。。。
    }
    在这里插入图片描述

  • 问题二
    有的音乐是几个相同的音阶连在一起的,比如
    mi mi mi re mi ,do re do la so
    那这个程序还是会按下第一个mi等很长时间才可以按第二个

解决思路:
对于第一个问题,可以尝试一下换用其他的获取键盘事件的方法;
对于第二个问题,可以尝试批量把录音文件剪短;

尝试其他方法获得键盘事件

找了三种方法
pyhook
tkinter
curses

这三个都可以读取键盘事件,可是无一例外的都需要一个GUI窗口
我想可能想要获取键盘事件必须要有一个window才行
因为电脑同时有很多进程在工作,有很多窗口例如浏览器、word文档、IDE,这些窗口都需要获得键盘事件。如果不选中某一个确定的窗口,计算机无法知道当前的键盘事件哪个进程调用的。
也将会出现一些奇奇怪怪的事情,比如你打开着跟你妈聊天的QQ界面,同时在浏览器上输入了可以描述的东西,按下enter键,嗖的一下,消息就到了你妈眼里。。。

于是放弃了这个方案

把音频文件剪短

之前做过一点音频的处理,用的软件叫Cool Edit Pro,还蛮好用的
因为有88个文件,得批量处理
先批量导入音频文件
在这里插入图片描述
从音频的波形图可以看出,钢琴声后面很大一部分都是很微弱甚至没有声音,剪之
在这里插入图片描述经过裁剪,发现对于高音效果很好,像上面的低音如果剪断会在结束的时候很突兀,从有声一瞬间变到无声,有一声小小的突变“砰~”

那该怎么办呢???

黔驴技穷的我问了问带佬们,果然有了新的方法

用pygame里的一个方法

1
2
3
pygame.mixer.init()
tone_1 = pygame.mixer.Sound('tone (1).wav')
tone_1.play()

啥问题都没有
按一下响一下
完事了,去您妈的多进程,去您妈的playsound,pygame牛批!

pygame.mixer.music模块的一些链接
Pygame详解(十四):music 模块
[BUG]pygame.mixer.music.play

最终代码

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import pygame

def window_init():
pygame.init()
pygame.mixer.init()
screen = pygame.display.set_mode((600, 400))
pygame.display.set_caption('keyboardpiano')

window_init()

tone_3 = pygame.mixer.Sound('tone (3).wav')
tone_6 = pygame.mixer.Sound('tone (6).wav')
tone_9 = pygame.mixer.Sound('tone (9).wav')
tone_12 = pygame.mixer.Sound('tone (12).wav')
tone_15 = pygame.mixer.Sound('tone (15).wav')
tone_18 = pygame.mixer.Sound('tone (18).wav')
tone_21 = pygame.mixer.Sound('tone (21).wav')
tone_24 = pygame.mixer.Sound('tone (24).wav')
tone_27 = pygame.mixer.Sound('tone (27).wav')
tone_30 = pygame.mixer.Sound('tone (30).wav')
tone_33 = pygame.mixer.Sound('tone (33).wav')
tone_36 = pygame.mixer.Sound('tone (36).wav')
tone_39 = pygame.mixer.Sound('tone (39).wav')
tone_42 = pygame.mixer.Sound('tone (42).wav')
tone_45 = pygame.mixer.Sound('tone (45).wav')
tone_48 = pygame.mixer.Sound('tone (48).wav')
tone_51 = pygame.mixer.Sound('tone (51).wav')
tone_54 = pygame.mixer.Sound('tone (54).wav')
tone_57 = pygame.mixer.Sound('tone (57).wav')
tone_60 = pygame.mixer.Sound('tone (60).wav')
tone_63 = pygame.mixer.Sound('tone (63).wav')
tone_66 = pygame.mixer.Sound('tone (66).wav')
tone_69 = pygame.mixer.Sound('tone (69).wav')
tone_72 = pygame.mixer.Sound('tone (72).wav')
tone_75 = pygame.mixer.Sound('tone (75).wav')
tone_78 = pygame.mixer.Sound('tone (78).wav')

def k_control():
while True:
print('true')
for event in pygame.event.get():
print('event in?')
if event.type == pygame.QUIT:
pygame.quit()
elif event.type == pygame.KEYDOWN:
print('key down?')
if event.key == pygame.K_q:
tone_3.play()
elif event.key == pygame.K_a:
tone_6.play()
elif event.key == pygame.K_z:
tone_9.play()
elif event.key == pygame.K_w:
tone_12.play()
elif event.key == pygame.K_s:
tone_15.play()
elif event.key == pygame.K_x:
tone_18.play()
elif event.key == pygame.K_e:
tone_21.play()
elif event.key == pygame.K_d:
tone_24.play()
elif event.key == pygame.K_c:
tone_27.play()
elif event.key == pygame.K_r:
tone_30.play()
elif event.key == pygame.K_f:
tone_33.play()
elif event.key == pygame.K_v:
tone_36.play()
elif event.key == pygame.K_t:
tone_39.play()
elif event.key == pygame.K_g:
tone_42.play()
elif event.key == pygame.K_b:
tone_45.play()
elif event.key == pygame.K_y:
tone_48.play()
elif event.key == pygame.K_h:
tone_51.play()
elif event.key == pygame.K_n:
tone_54.play()
elif event.key == pygame.K_u:
tone_57.play()
elif event.key == pygame.K_j:
tone_60.play()
elif event.key == pygame.K_m:
tone_63.play()
elif event.key == pygame.K_i:
tone_66.play()
elif event.key == pygame.K_k:
tone_69.play()
elif event.key == pygame.K_o:
tone_72.play()
elif event.key == pygame.K_l:
tone_75.play()
elif event.key == pygame.K_p:
tone_78.play()
pygame.display.update()

def main():
k_control()

if __name__ == '__main__':
main()

可以开心地弹琴啦!
在这里插入图片描述

追加一些问题记录

  • 问题: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
    73
    import 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:这里的代码没有专门性能优化,只是简单地实现了功能,有什么问题可以留言讨论哈~