pyxen.Music
A streamed audio track attached to an Entity as a component.
Music is decoded on-demand, making it suitable for longer tracks like background music and ambience.
For audio concepts, see Audio.
Constructor
Music(filename, *, volume=1.0, pan=0.0, loop=False)
filename is the one positional argument: the name of a music asset in the project, without its file extension. The other three are keyword-only and set the starting value of the property of the same name (see Component Properties).
Parameters
| Parameter | Type | Description |
|---|---|---|
filename | str | Name of the music asset, without its extension |
volume | float | Starting gain. Defaults to 1.0 |
pan | float | Starting stereo position, -1.0 to 1.0. Defaults to 0.0 |
loop | bool | Restart when the track ends. Defaults to False |
Example
theme = Music("theme", volume=0.6, loop=True)
Attaching Music
During spawn:
bgm = world.spawn(music=Music("theme"))
Or later:
entity.music = Music("theme")
Assigning music starts playback immediately.
Component Properties
Access via entity.music:
| Property | Type | R/W | Description |
|---|---|---|---|
loop | bool | R/W | Enable looping |
volume | float | R/W | Gain (clamped >= 0.0) |
pan | float | R/W | Stereo panning (-1.0 to 1.0) |
time | float | R | Current playback position (seconds) |
duration | float | R | Total length (seconds) |
finished | bool | R | True when playback has completed |
channels | list-like | R | The parts of the track, each with its own volume — see Channels |
beat | int | R/W | Beats since playback began. Assigning seeks — see Musical time |
bar | int | R/W | Bars since playback began. Assigning seeks |
phase | float | R | How far through the current beat, 0.0 to 1.0 |
beats_per_bar | int | R | Beats in a bar |
frames_to_beat | int | R | Frames until the next beat |
tempo | float | R/W | Playback speed multiplier — see Tempo |
bpm | int | R | Beats per minute, as the file states it |
loop
entity.music.loop = True
When enabled, the track restarts automatically after finishing. Typical for background music.
volume
entity.music.volume = 0.5
Values below 0.0 are clamped to 0.0.
pan
entity.music.pan = -1.0 # full left
entity.music.pan = 1.0 # full right
entity.music.pan = 0.0 # center
Clamped to the range -1.0 to 1.0.
time
t = entity.music.time
Read-only. Current playback position in seconds.
duration
d = entity.music.duration
Read-only. Total length of the track in seconds.
finished
if entity.music.finished:
world.destroy(entity)
Read-only. True when the track has finished playing (and loop is False).
Channels
music = entity.music
for i in range(len(music.channels)):
print(i, music.channels[i].volume)
music.channels[1].volume = 0.0 # mute part 1
A tracker module or a MIDI file is a set of independent parts playing from one clock. music.channels gives you one handle per part, and each has:
| Property | Type | R/W | Description |
|---|---|---|---|
index | int | R | The channel’s number |
volume | float | R/W | Gain of this part alone, clamped to 0.0–1.0 |
Every channel starts at 1.0. Because all the parts share one playhead, turning one down or up never drifts out of time with the rest. That makes it the way to change a soundtrack’s intensity: bring a part in when the action picks up, drop it when the player dies.
What a channel is depends on the format:
| Format | Channels |
|---|---|
| Tracker module | One per channel of the module (a typical .mod has 4) |
| MIDI | Always 16, one per MIDI channel. A channel the file does not use is simply silent |
| OGG | None: len(music.channels) is 0 |
Channel numbers start at 0. Asking for one past the end raises IndexError, and you cannot assign to music.channels[i] itself, only to its volume. A negative index counts from the end, as in any Python list: music.channels[-1] is the last channel.
On MIDI, setting a channel to 0.0 also cuts any note of that channel that is still ringing.
Musical time
if music.beat != last_beat:
last_beat = music.beat
pulse()
| Property | Meaning |
|---|---|
beat | Whole beats since playback began. It is monotonic: it keeps counting up through a loop instead of returning to 0, so you can compare it with last frame’s value without special-casing the wrap |
bar | Whole bars since playback began, also monotonic |
phase | Position inside the current beat, 0.0 up to (not including) 1.0. Where beat changes in steps, phase is continuous: use it for anything that should ease or decay between beats |
beats_per_bar | Beats in a bar |
frames_to_beat | Frames until the next beat lands, rounded up. Use it to start something on the beat instead of just after it |
Assigning to beat or bar seeks. music.bar = 8 jumps to the start of bar 8 in one write, with every channel staying in step. The next frame reads back the new position.
The counters measure time from the file, so which formats have them matters:
| Format | beat, bar, phase, frames_to_beat, beats_per_bar, bpm |
|---|---|
| Tracker module | Available. See below |
| MIDI | Available, read from the file. See below |
| OGG | Not available: beat, bar, beats_per_bar, frames_to_beat and bpm read -1, phase reads 0.0, and assigning beat or bar does nothing |
A game that syncs to music that might be OGG should therefore check music.beat >= 0 once and fall back to its own timer when it is -1.
Beats in a MIDI file
A MIDI file states its own musical time, and Pyxen reads it. A beat is the file’s quarter note, bpm and the length of a beat follow the file’s tempo changes as it plays, and beats_per_bar comes from the file’s first time signature, counted in quarter notes: 3/4 is 3, 6/8 is 3 too. A file that states no time signature is in four.
When a MIDI track loops, the count moves on to the next whole bar, so beat and bar keep counting up and every pass starts on the first beat of a bar. A seek lands exactly on the beat; a note that was already sounding before that point is not played.
Beats in a tracker module
A module does not state a time signature, so Pyxen uses the one definition a module does carry: its BPM. A tracker’s tick lasts 2.5 / BPM seconds, which makes the BPM in the file true exactly when a beat is 24 ticks: four rows at speed 6, or eight rows at speed 3. That is how Pyxen counts. Counting ticks rather than rows keeps the beat steady when the module changes speed.
A bar is four beats. That one is a convention, not data: a module in 3/4 will still report beats_per_bar as 4. If your tune is written with a different number of ticks to the beat, the beat counter will not line up with what you hear.
Tempo
music.tempo = 1.5 # 50% faster, same pitch
music.tempo = 1.0 # normal
tempo multiplies the speed the track asks for. Nothing is resampled, so the pitch does not change: the parts simply arrive sooner or later. bpm keeps reporting the file’s own value whatever tempo is.
Limits: 8.0 at most. At the slow end a tracker module will not go below 0.25, and a value it refuses is ignored (the property keeps its previous value); MIDI accepts down to 0.05. tempo has no effect on OGG, where it always reads 1.0.
beat, phase and frames_to_beat follow tempo, so a game synced to the music stays synced when you speed the music up.
Worked examples
A pulse on every beat
This assumes a tracker module named theme.mod in the project.
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 runs 0 -> 1 inside each beat, so this starts big on the beat and settles.
s = 1.0 + 0.5 * (1.0 - music.phase)
pulse.scale = (s, s)
# beat only changes once per beat.
if music.beat != last_beat:
last_beat = music.beat
print("beat", last_beat, "bar", music.bar)
Mute a part
Click to switch the second part of the track off and on.
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
Jump to a 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:
music.bar = 4 # drop straight into bar 4
Format
A music asset can be OGG Vorbis (.ogg), MIDI (.mid, .midi), or a tracker module — ProTracker (.mod), FastTracker II (.xm), Impulse Tracker (.it) or Scream Tracker 3 (.s3m).
The asset is found by base name, so Music("theme") picks up theme.ogg, theme.mod, or whichever of those extensions exists in the project. Passing the extension is the most common mistake: Music("theme.ogg") looks for theme.ogg.ogg and raises.
A tracker module is often a few kilobytes where the same tune as OGG is megabytes, and it loops seamlessly by construction — a good fit for a small game. It is also the format with the most to offer here: channels, beats, bars and tempo all work on a module, and on MIDI too. OGG has none of them (see Channels and Musical time).
Modules honour panning: ProTracker’s 8xx set-panning command moves a channel in the stereo field as the song plays.
Unlike Sound (WAV, loaded entirely into memory), Music is streamed and decoded on-demand — better suited for longer audio.
Common Errors
Missing Filename
Music()
ValueError: Music must have a file
Invalid Asset
Music("nonexistent")
Music("theme.ogg") # the extension belongs on the file, not in the name
ValueError: Music 'nonexistent' not found. Use the asset name without its extension (.ogg, .mid, .mod, .xm, .it, .s3m).
Snippets
In Pyxen’s home, under Snippets:
- Music() playback — Stream a MIDI file from the project.
- music.finished — Detect the end of a one-shot music track.