Instructions for danceFaster

(produced at 09:26 a.m. UTC on 2021-09-25)

This task is part of ps03 which is due at 23:59 EDT on 2021-09-28.

You have the option to work with a partner on this task if you wish. Working with a partner requires more work to coordinate schedules, but if you work together and make sure that you are both understanding the code you write, you will make progress faster and learn more.

You can submit this task using this link.

Put all of your work for this task into the file danceFaster.py
(which is provided among the starter files)

For this problem set, you have a choice: you must do tasks 1 and 2, but you only need to do one of tasks 3 and 4. Task 3 is graphics-oriented and works with turtle graphics, while task 4 (this task) is based on audio and music instead, using a new library called wavesynth.

This task will help you practice the material on custom functions and how to use several functions together to achieve a complicated result. It will also have you work with functions that have both side effects and return values.

For this task, you will work with an audio library called wavesynth.py which allows you to create .wav audio files by calling Python functions to specify notes to play. The library does some very simple sound synthesis, and although we're working on improving it, the sound quality is still pretty basic. Because this is a new library, this task may be more challenging than task 3, and you may wish to pick that task instead.

How to use wavesynth.py

Before you get started, you'll need to know how to use the provided wavesynth.py library. The functions in that library have docstrings, so you can read those and/or use the help function on them if you wish; there is also a wavesynth reference page which explains how the library works and how to use it in detail. Here is a shorter summary of the important functions and variables which you'll need for this task:

  1. addNote, addBeat, and addRest. These functions add a note (a musical sound with a specific tone), a beat (a percussive sound without a specific tone) or a rest (a period of silence) to your composition. Note that these functions don't immediately play sounds, instead they add sounds (or silence) to a "track" and later you can use a function like saveTrack or playTrack (the testing code includes both) to save that track to a file or actually play it from within Python (to use playTrack, you'll need to install the simpleaudio module).

    Each of these functions has one parameter: the duration (in seconds) of the note, beat, or rest to add. Things like the volume, pitch, and instrument used are controlled by other functions in wavesynth.py, and are the same for each note/beat/rest added until those other functions are used to change them, just like in turtle, the pen color and size stay the same until changed.

  2. halfStepUp, halfStepDown, stepUp, stepDown, leapUp, and leapDown. These functions increase or decrease the current pitch value by different amounts. Use them in between notes to sequence notes of different pitches. If you're uncertain of how many "steps" you want to use after reading the documentation for these functions, sticking with leapUp and leapDown is probably a safe thing to do, as those intervals create tones that shouldn't ever be too dissonant. Note that you cannot easily mix steps, half-steps, and leaps (i.e., there's no stable formula for how many steps are in a leap, or how many half-steps are in a step; it depends on which note you're stepping or leaping from).

  3. louder and quieter. These functions increase or decrease the current volume level. You can just call them without any arguments, or give them a number to control how much louder or quieter to set the volume.

  4. rewind and fastforward. These functions move forward or backward in time, so that the next note, rest, or beat added to the composition will start earlier or later than it would have normally. rewind in particular can be used to overlap multiple notes to form chords, or to overlap beats with notes to create a melody with drum accompaniment. Each takes a single argument: the number of seconds to move backward or forward in time. Note that normally, when you call addNote, addBeat, or addRest, the time will be moved forward to the end of the note, beat, or rest you added, so that calling those functions one after the other will add notes in sequence.

  5. printTrack, saveTrack, and playTrack. printTrack prints a summary of the notes that have been added to the current track. saveTrack requires a filename and saves the current track as a file with that name in the same folder that your code is in. This will erase any existing file with that name, so be careful! saveTrack (and also playTrack) will usually take a bit of time to finish, often roughly proportional to the actual duration of the track that you're trying to save or play, so be patient. playTrack tries to play the track directly, but to get this to work, you will need to install an extra Python library called simpleaudio, otherwise playTrack will just create an error. In Thonny, you can install simpleaudio using the following steps:

    1. In the "Tools" menu, select the "Manage packages" option.
    2. In the window that pops up, type simpleaudio into the search field and press ENTER.
    3. Information about the simpleaudio package should be displayed; click on the Install button to install it, which should be a fairly quick process. Note that installing simpleaudio is not 100% necessary: you can always just use saveTrack to save your tracks as .wav files and then use any media player program to play them back.

