Tutorial 13: Making Music
Musical? If not, you soon will be! In this tutorial we will be using the playNote()
function to create our very own music.
We have taken a very brief look at playNote()
before, during the for loops tutorial project. In this project we will going into much more detail.
playNote()
Let's take a quick look at the function and break down the arguments:
playNote( channel, waveType, frequency, volume, speed, pan )
The channel
argument tells FUZE which audio channel to play our desired note on. There are 16 channels to use, giving us up to 16 sounds playing at the same time, these could be single notes, music tracks or sound effects.
The frequency
argument is the frequency of the note we want to play. Another word for frequency is pitch. The higher the frequency, the higher the pith of the note.
Frequency is measured in something called Hertz (hz). This means cycles per second. If your eardrum vibrates at 440hz (a rate of 440 times a second), you will hear the note A! Humans can hear sounds from around 20hz to 20000hz. Anything outside of this range will not be audible to us.
The waveType
argument is the type of waveform we want to use to play the note. There are 5 different types to choose from in FUZE each with a number: Square (0), sawtooth (1), triangle (2), sine (3) and noise (4). Each of these waveforms has a very different sound to our ears. Check them all out!
The volume
argument is simply the loudness of the note. This value should be between 0 and 1, but can be pushed higher if desired.
The speed
argument describes the envelope shape of the note. This one's a little tricky to imagine. A low number in the speed
argument will result in a longer note duration. A higher number will result in a shorter note duration.
Finally, the pan
argument is the stereo position of the sound. Your Nintendo Switch console has two speaker channels, a left and a right. This number is the position of the sound between these channels. A value of 0.5
is right in the centre. A number closer to 0 will result in the sound moving to the left whereas a number closer to 1 will result in the sound moving further to the right.
A Quick Example
Let's plug some values into this function to make a sound play:
1. playNote( 0, 3, 432, 1, 0.5, 0.5 )
2. loop
3. clear()
4. update()
5. repeat
Here we have a small program which plays a note then enters a loop. Notice that the playNote()
line is not in the loop because we only want it to happen once. Our note will ring out for a few seconds before fading out.
Try changing these values to get different results.
note2Freq()
In FUZE we have a very clever function called note2Freq()
. When making music we don't tend to think about things in terms of frequency, but rather in terms of the note name. For example, the note A above middle C (also known as A4) is found at the frequency of 440hz. If we were telling a musician how to play a melody, we wouldn't list the frequencies!
Some time ago, a very clever chap called Dave Smith invented a way of sending data to electronic instruments to tell them which notes to play. This is called MIDI (Musical Instrument Digital Interface). In MIDI, there are 128 notes to choose from, each with a number. The note A4 we mentioned earlier is actually the number 69.
note2Freq()
receives one of these MIDI note numbers and converts it into the correct frequency number. This allows us to do some very helpful things when making music!
Start an empty project before moving on to this next part!
Creating an Array of Notes
If we want to use the names of notes to write music, we'll need to store the MIDI note numbers in memory. We can use a structure to do this, giving each piece of data a helpful name:
1. n = [
2. .c = 60,
3. .cs = 61,
4. .d = 62,
5. .ds = 63,
6. .e = 64,
7. .f = 65,
8. .fs = 66,
9. .g = 67,
10. .gs = 68,
11. .a = 69,
12. .as = 70,
13. .b = 71
14. ]
There we are. Now we have each note we would want to play stored in the properties of a structure called n
for note. We can now use something like n.d
to get the note D for example.
The notes with a letter "s" after them are the in-between notes called "sharps". We could also call these "flats", but their note would have to change. C sharp is the same pitch as D flat, but for the sake of simplicity we've stuck with just sharps.
So now we have a 12 note scale with all the in-between notes. This is called the chromatic scale and with is we can compose pretty much anything we like!
Before we compose a melody, we'll need a couple more variables. Notes have not only a pitch, but a length too. There are specific names for the length values of notes in music terms, we'll be using 4 of them: Semiquavers, quavers, crotchets and minims:
16. semiquaver = 0.25
17. quaver = 0.5
18. crotchet = 1
19. minim = 2
Here we are creating some variables to store the length values of each note type we will use. These are relative to one "beat" - a crotchet is one beat, a quaver is half the length of a crotchet (0.5), and a semiquaver is half the length of a quaver (0.25).
Now let's use these variables to compose a small melody, storing the data for each note we want to play in an array:
21. melody = [
22. [ .note = n.d, .spd = 40, .l = quaver ],
23. [ .note = n.e, .spd = 40, .l = quaver ],
24. [ .note = n.f, .spd = 40, .l = quaver ],
25. [ .note = n.g, .spd = 40, .l = quaver ],
26. [ .note = n.e, .spd = 20, .l = crotchet ],
27. [ .note = n.c, .spd = 40, .l = quaver ],
28. [ .note = n.d, .spd = 20, .l = minim * 4 ]
29. ]
Here's our melody! As you can see, we create an array called melody
which stores 7 structures. Each structure is a note in our melody, giving us a total of 7 notes. Each note has a .note
property which stores the value of the MIDI code retrieved from the notes structure, a .spd
value which will store the speed value for the playNote()
function and finally a .l
value which stores the note length.
Using this format you could write a piece of music very easily! Even if it is quite a bit of typing...
Before we go ahead with playing sound, we must first convert these note length values into real time values. The note length value tells us how long the note should be, not when in time the note should start. There is a clever way of doing this using a for loop:
31. endTime = 0
32. counter = 0
33.
34. for i = 0 to len( melody ) loop
35. temp = melody[i].l
36. melody[i].l = counter
37. counter += temp
38. repeat
39.
40. endTime = counter
First we create two variables. endTime
will eventually store the ending time of the whole melody. counter
will be used to keep track of the note times during the for loop.
Our for loop counts from 0 to the length of the melody
array using an i
variable. For each note in the melody, we store that note's length value in a variable called temp
. We then set that note's length value to be equal to the counter
variable. Since counter
begins at 0, the first note length value becomes 0. Perfect! We want our first note to happen instantly.
We then increase the counter
variable by the number held in the temp
variable, giving us a new point in time which the next note should begin at. This process happens for each note in the melody, converting each note's length value into a correct start time value.
When the for loop is completed, our counter
variable now stores the correct end time for the melody, so we assign the value of counter
to the endTime
variable.
We just need a couple more variables before starting the main loop in which the melody will play.
42. noteCount = 0
43. timer = 0
These two variables are important. noteCount
will be used as the index into the melody
array to play each note in sequence. timer
is a variable which will keep track of the amount of time which has passed. We will use the timer
variable as a trigger to tell FUZE when to move on to the next note in the melody
array.
Playing the Melody
Alright, let's put all of this to use:
45. loop
46. clear()
47.
48. if noteCount < len( melody ) then
49. if timer >= melody[noteCount].l then
50. note = note2Freq( melody[noteCount].note )
51. speed = melody[noteCount].spd
52. playNote( 0, 3, note, 1, speed, 0.5 )
53. noteCount += 1
54. endif
55. endif
56.
57. timer += ( 120 / 60 ) / 60
58.
59. if timer >= endTime then
60. stopChannel( 0 )
61. endif
62.
63. update()
64. repeat
Run the program once you've completed the code to hear the melody in all its glory. Jazzy!
In this loop we have a couple of clever if statements. Before we look at those, take a look at line 57:
57. timer += ( 120 / 60 ) / 60
This line of code is the part responsible for keeping time. The speed of a piece of music is called the tempo. This is measured in beats per minute, or BPM. Our melody is being played at 120 beats per minute.
Our loop is cycling 60 times per second, with the update()
function happening once each time.
To get the tempo right, we must figure out not how many beats per minute must take place, but how many beats per frame.
To achieve this, we take the beats per minute (120) and divide it by 60 to give us the number of beats per second (120 / 60
). We then divide that by the 60 frames happening in one second to give us beats per frame.
All of this means that our timer is now increasing at a rate that will give us 120 beats per minute at 60 frames per second. Clever!
Now let's take a look at the if statements:
48. if noteCount < len( melody ) then
49. if timer >= melody[noteCount].l then
50. note = note2Freq( melody[noteCount].note )
51. speed = melody[noteCount].spd
52. playNote( 0, 3, note, 1, speed, 0.5 )
53. noteCount += 1
54. endif
55. endif
First we check if the noteCount
variable is less than the length of the melody. Next, we check if the timer
variable has reached the start time of the current note in the melody.
If both of these conditions are true, we set a couple of local variables to make our playNote()
line much easier to read. The note
variable stores the frequency value of the note, calculated using the note2Freq()
function we talked about earlier.
The speed
variable stores the .spd
value from the melody
array.
We then use the playNote()
function to play the desired note using a nice soft sine wave on channel 0 with max volume and a central stereo position.
Before we finish the if statement, we increase the value of noteCount
to ensure that we play the next note in the sequence.
That's it!
Try changing the melody, adding your own notes, changing the timings and changing the beats per minute. There's nothing to get wrong here! It's all groovy!
Actually, before you do, let's apply a little reverb to this melody to really make it sound sweeter:
44. setReverb( 0, 8000, 0.5 )
Just before the start of the loop, add the line above to your program. This function sets an amount of reverberation, or echo, to a channel.
The first number in the brackets is a 0 for the channel. Since our melody is played using channel 0, we must make this a 0 to hear any effect!
The next number is the amount of miliseconds before we hear an echo.
The last number is the multiplier applied to the volume of the echo over time. A low number hear will cause a fast volume reduction, whereas a higher number will result in a slower volume reduction. This number can be between 0 and 1.
Now run the program and we should hear quite a difference!
Go ahead and make your own tunes!
Making Your Melody Loop
The program we have written will only play the melody once and then stop all sound from the channel:
59. if timer >= endTime then
60. stopChannel( 0 )
61. endif
With this if statement we check if the timer
variable has reached the value stored in the endTime
variable. If it has, we issue a stopChannel()
command to cease all sound from the channel. If we wanted our melody to loop, we could change this to the following:
59. if timer >= endTime then
60. noteCount = 0
61. timer = 0
62. endif
Now when our timer
reaches endTime
, instead the timer
and noteCount
are reset to 0, starting the whole thing again.
For reference, here is the complete project below, make sure yours is working properly before using it in another project!
1. n = [
2. .c = 60,
3. .cs = 61,
4. .d = 62,
5. .ds = 63,
6. .e = 64,
7. .f = 65,
8. .fs = 66,
9. .g = 67,
10. .gs = 68,
11. .a = 69,
12. .as = 70,
13. .b = 71
14. ]
15.
16. semiquaver = 0.25
17. quaver = 0.5
18. crotchet = 1
19. minim = 2
20.
21. melody = [
22. [ .note = n.d, .spd = 40, .l = quaver ],
23. [ .note = n.e, .spd = 40, .l = quaver ],
24. [ .note = n.f, .spd = 40, .l = quaver ],
25. [ .note = n.g, .spd = 40, .l = quaver ],
26. [ .note = n.e, .spd = 20, .l = crotchet ],
27. [ .note = n.c, .spd = 40, .l = quaver ],
28. [ .note = n.d, .spd = 20, .l = minim * 4 ]
29. ]
30.
31. endTime = 0
32. counter = 0
33.
34. for i = 0 to len( melody ) loop
35. temp = melody[i].l
36. melody[i].l = counter
37. counter += temp
38. repeat
39.
40. endTime = counter
41.
42. noteCount = 0
43. timer = 0
44. setReverb( 0, 8000, 0.5 )
45.
46. loop
47. clear()
48.
49. if noteCount < len( melody ) then
50. if timer >= melody[noteCount].l then
51. note = note2Freq( melody[noteCount].note )
52. speed = melody[noteCount].spd
53. playNote( 0, 3, note, 1, speed, 0.5 )
54. noteCount += 1
55. endif
56. endif
57.
58. timer += ( 120 / 60 ) / 60
59.
60. if timer >= endTime then
61. stopChannel( 0 )
62. endif
63.
64. update()
65. repeat
Playing Multiple Melodies Simultaneously
Aha! So you want to harmonise?
Well, this is very possible. As we mentioned earlier, we have 16 channels to select from. This means we can have 16 melodies all playing at the same time if we want, although that might get a little bit difficult to listen to!
Start a new project file before moving on. We're going to need the same note data from before, so copy the following section into your new project:
1. n = [
2. .c = 60,
3. .cs = 61,
4. .d = 62,
5. .ds = 63,
6. .e = 64,
7. .f = 65,
8. .fs = 66,
9. .g = 67,
10. .gs = 68,
11. .a = 69,
12. .as = 70,
13. .b = 71
14. ]
15.
16. semiquaver = 0.25
17. quaver = 0.5
18. crotchet = 1
19. minim = 2
To make two melodies occur at the same time we simply need to use duplicates of our variables. The best way to do this is to convert the variables like noteCount
and endTime
into arrays. Of course, our melody
array will also need to be placed inside an array. Confusing? Worry not:
21. melodies = [
22. [
23. [ .note = n.d, .spd = 40, .l = quaver ],
24. [ .note = n.e, .spd = 40, .l = quaver ],
25. [ .note = n.f, .spd = 40, .l = quaver ],
26. [ .note = n.g, .spd = 40, .l = quaver ],
27. [ .note = n.e, .spd = 20, .l = crotchet ],
28. [ .note = n.c, .spd = 40, .l = quaver ],
29. [ .note = n.d, .spd = 20, .l = minim * 4 ]
30. ],
31. [
32. [ .note = n.e, .spd = 10, .l = minim ],
33. [ .note = n.a, .spd = 10, .l = minim ],
34. [ .note = n.d, .spd = 10, .l = minim ]
35. ]
36. ]
Here we've added another array of structures to our melodies
array. Let's say we wanted to access the 2nd note of the 2nd melodies array. That would look something like melodies[1][1].note
. If we wanted to access the 7th note of the first melodies array, that would be: melodies[0][6].note
.
Now we must modify the other parts of the code to use these arrays rather than single values. Since we need two separate note counters and end times, these will become small arrays too:
38. endTime = [ 0, 0 ]
39. noteCount = [ 0, 0 ]
40.
41. for i = 0 to len( melodies ) loop
42. counter = 0
43. for j = 0 to len( melodies[i] ) loop
44. temp = melodies[i][j].l
45. melodies[i][j].l = counter
46. counter += temp
47. repeat
48. endTime[i] = counter
49. repeat
Everything about this is exactly the same as before except it happens twice, once for each melody in our array.
We might want our melodies to be played in different wave types to give the sound of different instruments playing. We could use small arrays just like endTime
and noteCount
to store information to apply to each melody:
51. waveType = [ 3, 1 ]
52. octave = [ 24, 12 ]
53. volume = [ 0.3, 0.2 ]
Here we have three small arrays which store the wave type, octave and volume modifier for each melody.
Before we look at the main loop, let's set our timer
variable and the reverb for each channel:
55. setReverb( 0, 8000, 0.5 )
56. setReverb( 1, 8000, 0.5 )
57.
58. timer = 0
Now let's create the main loop.
60. loop
61. clear()
62.
63. for i = 0 to len( melodies ) loop
64. if noteCount[i] < len( melodies[i] ) then
65. if timer >= melodies[i][noteCount[i]].l then
66. note = note2Freq( melodies[i][noteCount[i]].note + octave[i] )
67. speed = melodies[i][noteCount[i]].spd
68. playNote( i, waveType[i], note, volume[i], speed, 0.5 )
69. noteCount[i] += 1
70. endif
71. endif
72. repeat
73.
74. timer += ( 120 / 60 ) / 60
75.
76. if timer >= endTime[0] and timer >= endTime[1] then
77. noteCount[0] = 0
78. noteCount[1] = 0
79. timer = 0
80. endif
81.
82. update()
83. repeat
There we have it! Run the program to hear out two melodies playing simultaneously. Beautiful!
This project will work in just about any scenario you can imagine. The loop we are using would be your main game loop and you might want it to trigger only at certain times. Feel free to use this template for your own projects!
Happy composing and see you in the next tutorial!
For reference, here is the completed project just below in case you struggled to get the sections right. Feel free to start a new project and copy the whole project below:
1. n = [
2. .c = 60,
3. .cs = 61,
4. .d = 62,
5. .ds = 63,
6. .e = 64,
7. .f = 65,
8. .fs = 66,
9. .g = 67,
10. .gs = 68,
11. .a = 69,
12. .as = 70,
13. .b = 71
14. ]
15.
16. semiquaver = 0.25
17. quaver = 0.5
18. crotchet = 1
19. minim = 2
20.
21. melodies = [
22. [
23. [ .note = n.d, .spd = 40, .l = quaver ],
24. [ .note = n.e, .spd = 40, .l = quaver ],
25. [ .note = n.f, .spd = 40, .l = quaver ],
26. [ .note = n.g, .spd = 40, .l = quaver ],
27. [ .note = n.e, .spd = 20, .l = crotchet ],
28. [ .note = n.c, .spd = 40, .l = quaver ],
29. [ .note = n.d, .spd = 20, .l = minim * 4 ]
30. ],
31. [
32. [ .note = n.e, .spd = 10, .l = minim ],
33. [ .note = n.a, .spd = 10, .l = minim ],
34. [ .note = n.d, .spd = 10, .l = minim ]
35. ]
36. ]
37.
38. endTime = [ 0, 0 ]
39. noteCount = [ 0, 0 ]
40.
41. for i = 0 to len( melodies ) loop
42. counter = 0
43. for j = 0 to len( melodies[i] ) loop
44. temp = melodies[i][j].l
45. melodies[i][j].l = counter
46. counter += temp
47. repeat
48. endTime[i] = counter
49. repeat
50.
51. waveType = [ 3, 1 ]
52. octave = [ 24, 12 ]
53. volume = [ 0.3, 0.2 ]
54.
55. setReverb( 0, 8000, 0.5 )
56. setReverb( 1, 8000, 0.5 )
57.
58. timer = 0
59.
60. loop
61. clear()
62.
63. for i = 0 to len( melodies ) loop
64. if noteCount[i] < len( melodies[i] ) then
65. if timer >= melodies[i][noteCount[i]].l then
66. note = note2Freq( melodies[i][noteCount[i]].note + octave[i] )
67. speed = melodies[i][noteCount[i]].spd
68. playNote( i, waveType[i], note, volume[i], speed, 0.5 )
69. noteCount[i] += 1
70. endif
71. endif
72. repeat
73.
74. timer += ( 120 / 60 ) / 60
75.
76. if timer >= endTime[0] and timer >= endTime[1] then
77. noteCount[0] = 0
78. noteCount[1] = 0
79. timer = 0
80. endif
81.
82. update()
83. repeat
Functions and Keywords used in this Tutorial
clear(), else, endIf, for, if, loop, note2Freq(), playNote(), repeat, setReverb, then, update()