Pygame mixer only plays one sound at a time - python

Here is my code:
pygame.mixer.init(frequency=22050,size=-16,channels=4)
sound1 = pygame.mixer.Sound('sound1.wav')
sound2 = pygame.mixer.Sound('sound2.wav')
chan1 = pygame.mixer.find_channel()
chan2 = pygame.mixer.find_channel()
chan1.queue(sound1)
chan2.queue(sound2)
time.sleep(10)
I would think it would play sound1 and sound2 simultaneously (queue is non-blocking and the code immediately hits the sleep).
Instead, it plays sound1 and then plays sound2 when sound1 is finished.
I've confirmed both channels are distinct objects in memory so find_channel isn't returning the same channel. Is there something I'm missing or does pygame not handle this?

See Pygame Docs, It says:
Channel.queue - queue a Sound object to follow the current
So, even though your tracks are playing on different channels, so if you force each sound to play, they will play simultaneously.
And for playing multiple sounds:
Open all sound files, and add the mixer.Sound object to a list.
The loop through the list, and start all the sounds.. using sound.play
This forces all the sounds to play simultaneously.
Also, make sure that you have enough empty channels to play all sounds, or else, some or the other sound sound will be interrupted.
So in code:
sound_files = [...] # your files
sounds = [pygame.mixer.Sound(f) for f in sound_files]
for s in sounds:
s.play()
You could also create a new Channel or use find_channel() for each sound..
sound_files = [...] # your files
sounds = [pygame.mixer.Sound(f) for f in sound_files]
for s in sounds:
pygame.mixer.find_channel().play(s)

The only thing i can think of is that chan1 and chan2 are same, even though they are different objects, they can be pointing to the same channel.
Try queueing right after getting a channel, that way you are sure to get a different channel with find_channel(), since find_channel() always returns a non-busy channel.
Try this:
pygame.mixer.init(frequency=22050,size=-16,channels=4)
sound1 = pygame.mixer.Sound('sound1.wav')
sound2 = pygame.mixer.Sound('sound2.wav')
chan1 = pygame.mixer.find_channel()
chan1.queue(sound1)
chan2 = pygame.mixer.find_channel()
chan2.queue(sound2)
time.sleep(10)

Related

Why does pygame.mixer.Sound().play() return None?

According to the pygame documentation, pygame.mixer.Sound().play() should return a Channel object. It actually does.
But sometimes, it seems to return None because the very next line, I get this error:
NoneType has no attribute set_volume
when I try to type
channel = music.play(-1)
channel.set_volume(0.5)
Of course the error can happen because the sound is very short, but the error can't come from there (Is 5'38" shorter than the time python needs to shift from one line to the next?)
I also Ctrl+H all the code to see if I typed somewhere channel = None (because I use multiple threads) - Nothing.
Does anybody had the same problem? Is that a pygame bug?
I use python 3.8.2, pygame 2.0.1 and Windows.
Currently I bypass the error rather than fix it like that:
channel = None
while channel is None:
channel = music.play()
channel.set_volume(0.5)
But... it doesn't seem to help very much: the game freezes, because pygame constantly returns None.
Sound.play() returns None if it can't find a channel to play the sound on, so you have to check the return value. Using the while loop is obviously a bad idea.
Note that you can either set the volumne of not only an entire Channel, but also on a Sound object, too. So you could set the volume of your music sound before trying to play it:
music.set_volume(0.5)
music.play()
If you want to make sure that Sound is played, you should get a Channel before and use that Channel to play that Sound, something like this:
# at the start of your game
# ensure there's always one channel that is never picked automatically
pygame.mixer.set_reserved(1)
...
# create your Sound and set the volume
music = pygame.mixer.Sound(...)
music.set_volume(0.5)
music.play()
# get a channel to play the sound
channel = pygame.mixer.find_channel(True) # should always find a channel
channel.play(music)

Tkinter and waiting