To demonstrate how how these functions can be used together, we've included an example of how to use the library, which produces a very basic sequence of rising and then falling tones.

Now that you've seen an example of how things work, it's time to write some code that uses functions to create music.

The Jig

For this task, you will be writing code to create a classic Irish jig tune (the first part of "The Irish Washerwoman") that involves several repeated phrases (note: the audio can be quite loud, so you may want to turn down your sound first until you can find the right volume):


Click here to download jig.wav

To make the task more interesting, as you can hear in the clip above, each note in the song is slightly shorter than the last, so the pace of the song gradually speeds up as it progresses. To achieve this, the duration of each note that we create will be set to 99% of the duration of the previous note. Over the course of the 48 notes in the song, this will shorten the duration of the last note relative to the first note by a factor of 0.9947, or 0.6236, so the duration of the final note will be about 62.4% of the duration of the first note.

To complete this task, you will need to write a total of seven functions, outlined in the subtasks below. These functions will make use of functions from the wavesynth.py module to create the jig melody.

Note: you must include a non-empty docstring for each function you write.

wavesynth.py Usage

For this task, you will only need the functions addNote, stepUp, stepDown, and setPitch. The setPitch function wasn't explained previously, but it simply sets the current pitch to a specific value, rather than moving it up or down by a certain amount.

The provided starter code also uses playTrack, saveTrack, and printTrack to play and show the results of your code.

Subtask A: threeNotes

The jig tune that we're creating has a lot of identical three-note sequences in it, and so to start with, we'll create a function called threeNotes that allows us to add three notes to the current track at once, and to control their pitch relationships. This will vastly simplify the remaining functions.

threeNotes must accept three parameters (in this order):

  1. The number of steps up between the first and second notes.
  2. The number of steps up between the second and third notes.
  3. The duration to be used for the first note.

Negative values for the first or second parameter will place the second or third notes lower than their preceding notes instead of higher.

threeNotes must call addNote three times to add three notes back-to-back, using the given pitch intervals between them. Since each note will have a shorter duration than the last, the first note will use the provided duration value as-is, but the second note will use 99% of that duration (multiply by 0.99) and the third note will use 99% of the second note's duration. In order for subsequent functions to correctly continue the accelerating pace of the song, threeNotes must also return the duration of the last note that it added.

Finally, although this is not required, to make your life easier, threeNotes should maintain an invariant: by the end of the function, the current pitch should be returned to the initial pitch, even though the last note will often not be at that pitch. Doing this will vastly simplify things when you get to the more complex functions in this task.

We've provided some testing code which uses optimism to test threeNotes. You may want to comment out some of the more advanced tests while you're working on threeNotes, and you can add detailLevel(2) to get the tests to print more details if they're failing.

This example shows what should be printed when threeNotes runs, and also indicates what the return value should be, and includes an audio snippet you can play so you can hear what it should sound like.

Subtask B: Assembling Phrases

The song that we want to play has a few key three-note phrases that repeat themselves. For example, skipping the first two introductory notes, the next three notes have a high-low-low pattern, followed by three more notes in a low-high-high pattern. Those two patterns are followed by an up-down-up pattern, and then a pattern of three falling notes. Then that entire set of four 3-note patterns repeats itself three times (albeit with a shift up in pitch for the middle copy) to form the bulk of the melody.

We could of course write a single function which simply added all of the specific notes of the song, but that wouldn't be a good example of modular problem solving. So instead, we are requiring you to write four functions which capture these four different repeating patterns, plus a combining function that puts them all together so we can repeat them as a unit. Each of the four three-note pattern functions is actually really simple, since we've done a good job of abstraction and defined our versatile threeNotes function first. Here are the functions you need to define:

