pyxen.Music

附加到 Entity 作为组件的流式音频轨道。

Music 按需解码,适合较长的曲目如背景音乐和环境音。

有关音频概念,请参见音频。


构造函数

Music(filename, *, volume=1.0, pan=0.0, loop=False)

filename 是唯一的位置参数:项目中音乐素材的名称,不带文件扩展名。其余三个只能用关键字传入,用来设定同名属性的初始值(见组件属性)。


参数

参数类型描述
filenamestr音乐素材的名称,不带扩展名
volumefloat初始增益。默认为 1.0
panfloat初始立体声位置,-1.0 到 1.0。默认为 0.0
loopbool曲目结束后重新开始。默认为 False

示例

theme = Music("theme", volume=0.6, loop=True)

附加 Music

在生成时:

bgm = world.spawn(music=Music("theme"))

或稍后:

entity.music = Music("theme")

分配音乐会立即开始播放。


组件属性

通过 entity.music 访问:

属性类型读/写描述
loopbool读/写启用循环
volumefloat读/写增益(限制 >= 0.0)
panfloat读/写立体声声像(-1.0 到 1.0)
timefloat只读当前播放位置(秒)
durationfloat只读总时长(秒)
finishedbool只读播放完成时为 True
channels类列表只读曲目的各个声部,每个都有自己的 volume — 见“声道”一节
beatint读/写自播放开始的拍数。赋值会跳转 — 见“音乐时间”
barint读/写自播放开始的小节数。赋值会跳转
phasefloat只读当前这一拍已经走过的比例,0.0 到 1.0
beats_per_barint只读每小节的拍数
frames_to_beatint只读距离下一拍的帧数
tempofloat读/写播放速度倍率 — 见“速度”
bpmint只读文件所标明的每分钟拍数

loop

entity.music.loop = True

启用后,曲目结束后自动重新开始。适用于背景音乐。


volume

entity.music.volume = 0.5

低于 0.0 的值被限制为 0.0。


pan

entity.music.pan = -1.0  # full left
entity.music.pan =  1.0  # full right
entity.music.pan =  0.0  # center

限制在 -1.0 到 1.0 范围内。


time

t = entity.music.time

只读。当前播放位置(秒)。


duration

d = entity.music.duration

只读。曲目总时长(秒)。


finished

if entity.music.finished:
    world.destroy(entity)

只读。当曲目播放完成时为 True(且 loop 为 False)。


声道

music = entity.music
for i in range(len(music.channels)):
    print(i, music.channels[i].volume)

music.channels[1].volume = 0.0    # 将声部 1 静音

追踪器模块或 MIDI 文件是由一个时钟驱动的多个独立声部。music.channels 为每个声部提供一个句柄,每个句柄都有:

属性类型读/写描述
indexint只读声道编号
volumefloat读/写仅此声部的增益,限制在 0.0–1.0

每个声道初始为 1.0。由于所有声部共用一个播放头,调大或调小其中一个,永远不会与其余声部错开。这就是改变配乐强度的方式:动作加剧时加入一个声部,玩家死亡时撤掉它。

声道是什么,取决于格式:

格式声道
追踪器模块模块的每个声道一个(典型的 .mod 有 4 个)
MIDI始终为 16 个,每个 MIDI 通道一个。文件没有用到的声道只是静音
OGG没有:len(music.channels) 为 0

声道编号从 0 开始。请求超出末尾的编号会抛出 IndexError,并且不能对 music.channels[i] 本身赋值,只能对它的 volume 赋值。负索引与 Python 列表一样从末尾开始计数:music.channels[-1] 是最后一个声道。

在 MIDI 中,把某个声道设为 0.0 还会切断该声道上仍在发声的音符。


音乐时间

if music.beat != last_beat:
    last_beat = music.beat
    pulse()
属性含义
beat自播放开始的整拍数。它是单调的:经过循环时不会回到 0,而是继续累加,因此可以直接与上一帧的值比较,无需特殊处理回绕
bar自播放开始的整小节数,同样单调
phase当前这一拍内的位置,从 0.0 到(不含)1.0。beat 是阶梯式变化,phase 则是连续的:任何需要在拍与拍之间平滑变化或衰减的效果都用它
beats_per_bar每小节的拍数
frames_to_beat距离下一拍到来的帧数(向上取整)。用它让某件事恰好在拍点上开始,而不是稍晚一点

给 beat 或 bar 赋值会跳转播放位置。 music.bar = 8 一次写入就跳到第 8 小节的开头,所有声道保持同步。下一帧读到的就是新位置。

这些计数器根据文件来度量时间,因此格式很重要:

格式beat、bar、phase、frames_to_beat、beats_per_bar、bpm
追踪器模块可用。见下文
MIDI可用,从文件中读取。见下文
OGG不可用:beat、bar、beats_per_bar、frames_to_beat 和 bpm 读出 -1,phase 读出 0.0,给 beat 或 bar 赋值不会产生任何效果

