Take-home Assessment #4: Arpeggiator
(Note: this assignment is due on Thursday October 10th instead of our usual Monday deadline.)
In the previous take-home assessment, you built a beat machine, a way to use computers to assist in the process of building rhythmic patterns. Can we use computer technology to similarly build up melodic patterns? In this assessment, we'll build a software-based arpeggiator that does exactly that, practicing our list transformation techniques along the way!
For this take-home assessment, please write your code in the provided starter file:
And turn in your code when you are done!
About arpeggiators
An arpeggio is when the notes of a chord are played sequentially rather than simultaneously. You've likely heard arpeggios whenever you listed to guitar-based music:
Sweeping arpeggios of the sort highlighted in the video take immense amount of skill to develop and execute in a live context. However, most modern keyboards now feature the ability to generate these arpeggios by simply holding down the relevant notes on the keyboard. Additionally, with these devices you can customize the arpeggio to play the notes of the chord in different orders, rhythms, and speeds!
To learn more about arpeggiators, check out this introductory video from Sweetwater:
In Scamper, we can achieve this effect relatively easily using seq to create a sequential pattern and then repeat to repeat that pattern as desired:
(import music)
(define arp-example
(seq (note 58 sn)
(note 61 sn)
(note 65 sn)
(note 68 sn)))
(display arp-example)
(repeat 10 arp-example)
In this assessment, we'll build up a library of functions that allow us to put together patterns of arpeggiated chords in a composable manner, ala functional programming!
Reminder about code style and testing
Now that we introduced the style guidelines for the course, we expect you to follow them for all your programs moving forward! Please utilize appropriate style as outlined by our style guidelines when developing your programs to get the most benefit out of the readability that good style provides.
Additionally, make sure to include appropriate test cases (via test-case) and examples of your functions as required by these instructions.
Please do not include any extraneous output beyond that which we prescribe, so that we can quickly assess your program's correct.
You may include the code that you use, e.g., to debug your program.
However, please make sure it is commented out when you submit your assignment!
Part 1: Chords to notes
In a previous lab, we built up functions to play musical intervals. Similarly, before we dive into the arpeggiator proper, we'll build up functions that help us specify various chords.
Recall that in our system, we specify the pitch of a note with a number. This pitch value corresponds to one of the note values used in western music theory:
For example, the note value 60 corresponds to a "middle C" or a C note at octave 3.
The unit of this "note value" is the semitone the smallest interval between pitches recognized in this system.
- Note value 61 (C#) is one semitone away from C,
- Note value 62 (D) is 2 two semitones away from C,
And so forth. A chord, therefore is a collection of notes separated by pattern of these semitones. For example, a major chord is a collection of notes consisting of:
- A root note value, e.g., note value 60 (middle C)
- A note 4 semitones away, e.g., note value 64 (E)
- A note 7 semitones away, e.g., note value 67 (G)
(import music)
(define root 60)
(seq (note root en)
(note (+ root 4) en)
(note (+ root 7) en))
Other chords feature different patterns of semitones. Common chord types can be found on Wikipedia, linked below:
Problem 1a: chord-notes
To begin, write a function (chord-notes root symbol) that takes:
- A
rootnote specified as a numeric MIDI value and - A valid chord
symbolgiven as a string.
And returns a list of midi note values corresponding to the notes of the specified chord.
Value chord symbols are the "long symbol" names found in the linked Wikipedia article.
These symbols also appear as a list in the starter file:
;;; A list of the chord symbols supported by chord-notes.
;;; Taken from: https://en.wikipedia.org/wiki/Chord_(music)#Examples
(define chord-symbols
(list "maj" "maj6" "dom7" "maj7" "aug" "aug7"
"min" "min6" "min7" "minmaj7" "dim" "dim7"))
Note that we use "maj" to represent a major triad chord which has no long name in the Wikipedia table.
Also note that since it is a precondition of the function to receive a valid chord symbol, you do not need to include an explicit check for when the symbol is not one of the strings in the list above.
The starter file has a fairly comprehensive test suite for this function. However, still write two additional tests for this function demonstrating its correctness.
Problem 1b. chord-symbol->chord
Next, write a function (chord-symbol root symbol dur seq-or-par) that takes
- A
rootnote specified as a numeric MIDI value, - A valid chord
symbolgiven as a string, - A note duration
durthat will be given to each note, and - Either the
seqorparfunctions from the music library.
And returns a complete chord (as a playable composition) specified by the arguments.
Whether seq or par is passed to the seq-or-par argument will determine whether the chord is played as sequential notes, i.e., an arpeggio, or as parallel notes, i.e., a true chord.
In addition to the provided starter test case, write two additional tests for this function demonstrating its correctness.
Problem 1c. all-chords
Finally, with a function capable of creating a single chord, write a function (all-chords root dur seq-or-par) that creates a list of pairs which each pair contains:
- The chord symbol name.
- A composition that plays the chord according to the given parameters.
To do this, utilize the chord-symbols list given to you in the starter code.
Additionally, the pair function from the standard library allows you to create a pair:
(import music)
(pair -1 1)
(pair "maj" (par (note 60 qn)
(note 64 qn)
(note 67 qn)))
You do not need to write tests for this function, but you should use the two example invocations of all-chords given in the starter file to check your work.
Note that since you tested the previous functions thoroughly (and presumably, used them in all-chords in a compositional manner), all-chords is likely already correct!
Nevertheless, you should use these examples to try out your work and hear the different chord sounds!
Part 2: ARP
Next, let's write our arpeggiator functions! These functions will all take two things as input:
- A list of MIDI note values (e.g., generated from
chord-notes) and - A duration value that determines the length of each note,
And will generate an arpeggio, i.e., a composition consisting of the chord notes played sequentially, according to different patterns.
We describe each of the patterns below and give an example of the output.
This output is also reflected in each test-case given in the starter file.
You should also give two additional test cases for each function demonstrating their correctness.
Problem 2a. plain-arp
(plain-arp notes dur) creates an arpeggio from the given notes played "as-is,"
i.e., in sequential order as given in the list.
(import music)
(define notes (list 40 50 60 70 80 90))
; (plain-arp notes qn) --> ...
(seq (note 40 qn) (note 50 qn) (note 60 qn)
(note 70 qn) (note 80 qn) (note 90 qn))
Problem 2b. reverse-arp
(reverse-arp notes dur) creates an arpeggio from the given notes played in reverse order relative to the given list.
(import music)
(define notes (list 40 50 60 70 80 90))
; (reverse-arp notes qn) --> ...
(seq (note 90 qn) (note 80 qn) (note 70 qn)
(note 60 qn) (note 50 qn) (note 40 qn))
Problem 2c. sorted-arp
(sorted-arp notes dur) creates an arpeggio from the given notes played in sorted order according to ascending note value.
(import music)
(define notes (list 60 80 90 40 50 70))
; (sorted-arp notes qn) --> ...
(seq (note 40 qn) (note 50 qn) (note 60 qn)
(note 70 qn) (note 80 qn) (note 90 qn))
Problem 2d. splice-arp
(splice-arp notes dur) creates an arpeggio from the given notes where the second half of the notes are played first and the first half of the notes are played second.
(Hint: list-take and list-drop would be useful here.)
(import music)
(define notes (list 40 50 60 70 80 90))
; (splice-arp notes qn) --> ...
(seq (note 70 qn) (note 80 qn) (note 90 qn)
(note 40 qn) (note 50 qn) (note 60 qn))
Problem 2e. random-arp
(random-arp notes dur) create an arpeggio where the notes are randomly drawn from notes.
The number of notes in the arpeggio is the same as the length of notes.
A note from notes may appear zero or multiple times in the resulting arpeggio.
To implement this function, you will need a new library function that behaves rather simply, but we'll have much more to say about it later in the course:
;;; (random n) -> list? ;;; n: integer?, n >= 0 ;;; Returns a random number in the range 0 to n (exclusive). (random 10) (random 10) (random 10)
If you refresh this page, you'll see that each call to random above will take on a different value in the range 0 through 10 (exclusive)!
Use random in conjunction with list-ref to achieve the effect that random-arp is looking for!
Note that because random-arp generates random output, it is more difficult to test!
One corner test case is provided for you in the starter code to give you a sense of how you might test the function.
Rather than writing additional test-cases, call you function in the file and simply re-run and check the output of your function multiple times to get a sense of whether it is wormking!
Problem 2f. Factoring out the redundancy
At this point, you may have noticed that problem 2f was at the start of the part 2 section in the starter code! Why?
Now that you have written five versions of arp, you should have noticed some significant redundancy in the code you have written!
For this final problem of this part, go back and factor our the redundancy in your arp functions, defining helper functions before the various implementations of your *-arp functions.
Make sure to write doc comments and at least two tests for each function that you write.
(Hint: there is a specific point of redundancy between the various arp functions we want you to factor out in your implementation!
Recall that the way we reduce these redundancies is by creating a generalizing function that captures the shared behavior and allows for variation when it occurs in function parameters.
You will need to do the same here, but the necessary variation isn't value-oriented, it is behavior-oriented!
What type of value could I pass to my general arp function to capture this variation?)
Part 3: Progressions
Our arpeggiator works over individual chords. However, let's now lift this behavior to progressions, i.e., collections of chords. If we think of individual chords as words, then progressions are sentences, conveying complete (musical) thoughts!
With the machinery we've developed in this assessment, we can represent a progression as a list of pairs of root notes and symbol names. For example, the following example chord progression can be found in the starter code.
(import music)
;;; An example chord progression: a (sort-of) ii-V-i in Bb.
(define example-progression
(list (pair 60 "min7")
(pair 65 "dom7")
(pair 60 "dim7")
(pair 58 "min7")))
;;; Each chord of the progression played as true chords
(seq (par (note 60 qn) (note 63 qn) (note 67 qn) (note 70 qn))
(par (note 65 qn) (note 69 qn) (note 72 qn) (note 75 qn))
(par (note 60 qn) (note 63 qn) (note 66 qn) (note 69 qn))
(par (note 58 qn) (note 61 qn) (note 65 qn) (note 68 qn)))
;;; The notes of the progression played sequentially as a collection
;;; of arpeggios
(seq (note 60 sn) (note 63 sn) (note 67 sn) (note 70 sn)
(note 65 sn) (note 69 sn) (note 72 sn) (note 75 sn)
(note 60 sn) (note 63 sn) (note 66 sn) (note 69 sn)
(note 58 sn) (note 61 sn) (note 65 sn) (note 68 sn))
Write a function (progression->notes progression) that takes a progression, a list of pairs of root note values and chord symbols, and returns a list of note values drawn from each chord in the progression in sequential order.
In addition to the test case given in the starter code for example-progression come up with 2 additional test cases for this function.
Additionally, observe in the starter code how we can combine our various arp functions with progression->notes to create some cool musical effects!
Part 4: Freestylin'
Now we have a complete arpeggiator toolkit where we can specify a musical progression and output an arpeggiation of that progression!
To close this assessment, put your tools together to create a musical composition of your design.
The only requirement is that you should combine one or more calls to progression->notes with one or more calls to your arp functions to generate some non-trivial music!
Additionally, you may choose to include percussion, e.g., a simply quarter note bass drum or even your beat machine from the previous assessment, to provide a rhythmic groove!
Include any such new code under the final part in the source file.
You should use display to make your composition playable and include a description of the composition before displaying it via the description function.
Rubric
R or above
- Displays a good faith attempt to complete every required part of the assignment.
M or above
- Includes the specified file,
arpeggiator.scm. - Includes an appropriate header on all submitted files that includes the course, author, etc.
- Code runs in Scamper without errors.
- Documents and names all core procedures correctly.
- Code generally follows style guidelines.
- Code includes required tests.
- Basic functionality of all core functions is present and correct:
- Freestyle composition is present and operational.
E
- Function documentation is complete and appropriately evocative of each function's behavior.
- Code follows style guidelines completely, with at most three minor errors present.
- Code is well-designed, avoiding repeated work through decomposition and appropriate language constructs.
- Each set of tests includes at least one edge case (e.g., an empty list, if appropriate).
- Implementation of all core functions is completely correct.