PS5b

Home Assignments Lecture Blog Resources Discussion Group

Guitar Hero: GuitarString implementation and SFML audio output (part B)

In Part B, we implement the Karplus-Strong guitar string simulation, and generate a stream of string samples for audio playback under keyboard control.

See http://www.cs.princeton.edu/courses/archive/fall13/cos126/assignments/guitar.html for the full assignment.

GuitarString Implementation

Write a class named GuitarString that performs the Karplus-Strong string simulation described in the Princeton assignment.

Our version of the class should implement the following API:

class GuitarString
---------------------------------------------------------------------------------------------------
          GuitarString(double frequency)       // create a guitar string of the given frequency
                                               //   using a sampling rate of 44,100
          GuitarString(vector<sf::Int16> init) // create a guitar string with
                                               //   size and initial values are given by the vector
     void pluck()                              // pluck the guitar string by replacing the buffer
                                               //   with random values, representing white noise
     void tic()                                // advance the simulation one time step
sf::Int16 sample()                             // return the current sample
      int time()                               // return number of times tic was called so far
---------------------------------------------------------------------------------------------------

Notes

  • In the GuitarString private member variables declarations, you must declare a pointer to a RingBuffer rather than declaring a RingBuffer object itself. Then in the GuitarString constructor you must use the new operator.
    • This is because you can't allow the ring buffer to be instantiated until the GuitarString constructor is called at run time (you don't know how big a ring buffer to make until given the frequency of the string).
    • See http://stackoverflow.com/questions/12927169/how-can-i-initialize-c-object-member-variables-in-the-constructor for an explanation.
    • Because the ring buffer contained in the guitar string class will be a pointer to a ring buffer, you'll need to use the dereference operator (*) to get at the ring buffer object itself.
    • Remember to explicitly delete the ring buffer object in the GuitarString's destructor.
  • In the GuitarString(double frequency) constructor, you must using the ceiling function when calculating the size of the ring buffer. See http://www.cplusplus.com/reference/cmath/ceil/ for details.
  • In the pluck method, you must fill the guitar string's ring buffer with random numbers over the sf::Int16 range. sf::Int16 is a short integer, which can hold values from -32768 to 32767.
Here is a snippet of code that can be used to generate a random number in this range: (sf::Int16)(rand() & 0xffff)
  • Also in pluck, the guitar string's ring buffer might already be full. So you should either empty it (by dequeuing values until it's empty), or by deleting it and making a new one which you'll then fill up.
Or, you could add a new method to your ring buffer, empty(), which would set the _first and _last index member variables to 0, and the _full boolean to false. (This would be the most efficient solution.)

Testing your GuitarString implementation

Before you proceed to generate sound, test that your GuitarString is implemented correctly!

Do this by compiling it against this test file: Attach:GStest.cpp.

Build instructions are at the top of the file.

SFML Audio Output

There are two parts of generating audio: (1) getting values out of the GuitarString object and into SFML audio playback object, and (2) playing the audio objects when key press events occur.

These are explained below, followed by demonstration code.

Getting samples out of GuitarString and into SFML Sound

In the sample code provided by Princeton, the stream of values from the GuitarString object is retrieved one value at a time and immediately given to an audio playback object.

This is an elegant solution and allows us to mix together signals from two (or more) GuitarStrings by averaging their values. (This is based on a similar superposition theorem that we used in the 2D physics simulation.)

For SFML, we have to have an existing sf::SoundBuffer that's created with a vector of sound samples. This SoundBuffer is created from a vector of sf::Int16s.

Then we create an sf::Sound object from the sf::SoundBuffer. The sf::Sound object can then be played.

So the whole sequence is:

Playing SFML Sounds when key presses occur

We'll use SFML to create an electronic keyboard:

  • When the “a” key is pressed, a sound corresponding to concert A (440 Hz) should be played.
  • When the “c” key is pressed, a C note should be played.

To handle the keypress events, we'll open an SFML window, and look for sf::Event::KeyPressed events.

When we get one, we'll see if its event.key.code is equal to sf::Keyboard::A or sf::Keyboard::C.

If so, we'll play the appropriate sound.

See the sample code below for how to do this.

GuitarHeroLite.cpp demo file

Here is runnable sample code that when given a correct implementation of GuitarString, will play a 440 Hz A string when the “a” key is pressed, and the corresponding C note when the “c” key is pressed.

In the first half of the code, two GuitarString objects are created (one for each frequency), and each is cranked to produce a stream of audio samples that are loaded into a sf::Int16 vector. Those vectors are made into sf::SoundBuffers, and those are made into playable sf::Sound objects.

In the second half of the code, an SFML window and event loop is set up to play the sounds when the “a” or “c” keys are pressed.

