当前位置:   article > 正文

Python pygame(GUI编程)模块最完整教程(8)_pygame中note_on调节音调

pygame中note_on调节音调

上一篇文章:

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

MIDI格式 - 简书

23.1 通过MIDI输出声音

pygame.midi模块操作MIDI(乐器数字接口)。各种电子乐器与计算机通过MIDI进行交互。管理MIDI输入,可以从一些MIDI输入设备获取信息;将MIDI输出,则可以模拟某种乐器播放音符。

和pg.camera一样,midi不会自动导入到pygame中,所以需要额外的导入和初始化。

  1. import pygame as pg
  2. from pygame import midi
  3. pg.init()
  4. midi.init()

下面的程序演示了如何通过midi播放从低到高的音阶(相邻音符之间相差一个半音)。

  1. import pygame as pg
  2. from pygame import midi
  3. pg.init()
  4. midi.init()
  5. port = midi.get_default_output_id() # 获取默认的MIDI输出
  6. midi_out = midi.Output(port)
  7. # 速度范围0 - 127,速度127时音符持续时间最长
  8. velocity = 100
  9. # 声调最高音符名为127
  10. max_note = 127
  11. try:
  12. for note in range(0, max_note + 1):
  13. print(note)
  14. midi_out.note_on(note, velocity) # 按下音符
  15. # 音符播放是非阻塞的,不会等一个音符播完再执行下面的代码
  16. pg.time.wait(800) # 等待一段时间后再执行代码
  17. midi_out.note_off(note) # 释放音符
  18. finally: # 退出midi
  19. del midi_out
  20. 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

下面的示例将所有音色的声音都播放了一遍。

  1. import pygame as pg
  2. from pygame import midi
  3. pg.init()
  4. midi.init()
  5. port = midi.get_default_output_id() # 获取默认的MIDI输出
  6. midi_out = midi.Output(port)
  7. velocity = 100
  8. max_instrument = 72
  9. try:
  10. for instrument in range(70, max_instrument + 1):
  11. print(instrument)
  12. midi_out.set_instrument(instrument) # 设置音色
  13. midi_out.note_on(80, velocity) # 按下音符
  14. pg.time.wait(800)
  15. midi_out.note_off(80) # 释放音符
  16. finally:
  17. del midi_out
  18. midi.quit()

23.3 写入原始MIDI信息数据

midi.Output.write用于写入较多的MIDI数据,来控制音符的播放。

Output.write(data) -> None

data必须是一个列表,其中包含多个小列表,每个列表都代表一个MIDI事件(为便于理解以下称作“操作”)。操作列表由信息列表、时间戳组成。信息列表一般是3个值或1个值,第一个值表示状态位,后面的值表示数据位(可选,默认为0)。例如下面这个data列表:

  1. port = midi.get_default_output_id()
  2. midi_out = midi.Output(port, latency=1) # 设置延迟为1
  3. program_change = 192
  4. note_on = 144
  5. note_off = 128
  6. data = [
  7. [[program_change, 40, 0], 0],
  8. [[note_on, 80, 127], 0],
  9. [[note_off, 80, 127], 1000],
  10. ]
  11. 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列表是有顺序的,操作必须按照时间戳从小到大排列,不然有些操作无法正确执行。例如:

  1. data = [
  2. [[program_change, 70, 0], 0],
  3. [[note_on, 80, 127], 0],
  4. [[note_on, 100, 127], 1000],
  5. [[note_off, 80, 127], 2000],
  6. [[note_off, 100, 127], 2000],
  7. ]
  8. '上面是正确的排列 (0 <= 1000 <= 2000 <= 2000)'
  9. data = [
  10. [[program_change, 70, 0], 0],
  11. [[note_on, 80, 127], 0],
  12. [[note_off, 80, 127], 2000],
  13. [[note_on, 100, 127], 1000],
  14. [[note_off, 100, 127], 2000],
  15. ]
  16. '上面是错误的排列,时间戳没有从小到大排列'
  17. '(0 <= 2000 !< 1000 <= 2000)'
  18. '如果使用这种方法排列将执行不到[[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操作。

  1. midi_output.write_sys_ex(0, '\xF0\x7D\x10\x11\x12\x13\xF7')
  2. # 相当于:
  3. midi_output.write_sys_ex(pg.midi.time(), # midi计时器的时间
  4. [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。例如:

  1. import os
  2. 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触屏输入
windowpygame多窗口创建
video包括图像、纹理等

下一篇文章

制作中(可能要等很久)……

预告:作者计划在完成一个大项目后发布easy_pygame游戏引擎。该引擎包含一些特色功能:

  • 静态和动态的多状态精灵
  • 动作系统(比如:淡入淡出、移动、缩放、旋转、振动)
  • 图文混排渲染
  • 异步场景类(使用状态机控制,便于pygbag打包);可支持动作系统的场景类
  • 完美碰撞检测函数
  • 存档功能(进行了一定的md5加密,但是容易被python用户破解)
  • 多背景音乐管理
  • 相对位置转换成绝对位置后再获取文件路径,同时支持pyinstaller的资源内置
  • 封装了缓存算法的图片载入
  • 帧序列处理(包括单张图片上多帧、多图片)
  • pg.time.get_ticks()的时间计算优化
  • 如有必要,可能还会添加一些基本的UI组件模型,以及一些游戏拓展库和一些便捷打包工具

由于该游戏引擎缺乏调试,可能会有很多的bug。以后若有建议请提出。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/354907
推荐阅读
相关标签
  

闽ICP备14008679号