用Python和电脑键盘做一个电子琴(硬核)

前言

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

那么第一步,先找音频~

用Python和电脑键盘做一个电子琴(硬核)_第1张图片
苦苦寻找
用Python和电脑键盘做一个电子琴(硬核)_第2张图片
用Python和电脑键盘做一个电子琴(硬核)_第3张图片
唉,不好找啊:(
在这里插入图片描述
去百度网盘康康!

用Python和电脑键盘做一个电子琴(硬核)_第4张图片
。。。
再去网易云
用Python和电脑键盘做一个电子琴(硬核)_第5张图片
怎么办呐QAQ
终于。。。!
用Python和电脑键盘做一个电子琴(硬核)_第6张图片
找到了!!!
csdn上好多
下载链接,一键直达
这么多资源,是不是有人做过?!
用Python和电脑键盘做一个电子琴(硬核)_第7张图片
下载了音频文件,接下来开始写代码

用python实现

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

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

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

再用键盘控制

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

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个音阶
无需等待

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”的一声
    。。。
    }
    用Python和电脑键盘做一个电子琴(硬核)_第8张图片

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

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

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

找了三种方法
pyhook
tkinter
curses

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

于是放弃了这个方案

把音频文件剪短

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

那该怎么办呢???

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

用pygame里的一个方法

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

最终代码

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):这个函数关闭了当前按键的五个之前的所有播放进程

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

你可能感兴趣的:(python,idea)