Plucking a Guitar String


Goals

Submission

Submit RingBuffer.java, GuitarString.java, GuitarHero.java, and a completed readme_guitar.txt using the submission link on the left. Optional: Submit a fully functional GuitarHeroVisualizer.java for extra credit (described below). If Your GuitarHeroVisualizer requires any additional files, you may submit them in a zip file named extra.zip.

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 (in which a physical description of a musical instrument is used to synthesize sound electronically).

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 -1/2 and +1/2) at N equally spaced points (in time), where N equals the sampling rate (44,100) divided by the fundamental frequency (rounding the quotient up to the nearest integer).

Why it works? 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.

Getting started

Part I: Ringbuffer

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
-----------------------------------------------------------------------------------------
        RingBuffer(int capacity)  // create an empty ring buffer, with given max capacity
    int size()                    // return number of items currently in the buffer
boolean isEmpty()                 // is the buffer empty (size equals zero)?
boolean isFull()                  // is the buffer full  (size equals capacity)?
   void enqueue(double x)         // add item x to the end
 double dequeue()                 // delete and return item from the front
 double peek()                    // return (but do not delete) item from the front

Part II: GuitarString

Next, create a data type to model a vibrating guitar string. Write a class named GuitarString that implements the following API:
public class GuitarString
------------------------------------------------------------------------------------------------------------------------
       GuitarString(double frequency)  // create a guitar string of the given frequency, using a sampling rate of 44,100
       GuitarString(double[] init)     // create a guitar string whose size and initial values are given by the array
  void pluck()                         // set the buffer to white noise
  void tic()                           // advance the simulation one time step
double sample()                        // return the current sample
   int time()                          // return number of tics

Interactive guitar player. GuitarHeroLite.java is a sample GuitarString client that plays the guitar in real-time, using the keyboard to input notes. 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, we play the sum of all string samples. After you've completed RingBuffer and GuitarString, run GuitarHeroLite in order to check to see if everything works properly. You should hear two different pitches corresponding to A and C everytime you press the key.



  public class GuitarHeroLite {
      public static void main(String[] args) {

          // create two guitar strings, for concert A and C
          double CONCERT_A = 440.0;
          double CONCERT_C = CONCERT_A * Math.pow(2, 3.0/12.0);  
          GuitarString stringA = new GuitarString(CONCERT_A);
          GuitarString stringC = new GuitarString(CONCERT_C);

          while (true) {

              // check if the user has typed a key; if so, process it   
              if (StdDraw.hasNextKeyTyped()) {
                  char key = StdDraw.nextKeyTyped();
                  if      (key == 'a') { stringA.pluck(); }
                  else if (key == 'c') { stringC.pluck(); }
              }

              // compute the superposition of samples
              double sample = stringA.sample() + stringC.sample();
  
              // play the sample on standard audio
              StdAudio.play(sample);
  
              // advance the simulation of each guitar string by one step   
              stringA.tic();
              stringC.tic();
          }
       }
  }


Part III: GuitarHero

Write a program GuitarHero that is similar to GuitarHeroLite, but supports a total of 37 notes on the chromatic scale from 110Hz to 880Hz. In general, make the i'th character of the string below play the i'th note.
String keyboard = "q2we4r5ty7u8i9op-[=zxdcfvgbnjmk,.;/' ";
This keyboard arrangement imitates a piano keyboard: The "white keys" are on the qwerty and zxcv rows and the "black keys" on the 12345 and asdf rows of the keyboard.
Piano keyboard

Frequently Asked Questions

I get an ArrayOutOfBounds or NullPointerException error in RingBuffer. What could cause this? Does your constructor correctly initialize all of the instance variables? Did you allocate memory for your array? Did you inadvertently redeclare an instance variable in a method or constructor, thereby shadowing the instance variable with the same name?

How do I round a double up to the nearest int? Rounding up to the nearest int is the same as taking the ceiling; Java provides Math.ceil() for this purpose.

What happens if I call StdAudio.play(x) where x is greater than 1 or less than -1? The value is clipped—it is replaced by the value 1.0 or -1.0, respectively.

I get a Ring buffer underflow error in GuitarHeroLite before I type any keystrokes. Why? Did you forget to initialize the ring buffer to contain N zeros in your GuitarString constructor?

When I run GuitarHeroLite for the first time, I hear no sound. What am I doing wrong? Make sure you have tested with the main() provided for GuitarString. If that works, it is likely something wrong with pluck() since the main() provided for GuitarString does not test that method. To diagnose the problem, print out the values of sample() and check that they become nonzero after you type lower case characters 'a' and 'c'.

When I run GuitarHeroLite, I hear static (either just one click, and then silence or continual static). What am I doing wrong? It's likely that pluck() is working, but tic() is not. The best test is to run the main() provided for GuitarString.

How do I use keyboard.indexOf(key)? If keyboard is a String and key is a character, then keyboard.indexOf(key) return the integer index of the first occurrence of the character key in the string keyboard (or -1 if it does not occur).

Should I hardwire the constants 44,100, 110.0, 440.0, 880.0, and 37 in my program? No, in general, we will deduct if you use an unnamed constant (such as 37) in your program more than once. We recommend using the name SAMPLING_RATE for 44,100 and CONCERT_A for 440. But you need not name all of the constants in the formula 2(i - 24) / 12.

Do I need to follow the prescribed API? Yes, 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 may not add public methods or instance variables to the API; however, you may add private methods (which are only accessible in the class in which they are declared). You may also add private instance variables for data that must be shared between methods.

Extra credit 1.

Write a program GuitarHeroVisualizer.java (by modifying GuitarHero.java) that plots the sound wave in real-time, as the user is playing the keyboard guitar. The output should look something like this, but change over time.
  Sampling from Karplus-Strong
You should not redraw the wave on every sample. Instead, draw the wave of the last n samples every n timesteps for an appropriate value of n. Experiment with different values of n to find one that you think looks good and draws smoothly. There is more than one way to handle the drawing — there is not a "right" way to do this. You may also do a different visualization, as long as it is tied to the audio samples.

Since the Processing library's draw loop cannot run faster than a few hundred times per second, we must use our own while loop to generate the sound samples, as you did in the main part of the assignment. Consequently, we can't use the Processing library for the visualization in this extra credit, and instead must rely on the textbook's StdDraw library.

Therefore, you should use the StdDraw library to complete this extra credit. The textbook contains many examples of how to use it, and the concept of how to use it is not all that different from how we use the Processing library.

Extra credit 2.

Bring your laptop to recitation and perform a piece for your classmates.

Challenge for the bored.

Modify the Karplus-Strong algorithm to synthesize a different instrument. 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. This is a challenge for the bored, so you will not receive extra credit for it. But you may use it as the basis for you visualizer and/or your performance in class. Alexander Strong suggests a few simple variants you can try:

Enrichment.




This assignment was developed by Andrew Appel, Jeff Bernstein, Maia Ginsburg, Ken Steiglitz, Ge Wang, and Kevin Wayne.
Copyright © 2005