In this lesson, students will create a "Soundboard" App. This application will allow users to create a "song" by pressing the number keys to add a "note." Through this app, students will utilize almost all of coding concepts they have learned throughout this Unit, as well as the new techniques of nested loops and relative indexing.
Students will be able to:
This lesson is designed with the primary purpose of bringing together all the concepts that students have learned throughout this Unit. In particular, the application makes great use of repeat while loops, Arrays, and KeyboardEvents. The app incorporates the techniques that students have already learned, but also builds upon them by introducing slightly more complicated uses. These new techniques, nested loops and relative indexing, are based on the basic concepts that students already know, but simply push them a little further.
Since this application uses many concepts from throughout the Unit, it is a good idea to quickly review them. Don't take too long covering any one of these topics though, but simply remind students of the general usage or format. Below are the most crucial topics that you should consider for review.
In this Activity, students will bring together everything they've learned throughout the Unit to create a new application. To start, they will make a simple "Soundboard" app which lets the user input up to 10 notes, play the stored song, and clear the stored song so they can input a new one. This simple version requires students to use KeyboardEvents, conditional if-statements, Arrays, and repeat while (or repeat until) loops. Next, students refactor the simple version of the app to allow unlimited length songs, as well as an action to calculate the median frequency stored in the song. These new features will introduce the concepts of nested loops and relative indexing to the students. Nested loops are simply loops that are within another loop, and thus these inner loops run through a full set of iterations each time the outer loop runs. Relative indexing is a way to access items stored in an Array by a position relative to another variable, such as using "array:Get(counter + 10)" or "array:Get(counter / 2)" instead of simply "array:Get(counter)."
For today's lesson, we'll be creating a "Soundboard" app, which will let users create a "song" by adding "notes" one by one using the number keys. Below is a template for this program, which contains the blank actions of "PressedKey," "PlaySong," and "ClearSong." We have already completed the "Main" and "CreateGame" actions for you. We will add more actions later, but for now we'll focus on getting a simple version up and running. As with Lesson 14, we recommend that you write your code in a text editor so you can save your work and follow each step without having to scroll up and down repeatedly. Then you can simply copy your code into the online IDEs when it comes time to test the program.
use Libraries.Compute.Math use Libraries.Containers.Array use Libraries.Sound.Audio use Libraries.Sound.AudioSamples use Libraries.Game.Game use Libraries.Interface.Events.KeyboardEvent use Libraries.Interface.Events.KeyboardListener use Libraries.Curriculum.AudioGame.Song use Libraries.Curriculum.AudioGame.AudioGame class Main is AudioGame, KeyboardListener Song song action Main StartGame() end action CreateGame() AddKeyboardListener(me) end action PressedKey(KeyboardEvent event) end action PlaySong end action ClearSong end end
As shown from the template above, we have also provided you a class variable: "Song song." This variable is of a special type created specifically for this lesson, which creates, stores, and plays up to a ten-second long song. We'll explain how this type is used as we use it, so don't worry about not knowing the ins and outs of how it functions.
For this action, we need to write code that creates a sound of different frequency based on the key that was pressed, as well as keys for playing and clearing the song. To start, we'll set up the frequency input.
Next, we need to add a note through the "sound" class variable. To do so, we can use the "GenerateSound" action, which accepts a frequency. However, we also need to make sure that we only call GenerateSound if the user actually pressed a number key. Since frequency starts at 0 before the if/elseif statements, we can check if frequency is still 0 after the if/elseif statements to determine if one of the number keys was pressed. Finally, we need to make sure that the "sound" variable is not already full, since each ConvenientSound object can only hold 10 sounds at once. We can check if it's full through the "IsFull" action, which returns true if it's full, and false if it is not.
While testing your code so far, you were limited to simply playing the sound you were adding only when first creating it. At this stage, we can hardly call it a song. As such, we'll next work on the "PlaySong" action, which will play through all notes stored within the "song" class variable.
Before we tackle the PlaySong action, we need to explain a few more things about the "Song" class. Each Song object has an Array of AudioSamples, which can be obtained from the value-returning action of "GetAudioArray." These AudioSamples can be loaded into an Audio object and then played. Once we obtain the Array of AudioSamples, we can simply use a repeat while loop to play each AudioSample.
Audio audio audio:Load(arrayOfAudioSamples:Get(counter)) QueueAudio(audio)
Now that we have finished the PlaySong action, we need to set up a way for users to call it. To do so, we will return to the "PressedKey" action, where we'll simply add another elseif to the first conditional to call the PlaySong action when the user presses the spacebar.
Note that you don't need to alter the second conditional where we add a note to the "song" variable. This is because we don't change the value of the frequency variable, so it remains at 0 if the spacebar is pressed, and the second conditional is not taken.
At this point, users should be able to input notes into a song, and then press the spacebar to play the stored song. However, what if the user makes a mistake, inputting the wrong note? It's annoying to have to restart the program to start over, so next we'll write the "ClearSong" action. Fortunately, this action is very easy; in fact, it's only one line. To clear the song, we simply need to call the "Clear" action of the "song" class variable. You may think it's silly to have an action for this one line of code, but we will edit it later when we make our application more complicated.
Don't forget, we also need to set up a way for the user to call the ClearSong action while running the program. To do so, we will once again return to the "PressedKey" action and add another elseif to the first conditional to call the ClearSong action when the user presses the escape key.
Although we now have a version up and running, you may have noticed that it's rather limited. In particular, we can only have up to 10 notes at any one time. So, instead of having just one Song object, let's make an Array of them. This will require a bit of refactoring to make our current code work again.
To start, directly under the line of "class Main is AudioGame, KeyboardListener," replace the single Song object with an Array of Song objects.
Next, we need to change the conditional where we call the "GenerateSound" action in the "PressedKey" action. In particular, we need to set up when to add a new Song object to the Array.
Now that we have fixed the PressedKey action, we need to refactor the PlaySong action. To do so, we will need to make use of a new technique called "nested loops." Just like nested conditionals, nested loops are a loop contained within a loop. However, nested loops are a bit more complicated, mostly because of the counter. Since this is a new technique, we've written the action out for you below and will explain how it works (don't worry, you'll get a chance to try it out for yourself soon). Much of this action should look familiar to you, as the nested loop is almost the same as the loop in the original PlaySong action we wrote.
integer outerCounter = 0 repeat while outerCounter < arrayOfSongs:GetSize() integer innerCounter = 0 Song currentSong = arrayOfSongs:Get(outerCounter) Array
arrayOfAudioSamples = currentSong:GetAudioArray() repeat while innerCounter < arrayOfAudioSamples:GetSize() Audio audio audio:Load(arrayOfAudioSamples:Get(innerCounter)) QueueAudio(audio) innerCounter = innerCounter + 1 end outerCounter = outerCounter + 1 end
The first thing to notice is that we have two counters: one for the outside loop, and one for the inside loop. Notice how the declaration for the "innerCounter" is inside of the outside loop. This means that, for every iteration of the outside loop, the "innerCounter" is set to 0, the inner loop runs from 0 to the size of the "arrayOfAudioSamples," and then increments the "outerCounter" by one before running the next iteration of the outer loop. Thus, nested loops tend to run through many more iterations than they may seem to, but it is easily calculated through multiplication. For example, consider that we have 3 Song objects in the "arrayOfSongs" and that each of those Song objects is full (so the "arrayOfAudioSamples" will have 10 elements for all three). On the first iteration of the outside loop with the first Song, the inner loop runs through 10 iterations, one for each element in the AudioSamples Array. The second iteration of the outer loop will have the inner loop go through another 10 iterations, and then the third iteration of the outer loop will have the inner loop go through yet another 10 iterations of the inner loop, for 30 iterations total.
Finally, the last bit of refactoring we need to do is in the "ClearSong" action. We need to rewrite this action entirely, but that's not saying much considering it was only one line. Now that we have an Array of Songs, we can actually clear it without using the "Clear" action. Instead we can simply remove all the items from the Array by repeatedly calling the "RemoveFromFront" action.
Now your program should be completely refactored, and can now our songs can have as many notes as you want. Test your code below and ensure it is working properly. As a side note, be careful about how many individual notess you add to your song. While we have the functionality to have as many notes as we want, it could take a long time for the song to play, and you are locked out from doing anything until the song is finished when you try to play it.
The final functionality we will add to our app is an action to find the Median frequency. This action will put the frequency of each note in the Array of Song into an Array of numbers, sort it, and then calculate the middle value, or median. To start, we'll focus on putting the frequencies into a single Array.
After those two loops, we should have our Array of frequency filled with all of the frequencies in the song. However, to obtain the median, we need to sort the frequencies in order. Fortunately, we can easily do this by calling the "Sort" action, as shown in the following line of code.
Finally, we simply need to get the middle value. This is very easy using a concept called "relative indexing." When accessing items in an Array, we've been using variables like counters and the Array's size, but we can also perform computataions during the "Get" action. For example, suppose you had "array:Get(counter + 10)." In this case, we access the Array element located 10 indexes higher than the counter. You can do any other calculations as well, such as subtraction, multiplication, division, and modulo. While relative indexing is extremely helpful or even essential for certain problems, you need to be careful not to try and obtain an element that doesn't exist. Using the previous example, if our Array had 20 elements in it, then using "Get(counter + 10)" would cause an error for any counter greater than 10, since the last index would be 19. At the same time, a counter of -10 would actually work, as -10 plus 10 gives the valid index of 0.
For this problem, we want the median of our sorted Array of frequencies, so we want to use a variable equal to our Array's size, and then use relative indexing to get the middle value. There are two cases we need to consider: when there are an even number of elements in the Array, and when there are an odd number of elements in the Array. We can determine whether the size variable is even or odd using the modulo operator, which returns the remainder after dividing two numbers. In this case, we'll want to use "size mod 2," which will equal 0 when size is even and will equal 1 when size is odd. When size is even, we'll need to add together the middle two elements and average them, and when size is odd, we'll need to simply get the middle element.
Lastly, we need to return to the "PressedKey" action and map a key to call the Median action. We'll do this by simply adding another elseif to the conditional that compares event:keyCode to event:ENTER. However, within this elseif, we need to add in a nested if statement to see if the "arrayOfSongs" has at least one element in it. It wouldn't make sense to calculate a median on an empty list, after all, so we'll simply avoid making the action call in this case. You should also add an else statement to output an informative error message for the user that the song is empty, and thus has no median. Error messages like this are standard coding practice, since other people running your program probably don't know how it works and would otherwise be confused by unexpected results or program crashes.
And with that, our app is complete! Test it out thoroughly in the IDE below, and feel free to add any additional features, such as other statistics.
If students have had time to brainstorm or create additional features for the app, give them an opportunity to share. Brainstorm other ways that this stored data could be processed, and the types of effects that could be produced as a result. Some students may wish to extend this project on the Practice Create Performance Task they will complete in the next lesson.