I am making a mp3 player using python 3 and Tkinter, using pygame.mixer for the MP3s. When you input a directory and press play it auto finds all the MP3s in the directory (using glob) and adds them to a list. I use a for loop to load then play the music, but when it plays it starts the next song immediately and automatically repeats till the end of the list where it plays only that song. I tried using time.sleep() which works but freezes the program. How can I wait till the song is over to start the next one and not freeze the program.
My current code (just the function to play the music):
def play():
print("playing")
global playing
playing = True
direct = directorye.get()
direct = direct.lower()
if direct.endswith(".mp3"):
mixer.music.load(directorye.get())
mixer.music.play()
else:
dire = r"C:\Users\agweb\Downloads\Music" #for the time being I just hard coded the directory
mp3s = glob.glob(dire + r"\*.MP3") # adds all MP3s in directory to list
#mp3s =+ glob.glob(directorye.get() + "*.MP3")
for i in mp3s:
print(i)
mixer.music.load(i)
mixer.music.play()
audio = MP3(i)
time.sleep(audio.info.length) #works but freezes program
There are several approaches possible.
Since tkinter is thread-safeish(*) on Python 3, probably the easiest way is to start a slightly modified version of your play function as a second thread.
In the global context, create a threading.Event. I would suggest the name stop_playing.
In the play function, only sleep for a little interval at a time, checking if stop_playing is set:
for i in mp3s:
mixer.music.load(i)
mixer.music.play()
audio = MP3(i)
playtime = 0
interval = 100 # ms
while playtime < audio.info.length:
time.sleep(interval)
playtime += interval
if stop_playing.is_set():
# TODO: stop the current playback.
# Then exit the thread.
return
The new callback for the "Play" button should create a thread for play() and start() it.
The callback for the "Stop" button should simply call stop_playing.set(). That will cause the playing thread to stop.
(* Provided it uses a tcl compiled with threading enabled.)
This solution work you can test it
from threading import Thread
import pygame
from itertools import cycle
def put_next_music_inside(loop_on_musics, stop_event):
for cur_music in loop_on_musics:
while pygame.mixer.music.peek(stop_event):
pass
pygame.event.get(stop_event)
pygame.mixer.music.load(cur_music)
pygame.mixer.music.play()
pygame.mixer.init()
pygame.display.init()
END_OF_MUSIC_EVENT = -100
pygame.mixer.music.set_endevent(END_OF_MUSIC_EVENT)
list_of_music = []#fill it with musics'path
first_music = list_of_music[0]
del list_of_music[0]
list_of_music.append(first_music)
loop_on_musics = cycle(list_of_music)
t = Thread(target=put_next_music_inside, args=(loop_on_musics, END_OF_MUSIC_EVENT))
t.start()
pygame.mixer.music.load(first_music)
pygame.mixer.music.play()

how to use pyglet to play sound in a mayavi animation?

I want to play sound using pyglet in a mayavi animation loop, but I found that pyglet worked not well with 'yield', which has to use in a mayavi animation. The situation is that, it just can't start a new loop when the sound played and animation done once, here are some of my codes, any ideas?
The pyglet can play sound in a for-loop, but can't use yield.
#mlab.animate(delay=delays)
def animate():
f = mlab.gcf()
while True:
for i in range(frames_num):
# update sound
sound = 'shiping/shiping_%d.wav'%i
sound_adjust = pyglet.resource.media(sound, streaming=False)
sound_adjust.play()
# update scene
print('Update scene >>', time.time())
function_to_update_scene()
# with out 'yield' it works well
yield
animate()
Any other modules suggest can also be accepted. The matter is that I need to update sound quickly within 20ms.
I at last solved this by using winsound module. Using
winsound.PlaySound(sound, winsound.SND_FILENAME | winsound.SND_ASYNC)
to replace
sound_adjust = pyglet.resource.media(sound, streaming=False)
sound_adjust.play()
to play defined sound asynchronously. Off course you have to import winsound at the very beginning.

How would I be able to pan a sound in pygame?

