Events, Listeners und Callbacks in Python

Sempervivum

Erfahrenes Mitglied
Hallo, ich mache gerade erste Gehversuche in Python und bin ein wenig irritiert, dass ich beim Eventlistening gar keine Callbacks finde, so wie ich sie von Javascript her kenne. Statt dessen muss ich die Eventqueue zyklisch pollen. Konkret geht es um einen MP3-Player mit Playlist mit pygame. Ich muss dort das ended-Event beobachten, um in der Playlist auf das nächste Lied weiter zu schalten. Ich habe es so gelöst, dass ich das Polling mit einer while-Schleife mache und diese in einem extra Thread laufen lasse, damit sie das Skript nicht blockiert. Timer gibt es anscheinend auch aber ich finde nichts, das wirklich ereignisgesteuert mit einem Callback arbeitet, nur Hinweise auf Bibliotheken. Ist das wirklich so, dass es das nativ nicht gibt oder übersehe ich noch etwas?
Beste Grüße - Ulrich
 

Technipion

Erfahrenes Mitglied
Hey,
bin mal kurz in die Dokumentation von PyGame abgetaucht und habe das hier herausgesucht:
pygame.mixer — pygame v2.0.1.dev1 documentation
pygame.mixer — pygame v2.0.1.dev1 documentation

Wenn ich das richtig verstehe wird von der .play()-Funktion ein Channel zurückgegeben. Auf dem kannst du dann set_endevent() aufrufen um dich informieren zu lassen, sobald der Sound fertiggespielt hat (oder unterbrochen wurde).

Gruß Technipion


Okay, streich das alles.
Ich muss dort das ended-Event beobachten, um in der Playlist auf das nächste Lied weiter zu schalten
Eben erst gesehen. Mea culpa :rolleyes:

Tatsächlich basiert PyGame auf der Idee einer Hauptschleife, denn es ist als Game-Engine gedacht (surprise!) und die funktionieren im Prinzip alle so.
Wie du diesem Tutorial entnehmen kannst, sollte das ganze so aussehen:
Python:
while True:
    pygame.display.update()
    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            sys.exit()

Aber die Hauptschleife soll ja gerade das Python-Programm blockieren. Was machst du denn nebenbei, weshalb dich das Blocking stört?
 
Zuletzt bearbeitet:

Sempervivum

Erfahrenes Mitglied
Danke für diese Erklärung. Ohne das Tutorial gelesen zu haben, hatte ich es auf Grund eines anderen Beispiels fast genau so implementiert:
Code:
    def watch():
        global playlist, idx
        # Event fuer "music end" vorbereiten:
        MUSIC_END = pygame.USEREVENT+1
        pygame.mixer.music.set_endevent(MUSIC_END)
        running = True
        while running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False
                if event.type == MUSIC_END:
                    # Das Lied ist beendet, Index erhoehen:
                    idx = idx+1
                    if (idx >= len(playlist)):
                        idx=0
                    # Naechstes Lied laden und starten:
                    pygame.mixer.music.load(playlist[idx])
                    pygame.mixer.music.play()
                    
    idx = 3
    inited = False
    pygame.init()
    pygame.mixer.init()
    playlist = glob.glob('pfad/*.mp3')

    # Events in neuem Thread beobachten:
    _thread.start_new_thread(watch, ())
Aber die Hauptschleife soll ja gerade das Python-Programm blockieren. Was machst du denn nebenbei, weshalb dich das Blocking stört?
Meine Anwendung ist nicht primär ein Spiel sondern ein Microcontroller auf Raspi-Basis, der alles mögliche steuert und ausliest. Das pygame wird lediglich nebenbei als MP3-Player eingesetzt bzw. zweckentfremdet, daher muss alles andere parallel funktionieren.

Jetzt habe ich auch bemerkt, dass ich das insofern missverstanden hatte, dass diese Art des Eventhandling generell für Python gilt, aber das trifft nicht zu sondern offenbar ist es eine Lösung speziell für dieses pygame und macht dort auch Sinn.

Damit sehe ich das Ganze jetzt etwas klarer, vielen Dank für die Erklärungen.
 

Technipion

Erfahrenes Mitglied
Fand die Idee jetzt doch ganz interessant. So als kleine Fingerübung zum Thema Threading habe ich mich auch mal an einer Implementierung versucht:
Python:
# Play audio file and go back to do something else

import pygame
import time, queue
import itertools
from pygame.locals import *
from threading import Thread


play_queue = queue.Queue()
finished_queue = queue.Queue()


def main():
    gthread = Thread(target=game_thread)
    gthread.start()
   
    playlist = ['song1.ogg',
                'song2.ogg',
                'song3.ogg']
   
    playlist = itertools.cycle(playlist)
   
    song = next(playlist)
    play_queue.put(song)
   
    while True:
        time.sleep(1)
       
        if not finished_queue.empty():
            elem = finished_queue.get()
           
            if not isinstance(elem, str):
                break
           
            print(elem, 'has finished playing.')
            finished_queue.task_done()
           
            song = next(playlist)
            play_queue.put(song)
   
    play_queue.put(None) # tell gthread to quit
    gthread.join()


def game_thread():
    pygame.init()
   
    FPS = 30
    FramePerSec = pygame.time.Clock()
   
    DISPLAYSURF= pygame.display.set_mode((300, 300))
    DISPLAYSURF.fill((128, 128, 128))
   
    pygame.display.set_caption('MP3 Player')
   
    MUSIC_END = pygame.USEREVENT + 1
    pygame.mixer.music.set_endevent(MUSIC_END)
   
    running = True
    song = None
   
    while running:
        pygame.display.update()
        FramePerSec.tick(FPS)
       
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                running = False
            if event.type == MUSIC_END:
                finished_queue.put(song)
       
        if not play_queue.empty():
            song = play_queue.get()
            play_queue.task_done()
           
            if isinstance(song, str):
                try:
                    pygame.mixer.music.load(song)
                except pygame.error:
                    # file could not be opened (or decoded)
                    finished_queue.put(song)
               
                pygame.mixer.music.play()
                print(f'playing {song}...')
            else:
                pygame.quit()
                running = False
   
    finished_queue.put(None) # tell main thread I quit
    print('pygame stopped.')


if __name__ == '__main__':
    # this will keep other instances spawned by multiprocessing
    # from unwantedly running the main program
    main()

Über die Queues können die Threads ganz gut miteinander kommunizieren. Der Hauptthread könnte hier alles Mögliche machen, solange er nur ab und zu prüft, ob ein neues Lied aufgelegt werden muss. Oder aber er kann steuernd eingreifen und einfach ein neues Lied auflegen.

Man könnte das natürlich noch verfeinern. Es spricht z.B. nichts dagegen, evtl. Exceptions die auf der pygame-Seite auftreten durch die Queue zu senden und im Hauptthread auszuwerten. Im Moment ist es nur ein primitiver Listenabspieler.

Ah und ich würde zum Testen relativ kurze Musikstückchen nehmen ;)

Bin gespannt auf deine Meinung dazu.
Gruß Technipion