Making Things Go Boom - Beginning Python Games Development With Pygame (2015)

Beginning Python Games Development With Pygame (2015)

CHAPTER 10

image

Making Things Go Boom

Sound is an essential component of any game as it gives instant feedback from the virtual world. If you were to play a game with the audio turned down, you would likely find it a very passive experience because we expect events to be accompanied by sound.

Like other aspects of creating a game, good sound effects require a lot of creativity. Selecting an appropriate set of sound effects for in-game action can make the difference between the visuals working or completely failing to impress.

This chapter explores the Pygame modules you can use to add sound effects and music to your game. We will also cover how to use free software to create and edit your sounds.

What Is Sound?

Sound is essentially vibrations, typically traveling through the air but potentially through water or other substances. Just about everything can vibrate and pass vibrations to the air in the form of sound. For instance, as I type I can hear a “clack-clack” noise caused by the plastic keys colliding with the surface underneath. The plastic vibrates very quickly and pushes the surrounding air molecules, which in turn push other molecules, and send out a chain reaction that eventually reaches my ears, to be interpreted as sound.

The more energy in a vibration, the louder the sound will be. A key press is relatively quiet because not much force is required to push a key down, but if I were to strike the keyboard with a lot of force—say with a sledgehammer—the sound would be louder because there would be more energy in the vibrations.

Sound can also vary in pitch, which is the speed of the vibrations in the air. Some materials, such as metal, tend to vibrate very quickly and will produce a high-pitched noise when hit with something. Other materials vibrate at different rates and produce different pitches.

Most sounds are a complex mixture of variations in both pitch and volume. If I were to drop a glass onto a stone floor, the initial impact would create a loud noise followed by a variety of sounds from the shards as they vibrate and fall back to the ground. The combination of all these sounds produces a noise that we would recognize as a glass smashing.

Sound can also be altered before it reaches the ears of the listener. We are all familiar with how a wall between you and someone talking muffles the sound and makes it difficult to understand. This is because sound can travel through walls as well as air, but it reduces the volume and changes the vibrations on the way. Sound may also bounce off some surfaces and create effects such as echoes. Replicating such physical effects in games is a great way to enhance the illusion of the visuals. If the game character enters a large cavern, it will be much more convincing if the sound of his footsteps echoed as he walked. But like with most aspects of game design, a little artistic license is allowed. There is no sound in space because there is no atmosphere for it to travel through, but I would still expect my laser cannons to produce a satisfying zap noise!

Storing Sound

Early computer games used chips that created simple tones to produce electronic bleeps and whistles, but were incapable of producing complex sounds. These days, gaming hardware can store and reproduce real-life sounds to create a rich extra dimension to the gameplay. The sound card on your computer can both record and play high-quality audio.

Sound can be represented as a wave. Figure 10-1 shows a sound wave that represents a brief portion of a sound (a fraction of a second)—a full sound would be much longer and more complex. The wave shows how the energy, or amplitude, of the sounds varies over time.

Sound waves form a number of peaks and troughs; the greater the difference in the amplitude of these peaks and troughs, the greater the volume of the sound. The pitch of the sound is determined by frequency of the waves (the distance in time between the peaks); the closer the peaks are together in time, the higher the sound will be.

9781484209714_Fig10-01

Figure 10-1. A sound wave

To store a sound on a computer, you must first convert it to digital form, which you can do using a microphone plugged in to the mic socket of your sound card, or the USB port for newer microphones designed specifically for computer use. When a microphone picks up sound, the wave is converted into an electrical signal, which is sampled at regular intervals by the sound card to produce a sequence of numbers that can be saved to a file. Samples are values representing the amplitude of a wave at a particular moment in time, and are used to reconstruct the wave when it is played back. The more samples you have, the more accurately the sound card can play back the sound. Figure 10-2 shows a wave reconstructed from a low sample rate, overlaid over the original. You can see that the sampled wave generally follows the real wave, but much of the detail is lost, resulting in low-quality sound. A higher sample rate would produce a wave that followed the original more closely, and would sound better when played back.

9781484209714_Fig10-02

Figure 10-2. Sample sound wave

Sample rates are measured in hertz (Hz), which means samples per second, or kilohertz (KHz), which means thousands of samples per second. Phone quality is about 6KHz and CD quality is 44KHz. Samples rates can go higher than CD quality, but only dogs and bats will be able to tell the difference!