Basically, what I want to accomplish here, is a way to pan the sound while the game is running. I'd like to make the volumes (left and right) change depending on the position of the player. For now I've got a simple code that I thought that would be a good test:
pygame.mixer.init()
self.sound = pygame.mixer.Sound("System Of A Down - DDevil #06.mp3")
print("I could get here")
self.music = self.sound.play()
self.music.set_volume(1.0, 0)
First, I tried something similar but with pygame.mixer.music, but I came to realize that there's no way to change the volumes separately this way, or so I think, then I changed to the code presented here.
Now it seems like the file is not able to be loaded, my guess is that the file is too big to be treated as a sound in pygame. Any idea of how I'd be able to make this work?
You can pan on the Channel like this:
from os import split, join
import pygame
import pygame.examples.aliens
pygame.init()
# get path to an example punch.wav file that comes with pygame.
sound_path = join(split(pygame.examples.aliens.__file__)[0], 'data', 'punch.wav')
sound = pygame.mixer.Sound(sound_path)
# mixer can mix several sounds together at once.
# Find a free channel to play the sound on.
channel = pygame.mixer.find_channel()
# pan volume full loudness on the left, and silent on right.
channel.set_volume(1.0, 0.0)
channel.play(sound)
https://www.pygame.org/docs/ref/mixer.html#pygame.mixer.Channel
It may be worth looking into a seperate audio library for this. In general i'd recommend PortAudio (which is C), but using the python bindings for it provided by PyAudio. This will give you much more control over the exact audio stream.
To simplify this even further, there is a library known as PyDub which is built ontop of PyAudio for a high level interface (it even has a specific pan method!).
from pydub import AudioSegment
from pydub.playback import play
backgroundMusic = AudioSegment.from_wav("music.wav")
# pan the audio 15% to the right
panned_right = backgroundMusic.pan(+0.15)
# pan the audio 50% to the left
panned_left = backgroundMusic.pan(-0.50)
#Play audio
while True:
try:
play(panned_left)
#play(panned_right)
If this is too slow or doesn't provide effective real-time implementations then I would definately give PyAudio a go because you will also learn a lot more about audio processing in the process!
PS. if you do use PyAudio make sure to check out the callback techniques so that the game you are running can continue to run in parallel using different threads.
This answer certainly can help you.
Basically, you get the screen width, then you pan the left according to player.pos.x / screen_width, and the right according to 1 - player.pos.y / screen_width like that:
Pseudocode:
channel = music.play() # we will pan the music through the use of a channel
screen_width = screen.get_surface().get_width()
[main loop]:
right = player.pos.x / screen_width
left = player.pos.x / screen_width
channel.set_volume(left, right)
Documentation:
pygame.mixer.Channel
pygame.mixer.Sound
pygame.mixer.Channel.set_volume

Music player for Python not working

I want to be able to play multiple songs through a playlist using python, but it will only play the last song on the list. Please help.
from pygame import mixer # Load the required library
from os import listdir
k = listdir('C:/LOCAL')
print(k)
mixer.init()
for x in k:
y = "C:/LOCAL/" + x
print y
mixer.music.queue(y)
mixer.music.load(y)
mixer.music.play()
Your problem is that you assume that playing music with pygame will pause the program until the music is finished - which is not the case. As a result, it tries starting a song, and then it's starting another, and another, etc.
There are a few ways of trying to correct this. You could either:
Use pygame events and "tell" pygame to fire an event when the song finishes (though this requires a display surface (window) to be opened within pygame), or
Detect the length of the song, and sleep for that amount of time (which is more compatible with your current code).
I'm going to assume that you would like to do option 2, since your code works better with it.
To get the length of an MP3 file (I've not tried it with any other types), you could use the Mutagen library.
Some example code to get the length of an MP3 file (in seconds):
from mutagen.mp3 import MP3
tracklength = MP3("/path/to/song.mp3").info.length
Then you could substitute the path with y, and time.sleep for the amount of time returned, before continuing to the next iteration of the loop.
Hope that helps.
(also, you don't need to queue a file before loading it - just load and play)

Categories

Resources