Open qwertyquerty opened 1 year ago
Can you provide a minimal code example?
import pygame as pg
pg.mixer.init()
screen = pg.display.set_mode((100, 100))
sfx = pg.mixer.Sound("sound.wav")
channel = pg.mixer.Channel(1)
channel.set_source_location(90, 200)
channel.play(sfx)
channel.queue(sfx)
while True:
pass
Here's the workaround I was thinking about from discord discussion:
import pygame
pygame.init()
sound = pygame.mixer.Sound(r"C:\Users\charl\Desktop\pygame-ce\examples\data\boom.wav")
sound2 = pygame.mixer.Sound(r"C:\Users\charl\Desktop\pygame-ce\examples\data\surfonasinewave.xm")
posteffect = pygame.mixer.Channel(-2)
posteffect.set_source_location(90,0)
sound.play()
while pygame.mixer.get_busy():
pass
sound2.play()
while pygame.mixer.get_busy():
pass
I had to edit the source to allow to me to make that negative channel, so here's a wheel for your system that will allow you to run it: pygame_ce-2.3.1.dev1-cp310-cp310-win_amd64.zip
This is because SDL_Mixer clears all effects on a channel when a sound finishes playing, and since Mix_SetPosition
uses the same API as applying any old effect, it gets cleared too. You don't even need to queue anything, just playing two sounds one after the other on the same channel has the same effect.
We have two options here:
test code:
import pygame
import time
pygame.mixer.init()
sfx = pygame.mixer.Sound("examples/data/car_door.wav")
channel = pygame.mixer.Channel(1)
channel.set_source_location(90, 200)
channel.play(sfx)
time.sleep(1)
channel.play(sfx) # plays normally, without position or distance
time.sleep(1)
Having the same issue. This bug makes it impossible to smoothly queue sounds with custom source locations. The bug doesn't affect .play(-1)
, which means that it can be used as a workaround for now in cases where a sound is just supposed to loop.
Had a look at this, and while I fixed it for .play
, there is no easy way to fix it for queued sounds (that I could find anyway). Changes here (I'd clean this up a bit before pr, but pushed it if anyone wants to see).
The issue comes from the fact that after a channel stops playing, it clears all effects applied to the channel, this includes Mix_SetPosition
. My workaround was to apply it every time it started playing, which worked nicely, for calling .play
one after another.
The reason this does not work for queued sounds is because a queued sounds starts playing from the Mix_ChannelFinished
callback. This gets called when a channel finishes playback, but before clearing effects, meaning whatever effects we set in this callback is instantly cleared (this is literally the two things mixer does when a channel stops: callback then clear effects).
There are some solutions, but all of them probably need some more effort than I'd like it to, and/or more hacky than I'd like it to.
Change it so that channels don't point to a specific SDL_Mixer channel, then we can find a free channel to play on. We would probably also need to reserve one extra, so that if all the channels are playing, there is still an extra left
Play a short, silent sound on an extra channel that ends really quickly that sets the effect. There could be some short delay between the sound playing and the effect applying. (Also, not sure how taxing this is for SDL_Mixer)
Play a short, silent sound on an extra channel that ends really quickly that plays the queued sound. Queued sound plays with effect applied instantly, but maybe there is a bit of delay on playing the queued sound. (Also, not sure how taxing this is for SDL_Mixer)
We just don't fix the issue. But I think that is the worst option of all.
I think the best solution here would be the first, but I'd like some opinion on which one we should go with, or if anyone has a better solution.
Here's a demonstration of a Python class (using channel subclassing and endevents) to create the desired behavior.
"""
Example program showing the use of Channel subclassing to make effects
(in this case, source location) persist between plays and queued plays
of sound effects.
Press 1: channel.play() the sound
Press 2: channel.queue() the sound
"""
import os
import pygame
pygame.init()
# Change the path to work with sound on your system
os.chdir(r"C:\Users\charl\Desktop\pygame-ce\examples\data")
sfx = pygame.mixer.Sound("boom.wav")
screen = pygame.display.set_mode((800, 600))
class LocationChannel(pygame.Channel):
END_EVENT_TYPE = pygame.event.custom_type()
def __init__(self, id: int):
self._angle_dist = None
self._queued = None
super().__init__(id)
super().set_endevent(self.END_EVENT_TYPE)
def set_source_location(self, angle: float, distance: float) -> None:
self._angle_dist = (angle, distance)
super().set_source_location(angle, distance)
def queue(self, sound: pygame.mixer.Sound) -> None:
if not self.get_busy():
self.play(sound)
else:
self._queued = sound
def play(
self,
sound: pygame.mixer.Sound,
loops: int = 0,
maxtime: int = 0,
fade_ms: int = 0,
) -> None:
if self._angle_dist:
super().set_source_location(*self._angle_dist)
super().play(sound, loops, maxtime, fade_ms)
def handle_events(self, event: pygame.Event) -> None:
if event.type == self.END_EVENT_TYPE:
if self._queued:
self.play(self._queued)
self._queued = None
channel = LocationChannel(1)
channel.set_source_location(90, 200)
clock = pygame.time.Clock()
while True:
screen.fill("purple")
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
raise SystemExit
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
pygame.quit()
raise SystemExit
if event.type == pygame.KEYDOWN and event.key == pygame.K_1:
print("channel.play(sfx)")
channel.play(sfx)
if event.type == pygame.KEYDOWN and event.key == pygame.K_2:
print("channel.queue(sfx)")
channel.queue(sfx)
channel.handle_events(event)
pygame.display.flip()
clock.tick(144)
I'm taking a look at this again because of the upcoming 2.5 release.
Thanks for the research @zoldalma999, I couldn't find an easy way around it either.
I do have a new idea thought, we could do an internal event and have a special case in event.c to catch it?
However, I'm leaning towards calling this a wontfix. If this is how they expect SDL mixer effects to behave, let's not put in a bunch of effort to fight it. This is how channel.set_volume works too, it's documented as "This only affects the current sound."
Although channel.set_volume does seem to be persistent if you only set 1 number (not doing panning), ugh...
So I think we should resolve this by adding a note to the docs for set_source_location that it only impacts the current sound.
Environment:
You can get some of this info from the text that pops up in the console when you run a pygame program.
Current behavior:
Any location of a channel set by set_source_location is reset to 0, 0 whenever a queued sound plays on a channel
Expected behavior:
The source location should remain the same