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()