用Python和电脑键盘做一个电子琴(硬核) - Go语言中文社区

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


前言

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

那么第一步,先找音频~

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

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

用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”的一声
    。。。
    }
    在这里插入图片描述

  • 问题二
    有的音乐是几个相同的音阶连在一起的,比如
    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里的一个方法

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()
版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/qq_41748900/article/details/99719061
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-02-13 14:33:30
  • 阅读 ( 1554 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