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

ParameterTypeDescription
filenamestrName of the music asset, without its extension
volumefloatStarting gain. Defaults to 1.0
panfloatStarting stereo position, -1.0 to 1.0. Defaults to 0.0
loopboolRestart 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:

PropertyTypeR/WDescription
loopboolR/WEnable looping
volumefloatR/WGain (clamped >= 0.0)
panfloatR/WStereo panning (-1.0 to 1.0)
timefloatRCurrent playback position (seconds)
durationfloatRTotal length (seconds)
finishedboolRTrue when playback has completed
channelslist-likeRThe parts of the track, each with its own volume — see Channels
beatintR/WBeats since playback began. Assigning seeks — see Musical time
barintR/WBars since playback began. Assigning seeks
phasefloatRHow far through the current beat, 0.0 to 1.0
beats_per_barintRBeats in a bar
frames_to_beatintRFrames until the next beat
tempofloatR/WPlayback speed multiplier — see Tempo
bpmintRBeats 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:

PropertyTypeR/WDescription
indexintRThe channel’s number
volumefloatR/WGain 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:

FormatChannels
Tracker moduleOne per channel of the module (a typical .mod has 4)
MIDIAlways 16, one per MIDI channel. A channel the file does not use is simply silent
OGGNone: 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()
PropertyMeaning
beatWhole 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
barWhole bars since playback began, also monotonic
phasePosition 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_barBeats in a bar
frames_to_beatFrames 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:

Formatbeat, bar, phase, frames_to_beat, beats_per_bar, bpm
Tracker moduleAvailable. See below
MIDIAvailable, read from the file. See below
OGGNot 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.