This file may be downloaded here: Attach:GuitarHeroLite.cpp.

// GuitarHeroLite.cpp
// Fred Martin, fredm@cs.uml.edu
// Sun Mar 16 18:51:06 2014
//
// based on Princeton's GuitarHeroLite.java 
// http://www.cs.princeton.edu/courses/archive/fall13/cos126/assignments/guitar.html

#include <iostream>
#include <string>
#include <exception>
#include <stdexcept>
#define _USE_MATH_DEFINES
#include <math.h>
#include <limits.h>
#include <vector>

#include <SFML/Graphics.hpp>
#include <SFML/System.hpp>
#include <SFML/Audio.hpp>
#include <SFML/Window.hpp>

#include "RingBuffer.hpp"
#include "GuitarString.hpp"

using namespace std;

#define CONCERT_A 440.0
#define SAMPLES_PER_SEC 44100

// this makes a vector of <sf::Int16> from the Karplus-Strong string simulation
vector<sf::Int16> makeSamplesFromString(GuitarString gs) {
  std::vector<sf::Int16> samples;

  gs.pluck();
  int duration = 8; // seconds
  int i;
  for (i= 0; i < SAMPLES_PER_SEC * duration; i++) {
    gs.tic();
    samples.push_back(gs.sample());
  }

  return samples;
}

int main()
{

  sf::RenderWindow window(sf::VideoMode(300, 200), "SFML Guitar Hero Lite");
  sf::Event event;
  double freq;
  vector<sf::Int16> samples;

  freq= CONCERT_A;
  GuitarString gs1 = GuitarString(freq);
  sf::Sound sound1;
  sf::SoundBuffer buf1;
  samples = makeSamplesFromString(gs1);
  if (!buf1.loadFromSamples(&samples[0], samples.size(), 2, SAMPLES_PER_SEC))
    throw std::runtime_error("sf::SoundBuffer: failed to load from samples.");
  sound1.setBuffer(buf1);

  freq= CONCERT_A * pow(2, 3.0/12.0);
  GuitarString gs2 = GuitarString(freq);
  sf::Sound sound2;
  sf::SoundBuffer buf2;
  samples = makeSamplesFromString(gs2);
  if (!buf2.loadFromSamples(&samples[0], samples.size(), 2, SAMPLES_PER_SEC))
    throw std::runtime_error("sf::SoundBuffer: failed to load from samples.");
  sound2.setBuffer(buf2);

  while (window.isOpen()) {

    while (window.pollEvent(event)) {

      switch (event.type) {
      case sf::Event::Closed:
	window.close();
	break;

      case sf::Event::KeyPressed:
	switch (event.key.code) {
	case sf::Keyboard::A:
	  sound1.play();
	  break;
	case sf::Keyboard::C:
	  sound2.play();
	  break;
        default;
          break;
	}

      default:
        break;

      }

      window.clear();
      window.display();

    }
  }
  return 0;
}

Your assignment

Once you have your GuitarString class implemented, extend GuitarHeroLite starter code per the Princeton assignment.

Follow the instructions that begin with the statement

“Write a program GuitarHero that is similar to GuitarHeroLite, but supports a total of 37 notes on the chromatic scale from 110Hz to 880Hz.”

Notice the statement

“Don't even think of including 37 individual GuitarString variables or a 37-way if statement! Instead, create an array of 37 GuitarString objects and use keyboard.indexOf(key) to figure out which key was typed.”

For our implementation, we actually need three parallel arrays (please use vectors):

  • a vector of 37 sf::Int16 vectors. Each individual sf::Int16 vector holds the audio sample stream generated by one GuitarString.
  • a vector of 37 sf::SoundBuffers. Each SoundBuffer object contains a vector of audio samples.
  • a vector of 37 sf::Sounds. Each Sound object contains a SoundBuffer. (It's the Sound object that can finally be played.)

You don't need a vector of GuitarStrings. Once you've plucked it and ticed it a bunch of times to get the sound samples out of it—and stored into the Int16 vector—you can throw it away and make a new one for the next frequency.

To turn in

You should turn in the following:

  • Your RingBuffer.cpp and associated RingBuffer.hpp
  • Your GuitarString.cpp and its GuitarString.hpp
  • Your GuitarHero.cpp file
  • A Makefile that builds an executable, or a clear discussion of the dependencies and how to build.
  • A filled-in copy of the ps5b-readme.txt

Turn in via:

submit fredm ps5b files

Grading rubric

FeatureValueComment
GuitarString implementation4 
GuitarString unit tests1evidence that your implementation passes the GStest.cpp tests
GuitarHero player implementation4transforming the Lite version into the full 37-note player per assignment
Makefile1Makefile or explicit build/link instructions included
readme2discussion is expected -- at least mentioning something per section.
Total12