因此,需要与可能是 OGG 的音乐同步的游戏应先检查一次 music.beat >= 0,当它为 -1 时退回到自己的计时器。

MIDI 文件中的拍

MIDI 文件自己写明了音乐时间,Pyxen 会读取它。一拍是文件中的四分音符,bpm 和一拍的长度在播放时跟随文件的速度变化,beats_per_bar 来自文件的第一个拍号,以四分音符计:3/4 为 3,6/8 也是 3。没有写明拍号的文件按四拍处理。

MIDI 曲目循环时,计数会前进到下一个完整小节:beat 和 bar 持续递增,每一遍都从某一小节的第一拍开始。跳转会精确落在拍上;在该位置之前就已发声的音符不会被播放。

追踪器模块中的拍

模块不标明拍号,所以 Pyxen 使用模块唯一具备的定义:它的 BPM。追踪器的一个 tick 持续 2.5 / BPM 秒,这使得文件中的 BPM 恰好在一拍为 24 个 tick 时成立:速度 6 时是四行,速度 3 时是八行。Pyxen 就是这样计数的。按 tick 而不是按行计数,可以在模块改变速度时保持拍的稳定。

一小节是四拍。这是一种约定,而不是数据:3/4 拍的模块仍然会把 beats_per_bar 报告为 4。如果你的曲子每拍的 tick 数不同,拍计数器就会与你听到的不一致。


速度

music.tempo = 1.5    # 快 50%,音高不变
music.tempo = 1.0    # 正常

tempo 是对曲目所要求速度的倍率。不做重采样,所以音高不会改变:各声部只是更早或更晚到来。无论 tempo 是多少,bpm 仍然报告文件自身的值。

限制:最大为 8.0。向慢的方向,追踪器模块不会低于 0.25,被拒绝的值会被忽略(属性保持原来的值);MIDI 可以低至 0.05。tempo 对 OGG 无效,始终读出 1.0。

beat、phase 和 frames_to_beat 会跟随 tempo,所以与音乐同步的游戏在你加快音乐时仍然保持同步。


完整示例

每一拍的脉冲

此示例假设项目中有一个名为 theme.mod 的追踪器模块。

from pyxen import *

last_beat = -1

def start():
    world.spawn(name="song", music=Music("theme", loop=True))
    world.spawn(name="pulse", x=160, y=90,
                shape=Circle(24, fill=(1.0, 0.5, 0.2, 1.0)))

def update():
    global last_beat
    music = world.get(name="song").music
    pulse = world.get(name="pulse")

    # phase 在每一拍内从 0 走到 1,所以圆在拍点上最大,随后回落。
    s = 1.0 + 0.5 * (1.0 - music.phase)
    pulse.scale = (s, s)

    # beat 每拍只变化一次。
    if music.beat != last_beat:
        last_beat = music.beat
        print("beat", last_beat, "bar", music.bar)

将某个声部静音

点击即可关闭、再打开曲目的第二个声部。

from pyxen import *

def start():
    world.spawn(name="song", music=Music("theme", loop=True))

def update():
    music = world.get(name="song").music
    if input.mouse.left.pressed:
        part = music.channels[1]
        if part.volume > 0.0:
            part.volume = 0.0
        else:
            part.volume = 1.0

跳到某个小节

from pyxen import *

def start():
    world.spawn(name="song", music=Music("theme", loop=True))

def update():
    music = world.get(name="song").music
    if input.mouse.left.pressed:
        music.bar = 4      # 直接跳到第 4 小节

格式

音乐素材可以是 OGG Vorbis(.ogg)、MIDI(.mid、.midi),或跟踪器模块 — ProTracker(.mod)、FastTracker II(.xm)、Impulse Tracker(.it)、Scream Tracker 3(.s3m)。

素材按基础名称查找:Music("theme") 会找到项目中存在的 theme.ogg、theme.mod 或其他上述扩展名的文件。传入扩展名是最常见的错误:Music("theme.ogg") 会去找 theme.ogg.ogg 并抛出异常。

同一首曲子做成 OGG 往往有好几兆字节,而跟踪器模块通常只有几千字节,并且天然可以无缝循环 — 很适合小体量的游戏。

它也是这里功能最丰富的格式:声道、拍、小节和速度在模块上都能使用。MIDI 有声道和速度;OGG 两者都没有(见“声道”和“音乐时间”两节)。

模块支持声像:ProTracker 的 8xx 设置声像命令会在曲子播放时让声道在立体声场中移动。

与 Sound(WAV,完全加载到内存)不同,Music 是流式且按需解码的——更适合较长的音频。


常见错误

缺少文件名

Music()
ValueError: Music must have a file

无效素材

Music("nonexistent")
Music("theme.ogg")     # 扩展名写在文件上,不要写进名称里
ValueError: Music 'nonexistent' not found. Use the asset name without its extension (.ogg, .mid, .mod, .xm, .it, .s3m).