Sound Formats

Like images, there are a number of different file formats for digital audio that affect the quality and file size. Pygame supports two audio formats for sound effects: WAV (uncompressed only) and Ogg. Most, if not all, software that deals with sound can read and write WAV files. Support for Ogg is not quite as universal but is still very common. If an application doesn’t support Ogg directly, it may be possible to add it via an upgrade or plug-in.

There are a number of attributes for sound that can affect the quality and file size:

· Sample format—The size of a single sample, which is typically either 8-bit or 16-bit integers, although some formats do support float samples. Generally you should use 16-bit when storing sound files, because it can reproduce CD-quality sound and is best supported by sound cards.

· Sample rate—The number of samples stored per second. The most common values for sample rate are 11025Hz, 22050Hz, or 44100Hz, but there are a number of other potential values. Higher sample rates produce better-quality sound but will result in larger files.

· ChannelsSound files can be mono (a single channel of sound), or stereo (an individual channel for the left and right speakers). Stereo sounds better, but uses up to twice the amount of memory in uncompressed audio files.

· CompressionSound can generate large files. For example, a one-minute-long, 44100Hz, 16-bit, stereo audio will create approximately 10MB of data. Fortunately, audio can be compressed so that it fits into a much smaller space. Pygame doesn’t support compressed WAV files, but it does support the Ogg format, which has very good compression.

Deciding what combination of these attributes you need usually depends on how you will be distributing your game. If you will be distributing your game on a CD or DVD, you will probably have plenty of space to store high-quality sounds. However, if you want to distribute your game via e-mail or download, you may want to sacrifice a little quality for smaller files.

Creating Sound Effects

One way to create sound effects is to simply record your own. For instance, if you need an engine noise, the best way to create it is to record the sound of a real engine running. Other sounds are impractical or even impossible to capture, and a little creativity may be required to create an approximation—a good gunshot sound can be created by recording a balloon bursting and then using sound-editing software to extend and deepen the sound. Even phaser fire can be created by recording the sound of a pencil striking the grill of a metal desk fan and then increasing the pitch and adding a little echo.