Once you have defined all four of these simple pattern functions, it's time to define the phrase function, which combines them one after the other to produce a longer phrase. The phrase function must also have one parameter: the duration for the first note it adds. Just like the individual pattern functions, it must add shorter and shorter notes each time, and it must return the duration of the last note it produces. Unlike those functions, it may not call threeNotes directly: instead it must call each of those pattern functions once. Note that your phrase function will have to capture the return values from the functions it calls, then shorten those returned durations by one step (multiplying by 0.99) to use as the initial durations of the subsequent functions it calls. Your ever-reducing duration value will essentially be passed back and forth between phrase and the pattern functions it uses, going in as a parameter, and coming out as a shorter return value, before being shortened and then passed into the next function as a parameter, and so on.

The phrase notes start with whatever the current pitch is as the first note of an upDownDown pattern, then add a downUpUp pattern that begins 5 steps below the initial pitch for the first note, then add a middleLow pattern that starts on the same initial pitch as the first note, and finally include a threeFalling pattern which starts two steps above the initial note pitch.

We have provided a test for your phrase function near the bottom of the starter file. You may want to comment some of it out at first until you get things working. This example shows how phrase should behave.

Note that with only 12 notes in the phrase, the effect of the speedup is pretty subtle and hard to hear: the first note in the example is 0.2 seconds and the last is still 0.179 seconds. The speedup effect will be more pronounced when you play the full song.

Subtask C: The Full Song

After defining your phrase function, you're ready to create the jig function which adds notes for the entire song (well, the first part at least, but that's all we'll require).

The jig function takes one parameter: The duration for the first note. It must produce the following note patterns:

  1. To start things off, there's a two-note lead in, where each note uses 1/2 of the standard note duration value (but we still maintain that full value and shorten in by 1% after each of these half-duration notes. These two notes uses pitches D5 and C5 (use setPitch to set the pitch for the first note at least).
  2. Next, there are three copies of the pattern produced by the phrase function, with the first and third copies starting on the note B4, and the middle copy starting one step higher, on C5 (here again you can use setPitch before calling phrase).
  3. After the three copies of the common phrase, there's a middleLow pattern starting on C5, followed by a novel three-note sequence using pitches A4, D5, and C5, and then an upDownDown pattern starting on B4.
  4. Finally, there's a single ending note with double the standard note duration and pitch G4 (this doubled duration is based on the final shortened duration after the last note of the final upDownDown pattern).

Your jig function should only call addNote when it really needs to (e.g., for the first two notes and the final note), and it should rely on the other pattern functions that you've defined whenever possible (check the rubric for detailed core/extra requirements). Note that just like pattern did, you'll have to use variable(s) to keep track of the decreasing note duration, and capture return values from the functions you call to update this duration properly.

Like all of the other functions in this task, the jig function must return the final shortened duration value that it uses, although unlike the other functions, this won't exactly match the duration of the last note it adds, since that note is a double-length note (meaning: the return value from jig will be half of the duration of the last note that it adds).

We have provided testing code for the jig function at the bottom of the starter file. You will want to comment it out at first until you get things working. This example demonstrates how the jig function should work.

Notes

  • The names of your functions must match the names specified on the rubric, and they must have the exact number of parameters specified for each function.
  • You are allowed to define and call extra functions if you wish, as long as these don't invalidate the rubric rules for which functions must call which other functions (note that there are some restrictions about which functions can call addNote directly, for example).
  • As usual, you must not waste fruit or waste boxes, and you shouldn't define functions inside other functions.
  • The longer a song is, the longer it will take Python to process it when it's asked to play or save the song. Using shorter duration values during testing can help alleviate this problem. Especially when testing jig, expect to wait a few seconds (maybe as many as 10-20) for the audio to play, right before the "done playing the track" message is printed.
  • When checking to make sure that printed output is correct, remember that the free diffchecker tool can be very helpful. In particular, if your notes are close to correct it may be impossible to hear the difference, but the printed output may show that your durations are very slightly wrong. The optimism tests we've provided should also detect this.
  • For grading purposes, your submitted code may not use the print function, since we'll be testing what gets printed in certain cases.
  • Feel free to add further testing using optimism at the end of your file.
  • The durations displayed by wavesynth.py when it prints tracks are rounded off, but your code should not do any rounding.

