Python 84 - Sound!!
As one would hope, sound generation in pygame is pretty simple. I managed to work out the basics all by myself. Well, myself and the entire Internet.
Between the pygame.org docs and some other examples found on the Internet, I spiked in a Ship engine sound. This is not a good way to do it but it works:
class Game:
# noinspection PyAttributeOutsideInit
def init_pygame_and_display(self, testing):
if testing: return
pygame.init()
pygame.mixer.init()
pygame.display.set_caption("Asteroids")
self.clock = pygame.time.Clock()
self.screen = pygame.display.set_mode((u.SCREEN_SIZE, u.SCREEN_SIZE))
self.init_game_over()
self.init_score()
class Ship(Flyer):
thrust_sound = None
def power_on(self, dt):
self._accelerating = True
if not Ship.thrust_sound:
Ship.thrust_sound = pygame.mixer.Sound("sounds/thrust.wav")
Ship.thrust_sound.set_volume(0.1)
pygame.mixer.Sound.play(Ship.thrust_sound)
accel = dt * self._acceleration.rotate(-self._angle)
self.accelerate_by(accel)
I added the file thrust.wav
to a new folder in the build, sounds
, and there we are with a sound which I hesitate to call “thrust” right here among the gentility.
The result is good. Unfortunately for us this morning, QuickTime cannot record the machine’s own sounds, so a demonstration with sound will have to wait until I install some software. We’ll just concern ourselves this morning with the code.
The case in hand is simple, in that the thrust.wav file is very brief, so that playing it each time around the loop makes for a decent sound in this case. I’m not sure whether a second immediate call to play
, as we’re doing here, restarts the sound or has no effect if it is still playing.
- Added in Post
- Neither. It plays it again and again until it runs out of channels.
Pygame’s mixer
automatically runs multiple channels of sounds, so that it should be pretty reasonable to make a firing sound while also, um, accelerating, and so on.
It appears from the docs that when you create the sound with pygame.mixer.Sound(...)
, the file is loaded at that time. Music, apparently has explicit load
and unload
, probably because the files are larger. We won’t be using music according to our current plans.
Design
The spike shown above is pretty weak. Yes, it’ll be OK to init the mixer where we init pygame. But surely we’ll want to centralize the definition and operation of sounds. One issue with the code above is that it has broken a test:
self = <ship.Ship object at 0x11049b610>, dt = 0.5
def power_on(self, dt):
self._accelerating = True
if not Ship.thrust_sound:
> Ship.thrust_sound = pygame.mixer.Sound("sounds/thrust.wav")
E pygame.error: mixer not initialized
We do not init pygame during testing, as you can see in the code above, so the mixer isn’t initialized and therefore we cannot create our sound.
In operation, I can see a few needs:
- Initialize sounds all in one place;
- Embed start and stop sound calls in code;
- Call sounds by an abstract name, not a file name;
- Ignore calls if sound is not initialized;
I don’t think that I can foresee all the needs while just sitting here thinking, though I could tick through the possibilities and speculate fairly effectively. I’d surely get something wrong I always do.
Clearly we want only one sound-playing object. I say “clearly”, because I can’t see why I’d want two or more at the same time. Almost certainly chaos would ensue. Options include:
- Create a “singleton” class and use only class methods;
- Create a regular class and store a singleton instance;
- Put sound logic at the top level in a module.
I guess my own inclinations would be toward #2, a singleton instance.
We’ll want the instance to exist even when only running the tests, because there will be sound code sprinkled throughout the program. But we don’t want it making sounds. So the object will have to operate so that sound calls can be made and just exit.
How about this:
We’ll give the object a dictionary from sound name to Sound instance linked to an associated file. And if you call a sound operation naming a sound and it is not found in the dictionary, the call will just exit. We might need to provide different behavior if the dictionary is populated at all, so as to emit an error if you ask for a sound that doesn’t exist.
We can manage that, perhaps with a clever kind of dictionary, if we need the capability.
So maybe like this:
from sounds import player
...
player.play("acceleration", ...)
...
player.stop("acceleration")
We have a broken test right now! We can work on this and when the test runs, and sound still plays during the game, we have a decent starting platform.
I’ll make a new module sounds
. With a little effort, I have this much:
import pygame.mixer
class Sounds:
def __init__(self):
self.catalog = {}
def init_sounds(self):
self.add_sound("accelerate", "sounds/thrust.wav", 0.1)
def add_sound(self, name, file, volume=1.0):
if not pygame.mixer.get_init():
return
sound = pygame.mixer.Sound(file)
sound.set_volume(volume)
self.catalog[name] = sound
def play(self, name):
if name in self.catalog:
self.catalog[name].play()
player = Sounds()
class Ship:
def power_on(self, dt):
self._accelerating = True
player.play("accelerate")
accel = dt * self._acceleration.rotate(-self._angle)
self.accelerate_by(accel)
This allows the test to run green, because play
skips out if the key isn’t found. And the game makes the same growling noise as before.
I learn a bit by reading. I accidentally found sound.get_num_channels
, which returns the number of channels on which the sound is currently playing. So that’s interesting: it appears that every time I start the sound, it will run on a new channel, until it runs out of channels. I think the default is 8. So I change the code thus:
def play(self, name):
if name in self.catalog:
sound = self.catalog[name]
count = sound.get_num_channels()
if count == 0:
self.catalog[name].play()
Now it no longer starts a new sound every 1/60th of a second. The sound is now much more quiet, and I set the volume to 0.5 for a good compromise.
def init_sounds(self):
self.add_sound("accelerate", "sounds/thrust.wav", 0.5)
I’d like to try one more thing. The sound (I’ll work out how to play it for you as soon as I can) stops abruptly when you lift your finger from the J (accelerate) key. I try setting the sound’s fade-out time but it seems to make little difference. I’ll save the fine tuning for later.
This little bit of code seems to me to be going to work pretty well for playing my last-century game sounds. I do have all the wav files for Asteroids already, so I can plug them in and have more fun.
I’ll also try to come up with a way to add sound to the little videos.
A decent little spike, and a decent first object. I’m green and commit: initial Sounds class, ship acceleration plays thrust.wav.
Time for Sunday brekkers and TV. See you next time!