Guitar Hero
Start: 11/19/2019
Due: 12/06/2019, by the beginning of class
Collaboration: individual
Important links: style guidelines, Checklist, guitar.zip, gradesheet
This assignment was developed by Andrew Appel, Jeff Bernstein, Maia Ginsburg, Ken Steiglitz, Ge Wang, and Kevin Wayne, at Princeton University. Original language was Java using their stdlib
library.
Goal
Write a Kotlin program using the Processing and Minim libraries 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 the physical description of a musical instrument is used to synthesize sound electronically.
Background
We’ll first discuss the theory behind guitar synthesizer.
Digital audio
Before reading the rest of this assignment, please review the lecture note on digital audio.
Simulating plucking of a guitar string
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. 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 –1/2 and +1/2.
- 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 it works?
The two primary components that make the Karplus–Strong algorithm work are the ring buffer feedback mechanism and the averaging operation.
- 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 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.
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.
Tasks
Starting from the skeleton files we’re providing, you are to complete two Kotlin classes to obtain a program that plays guitar interactively. We’ll describe the specifications for the classes in detail below.
Files for this assignment
The file guitar.zip is a zipped gradle project containing these files
src/main/kotlin/GuitarHero.kt
src/main/kotlin/GuitarString.kt
readme.txt
build.gradle.kts
The file src/main/kotlin/GuitarString.kt
is a skeleton code for you to fill out.
The file src/main/kotlin/GuitarHero.kt
actually contains code for GuitarHeroLite.kt
to be described in the next subsection. You are supposed to evolve it into GuitarHero.kt
.
The file readme.txt
as usual contains this project's template for you to fill out and submit along with your Kotlin code.
The file build.gradle.kts
is your gradle build file. Don’t modify it unless you know what you’re doing.
Guitar string
The first thing you should do is add code to this GuitarString
class:
/* file: GuitarString.kt */
@file:Suppress("DEPRECATION")
import kotlin.math.roundToInt
import ddf.minim.*
const val DECAY_FACTOR = 0.996F // energy decay factor
class GuitarString(pitch: Float) : AudioSignal {
// DECLARE THE DATA STRUCTURE FOR RING BUFFER HERE
init {
// CODE FOR THE PRIMARY CONSTRUCTOR GOES HERE
}
// The following two member functions: pluck() & generate()
// are the heart of the Karplus-Strong algorithm.
// Fills the buffer with random noise.
fun pluck() {
// YOUR CODE GOES HERE
}
// Fills the [signal] array with samples from buffer while updating
// the buffer according to KS algorithm.
override fun generate(signal: FloatArray) {
// YOUR CODE GOES HERE
}
// AudioSignal requires both mono and stereo generate functions.
override fun generate(left: FloatArray, right: FloatArray) {
// YOUR CODE GOES HERE
}
}
The class should have 2 properties, a ring buffer and a pointer to its front.
The constructor should create the buffer whose size is determined by the pitch
parameter.
The pluck()
function should replace the items in the ring buffer with random values between −0.5 and +0.5.
Note that the GuitarString
class implements the AudioSignal
interface of the Minim library. This interface requires that the class implement code for two generate
methods
fun generate(signal: FloatArray)
fun generate(left: FloatArray, right: FloatArray)
The generate(signal: FloatArray)
function applies the Karplus-Strong algorithm described in the previous section. It works on the ring buffer, copying its content into the signal
channel (which is a simple float array), while generating new stuff to fill the buffer and moving the front pointer appropriately, until the signal
channel is full.
The code for the generate(left: FloatArray, right: FloatArray)
function is simple. It calls the generate(left)
to fill the left signal channel, and then copies the whole left signal channel into the right channel.
Interactive guitar player
This file GuitarHeroLite.kt
is a sample GuitarString
client that plays the guitar in real time, using the keyboard to input notes. When the user types the letter 'a'
, 'A'
, 'c'
, or 'C'
, the program plucks the corresponding string.
/* file: GuitarHeroLite.kt */
import kotlin.math.pow
import processing.core.*
import ddf.minim.*
const val CONCERT_A = 440.0F
lateinit var out: AudioOutput
lateinit var stringA: GuitarString
lateinit var stringC: GuitarString
fun main(args: Array<String>) {
PApplet.main("GuitarHeroLite")
}
class GuitarHeroLite : PApplet() {
override fun settings() {
size(512, 200)
}
override fun setup() {
background(0)
val minim = Minim(this)
/*
* Gets a line out from Minim, default bufferSize is 1024,
* default sample rate is 44100, bit depth is 16
*/
out = minim.getLineOut(Minim.STEREO)
// Creates 2 guitar strings, one for A and the other for C.
stringA = GuitarString(CONCERT_A)
stringC = GuitarString(CONCERT_A * 2F.pow(3F / 12F))
// Sets default size & alignment of text.
textSize(32F)
textAlign(CENTER)
}
override fun draw() {}
override fun keyPressed() {
background(0)
text(key, width / 2F, height / 2F)
if (key == 'a' || key == 'A')
stringA.pluck()
else if (key == 'c' || key == 'C')
stringC.pluck()
}
}
When you believe you have completed the GuitarString
class, you should immediately try running your program (through gradle). Try hitting keys on the keyboard, especially the a
and c
keys. If your program gives out the A
guitar-plucked note in response to the a
and A
keys, and the C
guitar-plucked note in response to the c
and C
keys, you can proceed to the next step.
Your next step is to evolve GuitarHeroLite.kt
to become GuitarHero.kt
. Your GuitarHero.kt
should differ from GuitarHeroLite.kt
in two respects.
GuitarHero.kt
supports 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 note to highest note:This keyboard arrangement imitates a piano keyboard: The "white keys" are on theval keyboard: String = "q2we4r5ty7u8i9op-[=zxdcfvgbnjmk,.;/' "
qwerty
andzxcv
rows and the "black keys" on the12345
andasdf
rows of the keyboard.keyboard
corresponds to a frequency of 440 × 2 (i − 24) / 12, so that the character'q'
is 110 Hz,'i'
is 220 Hz,'v'
is 440 Hz, and' '
is 880 Hz. Don't even think of including 37 individualGuitarString
variables or a 37-wayif
statement! Instead, create and initialize an array of 37GuitarString
objects and usekeyboard.indexOf(key)
to figure out which key was typed. If a keystroke does not correspond to one of the 37 possible notes, ignore it.The
draw()
method ofGuitarHero.kt
is not empty. It should contain code to animate the sounds the guitar strings are making. To do this,draw()
should display the waveforms of the signals from both the left and right channels of theout
AudioOutput variable.
Extra credit
Write a program AutoGuitar.kt
that will automatically play music using GuitarString
objects. A few ground rules:
- Your program should read the data from a file. Submit an accompanying data file along with your program.
- The duration of your composition must be between 10 and 120 seconds.
- Your program must behave consistently on different machines.
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 the checklist for some concrete ideas.
Gradesheet
We will use this gradesheet when grading your lab.
Submission
Zip up all your Kotlin source code, and the completed readme.txt
file, and submit the zip file via Moodle. You may submit additional .kt
and .txt
files for extra credit too.