坤坤音效键盘说明
坤坤音效键盘说明:
- 单独按下
j
、
n
、
t
、
m
j、n、t、m
j、n、t、m 按键,会对应触发 “鸡”、“你”、“太”、“美” 音效。
- 连续按下
j
n
t
m
jntm
jntm 按键,会触发 “鸡你太美” 的较长音效。
- 连续按下
n
g
m
ngm
ngm 按键,会触发 “你干嘛” 的较长音效。
- 按下Esc按键,会触发 “鸡你太美版《澎湖湾》” 的长音效。
- 按下左Ctrl键,会触发 “鸡你太美版《想某人》” 的超长音效。
- 按下小键盘上的数字键或小数点键,停止播放音频并终止程序。
说明一下: 对于连续按键触发的音效,不要求快速连续按下,只要连续即可。
坤坤音效键盘效果展示
说明一下: 为了让大家知道我按下了哪些按键,视频中将我按下的按键进行了打印。
代码实现
安装第三方库
该程序需要用到以下两个第三方库:
- playsound模块: 使用该模块中的playsound函数来播放音频。
- pynput模块: 使用该模块中的Listener对象来监听键盘按键。
在命令行或终端中输入以下命令进行安装:
pip install pynput==1.6.8
pip install playsound==1.2.2
说明一下: 这里下载第三方库时最好不要下载最新版本的。
准备音频
准备几个想要播放的音频,在Python程序所在目录下创建一个子目录,将这些音频文件放到这个子目录当中。比如:
说明一下: 博主的这些音频文件是在B站上找到的,大家可以去各个资源网站上下载音频文件,也可以自行录音。
监听键盘
监听键盘
监听键盘的步骤如下:
- 通过from关键字导入pynput模块中的keyboard模块。
- 创建一个listener对象,创建对象时为其设置一个回调函数。
- 调用listener对象的start方法,让listener开始监听键盘按键。
- 调用listener对象的join方法,防止程序直接退出(listener本质是一个线程)。
代码如下:
from pynput import keyboard
def onRelease(key):
print(f'用户输入: {key}')
listener = keyboard.Listener(on_release=onRelease)
listener.start()
listener.join()
此时每当按键被敲击时,listener就会自动调用我们设置的回调函数,进而打印出被敲击的按键。
说明一下:
- Listener的构造函数主要有两个参数,一个是on_press,另一个是on_release,设置给on_press的回调函数会在按键被按下时调用,而设置给on_release的回调函数会在按键被释放时调用。
- 构造Listener对象时,设置给on_press和on_release的回调函数必须有一个参数,该参数在按键被按下或释放时由Listener自动传入,表示被按下或释放的按键。
普通键和特殊键
虽然在回调函数中通过print能够直接打印出被按下的按键,但实际这个参数并不是字符串类型的,我们不能将该参数直接与字符串进行比较,这样得不到正确的比较结果。
正确的做法如下:
- 如果用户按下的是普通键(键盘上所有的字母、数字、符号),可以通过参数对象的char成员变量得知用户按下的是哪个按键,这个char成员变量的类型是字符串str类型的。
- 如果用户按下的是特殊键(普通键以外的按键),可以通过参数对象的name成员变量得知用户按下的是哪个按键,这个name成员变量的类型是字符串str类型的。
但实际我们并不知道用户本次按键按下的是普通键还是特殊键,并且如果用户按下的是普通键,那么参数对象是没有name成员变量的,反之,如果用户按下的是特殊键,那么参数对象是没有char成员变量的。如果访问了不存在的成员变量,那么程序就会抛出异常AttributeError 。
这时可以借助异常来进行处理:
- 将处理普通键的代码逻辑放到try块中,将处理特殊键的代码逻辑放到except块中。
- 当用户按下按键后,会先执行try块中的代码逻辑,如果用户按下的是普通键,那么程序不会抛出异常,正常执行。
- 如果用户按下的是特殊键,那么当访问参数对象的char成员变量时就会抛出异常
AttributeError ,但由于我们对异常AttributeError 进行了捕捉,因此程序不会终止,此时执行流会跳转到except块中,执行except块中处理特殊键的代码逻辑。
代码如下:
def onRelease(key):
try:
print(f'用户输入: {key.char}')
print(type(key.char))
except AttributeError:
print(f'用户输入: {key.name}')
print(type(key.name))
判断特殊键的另一种方式
当用户按下的是特殊键时,除了通过参数对象的name成员变量得知用户按下的是哪个按键之外,还可以通过如下方式进行比较:
if key.name == 'ctrl_l':
print('用户按下的是左Ctrl键')
if key == keyboard.Key.ctrl_l:
print('用户按下的是左Ctrl键')
说明一下: Key是keyboard模块中的一个枚举类,Key中枚举出了各个特殊键。
播放音频
播放音频
播放音频的步骤如下:
- 通过from关键字导入playsound模块中的playsound函数。
- 调用playsound函数时,传入需要播放的音频的路径。
代码如下:
from playsound import playsound
playsound('sound/j.mp3')
编写逻辑
建立映射关系
为了能够快速获得一个字符串对应的音频路径,可以使用字典建立字符串与对应音频的映射关系。
代码如下:
letterToAudio = {
'j': 'sound/j.mp3',
'n': 'sound/n.mp3',
't': 'sound/t.mp3',
'm': 'sound/m.mp3',
'jntm': 'sound/jntm.mp3',
'ngm': 'sound/ngm.mp3',
'esc': 'sound/phw.mp3',
'ctrl_l': 'sound/xmr.mp3'
}
编写逻辑
代码逻辑的编写如下:
- 将处理普通键的代码逻辑放到try块中,将处理特殊键的代码逻辑放到except块中。
- 为了实现特定连续按键触发特定音频的功能,需要用history变量记录历史敲击过的字母,每当按键被敲击时就可以通过history变量来判断是否触发连续字母音效了。
- 在处理普通键时,需要优先判断是否触发连续字母音效,如果没有触发连续字母音效再判断是否触发单字母音效,因为触发连续字母音效的最后一个字母可能也会触发单字母音效。
- 在处理特殊键时,直接判断用户按下的按键是否会触发音效即可。
代码如下:
history = ''
def onRelease(key):
global history
audio = ''
try:
print(f'用户输入: {key.char}')
if len(history) < 4:
history += key.char
else:
history = history[1:] + key.char
if history == 'jntm':
audio = letterToAudio[history]
elif history[-3:] == 'ngm':
audio = letterToAudio[history[-3:]]
elif key.char in 'jntm':
audio = letterToAudio[key.char]
except AttributeError:
print(f'用户输入: {key.name}')
history = ''
if key == keyboard.Key.esc:
audio = letterToAudio['esc']
elif key == keyboard.Key.ctrl_l:
audio = letterToAudio['ctrl_l']
if audio != '':
playsound(audio)
说明一下:
- 变量history没必要将历史敲击过的字母全部记录下来,因为这里触发连续字母音效的最长连续字母就是
'jntm' ,长度为4,因此history只需要记录最近4次敲击过的字母即可。 - 在判断是否触发
'ngm' 的连续音效时,history的长度可能为3,也可能为4,这时需要通过负索引的方式对history进行切片操作,保证是在用history中的后三个字母在进行判断。
引入线程
当前程序存在的问题
现在我们编写的代码已经可以运行了,但当前的效果体验并不好:
- 在播放音频的时候我们打字会卡顿,并且在当前音频未播放完之前的按键无法触发其他音频。
- 根本原因就是因为此时监听键盘按键和播放音频都是由同一个线程处理的,因此线程在播放音频的时候无法监听键盘按键。
为了解决这个问题,我们可以在调用playsound播放音频的时候创建一个线程,让该线程去执行播放音频的动作,而让当前线程继续进行按键监听操作。
引入线程
引入线程的步骤如下:
- 通过from关键字导入threading模块中的Thread类(threading是标准库中的模块,不需要额外安装)。
- 创建线程时需要创建一个Thread对象,然后调用Thread对象的start方法启动线程。
- 在创建Thread对象时,需要通过target参数指定该线程启动后要执行的程序例程,通过args参数指定调用该程序例程时需要传入的参数。
代码如下:
t = Thread(target=playsound, args=(audio, ))
t.start()
说明一下:
- 创建Thread对象时,传入的args参数的类型是元组类型,因此如果只需要传入一个参数,就需要以
(arg, ) 的方式传入,这后面这个逗号是不可省略的,否则就不是元组类型了。 - Python默认创建线程后,不管主线程是否执行完毕,都会等待子线程执行完毕才一起退出,因此主线程是否join子线程结果都一样。
- 如果这里调用了join,那么监听键盘按键的线程就会被阻塞,直到子线程将音频播放完毕,此时音频的播放过程变成了串行的。
完整代码
引入线程后的完整代码如下,此时在播放音频的时候敲键盘就不会存在卡顿现象,并且在音频播放期间能够再次触发其他音频。
from pynput import keyboard
from playsound import playsound
from threading import Thread
letterToAudio = {
'j': 'sound/j.mp3',
'n': 'sound/n.mp3',
't': 'sound/t.mp3',
'm': 'sound/m.mp3',
'jntm': 'sound/jntm.mp3',
'ngm': 'sound/ngm.mp3',
'esc': 'sound/phw.mp3',
'ctrl_l': 'sound/xmr.mp3'
}
history = ''
def onRelease(key):
global history
audio = ''
try:
print(f'用户输入: {key.char}')
if len(history) < 4:
history += key.char
else:
history = history[1:] + key.char
if history == 'jntm':
audio = letterToAudio[history]
elif history[-3:] == 'ngm':
audio = letterToAudio[history[-3:]]
elif key.char in 'jntm':
audio = letterToAudio[key.char]
except AttributeError:
print(f'用户输入: {key.name}')
history = ''
if key == keyboard.Key.esc:
audio = letterToAudio['esc']
elif key == keyboard.Key.ctrl_l:
audio = letterToAudio['ctrl_l']
if audio != '':
t = Thread(target=playsound, args=audio)
t.start()
listener = keyboard.Listener(on_release=onRelease)
listener.start()
listener.join()
打包成exe程序
一、打包资源文件夹
当前项目播放音频时需要用到的音频文件就叫做资源文件,博主将这些资源文件放在了一个名为sound的文件夹中。如下:
二、修改KunKunKeyboard.py文件
我们需要在KunKunKeyboard.py文件中加入如下函数,该函数是用于生成资源文件的访问路径的。
def resource_path(relative_path):
if getattr(sys, 'frozen', False):
base_path = sys._MEIPASS
else:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
代码中所有使用资源文件路径的地方,都需要通过调用该函数来生成资源文件的访问路径,然后再通过这个生成的路径来访问资源文件,我们只需要将字典中的内容更改一下即可。如下:
letterToAudio = {
'j': resource_path(os.path.join('sound', 'j.mp3')),
'n': resource_path(os.path.join('sound', 'n.mp3')),
't': resource_path(os.path.join('sound', 't.mp3')),
'm': resource_path(os.path.join('sound', 'm.mp3')),
'jntm': resource_path(os.path.join('sound', 'jntm.mp3')),
'ngm': resource_path(os.path.join('sound', 'ngm.mp3')),
'esc': resource_path(os.path.join('sound', 'phw.mp3')),
'ctrl_l': resource_path(os.path.join('sound', 'xmr.mp3'))
}
说明一下: join是os.path模块中的一个函数,它的作用是将多个路径进行拼接。
三、准备图标文件
如果你想要修改生成的exe程序的图标的话,那么你需要准备一个
32
×
32
32\times32
32×32像素的图片文件,图片文件需要为ico格式,可以使用百度的JPG在线转ICO:https://www.aconvert.com/cn/icon/jpg-to-ico/
将生成的图标文件KunKunKeyboard.py的同级目录下。如下:
三、生成KunKunKeyboard.spec文件并修改
在命令行或终端中输入以下命令,生成KunKunKeyboard.spec文件:
pyi-makespec -F -i logo.ico KunKunKeyboard.py
此时在KunKunKeyboard.py的同级目录下会生成一个KunKunKeyboard.spec文件。如下:
此时,打开KunKunKeyboard.spec文件,并将做如下修改:
说明一下:
- 在修改之前,datas的值为一个空列表,即
datas=[] 。 - 这里修改的意思是,将KunKunKeyboard.py目录下的sound目录及其该目录中的文件加入生成的exe程序中,在运行时放在临时文件的根目录下,名称为sound。
四、生成exe程序
最后在命令行或终端中输入以下命令:
pyinstaller KunKunKeyboard.spec
此时在KunKunKeyboard.py的同级目录下会生成一个dict目录,生成的exe程序就在该目录中。如下:
拓展内容(非必须)
如果在修改KunKunKeyboard.spec时,同时将console的值设置为False。如下:
那么此时生成的exe程序在运行时将不会弹出窗口,程序运行后也不会在任务栏显示。需要注意的是,如果要这样做,需要将程序中两处调用print函数的地方注释掉,因为此时没有窗口供print输出,如果print被调用那么程序将会抛异常。
此时要终止这个程序有以下几种方法:
- 打开任务管理器,找到该进程将其终止。
- 关机重启。
- 按下小键盘上的数字键,让程序因抛异常而终止。
说明一下: 代码中没有考虑小键盘中的数字按键和小数点按键,当这些按键被按下时会抛出没有被捕获到的异常,可以将这些按键当作暂停键。
|