Image Tip If you want to record sounds outside, and don’t want to drag a laptop around with you, buy a cheap Dictaphone. The quality may suffer a little because you are not recording straight to high-quality digital, but you can always clean up the sound with Audacity (http://audacity.sourceforge.net/) or similar software.

To start recording sound effects, you will need a microphone. You can use either a standard microphone with a 2.5mm jack that plugs in to the mic socket of your sound card, or a USB microphone that is specifically designed for computers. Both will give good results, but it is best to avoid headset microphones because they are optimized for recording voice rather than general sound effects.

In addition to the microphone, you will need software to sample the sound and save it to your hard drive. Most operating systems come with a basic program that can record sound, but you will likely get better results from other sound software such as Audacity (Figure 10-3), which is an open source application for Windows, Mac OS X, and Linux. You can download Audacity (for free) from http://audacity.sourceforge.net.

9781484209714_Fig10-03

Figure 10-3. Audacity

To record sound with Audacity, click the record button (the red circle) to start recording, and then click the stop button (the yellow square) to finish. The waveform of the sound is then displayed in the main window, which has a number of controls that you can use to examine the wave and select portions of it.

Audacity has many features that you can use to edit sound. Multiple sounds can be mixed together, and you can cut and paste between them. You can also apply various effects that can improve the quality of your sounds—or completely change them!

To apply a sound with Audacity, select a portion of audio you want to alter, then select one of the effects from the Effect menu. Here are a few of the effects you can use:

· Amplify—Makes a sound louder. Generally you should store your sounds as loud as possible, without clipping. Clipping occurs when the amplitude of a wave is greater than the range that can be stored, and reduces the quality of the sound. If the top or bottom of the wave is a horizontal line, it means the sound has been clipped.

· Change Pitch—Makes the sound higher or lower. If you increase the pitch of a voice, it will sound like it is on helium, and if you lower the pitch it will sound deeper and God-like. Changing the pitch is a good way to turn one sound into another. If you were to record two metal spoons colliding and lower the pitch, it would sound something like a sword hitting armor.

· Echo—Adds an echo to the sound. Adding Echo can make your effects sound like they are in anything from an empty room to an enormous cave.

· Noise Removal—If you are not lucky enough to have access to a sound studio and professional equipment, your recordings may have a slight hissing noise, which is a result of background noise and imperfections in the microphone and sound card. The Noise Removal effect does a good job of cleaning up your sounds.

Once you have finished editing your sound effects, you can export them as a variety of other formats, including WAV and Ogg. It is a good idea to keep the original files so you can export them to a different format if you need to.

Image Caution Recording sounds from a movie may seem like a good way to get interesting effects for your game, but you will likely break copyright laws—so it is best avoided.

Stock Sound Effects

You can also buy sound effects on CD or download them from the Web. These sound effects are high quality because they were produced in sound studios and professionally edited. A popular website I have used personally is Sounddogs (http://www.sounddogs.com/). Their CDs are expensive, but you can also purchase effects individually and pay by the second. If you just need a dozen or so short sound effects for your game, the price is reasonable.

There are also a number of sources on the Web for free sound effects. The Pygame wiki contains a page that lists some good sites (www.pygame.org/wiki/resources). You may also be able to find more good sites by searching the Web.

Playing Sounds with Pygame

You can play sound effects with Pygame through the pygame.mixer interface. Before you can use the mixer, it must first be initialized with a number of parameters that define what kind of sound you will be playing. This can be done with the pygame.mixer.init function, but on some platforms the mixer is initialized automatically by pygame.init. Pygame provides a pyame.mixer.pre_init function that you can use to set the parameters for this automatic initialization. Both of the initialization functions take the following four parameters:

· frequency—This is the sample rate of the audio playback and has the same meaning as the sample rate of sound files. A high sample rate can potentially reduce performance, but even old sound cards can easily handle a frequency setting of 44100, which is CD quality. Another common setting for frequency is 22050, which doesn’t sound quite as good.

· size—This is the size, in bits, of the audio samples for the playback. The sample size can be either 8 or 16. A value of 16 is best because the sound quality is much higher than 8, with the same performance. This value can also be negative, which indicates that the mixer should use signed samples (required by some platforms). There is no difference in sound quality between signed or unsigned samples.

· stereo—This parameter should be set to 1 for mono sound or 2 for stereo sound. Stereo is recommended because it can be used to create the illusion that sound is coming from a particular point on the screen.

· buffer—This is the number of samples that are buffered for playback. Lower values result in lower latency, which is the time between asking Pygame to play a sound and the time you actually hear it. Higher values increase latency but may be necessary to avoid sounddropout, which can cause annoying pops and clicks over the sound. I have found that a value of 4096 works best for 44100, 16-bit stereo sound. This value must always be a power of 2.

Here’s how you would initialize the mixer for 16-bit, 44100Hz stereo sound:

pygame.mixer.pre_init(44100, 16, 2, 4096)
pygame.init()

The call to pygame.mixer.pre_init sets the parameters for the mixer, which is initialized in the call to pygame.init. If you need to change any of the parameters after Pygame has been initialized, you must first quit the mixer with a call to pygame.mixer.quit before calling pygame.mixer.init to reinitialize.

Sound Objects

Sounds objects are used to store and play audio data, read from either WAV or Ogg files. You can construct a Sound object with pygame.mixer.Sound, which takes the filename of the sound file, or a Python file object containing the data. Here’s how to load a sound file calledphaser.ogg from the hard drive:

phaser_sound = Pygame.mixer.Sound("phaser.ogg")

You can play a Sound object with the play method, which takes two optional parameters: loop and maxtime. Setting a value for loop makes the sound repeat after it is initially played. For instance, if loop is set to 5, the sound will play through and then repeat 5 times (6 times in total). You may also set loop to the special value of –1, which causes the sound to play continuously, until you call the stop method.

The maxtime parameter is used to stop playback after a given number of milliseconds, which is useful for sounds that are designed to loop (play continuously) because you can specify exactly how long they will play for.

If the call to play was successful, it will return a Channel object (see the next section); otherwise, it will return None. Here’s how to play the phaser sound a single time:

channel = phaser_sound.play()

This line would play a five-second-long burst of phaser fire:

channel = phaser_sound.play(–1, 5000)

See Table 10-1 for a full list of the methods of Sound objects.

Table 10-1. Methods of Sound Objects

Method

Purpose

fadeout

Gradually reduces the volume of the sound on all channels. fadeout takes a single parameter, which is the length of the fade in milliseconds.

get_length

Returns the length of the sound, in seconds.

get_num_channels

Counts how many times the sound is playing.

get_volume

Returns the volume of the sound, as a float between 0.0 and 1.0, where 0.0 is silent and 1.0 is full volume.

play

Plays the sound. See the section “Sound Objects” for a description of the parameters. The return value is a Channel object, or None if Pygame was unable to play the sound.

set_volume

Sets the volume of the sound when it is played back. The parameter is a float between 0.0 and 1.0, where 0.0 is silent and 1.0 is full volume.

stop

Immediately stops any playback of the sound.

Sound Channels

A channel is one of several sound sources that are mixed together by the sound card, and are represented in Pygame by Channel objects. The play method of Sound objects returns a Channel object for the channel that will play the sound, or None if all channels are busy playing. You can retrieve the number of available channels by calling the pygame.mixer.get_num_channels function. If you find that you don’t have enough channels to play all the sounds you need to, you can create more by calling the pygame.mixer.set_num_channels function.

If you just want the sound to play all the way through at full volume, you can safely ignore the return value of Sound.play. Otherwise, there are some useful effects you can create with the methods of Channel objects. One of the most useful is the ability to set the volume of the left and right speakers independently, which can be used to create the illusion that a sound is coming from a particular point on the screen—an effect known as stereo panning. The set_volume method of Channel objects can take two parameters: the volume of the left speaker and the volume of the right speaker, both as a value between 0 and 1. Listing 10-1 shows a function that calculates the volume of the speakers given the x coordinate of the sound-producing event and the width of the screen. The further away the x coordinate is from the speaker, the lower the volume will be, so as a point moves across the screen from left to right, the left speaker will decrease in volume, while the right speaker will increase in volume.

Listing 10-1. Function That Calculates Stereo Panning

def stereo_pan(x_coord, screen_width):

right_volume = float(x_coord) / screen_width
left_volume = 1.0 - right_volume

return (left_volume, right_volume)

Listing 10-2 shows how you might use the stereo_pan function to play a sound effect for an exploding tank. Figure 10-4 shows how the position of the explosion relates to the values for the left and right channels.

Listing 10-2. Using the stereo_pan Function

tank.explode() # Do explosion visual
explosion_channel = explosion_sound.play()
if explosion_channel is not None:
left, right = stereo_pan(tank.position.x, SCREEN_SIZE[0])
explosion_channel.set_volume(left, right)

9781484209714_Fig10-04

Figure 10-4. Setting the stereo panning of an explosion

Image Tip If you update the stereo panning every frame for a moving sprite, it will enhance the stereo effect.

Generally it is best to leave the task of selecting a channel to Pygame, but it is possible to force a Sound object to play through a particular channel by calling the play method of the Channel object, which takes the sound you want to play followed by the number of times you want it to repeat and the maximum time you want it to play. One reason to do this is to reserve one or more channels for high-priority sounds. For instance, you might not want background ambient noise to block the player’s gunfire. To reserve a number of channels, call thepygame.mixer.set_reserved function, which prevents a number of channels from being considered by the Sound.play method. For instance, if you call pygame.mixer.set_reserved(2), Pygame will not select channel 0 or 1 when calling play from a Sound object.Listing 10-3 shows how to reserve the first two channels.

Listing 10-3. Reserving Channels

pygame.mixer.set_reserved(2)
reserved_channel_0 = pygame.mixer.Channel(0)
reserved_channel_1 = pygame.mixer.Channel(1)

Here’s how you would force a sound to be played through one of the reserved channels:

reserved_channel_1.play(gunfire_sound)

See Table 10-2 for a complete list of the methods of Channel objects.

Table 10-2. Methods of Channel Objects

Method

Purpose

fadeout

Fades out (reduces volume) a sound over a period of time, given in milliseconds.

get_busy

Returns True if a sound is currently playing on the channel.

get_endevent

Returns the event that will be sent when a sound finishes playing, or NOEVENT if there is no end event set.

get_queue

Returns any sound that is queued for playback, or None if there is no queued sound.

get_volume

Retrieves the current volume of the channel as a single value between 0.0 and 1.0 (does not take into account the stereo volume set by set_volume).

pause

Temporarily pauses playback of any sound on this channel.

play

Plays a sound on a specific channel. Takes a Sound object and optional values for looping and max time, which have the same meaning as Sound.play.

queue

Plays a given sound when the current sound has finished. Takes the Sound object to be queued.

set_endevent

Requests an event when the current sound has finished playing. Takes the id of the event to be sent, which should be above USEREVENT (a constant in pygame.locals), to avoid conflict with existing events. If no parameter is given, Pygame will stop sending end events.

set_volume

Sets the volume of this channel. If one value is given, it is used for both speakers. If two values are given, the left and right speaker volume is set independently. Both methods take the volume as a value between 0.0 and 1.0, where 0.0 is silent and 1.0 is the maximum volume.

stop

Instantly stops playback of any sound on the channel.

unpause

Resumes playback of a channel that has been paused.

Mixer Functions

We’ve covered a number of functions in the pygame.mixer module. Table 10-3 presents a complete list of them.

Table 10-3. Functions in pygame.mixer

Function

Purpose

pygame.mixer.Channel

Creates a Channel object for a given channel index.

pygame.mixer.fadeout

Gradually reduces the volume of all channels to 0. Takes the fade time (in milliseconds).

pygame.mixer.find_channel

Finds a currently unused channel and returns its index.

pygame.mixer.get_busy

Returns True if sound is being played (on any channel).

pygame.mixer.get_init

Returns True if the mixer has been initialized.

pygame.mixer.get_num_channels

Retrieves the number of available channels.

pygame.mixer.init

Initializes the mixer module. See the beginning of the section for an explanation of the parameters.

pygame.mixer.pause

Temporarily stops playback of sound on all channels.

pygame.mixer.pre_init

Sets the parameters for the mixer when it is automatically initialized by the call to pygame.init.

pygame.mixer.quit

Quits the mixer. This is done automatically when the Python script ends, but you may need to call it if you want to reinitialize the mixer with different parameters.

pygame.mixer.set_num_channels

Sets the number of available channels.

pygame.mixer.Sound

Creates a Sound object. Takes a filename or a Python file object containing the sound data.

pygame.mixer.stop

Stops all playback of sound on all channels.

pygame.mixer.unpause

Resumes playback of paused sound (see pygame.mixer.pause).

Hearing the Mixer in Action

Let’s write a script to experiment with sound effects in Pygame. If you run Listing 10-4 you will see a white screen with a mouse cursor. Click anywhere on the screen to throw out a silver ball that falls under gravity and plays a sound effect when it bounces off the edge or bottom of the screen (see Figure 10-5).

Listing 10-4. Mixer in Action (bouncesound.py)

import pygame
from pygame.locals import *
from random import randint
from gameobjects.vector2 import Vector2

SCREEN_SIZE = (640, 480)

# In pixels per second, per second
GRAVITY = 250.0
# Increase for more bounciness, but don't go over 1!
BOUNCINESS = 0.7

def stero_pan(x_coord, screen_width):

right_volume = float(x_coord) / screen_width
left_volume = 1.0 - right_volume

return (left_volume, right_volume)

class Ball(object):

def __init__(self, position, speed, image, bounce:sound):

self.position = Vector2(position)
self.speed = Vector2(speed)
self.image = image
self.bounce:sound = bounce:sound
self.age = 0.0

def update(self, time_passed):

w, h = self.image.get_size()

screen_width, screen_height = SCREEN_SIZE

x, y = self.position
x -= w/2
y -= h/2

# Has the ball bounce
bounce = False

# Has the ball hit the bottom of the screen?
if y + h >= screen_height:
self.speed.y = -self.speed.y * BOUNCINESS
self.position.y = screen_height - h / 2.0 - 1.0
bounce = True

# Has the ball hit the left of the screen?
if x <= 0:
self.speed.x = -self.speed.x * BOUNCINESS
self.position.x = w / 2.0 + 1
bounce = True

# Has the ball hit the right of the screen
elif x + w >= screen_width:
self.speed.x = -self.speed.x * BOUNCINESS
self.position.x = screen_width - w / 2.0 - 1
bounce = True

# Do time based movement
self.position += self.speed * time_passed
# Add gravity
self.speed.y += time_passed * GRAVITY

if bounce:
self.play_bounce:sound()

self.age += time_passed

def play_bounce:sound(self):

channel = self.bounce:sound.play()

if channel is not None:
# Get the left and right volumes
left, right = stero_pan(self.position.x, SCREEN_SIZE[0])
channel.set_volume(left, right)

def render(self, surface):

# Draw the sprite center at self.position
w, h = self.image.get_size()
x, y = self.position
x -= w/2
y -= h/2
surface.blit(self.image, (x, y))

def run():

# Initialise 44KHz 16-bit stero sound
pygame.mixer.pre_init(44100, 16, 2, 1024*4)
pygame.init()
pygame.mixer.set_num_channels(8)
screen = pygame.display.set_mode(SCREEN_SIZE, 0)

print(pygame.display.get_wm_info())
hwnd = pygame.display.get_wm_info()["window"]
x, y = (200, 200)

pygame.mouse.set_visible(False)

clock = pygame.time.Clock()

ball_image = pygame.image.load("ball.png").convert_alpha()
mouse_image = pygame.image.load("mousecursor.png").convert_alpha()

# Load the sound file
bounce:sound = pygame.mixer.Sound("bounce.wav")

balls = []

while True:

for event in pygame.event.get():

if event.type == QUIT:
pygame.quit()
quit()

if event.type == MOUSEBUTTONDOWN:

# Create a new ball at the mouse position
random_speed = ( randint(-400, 400), randint(-300, 0) )
new_ball = Ball( event.pos,
random_speed,
ball_image,
bounce:sound )
balls.append(new_ball)

time_passed_seconds = clock.tick() / 1000.

screen.fill((255, 255, 255))

dead_balls = []

for ball in balls:

ball.update(time_passed_seconds)
ball.render(screen)

# Make not of any balls that are older than 10 seconds
if ball.age > 10.0:
dead_balls.append(ball)

# remove any 'dead' balls from the main list
for ball in dead_balls:

balls.remove(ball)

# Draw the mouse cursor
mouse_pos = pygame.mouse.get_pos()
screen.blit(mouse_image, mouse_pos)

pygame.display.update()

if __name__ == "__main__":

run()

9781484209714_Fig10-05

Figure 10-5. bouncesound.py

When the update method of the Ball class detects that the sprite has hit the edge or bottom of the screen, it reverses the direction of the sprite and calls the play method of the Sound object. The sprite’s x coordinate is used to calculate the volume of the left and right speakers, so that the sound appears to be emitted from the point where the sprite has hit the screen boundary. The effect is quite convincing—if you close your eyes, you should still be able to tell where the ball bounces!

If you create a lot of sprites by clicking the mouse quickly, you will probably be able to detect that some bounces stop producing the sound effect. This happens because all available channels are used up playing the same sound effect, and new sounds can only play when a channel becomes free.

Playing Music with Pygame

Although the pygame.mixer module can play any kind of audio, it is usually not a good idea to use it for music, because music files tend to be large and will put a strain on the computer’s resources. Pygame provides a submodule of pygame.mixer called pygame.mixer.musicthat can read (and play) music files a piece at a time rather than all at once, which is known as streaming the audio.

Image Note Music may be the most common use of pygame.mixer.music, but you can use it to stream any large audio file, such as a voiceover track.

Obtaining Music

If you are not lucky enough to be blessed with musical talent, you may have to use music that someone else has created. There are a number of stock music websites where you can purchase music files for use in games or download them for free. You can find a selection of both commercial and free music sites on the Pygame wiki (http://www.pygame.org/wiki/resources).

Image Caution It may be tempting to select a few tracks from your music collection and have your favorite band as the soundtrack for your game, but this breaks copyright laws and should be avoided!

Playing Music

The pygame.mixer.music module can play MP3 and Ogg music files. The MP3 support varies from platform to platform, so it is probably best to use the Ogg format, which is well supported across platforms. You can convert MP3 to Ogg with Audacity or another sound-editing application.

To play a music file, first call the pygame.mixer.music.load function with the filename of the music track you want to play, and then call pygame.mixer.music.play to begin the playback (see Listing 10-5). Everything you may want to do with the module can be done with the pygame.mixer.music module (there is no Music object because only a single music file can be streamed at once). There are functions to stop, rewind, pause, set volume, and so forth. See Table 10-4 for a complete list.

Assuming you had a filename techno.ogg (see the section on using Audacity to convert music to .ogg), you could play music like:

Listing 10-5. Playing a Music File

pygame.mixer.music.load("techno.ogg")
pygame.mixer.music.play()

Table 10-4. The pygame.mixer.music Functions

Function

Purpose

pygame.mixer.get_busy

Returns True if music is currently playing.

pygame.mixer.music.fadeout

Reduces the volume over a period of time. Takes the fade time as milliseconds.

pygame.mixer.music.get_endevent

Returns the end event to be sent or 0 if there is no event.

pygame.mixer.music.get_volume

Returns the volume of the music; see set_volume.

pygame.mixer.music.load

Loads a music file for playback. Takes the filename of the audio file.

pygame.mixer.music.play

Starts playing a loaded music file. Takes the number of times you want the music to repeat after it is initially played through, followed by the point you want to begin playback (in seconds). If you set the first value to –1, it will repeat until you call pygame.mixer.stop.

pygame.mixer.music.rewind

Restarts the music file at the beginning.

pygame.mixer.music.set_endevent

Requests an event to be sent when the music has finished playing. Takes the id of the event to be sent, which should be above USEREVENT (a constant in pygame.locals) to avoid conflict with existing events. If no parameter is given, Pygame will stop sending end events.

pygame.mixer.music.set_volume

Sets the volume of the music. Takes the volume as a value between 0.0 and 1.0, where 0.0 is silence and 1.0 is full volume. When new music is loaded, the volume will be reset to 1.0.

pygame.mixer.music.stop

Stops playback of the music.

pygame.mixer.music.unpause

Resumes playback of music that has been paused.

pygame.muxer.music.get_pos

Returns the time that the music has been playing for, in milliseconds.

pygame.muxer.music.pause

Temporarily pauses playback of the music.

pygame.muxer.music.queue

Sets a track that will be played when the current music has finished. Takes a single parameter that is the filename of the file you want to play.

Hearing Music in Action

Let’s use the pygame.mixer.music module to create a simple jukebox. Listing 10-6 reads in a list of Ogg files from a path on your hard drive and displays a number of familiar hi-fi-like buttons that you can use to play, pause, or stop the music and move through the list of tracks (seeFigure 10-6). If you change the MUSIC_PATH value at the top of the listing, you can make it play from your own collection.

Listing 10-6. A Pygame Jukebox (jukebox.py)

import pygame
from pygame.locals import *

from math import sqrt
import os
import os.path

# Location of music on your computer
MUSIC_PATH = "./MUSIC"
SCREEN_SIZE = (800, 600)

def get_music(path):

# Get the filenames in a folder
raw_filenames = os.listdir(path)

music_files = []
for filename in raw_filenames:

# We only want ogg files
if filename.endswith('.ogg'):
music_files.append(os.path.join(MUSIC_PATH, filename))

return sorted(music_files)

class Button(object):

def __init__(self, image_filename, position):

self.position = position
self.image = pygame.image.load(image_filename)

def render(self, surface):

# Render at the center
x, y = self.position
w, h = self.image.get_size()
x -= w /2
y -= h / 2

surface.blit(self.image, (x, y))

def is_over(self, point):

# Return True if a point is over the button
point_x, point_y = point
x, y = self.position
w, h = self.image.get_size()
x -= w /2
y -= h / 2

in_x = point_x >= x and point_x < x + w
in_y = point_y >= y and point_y < y + h

return in_x and in_y

def run():

pygame.mixer.pre_init(44100, 16, 2, 1024*4)
pygame.init()
screen = pygame.display.set_mode(SCREEN_SIZE, 0)

default_font = pygame.font.get_default_font()
font = pygame.font.SysFont("default_font", 50, False)

# Create our buttons
x = 100
y = 240
button_width = 150

# Store the buttons in a dictionary, so we can assign them names
buttons = {}
buttons["prev"] = Button("prev.png", (x, y))
buttons["pause"] = Button("pause.png", (x+button_width*1, y))
buttons["stop"] = Button("stop.png", (x+button_width*2, y))
buttons["play"] = Button("play.png", (x+button_width*3, y))
buttons["next"] = Button("next.png", (x+button_width*4, y))

music_filenames = get_music(MUSIC_PATH)

if len(music_filenames) == 0:
print("No OGG files found in ", MUSIC_PATH)
return

white = (255, 255, 255)
label_surfaces = []

# Render the track names
for filename in music_filenames:

txt = os.path.split(filename)[-1]

print("Track:", txt)

txt = txt.split('.')[0]
surface = font.render(txt, True, (100, 0, 100))
label_surfaces.append(surface)

current_track = 0
max_tracks = len(music_filenames)

pygame.mixer.music.load( music_filenames[current_track] )

clock = pygame.time.Clock()

playing = False
paused = False

# This event is sent when a music track ends
TRACK_END = USEREVENT + 1
pygame.mixer.music.set_endevent(TRACK_END)

while True:

button_pressed = None

for event in pygame.event.get():

if event.type == QUIT:
pygame.quit()
quit()

if event.type == MOUSEBUTTONDOWN:

# Find the pressed button
for button_name, button in buttons.items():
if button.is_over(event.pos):
print(button_name, "pressed")
button_pressed = button_name
break

if event.type == TRACK_END:
# If the track has ended, simulate pressing the next button
button_pressed = "next"

if button_pressed is not None:

if button_pressed == "next":
current_track = (current_track + 1) % max_tracks
pygame.mixer.music.load( music_filenames[current_track] )
if playing:
pygame.mixer.music.play()

elif button_pressed == "prev":

# If the track has been playing for more that 3 seconds,
# rewind i, otherwise select the previous track
if pygame.mixer.music.get_pos() > 3000:
pygame.mixer.music.stop()
pygame.mixer.music.play()
else:
current_track = (current_track - 1) % max_tracks
pygame.mixer.music.load( music_filenames[current_track] )
if playing:
pygame.mixer.music.play()

elif button_pressed == "pause":
if paused:
pygame.mixer.music.unpause()
paused = False
else:
pygame.mixer.music.pause()
paused = True

elif button_pressed == "stop":
pygame.mixer.music.stop()
playing = False

elif button_pressed == "play":
if paused:
pygame.mixer.music.unpause()
paused = False
else:
if not playing:
pygame.mixer.music.play()
playing = True

screen.fill(white)

# Render the name of the currently track
label = label_surfaces[current_track]
w, h = label.get_size()
screen_w = SCREEN_SIZE[0]
screen.blit(label, ((screen_w - w)/2, 450))

# Render all the buttons
for button in list(buttons.values()):
button.render(screen)

# No animation, 5 frames per second is fine!
clock.tick(5)
pygame.display.update()

if __name__ == "__main__":
run()

9781484209714_Fig10-06

Figure 10-6. The jukebox script

The jukebox uses the pygame.mixer.set_endevent function to request an event to be sent when a track finished playing. Pygame doesn’t provide an event for this, but you can easily create one of your own by using an id value that is greater than USEREVENT (a constant inpygame.locals). Listing 10-6 uses the id TRACK_END, which has the value of USEREVENT + 1. When the TRACK_END event is detected in the main event loop, it starts streaming the next music file so that the tracks are played sequentially.

Summary

Sound is a creative medium and it may require a lot of experimentation to perfect the audio in a game. Selecting good sound effects is essential because the player will likely hear them many times—and bad or annoying audio will quickly discourage further play. The same is true for the soundtrack, which has equal potential to either enhance or annoy.

The pygame.mixer module provides Pygame’s sound effects capabilities, which allow you to load and play sound files on one of a number of channels. When sound is played through a Sound object, Pygame will automatically allocate a free channel. This is the simplest approach, but you have the option of managing channels yourself by playing the sound on a specific channel. I recommend this only if you will be playing many sounds and want to prioritize them.

Although you can play any audio with Sound objects, it is best to play music with the pygame.mixer.music module because it can stream the audio, rather than loading the entire file into memory. The music module provides a number of simple functions that you can use to manage the music in your game.

The next chapter covers more techniques that you can use to create convincing 3D visuals in a game.