pyxen.Music

Une piste audio en streaming attachée à une Entity en tant que composant.

La musique est décodée à la demande, ce qui la rend adaptée aux pistes longues comme la musique de fond et l’ambiance.

Pour les concepts audio, consultez Audio.


Constructeur

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

filename est l’unique argument positionnel : le nom d’un asset musical du projet, sans son extension de fichier. Les trois autres sont réservés aux mots-clés (keyword-only) et fixent la valeur de départ de la propriété du même nom (voir Propriétés du composant).


Paramètres

ParamètreTypeDescription
filenamestrNom de l’asset musical, sans extension
volumefloatGain de départ. 1.0 par défaut
panfloatPosition stéréo de départ, de -1.0 à 1.0. 0.0 par défaut
loopboolRecommencer à la fin de la piste. False par défaut

Exemple

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

Attacher une Music

Lors du spawn :

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

Ou plus tard :

entity.music = Music("theme")

L’assignation d’une musique lance la lecture immédiatement.


Propriétés du composant

Accès via entity.music :

PropriétéTypeL/EDescription
loopboolL/EActiver la lecture en boucle
volumefloatL/EGain (minimum 0.0)
panfloatL/EPanoramique stéréo (-1.0 à 1.0)
timefloatLPosition de lecture actuelle (secondes)
durationfloatLDurée totale (secondes)
finishedboolLTrue lorsque la lecture est terminée
channelslisteLLes parties de la piste, chacune avec son propre volume — voir la section Canaux
beatintL/ETemps écoulés depuis le début de la lecture. L’assignation déplace la lecture — voir Temps musical
barintL/EMesures écoulées depuis le début de la lecture. L’assignation déplace la lecture
phasefloatLAvancement dans le temps courant, de 0.0 à 1.0
beats_per_barintLNombre de temps dans une mesure
frames_to_beatintLNombre de frames avant le prochain temps
tempofloatL/EMultiplicateur de vitesse de lecture — voir Tempo
bpmintLBattements par minute, tels que le fichier les indique

loop

entity.music.loop = True

Lorsqu’activé, la piste redémarre automatiquement après la fin de la lecture. Usage typique pour la musique de fond.


volume

entity.music.volume = 0.5

Les valeurs inférieures à 0.0 sont limitées à 0.0.


pan

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

Limité à la plage -1.0 à 1.0.


time

t = entity.music.time

Lecture seule. Position de lecture actuelle en secondes.


duration

d = entity.music.duration

Lecture seule. Durée totale de la piste en secondes.


finished

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

Lecture seule. True lorsque la piste a fini de jouer (et que loop est False).


Canaux

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

music.channels[1].volume = 0.0    # couper la partie 1

Un module tracker ou un fichier MIDI est un ensemble de parties indépendantes jouées à partir d’une seule horloge. music.channels donne une poignée par partie, et chacune a :

PropriétéTypeL/EDescription
indexintLLe numéro du canal
volumefloatL/EGain de cette partie seule, limité à 0.0–1.0

Chaque canal démarre à 1.0. Comme toutes les parties partagent une seule tête de lecture, en baisser ou en monter une ne dérive jamais par rapport aux autres. C’est le moyen de faire évoluer l’intensité d’une bande-son : faire entrer une partie quand l’action s’accélère, la couper quand le joueur meurt.

Ce qu’est un canal dépend du format :

FormatCanaux
Module trackerUn par canal du module (un .mod typique en a 4)
MIDIToujours 16, un par canal MIDI. Un canal que le fichier n’utilise pas reste simplement silencieux
OGGAucun : len(music.channels) vaut 0

La numérotation commence à 0. Demander un canal au-delà du dernier lève IndexError, et on ne peut pas assigner music.channels[i] lui-même, seulement son volume. Un indice négatif compte depuis la fin, comme dans toute liste Python : music.channels[-1] est le dernier canal.

En MIDI, mettre un canal à 0.0 coupe aussi les notes de ce canal qui résonnent encore.


Temps musical

if music.beat != last_beat:
    last_beat = music.beat
    pulse()
PropriétéSignification
beatTemps entiers écoulés depuis le début de la lecture. Il est monotone : il continue de croître à travers une boucle au lieu de revenir à 0, ce qui permet de le comparer à sa valeur de la frame précédente sans traiter le retour au début
barMesures entières écoulées depuis le début de la lecture, également monotone
phasePosition dans le temps courant, de 0.0 jusqu’à (non inclus) 1.0. Là où beat change par paliers, phase est continue : utilisez-la pour tout ce qui doit s’adoucir ou décroître entre deux temps
beats_per_barNombre de temps dans une mesure
frames_to_beatNombre de frames avant que le prochain temps arrive, arrondi à l’entier supérieur. Servez-vous-en pour lancer quelque chose sur le temps plutôt que juste après

Assigner beat ou bar déplace la lecture. music.bar = 8 saute au début de la mesure 8 en une seule écriture, tous les canaux restant synchronisés. La frame suivante relit la nouvelle position.

Les compteurs mesurent le temps à partir du fichier ; le format compte donc :

