pygame-community / pygame-ce

🐍🎮 pygame - Community Edition is a FOSS Python library for multimedia applications (like games). Built on top of the excellent SDL library.
https://pyga.me
825 stars 131 forks source link

Queuing multiple musics with ``pygame.mixer.music.Queue`` #3058

Open bilhox opened 4 weeks ago

bilhox commented 4 weeks ago

Hello, I want to introduce a new feature called pygame.mixer_music.Queue, which is, as its name says about it, an object for handling several queued music at once compared to his friend pygame.mixer_music.queue . What features it can propose for a pygame user ? :

Below a code example of how it can work :

import pygame

pygame.init()

music_list = ["music1.wav", "music2.wav", "music3.wav"]
music_queue = pygame.mixer.music.Queue(music_list=music_list) # music_list is a kwd arg, and its default value is []
music_queue.add("music4.wav")
music_queue.remove("music2.wav")
music_queue.play(-1)

while(music_queue.is_playing):
  for event in pygame.event.get():
    if event.type == pygame.KEYDOWN:
      if event.key == pygame.K_a: # Skip to the next music
        music_queue.next()
      if event.key == pygame.K_r: # Rewind the whole music queue
        music_queue.restart()
      if event.key == pygame.K_t: # Rewind the music currently playing only
        music_queue.rewind()
      if event.key == pygame.K_p: # Pause music
        music_queue.stop()
      if event.key == pygame.K_u: # Unpause music
        music_queue.resume()

stubs preview by @damusss :

class Queue:
    def __init__(self, filenames: Optional[List[Union[str, Tuple[str, int]]]] = None) -> None: ...
    @property
    def filenames(self) -> List[str]: ...
    @filenames.setter
    def filenames(self, v: list[List[Union[str, Tuple[str, int]]]]): ...
    @property
    def index(self) -> int: ...
    @index.setter
    def index(self, v: int): ...
    def add(self, filename: str, loops: int = 0) -> None: ...
    def remove(self, filename: str) -> None: ...
    def pop(self, index: int) -> str: ...
    def get_busy(self) -> bool: ...
    def get_current(self) -> int: ...
    def get_next(self) -> int: ...
    def play(self, loops: int = 0, fade_ms: int = 0, index: int = 0) -> None: ...
    def play_next(self, loop: bool = True) -> None: ...
    def play_previous(self, loop: bool = True) -> None: ...
    def play_at(self, index: int) -> None: ...
    def stop(self) -> None: ...
    def resume(self) -> None: ...
    def restart(self) -> None: ...

While this can be just a python implementation, pygame.Channel has already a similar system with pygame.Sound , so I highly believe this can be pygame feature. @damusss tried to test and try to make an implementation in python, but keeping track of the music playing would involve having an update method, which doesn't follow how pygame.mixer works.

I would like to hear the opinion of the steering council about this.

damusss commented 4 weeks ago

for the record, a full pythonic implementation would require the update method but the implementation i added in #3057 uses a bit of C to remove the need for it.

Starbuck5 commented 4 weeks ago

Ok, so it's got all these features, cool, but what does this actually do for a game? What's the use case?

Maybe it shouldn't be called a Queue, because this is not a generic data structure, this is a very specific object. And it seems weird to have remove() and indexing on a queue.

But I don't understand the use case, so names are hard to come up with. Album ?

Also this would need to support file like objects, so things shouldn't be called filenames and be exclusively lists of strings.

bigwhoopgames commented 4 weeks ago

I ended up writing something very similar for my game. Specifically because I wanted the functionality of Sound objects but for music. I wanted the ability to preload each track and set per track volume levels or make "groups" of tracks for certain playback properties. There were a couple of other reasons I ended up writing it but I do agree that the current mixer.Music is very basic and could use some gussying up. I simply called mine a music manager but a 'playlist' or 'tracklist' would work for names too.

bilhox commented 4 weeks ago

what does this actually do for a game? What's the use case?

It allows to handle and manage multiple musics at once, and most precisely the possibility to play musics like a queue. It can be used in a game to play a list of music.

But I don't understand the use case, so names are hard to come up with. Album ?

I do agree with this name, this feature requires discussion about the actual features it comes up with. This is why every feedback is appreciated.

Thanks for the feedback ❤️ @Starbuck5 .

JiffyRob commented 4 weeks ago

