M1103 Problem Set 2

The goal of this problem set is to generate audio files from plain text descriptions of songs.

Download the file code.zip and add its content (the files note.cpp, note.h, song.cpp, song.h, test.cpp, asserts.h) to a new Code::Blocks project. Place the .txtfiles in the directory ../songs/ relative to your executable.

You can test your functions by executing the program whose main function is defined in the file test.cpp.

Problem 1 — Notes

A musical note is described by its pitch and its duration, which we describe separately.

The description of the pitch contains a character between A and G, and an octave number. The octave starts at C and ends at B. The following image contains the tones between the C of octave 4 (or C4) and the G of octave 5 (or G5):

Musical notes
Musical notes

These tones correspond to the white keys of a piano:

Keys of a piano
Keys of a piano

We note that the piano also contains black keys. They represent semitones, which are placed between certain white keys. More precisely, there are black keys between every pair of consecutive white keys, except between E and F, and between B and C. To describe a semitone, we take an adjacent tone and modify it with the character # or the character b directly after one of the characters A to G. The character # modifies a tone to the semitone above it. The character b modifies a tone to the semitone below it. For example, the string D#4 describes the semitone between the D and the E of octave 4. The string Eb4 describes the same semitone.

The frequency of a note is measured in Hertz. The frequency of the tone A4 is equal to 440 Hz. When passing from a tone to the same tone one octave higher, we multiply the frequency by 2. The frequency of A5 is thus equal to \(2\times 440\) Hz \(= 880\) Hz. The frequency of A3 is equal to \(440/2\) Hz = \(220\) Hz. More generally, since there are 12 semitones in an octave, the frequency of a note is equal to \[2^{n/12} \times 440 \text{ Hz}\] where \(n\) is the number of semitones from the note to A4 (negative if lower, positive if higher).

For more details, see https://en.wikipedia.org/wiki/Musical_note.

The duration of a note is measured in fractions of a measure (whose duration is determined by the song’s tempo). It is represented by a fraction of the form a/b. For example, the duration of a note whose length is a quarter measure is written 1/4.

The complete representation of a note is that of its pitch, followed by that of its duration, separated by the character @. For example, a note with pitch A4 whose length is a quarter measure is written A4@1/4.

a — Converting a Character to a Semitone

We start by calculating the semitone distance inside an octave to the C of the octave.

Finish the function letterToSemitone which takes a character and returns an integer. The returned integer should be equal to the number of semitones between C and the given character. For example, the character C gives the integer 0 and the character F gives the integer 5.

If the character does not represent a tone, throw an exception of type invalid_argument.

b — Converting a Character and an Accidental to a Semitone

You can use your function letterToSemitone. For that, it is useful to know that you can use a string like an array of characters. In particular, you can access to the length of a string str with str.size() and at its element at index i with str[i].

We will now generalize the conversion to take into account the accidentals # and b, all the while remaining inside of an octave. An example is F#.

Finish the function letterAndSignToSemitone which takes a string and returns an integer. The returned integer should be equal to the number of semitones between C and the given string.

If the string does not represent a semitone, throw an exception of type invalid_argument.

c — Converting a Complete Pitch to a Semitone

To decompose the string str into one part with the letter and the accidental and into a second part with the octave number, the member function substr of the class string will prove useful. You can find its documentation here. For example, the function call str.substr(0,2) returns a string containing the two first characters of the string str.

To transform a string representing an integer into a value of type int, use the function stoi. See its documentation here.

We will now also take into account the octave number and measure the semitones not relatively to the C of the current octave, but relatively to the tone A4. An example is A#3.

Finish the function stringToSemitone which takes a string and returns an integer. The returned integer should be equal to the number of semitones between A4 and the given string.

If the string does not represent a semitone, throw an exception of type invalid_argument.

d — Frequencies

Now that we know the distance in semitones to A4, we can calculate the frequency of a not by using the formula given above. You can use the expression round(440 * exp2((double)semi/12)) to calculate the frequency of a note that is at semi semitones from A4.

Finish the function stringToFrequency which takes a string and returns an integer. The returned integer should be equal to the frequency of the semitone given by the string, rounded to the nearest integer.

If the string does not represent a semitone, throw an exception of type invalid_argument.

e — The Class Note

The class Note encapsulates the data of a note, i.e., the frequency and the duration of a note. We have already treated the calculation of the frequency from a string. To complete a note, it remains to treat its duration.

We will hence write a constructor for the class Note that takes two strings, one for the frequency and one for the duration (e.g., A4 and 1/4). In order to use this data outside of the class, we will return them in the functions frequency() and duration().

Finish the constructor and the two functions described above. The function frequency() should return the frequency of the note in Hz rounded to the nearest integer. The function duration() should return the duration of the note in multiples of eighth measures.

f — Rests

We represent rests in a song by a note with frequency 0. To simplify, we fix the duration of a rest to 1/8. A rest will is by the empty string. We recall that a note is represented by a string of the form A4@1/4.

Finish the constructor of the class Note that takes one string. If the string represents a note, initialize the data members of the object with the given note. If the string is empty, initialize the frequency to 0 and the duration to 1/8.

Problem 2 — Songs

A song is defined by a sequence of notes. We will store them in a vector of notes. This vector will be encapsulated in the class Song. From this sequence of notes, we will generate a waveform to generate an audio file in the .wav format. The goal is to transform a .txt file containing a sequence of descriptions of notes into a .wav file that we can listen to.

Here’s an example of a text description of a part of a song:

G4@1/8
A4@1/8
G4@1/8
F4@1/8
E4@1/8
F4@1/8
G4@1/8

D4@1/8
E4@1/8
F4@1/8

a — Adding Notes

We start by adding a note to a song. For that, we will implement a member function addNote of the class Song to add one note at a time and updating the total duration of the song in eighth measures. The member function duration will return this total duration so that it can be used outside the class.

Finish the functions addNote and duration described above.

b — Waveforms

To generate the waveform of our song in the function waveform, we iterate over the vector of notes and concatenate their individual waveforms which are calculated by the function noteToWaveForm.

Finish the member function waveform described above.

c — Generating Audio Files

In theory, you have now programmed everything that is necessary to generate audio files from the .txt files which are contained in the .zip archive. If you have placed the .txt files in the correct directory, your program will generate .wav files, which you can then listen to in order to verify your solution.

back to the course web site

This work is a derivative of “Music” by David J. Malan of Harvard University, used under CC BY NC SA. This work is licensed under CC BY NC SA.