7. Guitar Hero (Partner Assignment) | COS 126 Spring'22 Search COS 126 Spring'22 COS 126 Spring'22 Syllabus Schedule Assignments Project Resources Exams People Light Dark Automatic 7. Guitar Hero (Partner Assignment) Goals To create user-defined data types in Java. To model and implement digital audio. Specifically to compute sound waveforms using a mathematical model of a musical instrument. To learn about efficient data structures that are crucial for application performance. Getting Started Download the project zip file for this assignment from TigerFile This is a partner assignment. (See instructions on Ed for creating a TigerFile group.) Background For this assignment, you will write a program to simulate plucking a guitar string using the Karplus–Strong algorithm. This algorithm played a seminal role in the emergence of physically modeled sound synthesis (where a physical description of a musical instrument is used to synthesize sound electronically). Approach / Design When a guitar string is plucked, the string vibrates and creates sound. The length of the string determines its fundamental frequency of vibration. We model a guitar string by sampling its displacement (a real number between –½ and +½) at \(n\) equally spaced points in time. The integer \(n\) equals the sampling rate (44,100 Hz) divided by the desired fundamental frequency, rounded up to the nearest integer. Plucking the string. The excitation of the string can contain energy at any frequency. We simulate the excitation with white noise: set each of the \(n\) displacements to a random real number between –½ and +½. The resulting vibrations. After the string is plucked, the string vibrates. The pluck causes a displacement that spreads wave-like over time. The Karplus–Strong algorithm simulates this vibration by maintaining a ring buffer of the \(n\) samples: the algorithm repeatedly deletes the first sample from the ring buffer and adds to the end of the ring buffer the average of the deleted sample and the first sample, scaled by an energy decay factor of 0.996. For example: Why does it work? The two primary components that make the Karplus–Strong algorithm work are the ring buffer feedback mechanism and the averaging operation. (From a mathematical physics viewpoint, the Karplus–Strong algorithm approximately solves the 1D wave equation, which describes the transverse motion of the string as a function of time.) The ring buffer feedback mechanism. The ring buffer models the medium (a string tied down at both ends) in which the energy travels back and forth. The length of the ring buffer determines the fundamental frequency of the resulting sound. Sonically, the feedback mechanism reinforces only the fundamental frequency and its harmonics (frequencies at integer multiples of the fundamental). The energy decay factor (0.996 in this case) models the slight dissipation in energy as the wave makes a round trip through the string. The averaging operation. The averaging operation serves as a gentle low-pass filter (which removes higher frequencies while allowing lower frequencies to to pass). Because it is in the path of the feedback, this has the effect of gradually attenuating the higher harmonics while keeping the lower ones, which corresponds closely to the sound that a guitar string makes when plucked. Implementation Tasks Overall Requirements You must implement three classes: RingBuffer.java GuitarString.java GuitarHero.java We provide two other classes: GuitarHeroLite.java Keyboard.java You MUST follow the prescribed API for each class. We will be testing the methods in the API directly. If your method has a different signature or does not behave as specified, you will lose a substantial number of points. You MUST NOT add public methods to the API; however, you MAY add private instance variables or methods (which are only accessible in the class in which they are declared). RingBuffer.java Your first task is to create a data type to model the ring buffer. Write a class named RingBuffer that implements the following API: public class RingBuffer {
// Creates an empty ring buffer with the specified capacity.
public RingBuffer(int capacity)
// Returns the capacity of this ring buffer.
public int capacity()
// Returns the number of items currently in this ring buffer.
public int size()
// Is this ring buffer empty (size equals zero)?
public boolean isEmpty()
// Is this ring buffer full (size equals capacity)?
public boolean isFull()
// Adds item x to the end of this ring buffer.
public void enqueue(double x)
// Deletes and returns the item at the front of this ring buffer.
public double dequeue()
// Returns the item at the front of this ring buffer.
public double peek()
// Tests this class by directly calling all instance methods.
public static void main(String[] args)
}
Since the ring buffer has a known maximum capacity, you can implement it using a double array of that length. For efficiency, use cyclic wrap-around: maintain an integer instance variable first that stores the index of the least recently inserted item and a second integer instance variable last that stores the index one beyond the most recently inserted item. To insert an item, put it at index last and increment last. To remove an item, take it from index first and increment first. When the value of either index (last, first) equals capacity, make it wrap-around by changing the index to 0. To determine the current number of valid entries in the ring buffer (and whether it is full or empty), you will also need a third integer instance variable size. Illustrating the double array as a ring helps understand the operation of the ring buffer. For example, the diagram below shows a ring buffer with a capacity of four (4), containing a single value. Requirements RingBuffer MUST throw a RuntimeException with a custom message if a client calls either dequeue() or peek() when the ring buffer is empty, or calls enqueue() when the ring buffer is full. The constructor MUST take time at most proportional to the capacity. Every other method MUST take constant time. GuitarString.java Create a data type to model a vibrating guitar string. Write a class named GuitarString that implements the following API: public class GuitarString {
// Creates a guitar string of the specified frequency,
// using a sampling rate of 44,100.
public GuitarString(double frequency)
// Creates a guitar string whose length and initial values
// are given by the specified array.
public GuitarString(double[] init)
// Returns the number of samples in the ring buffer.
public int length()
// Plucks this guitar string by replacing the ring buffer with white noise.
public void pluck()
// Advances the Karplus-Strong simulation one time step.
public void tic()
// Returns the current sample.
public double sample()
// Tests this class by directly calling both constructors
// and all instance methods.
public static void main(String[] args)
}
GuitarHero.java Implement an interactive guitar player. GuitarHeroLite.java is a GuitarString client that plays the guitar in real time. It relies on a helper class Keyboard.java that provides a graphical user interface (GUI) to play notes using the keyboard. When the user types the lowercase letter 'a' or 'c', the program plucks the corresponding string. Since the combined result of several sound waves is the superposition of the individual sound waves, it plays the sum of the two string samples. Requirements Write a program GuitarHero that is similar to GuitarHeroLite. GuitarHero MUST support a total of 37 notes on the chromatic scale from 110 Hz to 880 Hz. Use the following 37 keys to represent the keyboard, from lowest to highest note: String keyboardString = "q2we4r5ty7u8i9op-[=zxdcfvgbnjmk,.;/' ";
This keyboard arrangement imitates a piano keyboard: The white keys are on the qwerty and zxcv rows and the black keys are on the 12345 and asdf rows of the keyboard. Each character i of keyboardString corresponds to a frequency of \( 440 × 2^{(i−24)/12} \) Hz, so that the character 'q' is 110 Hz, 'i' is 220 Hz, 'v' is 440 Hz, and the space character is 880 Hz. You MUST NOT declare 37 individual GuitarString variables and/or utilize 37-way if statements! Instead, create and initialize an array of 37 GuitarString objects and call keyboardString.indexOf(key) to determine which key was played. Possible Progress Steps RingBuffer.java To help you understand the operational semantics of a ring buffer, you should complete the accompanying worksheet. Start with the RingBuffer.java template. You will need to define the instance variables, constructors, and instance methods. We recommend defining the instance variables as follows: private double[] rb; // items in the buffer
private int first; // index for the next dequeue or peek
private int last; // index for the next enqueue
private int size; // number of items in the buffer
Your constructor for RingBuffer will need to allocate and initialize an array of doubles using the new operator. Observe that you have to do this in the constructor (and not when you declare the instance variables) because you do not know the length of the array until the constructor is called. Implement RingBuffer in an incremental and iterative manner: implement one method at a time, together with a corresponding test in main(). In main(), write some tests using small examples, for example, using a RingBuffer of capacity four (4) based on the worksheet. You can implement a private helper method that prints a RingBuffer object, i.e., prints the current values for first, last, and size. This can be very useful to help you debug! After you implement all the methods for RingBuffer, test RingBuffer using the test client in the Testing section. Do not proceed until you have thoroughly tested your RingBuffer data type. GuitarString.java Start with the GuitarString.java template. You will need to fill in the instance variables, constructors, and instance methods. Implement the GuitarString(double frequency) constructor, which creates a RingBuffer of the desired capacity n (the sampling rate 44,100 divided by the frequency, rounded up to the nearest integer), and initializes it to represent a guitar string at rest by enqueuing n zeros. (Use Math.ceil and cast the result to an integer.) Implement the length() method, which returns the number of samples in the ring buffer. Implement the sample() method, which returns the value of the item at the front of the ring buffer. Use peek(). How can I test the constructor and these methods? Instantiate a few different GuitarString objects using the GuitarString(double frequency) constructor. Invoke length() on each GuitarString object. Is the value returned by length() what you expect? Invoke sample() on each GuitarString object. Is the value returned by sample() what you expect? Implement the GuitarString(double[]) constructor, which creates a RingBuffer of capacity equal to the length n of the array, and initializes the contents of the ring buffer to the corresponding values in the array. In this assignment, this constructor’s main purpose is to facilitate testing and debugging. How can I test the constructor and these methods? Instantiate a few different GuitarString objects using the GuitarString(double frequency) constructor. Invoke length() on each GuitarString object. Is the value returned by length() what you expect? Invoke sample() on each GuitarString object. Is the value returned by sample() what you expect? Implement pluck(), which replaces the n items in the ring buffer with n random values between −0.5 and +0.5. Use a combination of RingBuffer methods including dequeue() and enqueue() to replace the buffer with values between −0.5 and 0.5. When generating random values between −0.5 and +0.5, should I include the two endpoints? The assignment specification does not specify, so you are free to choose whichever convention you find most convenient. In Java, we typically include the left endpoint of an interval and exclude the right endpoint. For example, StdRandom.uniform(-0.5, 0.5) generates a uniformly random value in the interval [−0.5, 0.5) and s.substring(i, j) returns the substring of the String s beginning at index i and ending at index j - 1. How can I test the constructor and these methods? Instantiate a GuitarString object. Invoke length() and sample() on this object. Next invoke pluck() on this object. Next invoke length() and sample() again on each object. Are the values returned what you expect? Try this with a few GuitarString objects. Implement tic(). Use the enqueue(), dequeue(), and peek() methods. This method applies the Karplus–Strong update: delete the first sample from the ring buffer and adds to the end of the ring buffer the average of the deleted sample and the first sample, scaled by an energy decay factor. How can I test the constructor and these methods? Instantiate a GuitarString object. Invoke length() and sample() on this object. Next invoke pluck() on this object. Next invoke length() and sample() again on each object. Are the values returned what you expect? Try this with a few GuitarString objects. After you implement the methods for GuitarString, run GuitarString using the test client in the Testing section. GuitarHero.java Test GuitarString and RingBuffer using GuitarHeroLite. Which additional constructor and method does GuitarHeroLite test? To implement GuitarHero.java, use the code in GuitarHeroLite.java as a starting point. Create an array of GuitarString objects. Remember: to create an array of objects, you need to create the array, then you need to create each individual object. Can you play individual notes? Do the notes have the correct frequencies? Can you play a chord? Testing These provided tests only test the functionality of your code. The final main() that you submit should test all the public methods of that data type’s API. RingBuffer.java You can test your RingBuffer data type using the following main(). It enqueues the numbers 1 through \(n\), and then repeatedly dequeues the first two, and enqueues their sum. public static void main(String[] args) {
int n = Integer.parseInt(args[0]);
RingBuffer buffer = new RingBuffer(n);
for (int i = 1; i <= n; i++) {
buffer.enqueue(i);
}
double t = buffer.dequeue();
buffer.enqueue(t);
StdOut.println("Size after wrap-around is " + buffer.size());
while (buffer.size() >= 2) {
double x = buffer.dequeue();
double y = buffer.dequeue();
buffer.enqueue(x + y);
}
StdOut.println(buffer.peek());
}
You can run this test by trying various RingBuffer sizes, for example, for the command line: > java-introcs RingBuffer 10
Size after wrap-around is 10
55.0
> java-introcs RingBuffer 100
Size after wrap-around is 100
5050.0
GuitarString.java You can test GuitarString data type using the following main(). This test case creates a GuitarString with some sample values, and then tics it twenty five times. public static void main(String[] args) {
double[] samples= { 0.2, 0.4, 0.5, 0.3, -0.2, 0.4, 0.3, 0.0, -0.1, -0.3 };
GuitarString testString = new GuitarString(samples);
int m = 25; // number of tics
for (int i = 0; i < m; i++) {
double sample = testString.sample();
StdOut.printf("%6d %8.4f\n", i, sample);
testString.tic();
}
}
Run this test from the command line: > java-introcs GuitarString
0 0.2000
1 0.4000
2 0.5000
3 0.3000
4 -0.2000
5 0.4000
6 0.3000
7 0.0000
8 -0.1000
9 -0.3000
10 0.2988
11 0.4482
12 0.3984
13 0.0498
14 0.0996
15 0.3486
16 0.1494
17 -0.0498
18 -0.1992
19 -0.0006
20 0.3720
21 0.4216
22 0.2232
23 0.0744
24 0.2232
GuitarHero.java An A major chord. Your browser does not support the audio element. i - z i
-
z
An A major scale. Your browser does not support the audio element. i o - [ z d f v v f d z [ - o i
Type the following into your guitar to get the beginning of Led Zeppelin’s Stairway to Heaven. Multiple notes in a column are dyads and chords. Your browser does not support the audio element. w q q
8 u 7 y o p p
i p z v b z p b n z p n d [ i d z p i p z p i u i i
What is this familiar melody? (S == space) nn//SS/ ..,,mmn //..,,m //..,,m nn//SS/ ..,,mmn
Extra Credit - AutoGuitar.java Write a program AutoGuitar.java that will automatically play music using GuitarString objects. Requirements You MAY NOT use office hours for help on design questions. Of course, you may use office hours for debugging purposes. If you are working with a partner, you MUST do this part together or not at all. Your program MUST NOT accept command-line arguments. We will execute your program with the following command: > java-introcs AutoGuitar
Your program MUST NOT use standard input, standard output, or standard drawing. You MAY, however, submit an accompanying .txt file and read it using the In data type. The duration of your composition MUST be between 10 and 120 seconds. Since the sampling rate is 44,100 Hz, this translates to between 441,000 and 5,292,000 calls to StdAudio.play(). Your program MUST NOT have any infinite loops. You MAY create chords, repetition, and phrase structure using loops, conditionals, arrays, and functions. Also, feel free to incorporate randomness. You MAY also create a new music instrument by modifying the Karplus–Strong algorithm; consider changing the excitation of the string (from white noise to something more structured) or changing the averaging formula (from the average of the first two samples to a more complicated rule) or anything else you might imagine. See below for some ideas. You MAY submit additional .java or .txt files to support the extra credit, but do not modify RingBuffer.java, GuitarString.java, or GuitarHero.java. Submission Before submitting the assignment, please ensure you have read the COS 126 Style Guide and that your code follows our conventions. Style is an important component of writing code, and not following guidelines will result in deductions. Submit the files RingBuffer.java, GuitarString.java, GuitarHero.java, and a completed readme.txt file to TigerFile . You may also submit the optional file AutoGuitar.java along with any additional files needed. Enrichment (Click on the to expand and the to collapse the answer.) CHALLENGE: Write a program MidiGuitarHero that is similar to GuitarHero, but works with a MIDI keyboard controller keyboard or a MIDI Controller app running on your smartphone. The MIDI standard - Musical Instrument Digital Interface - is used by many electronic musical instruments. Background You will write a program MidiGuitarHero.java that is similar to GuitarHero.java, but works with a MIDI keyboard controller keyboard or a MIDI keyboard controller app (running on your smartphone or tablet connected to your laptop). MIDI (Musical Instrument Digital Interface) is a technical standard for digital music. More details can be found on the Wikepedia page. Approach The `MidiSource.java class, provided in the assignment project folder, maintains a queue of short MIDI messages. See javax.sound.midi.ShortMessage for details. The basic steps are: Connect a MIDI controller keyboard or app to a computer using USB. See the figure below. Implement a client, MidiGuitarHero, that uses the MidiSource and GuitarString classes. Create an array of GuitarString objects, where each GuitarString object corresponds to a key on a MIDI controller keyboard or app. Create a MidiSource object (with connectToSynth set to false). Start the MidiSource object so it can generate short MIDI messages each time a key is pressed/tapped. A MIDI message contains an integer code that corresponds to a pressed key. Access and interpret each short MIDI message generated. Similarly to GuitarHero, pluck the appropriate string, then sum all the samples, play the resulting sample, then tic all the strings MidiSource.java API public class MidiSource {
// Creates a MidiSource object that listens to the first found connected
// Midi input device. Set verbose to true for verbose output (standard out).
// Set connectToSynth to true use Java’s MIDI player; false to use the
// GuitarString sound
public MidiSource(boolean verbose, boolean connectToSynth)
// Starts the MidiSource object so it can produce messages.
public void start()
// Closes the MidiSource object.
public void close()
// Does the MidiSource have more messages (keys pressed)?
public boolean isEmpty()
// Returns code of the MIDI Controller key pressed
public int nextKeyPressed()
// Demonstrates the MidiSource class - playing from a
// keyboard or a Midi file - using the default Java synthesizer
public static void main(String[] args)
}
Implementation Tasks MidiGuitarHero.java Connect your Midi keyboard controller (via USB) to your laptop - see below for using a smartphone or tablet. On the command line terminal, enter the command: java-introcs MidiSource -p
Press some keys on your MIDI keyboard controller. You should see output similar to: MidiSource version .2
DEVICE 0: Gervill, OpenJDK, Software MIDI Synthesizer, Midi device available, Not a MIDI keyboard controller, trying next...
DEVICE 1: Port1, YAMAHA, YAMAHA PSR-1100 Port1, Midi device available, Valid MIDI controller connected.
ShortMessage: Command: NOTE_ON (144) Channel: 0 Number: 77 Velocity: 21
ShortMessage: Command: NOTE_ON (144) Channel: 0 Number: 77 Velocity: 0
Each time you press a key on the MIDI keyboard controller, a MIDI message is produced. The message contains: the command type the channel the data1 value the date2 value (For details, see javax.sound.midi.ShortMessage) For the most part, you will want to capture messages whose command type is NOTE_ON. The data1 value corresponds to the key number (similarly to the index in the String keyboard in GuitarHero.java) and the data2 value corresponds to the velocity of the pressed key. (Some MIDI controllers generate a NOTE_ON and a velocity value of 0 when releasing a key - you will want to ignore those messages.) The general structure of your code should look like this: Create an array of GuitarString objects. The size of the array depends on how many notes you want to be able to produce. You need to generalize the formula for the frequency of each GuitarString object, i.e., \(440 \times 2^{(k − 24) / 12}\). One way to do this is to determine the key number of A4 (concert A or 440 Hz). For example, here’s an 88-key piano keyboard with A4 (and C4) labeled: Create and start the MidiSource object. Similarly to GuitarHero, write a loop that gets the next short Midi message. Interpret the message, and if it’s a NOTE_ON with a velocity greater than zero (0), pluck the appropriate GuitarString object. Then compute the sum of the samples, play the sum and tic the GuitarString objects. Additional Challenges! When you execute the following command, try pressing other buttons on your Midi keyboard controller. By understanding other MIDI codes (e.g., CONTROL_CHANGE), you can use your Midi keyboard controller to change instruments. Of course, you need to create new music instruments by modifying the Karplus–Strong algorithm. java-introcs MidiSource -p
The MidiSource class can also generate MIDI messages from MIDI files. You can experiment by downloading chopstik.mid and executing the command: java-introcs MidiSource -p chopstik.mid
As you can see, different channels correspond to different instruments. If you have implemented multiple instruments, you can write an MidiAutoGuitar.java that plays your instruments from an existing MIDI file. MIDI Controllers MIDI Controller Keyboard and USB Almost any MIDI keyboard controller with a USB interface will suffice (for example, a 25-key MIDI keyboard controller can be purchased on Amazon for about $40). Connect the MIDI keyboard to your laptop. In the terminal window, execute the command: java-introcs MidiSource
Press the keys on the keyboard to see the MIDI messages (numbers) generated by each key. iPhone with iOS 11, Mac OS and USB Download/install the MIDI controller app from the Apple App Store. Connect your iPhone/iPad to your Mac laptop using the USB connector. On your Mac: Start the Audio Midi Setup application (in the Applications/Utilities folder) Under Window/Show Audio Devices and enable your iPhone In the terminal window on your Mac laptop, execute the command: java-introcs MidiSource
Start the MIDI controller app. Press the keys on the MIDI Controller app to see the MIDI messages (numbers) generated by each key. Android and USB Download/install/start the MIDI controller app from the Google Play Store. Connect your Android device to your laptop using the USB connector. On your Android device: Show the notification screen by dragging down from the top of the display. Tap Use device MIDI. (Different versions of Android may require different settings.) In the terminal window on your laptop, execute the command: java-introcs MidiSource
On the MIDI Controller app, tap Receivers for Keys and select Android USB Peripheral Port. Press the keys on the MIDI Controller app to see the MIDI messages (numbers) generated by each key. Synthesizing Additional Instruments Here are some approaches for synthesizing additional instruments. Some come from the paper of Karplus and Strong. Harp strings: Flipping the sign of the new value before enqueueing it in tic() will change the sound from guitar-like to harp-like. You may want to play with the decay factors to improve the realism, and adjust the buffer sizes by a factor of two since the natural resonance frequency is cut in half by the tic() change. Drums: Flipping the sign of a new value with probability 0.5 before enqueueing it in tic() will produce a drum sound. A decay factor of 1.0 (no decay) will yield a better sound, and you will need to adjust the set of frequencies used. Guitars play each note on one of six physical strings. To simulate this you can divide your GuitarString instances into six groups, and when a string is plucked, zero out all other strings in that group. Pianos come with a damper pedal which can be used to make the strings stationary. You can implement this by changing the decay factor on iterations where a certain key (such as Shift) is held down. While we have used equal temperament, the ear finds it more pleasing when musical intervals follow the small fractions in the just intonation system. For example, when a musician uses a brass instrument to play a perfect fifth harmonically, the ratio of frequencies is \( 3/2 = 1.5 \) rather than \(2^{7/12} ∼ 1.498 \). Write a program where each successive pair of notes has just intonation. Related Work ChucK. ChucK is a specialized programming language for real-time synthesis, composition, and performance originated by Ge Wang and Perry Cook at Princeton University. Here’s the Karplus–Strong algorithm in ChucK. Slide flute. Here’s a description of a physically modeled slide flute by Perry Cook. Electric guitar synthesis. In COS 325/MUS 315 Transforming Reality by Computer, Charlie Sullivan ‘87 extended the Karplus–Strong algorithm to synthesize electric guitar timbres with distortion and feedback. Here’s his Jimi Hendrix-ified version of The Star Spangled Banner. This assignment was developed by Andrew Appel, Jeff Bernstein, Alan Kaplan, Maia Ginsburg, Ken Steiglitz, Ge Wang, and Kevin Wayne. Copyright © 2005–2021 Published with Wowchemy — the free, open source website builder that empowers creators. Cite × Copy Download