Formatbeat, bar, phase, frames_to_beat, beats_per_bar, bpm
Module trackerDisponibles. Voir ci-dessous
MIDIDisponibles, lus dans le fichier. Voir plus bas
OGGIndisponibles : beat, bar, beats_per_bar, frames_to_beat et bpm valent -1, phase vaut 0.0, et assigner beat ou bar ne fait rien

Un jeu qui se synchronise sur une musique qui peut être en OGG doit donc vérifier une fois que music.beat >= 0 et se rabattre sur son propre minuteur quand la valeur est -1.

Les temps d’un fichier MIDI

Un fichier MIDI indique lui-même son temps musical, et Pyxen le lit. Un temps est la noire du fichier, bpm et la durée d’un temps suivent les changements de tempo du fichier au fil de la lecture, et beats_per_bar vient de la première signature rythmique du fichier, comptée en noires : 3/4 donne 3, et 6/8 aussi. Un fichier qui n’indique aucune signature rythmique est à quatre temps.

Quand une piste MIDI boucle, le compte passe à la mesure entière suivante : beat et bar continuent d’augmenter, et chaque passage commence sur le premier temps d’une mesure. Un saut tombe exactement sur le temps ; une note qui sonnait déjà avant ce point n’est pas jouée.

Les temps dans un module tracker

Un module n’indique pas de métrique, Pyxen utilise donc la seule définition qu’un module porte : son BPM. Un tick de tracker dure 2.5 / BPM secondes, ce qui rend le BPM du fichier exact précisément quand un temps vaut 24 ticks : quatre lignes à la vitesse 6, ou huit lignes à la vitesse 3. C’est ainsi que Pyxen compte. Compter des ticks plutôt que des lignes garde le temps stable quand le module change de vitesse.

Une mesure compte quatre temps. C’est une convention, pas une donnée : un module en 3/4 indiquera quand même beats_per_bar à 4. Si votre morceau est écrit avec un autre nombre de ticks par temps, le compteur ne correspondra pas à ce que vous entendez.


Tempo

music.tempo = 1.5    # 50 % plus vite, même hauteur
music.tempo = 1.0    # normal

tempo multiplie la vitesse que la piste demande. Rien n’est rééchantillonné, donc la hauteur ne change pas : les parties arrivent simplement plus tôt ou plus tard. bpm continue d’indiquer la valeur propre du fichier quel que soit tempo.

Limites : 8.0 au maximum. Dans le bas, un module tracker ne descend pas sous 0.25 et une valeur qu’il refuse est ignorée (la propriété garde sa valeur précédente) ; le MIDI accepte jusqu’à 0.05. tempo n’a aucun effet sur l’OGG, où il vaut toujours 1.0.

beat, phase et frames_to_beat suivent tempo : un jeu synchronisé sur la musique le reste quand vous l’accélérez.


Exemples complets

Une pulsation à chaque temps

Cet exemple suppose un module tracker nommé theme.mod dans le projet.

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 va de 0 à 1 dans chaque temps : le cercle est grand sur le temps, puis se calme.
    s = 1.0 + 0.5 * (1.0 - music.phase)
    pulse.scale = (s, s)

    # beat ne change qu'une fois par temps.
    if music.beat != last_beat:
        last_beat = music.beat
        print("beat", last_beat, "bar", music.bar)

Couper une partie

Cliquez pour couper puis rétablir la deuxième partie de la piste.

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

Sauter à une mesure

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      # sauter directement à la mesure 4

Format

Un asset musical peut être un fichier OGG Vorbis (.ogg), MIDI (.mid, .midi), ou un module tracker — ProTracker (.mod), FastTracker II (.xm), Impulse Tracker (.it) ou Scream Tracker 3 (.s3m).

L’asset est recherché par nom de base : Music("theme") trouve theme.ogg, theme.mod, ou celle de ces extensions qui existe dans le projet. Passer l’extension est l’erreur la plus fréquente : Music("theme.ogg") cherche theme.ogg.ogg et lève une exception.

Un module tracker pèse souvent quelques kilo-octets là où le même morceau en OGG en pèse plusieurs méga-octets, et il boucle parfaitement par construction — idéal pour un petit jeu.

C’est aussi le format qui offre le plus ici : canaux, temps, mesures et tempo fonctionnent tous sur un module. Le MIDI a les canaux et le tempo ; l’OGG n’a ni l’un ni l’autre (voir les sections Canaux et Temps musical).

Les modules respectent le panoramique : la commande 8xx de ProTracker place un canal dans le champ stéréo pendant que le morceau joue.

Contrairement à Sound (WAV, chargé entièrement en mémoire), Music est streamé et décodé à la demande — mieux adapté pour les fichiers audio longs.


Erreurs courantes

Nom de fichier manquant

Music()
ValueError: Music must have a file

Asset invalide

Music("nonexistent")
Music("theme.ogg")     # l'extension est sur le fichier, pas dans le nom
ValueError: Music 'nonexistent' not found. Use the asset name without its extension (.ogg, .mid, .mod, .xm, .it, .s3m).