I had several potential use cases for my games, and ended up programming something similar myself on several occasions:

  1. A lot of looping soundtracks start with a small fanfare at the beginning before looping. Some games that come to mind which did this include Super Mario Bros (and sequels), Sonic the Hedgehog (and sequels), Legend of Zelda (and sequels), and likely others (I liked those games so much I didn't ever play much else). This Queue would allow you to play the fanfare, with the looping portion queued up to play automatically. This would save me a lot of work having to wait for an event to trigger the next track, or checking if pygame.mixer.music is busy every frame.
  2. I could also see potential performance gains from preloading all of the soundtracks at the beginning. Generally I see games do that with most other assets, so having a way to do that for music, especially big chunky wav files, would be consistent and helpful. This especially holds true when the music track needs to be changed quickly in real time. Cutscenes (especially if someone is trying to button mash through it really fast like I do half the time) or boss fights with multiple stages come to mind here. Also WASM generally wants you to load everything at the beginning, so a unified interface for that might be something that pygbag can take advantage of. Maybe talk to pmp-p about that...?

As for naming, I always looped the music functionality in the same object as the one that handled sound effects and called the whole thing a SoundManager.

No matter what features you add or don't add, I would highly recommend making it subclassable and extensible like pygame.sprite.Group. That way I can add more niche things like per-track fading myself without having to reinvent the wheel completely. That's kinda how I see this feature - a springboard for further advancement that's just a bit more useful and intuitive than bare pygame.mixer.music.

At least that's my two cents. I've been making games with pygame for about 5 years now and as a user that's what I would like to see, but all you pygame developers have other things like maintainability to worry about and I have little experience there.

EDIT: Wait mixer.music streams audio from the file. That invalidates a significant portion of point 2. It would still be nice to have an API for loading my music filenames when I load my other assets.

bilhox commented 4 weeks ago

Also WASM generally wants you to load everything at the beginning, so a unified interface for that might be something that pygbag can take advantage of. Maybe talk to pmp-p about that...?

@pmp-p This is something you might want to answer.

Starbuck5 commented 4 weeks ago

A lot of looping soundtracks start with a small fanfare at the beginning before looping. Some games that come to mind which did this include Super Mario Bros (and sequels), Sonic the Hedgehog (and sequels), Legend of Zelda (and sequels), and likely others (I liked those games so much I didn't ever play much else). This Queue would allow you to play the fanfare, with the looping portion queued up to play automatically. This would save me a lot of work having to wait for an event to trigger the next track, or checking if pygame.mixer.music is busy every frame.

This is possible already with pygame.mixer.music.queue. play() the fanfare and queue loops of the looped portion.

I could also see potential performance gains from preloading all of the soundtracks at the beginning. Generally I see games do that with most other assets, so having a way to do that for music, especially big chunky wav files, would be consistent and helpful. This especially holds true when the music track needs to be changed quickly in real time. Cutscenes (especially if someone is trying to button mash through it really fast like I do half the time) or boss fights with multiple stages come to mind here. Also WASM generally wants you to load everything at the beginning, so a unified interface for that might be something that pygbag can take advantage of. Maybe talk to pmp-p about that...?

The difference between music and sounds in pygame-ce is that music is streamed, so I don't see any performance gains from preloading here. And regardless, the current implementation doesn't stage the musics as Mix_Music anyways.

EDIT: I now see your edit about music being streamed,

It would still be nice to have an API for loading my music filenames when I load my other assets

What do you mean?

Starbuck5 commented 4 weeks ago

I ended up writing something very similar for my game. Specifically because I wanted the functionality of Sound objects but for music. I wanted the ability to preload each track and set per track volume levels or make "groups" of tracks for certain playback properties. There were a couple of other reasons I ended up writing it but I do agree that the current mixer.Music is very basic and could use some gussying up. I simply called mine a music manager but a 'playlist' or 'tracklist' would work for names too.

Volume and other playback properties aren't supported by this proposed API though. The only playback property supported seems to be loops. And the proposed API doesn't preload in any meaningful sense besides storing the filenames.

Starbuck5 commented 4 weeks ago

Talked to bilhox on discord about this, here's what I think would be an interesting use case for a feature like this:

"My game has multiple worlds / stages / levels whatever, and I want to compile some music for each to be ambient music for the player. I want an object that I can say "go" and it plays my music in a random order continuously, with configurable fade-in/fade-out and silent periods in between as not to jar players with sudden changes. Each time I switch levels I will transition to a new one of these objects."

I think finding use cases here is a meaningful exercise, because the proposed API does not work for my given use case.

JiffyRob commented 4 weeks ago

This is possible already with pygame.mixer.music.queue. play() the fanfare and queue loops of the looped portion.

🤦 I have been up and down the docs so many times and I never saw that. Thanks!

What do you mean?

This is just a code architecture thing. Generally surfaces, sounds, text files, and any other assets are all preloaded at the beginning of game, with a nice splash screen and stuff. In my head music is an asset too, and it would make sense to process them at the beginning. For streamed music currently I can't do any of that because I generally have to load stuff right before I play it, so I generally just ignore them in my asset loading code. My OCD would really like an object to make and play with upon game load, instead of messing around with filenames later in the code, so I generally make one, just to store the filenames, so that all path processing is in one place. I've refactored my assets handling enough times (pyinstaller and pygbag, I'm looking at you!) where I'm a bit paranoid about this.

Also I still tout cutscenes and boss fights where the music needs to change seamlessly and sometimes in rapid succession. Just having a function that makes the music one level more of scary, or adds another level of atmosphere, when the next stage starts would make developing that sort of stuff that much nicer.

I could also see alternate soundtracks for stuff being an interesting use case. What if I have two variants on a level sountrack, just to keep things interesting? Picking a random index out of a Queue would be very useful here.

pmp-p commented 4 weeks ago

@bilhox pygame.mixer.music is not decently useable on WASM(browser) because that part of SDL2 is hard realtime. Wasi runtimes don't have audio yet.

As a result in pyodide there's poor support for audio or none at all (worker).

In Pygbag pygame.mixer.music works fine but under the hood is re-implemented in javascript : it is already multichannel/multitracks because using browser audio threads and a track cache.

So i'm +1 for handling multitrack music since feature is already available.

Preloading is always better since it make smooth transitions and avoid I/O error at runtime.

sidenote : i would discourage people from using .mp3 explicitely for that new feature and promote .ogg usage. pygame-ce on pyodide is not assisted to convert mp3 at pack/preload.

aatle commented 4 weeks ago

Personally, I'm 50/50 on whether pygame should have this.

Any game with multiple tracks will probably need to implement a music manager or system. But implementing it is a bit annoying with the basic music API: set_endevent() and having to check the event queue, or checking get_busy(). For me, this would be the most important reason for having a class for this. A built-in, highly extensible class can make using pygame easier, like the sprite module. Its implementation may also leverage pygame internals to improve its ease of use.

However, it may be best to leave it to the user so they can write it to their needs, similar to implementing animations or the event loop. (Though, using a built-in class would be optional.) Pygame is like this for a lot of things, providing basics and not abstractions, which helps to avoid bloat.


I advocate for the class name Playlist or Tracklist because playlists are the closest thing to this class. I think Album is a little imprecise. Also, the term for individual pieces of music should probably be either "song" or "track", in the implementation.

I'm going to propose another design/API for this class to address problems in extensibility and ease of use.

Class outline: Goals: easy to use (intuitive, simple), extendable and customizable, can cover many use cases, fits with `music` module and can be implemented I omitted most of the reasons for this design, for conciseness. - Is an mutable, ordered, indexed list of tracks (list attribute `.tracks`). A track can appear multiple times. - A track can be *any* object as long as the class 'knows how' to play it (extendable). - Stores an `index` property, publicly mutable. This indicates the 'position' of the playlist, kind of like the current video in a youtube playlist. - Playlist can `play()` the song at the current index *only*. There are additional methods that both edit the index and `play()`, such as `play_next()`. - `pause()` and `unpause()` methods. - `end()` method to end the currently playing song. - More features not listed here Now, some features for automatically running through the playlist. Implemented by invoking a user-defined callback when the playing song ends. These features are optional to use. - `track_end` mutable attribute (or any name) which holds the callback. By default (from `__init__`), value is `self.play_next` method. - bool `running` mutable attribute: whether to invoke the callback or not when a song ends. Like 'autoplay' switch. - convenience methods `start()` and `stop()` the playlist, that both set `running` and `play()` or `end()`. Looks intuitive to me. Extendable by overriding `play_song()` to play custom objects, such as to add per-track fadeout or loop or volume. Customizable with `track_end` callback, such as to randomize index or add wait period.

Let me know if there are any issues or possible improvements with this design.

bilhox commented 3 weeks ago

Hello,

Following what we said on discord, I analysed more precisely the situation and I think personally it's not the thing we're actually in need. This is why I came across a better idea, what's actually stopping us from having a pygame.Music object. This would be a more pythonic implementation of how you handle musics, and it would allow you in your game if you want to play several musics with different settings (like playing offset, fadein, fadeout). I'm just giving the main ideas of this feature, I'll open a different issue when I'll properly shape the feature.

That said, pygame.mixer.music.Queue could be turned to an example that makes use of pygame.Music.