Chapter 7. Doing more with lists

Topics:

Using lists in graphical examples, recording sound, manipulating sound

Constructs:

constants, local and global variables
list operations: +, *, reverse, slicing, extend

In the previous chapter, we had a first experience of working with lists. Lists are so central to programming in Python that we will spend a bit more time with them: we will look at other uses of lists in this chapter to get a deeper understanding of working with them.

We will do this with two different examples: First, we will see how we can use lists in our games, and then we will process sound files using list operations. Both of these examples will teach us something more about handling list, and data in general.

Let’s start by getting back to our game from the early chapters of this book. When we implemented that game, we have seen that our graphics program deals with actors (objects of the Actor class) to show various elements on screen. Using lists of actors enables us to improve our game.

We will start by looking at a variation of our Yellow Fish game; it is named Space Rescue.

code
Figure 7.1: The Space Rescue game

7.1. Lists of actors

The Space Rescue project (Figure 7.1) is essentially a copy of the Yellow Fish game with different graphics. Start by investigating it.

Exercise 7.1   Open the space-rescue game from the Chapter07 folder of the book projects. Run the program and investigate it. How is it similar to the yellow fish game? How is it different?

You will have seen when you tried out the program that it is essentially the same as yellow fish. We have changed the colour of the background, the graphics and some of the sounds, but the code is almost identical. The only two changes to the functionality are:

  • The asteroid (which previously was the shark), does not turn when it reaches the edge of the screen, but instead appears on the opposite edge.

  • We have added an end-of-game scene that lands the rocket on a planet when the player wins the game.

Exercise 7.2   Examine the source code of the new game version. Find the place where the new behaviour of the asteroid at the edge of the screen is implemented. Make sure that you understand this code. Explain how it works.

Exercise 7.3   Find the code that implements the end-of-game landing scene. What is the function called that implements this?

Exercise 7.4   Does this function use any constructs you are not familiar with?

7.1.1. Creating the astronauts

One small improvement to the game we can now make is to animate the astronauts. Currently, they do not move at all, which makes the scene very static. Since they are floating in space, we might start by making them slowly turn on the spot.

Exercise 7.5   Find the place in the program where the astronauts are created. What variable is the astronaut actor assigned to? Where else is that variable being used?

With the investigation in the previous exercise, you will have seen that the astronaut actor is being created and assigned to a variable, which is then never used. The variable is essentially discarded in our program. This is not a problem: The astronaut actors remain in the game, because they are held in the graphics world, and we can later get them back from there.

Let us prepare our task by initially rotating every astronaut to a different value, so that they are not all facing the same way.

Exercise 7.6   In the loop that creates the astronauts, use the set_rotation(rotation) method of the astronaut actor to assign a random rotation to the actor immediately after it is created. The rotation is specified in degrees, so the random value should be between 0 and 360.

7.1.2. Accessing the astronauts

We can now make all astronauts slowly turn by calling the turn method on each actor in every step of our main loop. To do this, we first need to get access to our astronauts again. Luckily, this is quite easy: the graphics world maintains a list of all actors in the world, and the strype.graphics library includes a function called get_actors() that can be used to get access to this list. We could, for example, get a list of all actors by simply writing

Strype code

The function has an optional parameter to provide a tag. If a tag is provided, only actors tagged with this tag will be returned. If you check the line where we created the astronauts, you will see that we used the tag "astronaut" for each of our astronaut actors, so we can now use this in our function call:

Strype code

This call will give us a list of all astronaut actors.

Concept The graphics system maintains a list of actors. We can obtain a list of all actors, or of selected actors, by using the get_actors() function.

Exercise 7.7   Find the get_actors() function in the library documentation. What is the default value of its parameter?

Exercise 7.8   Create a new function move_astronauts() (to mirror the move_rocket() and move_asteroid() functions). The function body can initially be empty. Make sure to call this function from your main loop.

Exercise 7.9   In this new function, use the get_actors() function to get a list of astronauts. Then write a for-loop that iterates over the astronauts list and makes each astronaut turn 1 degree. Test your program. Do the astronauts turn?

Exercise 7.10   Write an appropriate function comment for your new function.

Your astronauts should now be slowly turning while they are floating in space. One last thing that we will add is to make them all re-appear on the surface of the planet after the rocket has landed.

