Python pygame(GUI编程)模块最完整教程(8)
上一篇文章:
Python pygame(GUI编程)模块最完整教程(7)_Python-ZZY的博客-CSDN博客
总目录:
README.md · Python-ZZY/Python-Pygame最完整教程 - Gitee.com
23 进阶声音操作
参考资料:
https://pyga.me/docs/ref/sndarray.html
https://pyga.me/docs/ref/midi.html
23.1 通过MIDI输出声音
pygame.midi模块操作MIDI(乐器数字接口)。各种电子乐器与计算机通过MIDI进行交互。管理MIDI输入,可以从一些MIDI输入设备获取信息;将MIDI输出,则可以模拟某种乐器播放音符。
和pg.camera一样,midi不会自动导入到pygame中,所以需要额外的导入和初始化。
import pygame as pg
from pygame import midi
pg.init()
midi.init()
下面的程序演示了如何通过midi播放从低到高的音阶(相邻音符之间相差一个半音)。
import pygame as pg
from pygame import midi
pg.init()
midi.init()
port = midi.get_default_output_id() # 获取默认的MIDI输出
midi_out = midi.Output(port)
# 速度范围0 - 127,速度127时音符持续时间最长
velocity = 100
# 声调最高音符名为127
max_note = 127
try:
for note in range(0, max_note + 1):
print(note)
midi_out.note_on(note, velocity) # 按下音符
# 音符播放是非阻塞的,不会等一个音符播完再执行下面的代码
pg.time.wait(800) # 等待一段时间后再执行代码
midi_out.note_off(note) # 释放音符
finally: # 退出midi
del midi_out
midi.quit()
上面的代码首先通过get_default_output_id方法获取默认的MIDI设备,然后创建了一个Output对象,用于管理MIDI的输出。然后通过Output.note_on按下音符,等待800ms后释放音符。
最后通过finally代码块,无论是否出现异常都要退出midi。不显式退出虽然不太可能出现问题,但是官方建议最好还是加上finally来确保midi退出。
note_on方法可接受3个参数。第一个是音符编码,范围是0-127,相邻音符之间相差一个半音,数字越大音调越高,可参考MIDI音符代码表。第二个参数表示音符的响度(不知道为什么官方用速度来表示),范围同样是0-127,数字越大声音响度越大。第三个参数channel可选,表示播放的声道。与note_on相对,还有note_off方法用于释放音符,参数与之相同。
23.2 模拟乐器音色
pg.midi.Output对象还有一个方法set_instrument,可以将音符音色设置为不同乐器的音色。默认的音色是钢琴。
设置音色需要指定音色编码,所有编码可参考MIDI 128种音色码表。音色编码的范围仍为0-127。
pg.midi.Output.set_instrument(instrument_id, channel=0) -> None
下面的示例将所有音色的声音都播放了一遍。
import pygame as pg
from pygame import midi
pg.init()
midi.init()
port = midi.get_default_output_id() # 获取默认的MIDI输出
midi_out = midi.Output(port)
velocity = 100
max_instrument = 72
try:
for instrument in range(70, max_instrument + 1):
print(instrument)
midi_out.set_instrument(instrument) # 设置音色
midi_out.note_on(80, velocity) # 按下音符
pg.time.wait(800)
midi_out.note_off(80) # 释放音符
finally:
del midi_out
midi.quit()
23.3 写入原始MIDI信息数据
midi.Output.write用于写入较多的MIDI数据,来控制音符的播放。
Output.write(data) -> None
data必须是一个列表,其中包含多个小列表,每个列表都代表一个MIDI事件(为便于理解以下称作“操作”)。操作列表由信息列表、时间戳组成。信息列表一般是3个值或1个值,第一个值表示状态位,后面的值表示数据位(可选,默认为0)。例如下面这个data列表:
port = midi.get_default_output_id()
midi_out = midi.Output(port, latency=1) # 设置延迟为1
program_change = 192
note_on = 144
note_off = 128
data = [
[[program_change, 40, 0], 0],
[[note_on, 80, 127], 0],
[[note_off, 80, 127], 1000],
]
midi_out.write(data)
这个data列表包含3个MIDI操作,改变程序(也就是乐器音色),按下音符,松开音符。时间戳表示触发这个操作的时间。比如这个data就表示:在时间为0ms的时候,改变音色,按下音调为80,强度为127的音符;在时间为1000ms时(注意:不是距离上次操作经过了1000ms),松开音调为80,强度为127的音符。
需要注意的是:使用write的时候一般要把Output类的latency参数设为一个大于0的值。latency表示延迟,即写入数据之前会有一段延迟的时间,单位是ms。如果不设置延迟,那么时间戳是无效的,所有操作都会同时进行(个人觉得这个设置不太好)。因为我不需要太多延迟,所以将它设为1即可。如果延迟设为2000,则在write中的操作处理之前会延迟2000ms。延迟时间不会算进时间戳。
正如上面所说,每个操作包含操作信息,时间戳组成,那么前面的操作信息又代表什么呢?参见这篇文章:常见MIDI信息的状态位和数据位含义表。状态位的代码决定了后面数据位的含义。比如状态位144,就表示在频道1上按下音符;后面的数据位分别表示音符代码、响度代码。状态位128,则表示在频道1上释放音符;后面的数据位分别表示音符代码、响度代码。状态位192,则表示在频道1上改变音色;后面的数据位只有一个,表示乐器音色代码,由于只需要一个数据位,所以最后的数据位留空为0即可。
此外,还需要注意data列表是有顺序的,操作必须按照时间戳从小到大排列,不然有些操作无法正确执行。例如:
data = [
[[program_change, 70, 0], 0],
[[note_on, 80, 127], 0],
[[note_on, 100, 127], 1000],
[[note_off, 80, 127], 2000],
[[note_off, 100, 127], 2000],
]
'上面是正确的排列 (0 <= 1000 <= 2000 <= 2000)'
data = [
[[program_change, 70, 0], 0],
[[note_on, 80, 127], 0],
[[note_off, 80, 127], 2000],
[[note_on, 100, 127], 1000],
[[note_off, 100, 127], 2000],
]
'上面是错误的排列,时间戳没有从小到大排列'
'(0 <= 2000 !< 1000 <= 2000)'
'如果使用这种方法排列将执行不到[[note_on, 100, 127], 1000]这一段'
按照上面正确的代码播放,将达到这样的效果:首先改变音色,播放80音符;过1秒后播放100音符;再过1秒后同时结束两个音符的播放。
23.4 midi模块索引 - 乐器数字接口
事件:midi在输入和输出时会分别产生MIDIIN和MIDIOUT两个事件。
init() -> None, quit() -> None
初始化/退出midi。
Input(device_id) -> None
Input(device_id, buffer_size) -> None
创建一个midi输入对象,device_id是输入设备的标识符,buffer_id是可缓冲的输入事件数量。
Input.close() -> None
尝试关闭midi输入(在Windows系统上可能出现问题)。
Input.poll() -> bool
判断是否有midi的输入数据
Input.read(num_events) -> midi_event_list
读取并返回未被处理的midi输入事件,num_events代表事件的数量。
Output(device_id) -> None
Output(device_id, latency=0) -> None
Output(device_id, buffer_size=256) -> None
Output(device_id, latency, buffer_size) -> None
创建midi输出对象。buffer_size是输出事件的缓冲数量,latency是输出的延迟时间。
Output.abort() -> None
中止midi传输(可能导致数据还没发完时就终止了)
Output.close() -> None
关闭midi输出对象(Windows上可能有问题)
Output.note_off(note, velocity=None, channel=0) -> None
松开音符,note是音符编码,velocity是速度,channel是频道。
Output.note_on(note, velocity, channel=0) -> None
按下音符,note是音符编码,velocity是速度,channel是频道。
Output.set_instrument(instrument_id, channel=0) -> None
设置乐器音色
Output.pitch_bend(value=0, channel=0) -> None
微调midi输出时的音高。value为正数表示升高,负数表示降低,范围是-8192到+8191,每4096代表一个半音。例如:-8192表示降低两个半音(即一个全音),4096表示升高一个半音。
Output.write(data) -> None
写入多个MIDI操作
Output.write_short(status) -> None
Output.write_short(status, data1=0, data2=0) -> None
写入单个的MIDI操作,status是状态位,data1, data2分别是两个数据位。不考虑时间戳。
Output.write_sys_ex(when, msg) -> None
写入全是系统信息的MIDI操作。
midi_output.write_sys_ex(0, '\xF0\x7D\x10\x11\x12\x13\xF7')
# 相当于:
midi_output.write_sys_ex(pg.midi.time(), # midi计时器的时间
[0xF0, 0x7D, 0x10, 0x11, 0x12, 0x13, 0xF7])
get_default_input_id() -> default_id
获取默认的MIDI输入设备
get_default_output_id() -> default_id
获取默认的MIDI输出设备
get_device_info(an_id) -> (interf, name, input, output, opened)
get_device_info(an_id) -> None
获取给定设备ID的信息。返回值一般是元组,ID超出范围会返回None。元组中各个值的含义:interf表示描述设备接口的字符串(例如"ALSA"),name表示设备名称(例如"Midi Through Port-0"),input表示该设备是否为输入设备(1或0),output表示该设备是否为输出设备(1或0),opened表示该设备是否为开启状态。
midis2events(midi_events, device_id) -> [Event, ...]
将MIDI事件转换为pygame事件。midi_events也就是传递给Output.write的data参数中的每个操作列表。
time() -> time
返回midi计时器得到的时间(从midi.init开始算)
frequency_to_midi(midi_note) -> midi_note
将频率转换为MIDI音符代码(会圆整到最接近的那个音符)
midi_to_ansi_note(midi_note) -> ansi_note
将MIDI音符代码转换为ansi音符格式的字符串
MidiException(errno) -> None
MIDI异常。当MIDI设备找不到,或者其他原因时可能报错。
23.5 sndarray模块索引 - Sound与numpy数组
pg.sndarray用于pg.mixer.Sound和numpy数组之间的转换,和pg.surfarray一样依赖于numpy库。
声音数据是由每秒数千个样本组成的,每个样本都是在特定时刻的波的振幅。例如,在22khz格式中,阵列的单元号5是5/22000秒后的波的振幅。
array(Sound) -> array
将声音对象复制并转换为numpy数组,更改数组不会改变声音对象本身。
samples(Sound) -> array
将声音对象之间转换为numpy数组,更改数组将改变声音对象本身。
make_sound(array) -> Sound
将声音样本数组转换为Sound对象。
24 pygame探索
参考资料:pygame — pygame-ce v2.4.0 documentation
24.1 pygame主模块索引
pygame主模块里面并没有什么特别重要的内容,因为学习pygame的时候,读者已经了解了主模块中常用的函数,接下来将整理一些函数的用法。
以下是pygame主模块中唯一可能用到的4个东西(剩下的都没什么用)。
init() -> (numpass, numfail)
初始化pygame所有子模块,并返回初始化成功的数量和失败的数量。
其实在pygame的每个子模块中都有init和quit方法,用于初始化和退出单个的子模块。pygame功能很多,有时候并不用于单纯的编写游戏,有时候也会用于播放音乐等等。如果只需要使用pygame.mixer模块,就无需初始化整个的pygame,只需要调用pygame.mixer.init()而非pygame.init()。
quit() -> None
退出pygame,与init方法相反。这个方法会关闭pygame窗口。调用pg.quit方法并不会退出整个python程序,只会退出pygame。有时候有必要在退出时顺便调用sys.exit来确保python程序完全退出。
register_quit(callable) -> None
注册一个函数。当调用pygame.quit时,会调用它。
error
pygame.error继承于Exception,表示一些pygame相关的异常。可用于异常捕获。
24.2 环境变量
部分pygame的行为可以通过环境变量来控制,即操作os.environ。例如:
import os
os.environ["环境变量名"] = "环境变量值"
下面是一些可能用到的pygame相关环境变量及用法。
PM_RECOMMENDED_INPUT_DEVICE
设置默认的MIDI输入设备
PM_RECOMMENDED_OUTPUT_DEVICE
设置默认的MIDI输出设备
PYGAME_DISPLAY
pygame的显示索引(即pg.display.set_mode的display参数),默认为"0"
PYGAME_FORCE_SCALE
强制set_mode使用缩放显示模式,是"default"或"photo"。如果设置了“photo”,则缩放使用最慢但质量最高的各向异性缩放算法(如果可用)。必须在调用pygame.display.set_mode()之前设置
PYGAME_BLEND_ALPHA_SDL2
这使得pygame对所有alpha混合使用SDL2 blitter。SDL2绘制器有时比默认的要快,但使用不同的公式,因此最终颜色可能不同。必须在调用pygame.init()初始化所有导入的pygame模块之前设置。
PYGAME_HIDE_SUPPORT_PROMPT
是否隐藏pygame启动时打印的那一段提示,设置为"1"表示隐藏。
PYGAME_CAMERA
设置摄像机的后端,如"opencv"。必须在pg.camera初始化之前调用。
SDL_IME_SHOW_UI
是否显示输入候选框。必须在pg.display.set_mode前面调用
SDL_VIDEO_CENTERED
是否将窗口设在屏幕中央。必须在pg.display.set_mode前面调用
SDL_VIDEO_WINDOW_POS
设置pygame窗口的位置(左上角),格式为"x, y"。必须在pg.display.set_mode前面调用
SDL_VIDEO_ALLOW_SCREENSAVER
默认情况下,pygame运行时会禁用屏保,如果设为"1"则表示允许显示屏幕保护系统。
SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS
默认情况下,当窗口不在焦点中时,游戏手柄这样的设备不会更新。然而,使用这个环境变量,即使窗口在后台,也有可能获得操纵杆更新。必须在调用pygame.init()之前设置。
24.3 _sdl2
pygame._sdl2模块中包含了一些实验性的SDL2功能,可能在未来有更改。_sdl2中还包含一些子模块。_sdl2不会被自动导入到pygame里面。
_sdl2包含以下几个模块:
模块名 | 描述 |
controller | 操作控制器(如游戏手柄) |
touch | 触屏输入 |
window | pygame多窗口创建 |
video | 包括图像、纹理等 |
下一篇文章
制作中(可能要等很久)……
预告:作者计划在完成一个大项目后发布easy_pygame游戏引擎。该引擎包含一些特色功能:
- 静态和动态的多状态精灵
- 动作系统(比如:淡入淡出、移动、缩放、旋转、振动)
- 图文混排渲染
- 异步场景类(使用状态机控制,便于pygbag打包);可支持动作系统的场景类
- 完美碰撞检测函数
- 存档功能(进行了一定的md5加密,但是容易被python用户破解)
- 多背景音乐管理
- 相对位置转换成绝对位置后再获取文件路径,同时支持pyinstaller的资源内置
- 封装了缓存算法的图片载入
- 帧序列处理(包括单张图片上多帧、多图片)
- pg.time.get_ticks()的时间计算优化
- 如有必要,可能还会添加一些基本的UI组件模型,以及一些游戏拓展库和一些便捷打包工具
由于该游戏引擎缺乏调试,可能会有很多的bug。以后若有建议请提出。