Examples

Demo

This example provides a demonstration of how to use the wavesynth library functions together to create music. This program is not directly relevant to the current task, but should be helpful in understanding how the wavesynth functions fit together and interact.

In []:
# Import our sound synthesis module from wavesynth import * def quietBeat(duration): ''' Adds a beat, but quieter. The parameter controls the duration. Returns the volume level to the original level before it's done. ''' quieter(4) addBeat(duration) louder(4) # Add notes going up, with beats every 2 notes addNote(0.2) rewind(0.2) quietBeat(0.2) stepUp() addNote(0.2) stepUp() louder(2) # a bit louder addNote(0.2) rewind(0.2) quietBeat(0.2) stepUp() addNote(0.2) quieter(2) # quieter again # Add notes going down stepDown() addNote(0.2) rewind(0.2) quietBeat(0.2) stepDown() addNote(0.2) stepDown() addNote(0.2) rewind(0.2) quietBeat(0.2) # we're back at the original pitch and volume # print, save, and play the track that we created printTrack() saveTrack("updown.wav") playTrack()
Prints
at 0s a 0.2s keyboard note at A3 (60% vol) at 0s a 0.2s snare beat (12% vol) at 0.2s a 0.2s keyboard note at B3 (60% vol) at 0.4s a 0.2s keyboard note at C4 (100% vol) at 0.4s a 0.2s snare beat (20% vol) at 0.6s a 0.2s keyboard note at D4 (100% vol) at 0.8s a 0.2s keyboard note at C4 (44% vol) at 0.8s a 0.2s snare beat (9% vol) at 1s a 0.2s keyboard note at B3 (44% vol) at 1.2s a 0.2s keyboard note at A3 (44% vol) at 1.2s a 0.2s snare beat (9% vol)
Audio

Some notes about this example code:

  • In this example, we used addNote such that all of the notes have exactly the same duration.
  • We used louder and then quieter to emphasize some of the notes.
  • We created a function for adding quieter beats, which we used several times.
  • We used rewind to back up after notes so that simultaneous beats could be added to the composition. There was no need to use fastforward, because the beats we added brought the time position back to the end of the note/beat combo.
  • By the end of the track, the pitch and volume had returned to their original values, because uses of stepUp were balanced by uses of stepDown, and uses of louder were balanced by uses of quieter.
  • One of the library's weak points is that it's not incredibly fast. It may take a few seconds to process the audio before the file can be saved or playback can begin.
  • Another weak point is that the quality of the sounds is pretty basic...

threeNotes example

This example shows how threeNotes should work, including what it should print, and the audio it should produce.

In []:
setPitch(D4) final = threeNotes(-1, -2, 0.2) printTrack() print("Final duration was:", final) playTrack()
Prints
a 0.2s keyboard note at D4 (60% vol) and a 0.198s keyboard note at C4 (60% vol) and a 0.196s keyboard note at A3 (60% vol) Final duration was: 0.19602
Audio

Note that in this example and those that follow, to actually hear the notes you'd have to call playTrack().

phrase example

This example shows how phrase should work, including what it should print, and the audio it should produce.

In []:
setPitch(B3) final = phrase(0.2) printTrack() print("Final duration was:", final) playTrack()
Prints
a 0.2s keyboard note at B3 (60% vol) and a 0.198s keyboard note at G3 (60% vol) and a 0.196s keyboard note at G3 (60% vol) and a 0.194s keyboard note at D3 (60% vol) and a 0.192s keyboard note at G3 (60% vol) and a 0.19s keyboard note at G3 (60% vol) and a 0.188s keyboard note at B3 (60% vol) and a 0.186s keyboard note at G3 (60% vol) and a 0.185s keyboard note at B3 (60% vol) and a 0.183s keyboard note at D4 (60% vol) and a 0.181s keyboard note at C4 (60% vol) and a 0.179s keyboard note at B3 (60% vol) Final duration was: 0.17906765085174328
Audio