Exercise 7.11   Add a for-loop to the landing function, after the rocket has landed, to add 10 astronauts into the world, so that they appear in random locations on the surface of the planet. Note that actor objects cannot actually be re-entered into the world after they have been removed, so you will need to create 10 new astronaut actors here.

When you run your program, you will notice that the 10 astronauts all appear at once. This is because the for loop is so quick that we see them all appear at essentially the same time. To make this visually more interesting, we can slow down the execution of this for loop, using the pace function, to make the astronauts appear more slowly, one after the other.

Exercise 7.12   Set the pace of this new for-loop to 5, so that it executes slowly enough for you to see the astronauts appear one at a time.

Exercise 7.13   Add the playing of the click sound (the one that is used when an astronaut is picked up) into this loop. (You can do this by duplicating this line and then dragging it into place.)

A copy of the project with these changes implemented is in the book projects as space-rescue-v2.

With this series of exercises, we have seen how the graphics world uses lists to work with the actors and how we can use these lists to deal with more than one actor at a time. Again, lists are useful here to process a collection of items.

There are, however, a couple of topics that we have glossed over, and which we should discuss in more detail now.

7.2. Constants

(introduce constants…​)

7.3. Global and local variables

(explain variables here…​)

StrypeTip When you carefully inspect the creation of the astronauts and compare it to the creation of the shrimp in our earlier version, you might have noticed one further small difference: In the fish version, we wrote:

Strype code

while in the space version we changed this to

Strype code

The main change here is the assignment of the image before the loop. If we use the image literal inside the loop, we are creating 15 separate shrimp images. They all show the same picture, but they are all separate image instances. If, on the other hand, we use the image literal outside of the loop, only one image is created. This same image is then used for all the different actors. This is much more efficient than using multiple images unnecessarily. Using many separate images can slow down the execution of your program, especially if the images are large.

7.4. Manipulating sounds

For the remainder of this chapter, we will work with sound files, and we will see that sounds, too, can be viewed as lists – in this case, lists of samples.

A sample is a reading of the sound input (the microphone), and it is stored as a number. When these samples are read and stored quickly enough, we get a sound recording, and we can play it back later. Thus, a stored sound is a list of samples, which itself is a list of numbers.

Start by opening a project with a sound in it. We have provided such a project in the Chapter07 folder; it is called sounds.

Exercise 7.14   Open the sounds project. Add a statement to play the sound in it.

7.4.1. Sounds as lists of samples

In our project, we have a sound object. We can get the list of samples that makes up this sound by using the sound’s get_samples method.

Exercise 7.15   Look up the get_samples method in the library documentation. It is a method in the Sound class. What is the type of each element in the list it returns? What is the range of possible values?

Exercise 7.16   Write some code to get the samples from the sound in the project. Print out the length of the list. How many samples does it contain?

Exercise 7.17   The sample rate is the number of samples that is recorded per second. Calculate and print the sample rate for our sound. You can do this by dividing the number of samples by the length of the sound in seconds. (You can see the length in seconds when you hover with the mouse pointer over the sound literal in the editor.)

Concept A sound is stored as a list of samples. We can get the list of samples from a sound object using the get_samples() method.

7.4.2. Playing backwards

The first experiment we can do is to reverse the sound to play it backwards. We can do this by reversing our samples list – a first attempt might look like this:

Strype code

As the comment in the code snippet says: this does not work. The reason for this is that the get_samples() method gives us a copy of the sound’s samples. So by reversing the list, we are not changing the original sound.

We can fix this problem by creating a new sound from the reversed sample list and then playing the new sound:

Strype code

We see here that we can create a sound, using the Sound class’s constructor (writing the class name Sound with a parameter list), and providing the list of samples as the parameter.

Concept A sound object can be created from a list of samples by using the statement Sound(sample_list).

Exercise 7.18   Implement the code shown above: reverse the sound and play it backwards.

In the following sections, we will do a series of exercises where we manipulate sounds. You could continue using the sound sample provided in our project, but it will be much more fun if you do the exercises with your own sounds. Therefore, we will first discuss how you can record and use your own sounds.

7.4.3. Recording your own sounds

Many programs are available for recording and saving sounds, and several of them are free to use. We will use Audacity here for our screenshots (available from https://www.audacityteam.org). Audacity is a good choice because it is available for different operating systems, is very flexible, free and quite easy to use. There are many other sound editing programs out there, however, so feel free to use one of your choice.

code
Figure 7.2: A sound recording and editing program (Audacity)

Figure 7.2 shows the interface of Audacity. You see tools for recording and playback and various edit operations, and a wave form display of the recorded sound (the blue graph).

Exercise 7.19   Open a sound recording program and record a sound. Play it back. Does it come out as you expected? If not, delete it and try again.

When you record sounds, you often have a delay at the beginning and end of the recording (the straight horizontal line at the beginning and the end of the graph). If we use the sound like this, it will appear to play with a slight delay, because it will play the initial silence as well. Thus, we should trim the sound. "Trimming" means removing the unwanted parts at the beginning and end of the sound recording. You can usually do this in sound editing programs by selecting and then deleting the segment.

Exercise 7.20   Edit your sound file so that it includes only the exact sound you want. Remove any noise or silence at the beginning or end, or any parts in the middle that are not needed.

Once you are happy with your sound, we are ready to save it to disk.

7.4.4. Sound file formats

Sound files can be saved in many different formats and in different encodings, and this can get quite confusing very quickly.

Strype can use sounds saved in WAV, AIFF, AU and MP3 formats. Some of these formats, however, are what is known as “envelope formats” - they can contain different encodings, and not all of them may be playable.

A safe option for saving sound effects is to use the "WAV" format and a “signed 16 bit PCM” encoding. This is the safest format to ensure playback in many different systems. In our case, you should also ensure to save the sound in "mono" (not "stereo"), as this is needed to work with the sample list of the sound. (We could convert the sound from stereo to mono in our Python code, but we may as well do it here.) The sample rate you use is less important, as Strype will by default convert the sound to its own preferred sample rate.

In many sound recording programs, saving in this specific format is achieved by using an “Export” function, rather than the standard “Save” function. Make sure to save in this format.

Exercise 7.21   Export your sound to a WAV file. Then copy it from the file into your Strype program. Play it in Strype.

StrypeTip When making sounds for Strype, save them as signed 16 bit PCM WAV files before copying them into Strype.

7.4.5. Repeating a sound effect

We are now ready to make modifications to our sound by modifying the sample list, and then creating a new sound from the new samples.

Let us start by duplicating the sound. We can duplicate a list by using the multiplication operator: *. For example:

Strype code

will duplicate the list so that it contains its elements twice. The list then is:

[1,2,3,1,2,3]

We can also use the plus operator to append one list to the end of another:

Strype code

Let us experiment with this. Write some test code for the exercises below.

Exercise 7.22   Create a short list of numbers. Use the * 2 operation to duplicate the list and print it out.

Exercise 7.23   Try the same with multipliers other than 2.

Exercise 7.24   Use the + operator to add two lists together. Print out the result.

Exercise 7.25   Let’s get back to out sound sample list. Duplicate the sample list and create a new sound with the new list. Play the sound. What happens, compared to the original sound?

Concept We can join two lists together to form a single list by using the + operator: list1 + list2.

Concept List elements can be repeated multiple times to form a new list by using the * operator: list1 * 3. For example, [0, 1, 2] * 3 will produce [0, 1, 2, 0, 1, 2, 0, 1, 2].

7.4.6. Stutter effect

An effect that is often used as part of music is to play the start of a word repeatedly before playing the whole word. For example, if your original sound is the word "Hello":

code

then the stutter effect turns it into this:

code

As you can see, we will have to take a copy of the first part of our sound, multiply it to play it several times, and then append the original sound.

Remember that we can use slicing to create a copy of a part of a list. For example samples[:9000] will create a list holding the first 9000 elements of the samples list.

Exercise 7.26   Implement the stutter effect in your own project. Use slicing to create the prefix sound, multiplication to repeat the prefix, and addition to append the whole sound. Test it and make sure that it works.

Exercise 7.27   Try the effect with different numbers of repetitions for the prefix.

Exercise 7.28   Make a function for producing and playing the stutter effect. The function should have two parameters: the original sound object, and a parameter called count which specifies how often the prefix sound should be repeated. For example, play_stutter(hello, 4) would play the hello sound with 4 repetitions of their partial word first. In your 'My code' section, just call this function.

Exercise 7.29   Add another parameter to your function to specify the length of the prefix part of the effect (the number of samples for the prefix). Test it.

Exercise 7.30   If you still have the code for the backwards playing in your project, it is time to clean up now: Make a separate function for backward playing as well and move your code into it.

Exercise 7.31   Add an infinite loop in your code (like in the game examples). In this loop, check the keyboard: if the 1 key is pressed, play your sound backwards. If the 2 key is pressed, play the stutter effect.

A version of this project with an implementation of these effects is available in the Chapter07 folder as sounds-v2.

Exercise 7.32   Make a program that plays different sound effects in reaction to different key presses.

7.4.7. Pitch change: double speed

Next, we will create a "cartoon voice" sound effect. This effect plays the sound sample at double speed. This has two consequences: the sound will be shorter, and the pitch will be higher. (The pitch is how high or low a voice sounds.)

Looking at our sound sample list, the idea is very simple: we can simply delete every second sample in our list. If we then play the remaining samples at the same frequency, the sound is half the length, and we can observe how this changes the pitch.

Thus, for our program, the obvious question becomes: How do we best create a list that includes only every second element from the original samples?

It turns out, slicing comes to the rescue again.

The slicing operator has a third optional parameter which we have not used yet. A full slice definition with this parameter looks like this:

list[start:end:step]

As we have seen previously, the first two parameters specify the beginning and end of the slice we wish to create. The third parameter is a step size: A step size of 2, for example, will select every second element, a step size of 3 selects every third element, and so on.

Consider, for example, the following code:

Strype code

This code snippet will select elements from index 2 to index 9, with step size 2. Thus, it will print

[3, 5, 7, 9]

To fully understand this, we have to recall some details about lists and slices:

  • The first list index is 0, so by using 2 as the start of the slice, we are starting with the third element (not the second).

  • The end index of a slice (we have used 9) is not included in the slice.

  • The step with here is 2, so we get every second element.

Exercise 7.33   Assume a list my_list holds the numbers [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]. What is the list resulting from the expression my_list[2,9,2]? What is my_list[2,9,3]?

Exercise 7.34   Write a short test program to check your answer.

With just one more bit of information, we are ready to solve our problem: We know that we are permitted to leave out the start or end value of a slice. If we leave out the start, the slice starts at the beginning of the list; if we leave out the end value, the slice extends to the end.

The new bit of information is this: When we use a step value in a slice, we are permitted to leave out both the start and the end value. We then select elements at this step interval from the entire list. In other words

Strype code

has the effect that every_second is a new list which holds every second element from our original list, starting with the first: [1, 3, 5, 7].

Since our original goal in this section was exactly this – creating a list with every second sample – this is now very straightforward.

Exercise 7.35   Implement a pitch_up function in your sounds project. Test it.

Concept Slices from lists can include a step value. The syntax is list[start : end : step]. When we use a step value, the slice is created by copying elements from start to end, using the step size to advance. All three slice values are optional. If we want to use a step size across the entire list, we can write list[::step].

7.4.8. Pitch change: half speed

The next obvious thing to do is the opposite: Playing the sounds more slowly to lower the pitch. Once we have understood the pitch-up idea, it is quite obvious how we can do this: We just duplicate every sample in our list so that it appears twice in succession. In effect, the list [3, 7, 4] would become [3, 3, 7, 7, 4, 4]. If we do this to the samples list, the list is twice as long, the sound plays more slowly, and the pitch lowers.

Unfortunately, in this case, there is no ready-made operation in Python that achieves this in a single statement. As a result, we will have to write a loop that iterates through our original samples list and creates the new list we need.

The concept for this is quite straight forward: We iterate through the list of samples, and for every sample, add it twice to a new list:

Strype code

We are starting here with an empty list. The append method adds a single element to the end of the list, and we can see that we are doing it twice to duplicate every element.

We could add both elements in a single statement if we make a short 2-element list from our two samples and then attach this list, using the extend method:

Strype code

We have to be careful here: If we tried to add the [sample, sample] list using the append method, we would not get an error, but it would not do what we intend to do. It would instead add the 2-element list as a single element into our new list, which is not what we need. To add each element separately, we must use the extend method.

Exercise 7.36   The difference between append and extend can be quite subtle. Research and explain the difference between slow.extend([sample, sample]) and slow.append([sample, sample]). Draw a diagram to aid your explanation.

Exercise 7.37   Implement a function to play the sound at half speed.

Concept We can append a single element to a list using the list.append(elem) method. We can append every element from another list by using the list.extend(list2) method.

There are various other interesting sound effect we can create, now that we have seen how changing the samples list can create a new sound. We will suggest a few at the end of this chapter. Before we do this, however, it would be useful to create a simple interface for the user to more easily activate these effects.

code
Figure 7.3: A text-based menu for our sound effects

7.5. A simple text-based menu

In our current implementation, playing the sound effects was hard-coded in our program. Perhaps you wrote your code to play a single effect, disabling the calls that played the previous effects. Or perhaps you tried to play all effects one after the other – in this case, you will have discovered that the program does not actually wait for the sound to finish before continuing, and the sounds likely played overlapping. That is not a great effect.

It would be more useful to let the user choose an effect, in any order they like, from a list of available effects.

Implement such a choice now. Your task it to show a menu on the screen that looks something like the one shown in Figure 7.3.

For the main structure, use a while loop with a boolean variable exit that controls how long the loop continues. Then, in the loop, you can react to key presses, playing sound effects when needed, or setting this variable to True to exit:

Strype code

Exercise 7.38   Write a function to display the menu on screen. Make sure to choose an appropriate name for the function, and to write a good function comment. Call this function from your main code section.

Exercise 7.39   In your 'My code' section, write a loop that reacts to key presses (using the get_key() function from the strype.graphics library). If a specific key is pressed, play the appropriate effect. Do this for all the effects you have implemented.

A version of the program with these exercises implemented is available in sounds-v3.

There are, however, many more experiments that we can do with our sounds. Some things you can try are suggested in the following exercises, but you are alo encouraged to think about your own effects and trying them out.

Exercise 7.40   Implement a change in volume. You can decrease the volume by decreasing the values of each same, for example dividing each sample value by 2 or by 4. You can increase the volume by multiplying the sample values. (However, you should be careful not to increase any value above 1.0, since sample values must be in the range -1 to 1.)

Exercise 7.41   Implement an echo effect. You can do this by chopping off the last half of the sound and attaching it to the end of your original sound multiple times. But each time it is repeated, it should have a lower volume than before.

Exercise 7.42   Give your echo effect function a parameter that specifies how often it should echo. And make sure the reduction in volume is such that the last echo is just about audible.

Exercise 7.43   Give your use a choice of multiple different sounds to apply the effects to.

Exercise 7.44   Experiment with producing sounds from scratch by creating lists of numbers and playing them as sound samples.

Exercise 7.45   Use graphics to make an interface for this program. Use several actors showing icons for effects, and play the effect when the user clicks the icon.

Exercise 7.46   If you feel very adventurous (this is an advanced exercise): Create a visual waveform representation of your sound in your graphics interface, similar to the one used in Strype’s own built-in sound editing dialogue.

7.6. Summary

In this chapter, we have seen some more uses of lists in Python.

First, we have seen that the graphics world uses lists to manage the actors in it, and that we can obtain lists of actors from it (either a list of all actors, or lists of selected actors).

Then we have seen that sounds can also be represented as lists: lists of samples. Samples are numerical values, and by manipulating the list of samples, we can change a sound. Along the way, we have encountered several useful operations on lists, such as joining and repeating lists, and slicing with step values.

Concept summary

  • The graphics system maintains a list of actors. We can obtain a list of all actors, or of selected actors, by using the get_actors() function.

  • A sound is stored as a list of samples. We can get the list of samples from a sound object using the get_samples() method.

  • A sound object can be created from a list of samples by using the statement Sound(sample_list).

  • We can join two lists together to form a single list by using the + operator: list1 + list2.

  • List elements can be repeated multiple times to form a new list by using the * operator: list1 * 3. For example, [0, 1, 2] * 3 will produce [0, 1, 2, 0, 1, 2, 0, 1, 2].

  • Slices from lists can include a step value. The syntax is list[start : end : step]. When we use a step value, the slice is created by copying elements from start to end, using the step size to advance. All three slice values are optional. If we want to use a step size across the entire list, we can write list[::step].

  • We can append a single element to a list using the list.append(elem) method. We can append every element from another list by using the list.extend(list2) method.


1. https://shakespeareoxfordfellowship.org/shakespeare-by-the-numbers-what-stylometrics-can-and-cannot-tell-us