function:RenderSidePart pageleftbodycaption pageleftbody sidenote Main.Synthesizer-SideNote Main.SideNote Site.SideNote

# Synthesizer

This project is my adaptation of Kevin Wayne's wonderful nifty project called Guitar Heroine. The first part encourages making the Ring Buffer and testing with JUnit. Next, instead of a Guitar String, a more generic SoundWave super class is made. GuitarString and others can then be subclasses of SoundWave. Students can then experiment with different functions by writing a different strike method (known as the pluck method in Dr. Wayne's version)

Ring Buffer

After a note is struck, something vibrates. The strike or pluck causes a displacement which 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 buffer and adds to the end of the buffer the average of the first two samples, scaled by an energy decay factor of 0.994. Since the ring buffer has a known maximum capacity, implement it using a double array of that length. For efficiency, use cyclic wrap-around: Maintain one integer instance variable first that stores the index of the least recently inserted item; maintain 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 either index equals capacity, make it wrap-around by changing the index to 0. Implement RingBuffer to throw an exception if the client attempts to dequeue() from an empty buffer or enqueue() into a full buffer. # RingBuffer.java

/**
* The RingBuffer models a medium (such as 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 (.994 in this case) models the
* slight dissipation in energy as the wave makes a roundtrip through the medium.
*
*/
public class RingBuffer
{
private double[] buffer;
private int first, last, size;
/**
* Constructor initializes the things needed to make a cyclic wrap-around array
* @param capacity - the sampling rate 44,100 divided by frequency, rounded up to the nearest integer
*/
public RingBuffer(int capacity){
buffer = new double[capacity];
first = 0;
last = 0;
size = 0;
}
/**
* @return the number values in the buffer that are being used
*/
public int size(){

}
/**
* @return whether or not the buffer is empty (size equals zero)
*/
public boolean isEmpty(){
}
/**
* @return whether or not the buffer full  (size equals capacity)
*/
public boolean isFull(){
}
/**
* adds item x to the end.  To insert an item, put it at index last and increment last.
* If list equals the capacity, make it wrap-around by changing the index (last) to 0.
*
* Ideally, it should throw an exception if there is an attempt to enqueue into a
* full buffer.
*
* @param x
*/
public void enqueue(double x){
if (isFull()){
IllegalArgumentException exception =
new IllegalArgumentException("Cannot enqueue to a full buffer");
throw exception;
}
}
/**
* Deletes and return the item from the front
* To remove an item, take it from index first and increment first. If first
* equals the capacity, make it wrap-around by changing the index (first) to 0.
*
* Ideally it should throw an exception if there is an attempt to dequeue from
* an empty buffer.
*
* @return the item from the front.
*/
public double dequeue(){
if (isEmpty()){
IllegalStateException exception =
new IllegalStateException("Cannot dequeue from a empty buffer");
throw exception;
}

}
/**
* returns (but does not delete) the item from the front
* @return the item from the front
*/
public double peek(){
}
public int getFirst(){ return first;}
public int geLast(){ return last;}
}



# JUnit Test



import static org.junit.Assert.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
* The test class RingBufferTest.
*
* @version (a version number or a date)
*/
public class RingBufferTest
{
private RingBuffer ringBuff1;

/**
* Default constructor for test class RingBufferTest
*/
public RingBufferTest()
{
}

/**
* Sets up the test fixture.
*
* Called before every test case method.
*/
@Before
public void setUp()
{

}

/**
* Tears down the test fixture.
*
* Called after every test case method.
*/
@After
public void tearDown()
{
}

@Test
public void TestIsEmpty()
{
RingBuffer ringBuff2 = new RingBuffer(2);
assertEquals(true, ringBuff2.isEmpty());
assertEquals(0, ringBuff2.size());
}

@Test
public void TestIsFull()
{
ringBuff1 = new RingBuffer(2);
ringBuff1.enqueue(1.0);
ringBuff1.enqueue(2.0);
assertEquals(true, ringBuff1.isFull());
assertEquals(2, ringBuff1.size());
assertEquals(1.0, ringBuff1.peek(), 0.0);

}

@Test
public void TestDecueEncue()
{
RingBuffer ringBuff1 = new RingBuffer(3);
ringBuff1.enqueue(1.0);
ringBuff1.enqueue(2.0);
ringBuff1.enqueue(3.0);
assertEquals(3, ringBuff1.size());
assertEquals(true, ringBuff1.isFull());
assertEquals(1.0, ringBuff1.dequeue(), 0.0);
assertEquals(2, ringBuff1.size());
assertEquals(false, ringBuff1.isFull());
ringBuff1.enqueue(4.0);
assertEquals(true, ringBuff1.isFull());
assertEquals(3, ringBuff1.size());
assertEquals(1, ringBuff1.geLast());
assertEquals(2.0, ringBuff1.peek(), 0.0);
}
}



Starter code for the Syth:

# Visualizer.java

import java.awt.Color;
import java.awt.Graphics;

public class Visualizer
{
private int [] values;
private int x,y, width, height;//location and scale of drawing
private int n, oldest, newest; //in of able the data
public Visualizer(int size, int x, int y, int width, int height ){
this.n=size;
this.x = x;
this.y=y;
this.width=width;
this.height=height;
values = new int[n];
for (int i=0; i< n; i++)
values[i]=0;
oldest=0;
}

int value = (int)(height*2.0*(sample+0.5));
oldest=(oldest+1)%n;
}
public void drawLine(Graphics g){
g.setColor(Color.BLACK);
for (int i=0; i<(n-1); i++){
int index=(oldest+i)%(n-1);
g.drawLine(2*i+x, values[index]+y, 2*i+x+2 , values[index+1]+y);
}

}
public void drawBars(Graphics g){
g.setColor(Color.BLACK);
for (int i=0; i<n; i++){
int index=(oldest+i)%n;
g.drawLine(2*i+x, values[index]+y, 2*i+x , y+height);
}
}
}



# PianoKey.java

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Rectangle;

public class PianoKey {
public static final int WIDTH = 25;
public static final int BLACK_WIDTH = 16;

public static final int LENGTH = 150;
private String note;
private char key;
private boolean pressed;
private Color color;
private Rectangle rect;
private SoundWave sound;
private double freq;
public PianoKey(String note, char key, int left, int top, double freq){
pressed=false;
this.note=note;
this.key=key;
this.freq=freq;
setWave(0);
color=Color.WHITE;
rect = new Rectangle(left, top, WIDTH, LENGTH);
if(note.substring(1).equals("b")|| note.substring(1).equals("#")){
color = Color.BLACK;
rect.setSize(BLACK_WIDTH, 3*LENGTH/5);
}
}
/**
* You can add other types here as you define your own SoundWaves
* @param type
*/
public void setWave(int type){
if (type==0)
sound = new SoundWave(freq);
//else if (type==1)
//sound = new KotoWave(freq);
//else
//sound = new NewWave(freq);
}
public void setPressed(boolean p){
if (p && !pressed){
sound.strike();
}
pressed = p;
}
public void draw(Graphics g){
g.setColor(color);
if(pressed)
g.setColor(Color.RED);
g.fillRect(rect.x, rect.y, rect.width, rect.height);
g.setColor(Color.BLACK);
g.drawRect(rect.x, rect.y, rect.width, rect.height);
if (isBlack())
g.drawString(" "+key, rect.x, rect.y-18);
else
g.drawString("  "+key, rect.x, rect.y+LENGTH+18);
}
public boolean isBlack(){
return color.equals(Color.BLACK);
}
public void shiftLocation(){
rect.translate(BLACK_WIDTH/-2, 0);
}
public void strike(){
sound.strike();
}
public void tic(){
sound.tic();
}
public double sample(){
return sound.sample();
}
}



# SoundWave.java


public class SoundWave
{
protected RingBuffer rb;
protected int n;
protected double frequency;
/**
* The first constructor creates a RingBuffer of the desired capacity N
* (sampling rate 44,100 divided by frequency, rounded up to the nearest integer),
* and initializes it to represent a guitar string at rest by enqueueing N zeros.
* @param frequency, eg. 440.0 is concert A
*/
public SoundWave(double frequency){
this.frequency=frequency;
n = (int) (Math.floor(44100.0/frequency)+1);
rb = new RingBuffer(n);
for(int i=0; i <n ; i++)
rb.enqueue(0.0);
}

/**
* Replace the N items in the ring buffer with N values between -0.5 and +0.5.
*/
public void strike(){
for(int i=0; i< n; i++){
double value=0.5* Math.sin(i*2.0*Math.PI*frequency/44100.0);
rb.dequeue();
rb.enqueue(value);
}
}

/**
* Apply the Karplus-Strong update: delete the sample at the front of the ring buffer
* and add to the end of the ring buffer the average of the first two samples,
* multiplied by the energy decay factor.
*
* 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.
*/
public void tic(){
double a = rb.dequeue();
double b = rb.peek();
double value=0.994 * 0.5*(a+b);
rb.enqueue(value);
}
public double sample(){
return rb.peek();
}

}



# KotoWave.java

Here is one of many possible subclasses of SoundWave. Simply write a different strike method. Rather than a sine wave, fill the ring buffer with random values from -1/2 to +1/2


public class KotoWave extends SoundWave
{

public KotoWave(double frequency) {
super(frequency);
}
/**
* Replace the N items in the ring buffer with N random values between -0.5 and +0.5.
*/
public void strike(){
}
}


# NewWave.java

Here is a template for another SoundWave. Rather than a simple sine wave, try {$\frac{1}{5}\sin(2\theta)+\frac{1}{5}\cos(3\theta)$} where {$\theta=i*2\pi*\frac{frequency}{44100.0}$}. You will get different sounds if you use values other than 2 or 3. For further reading, check out http://www.soundonsound.com/sos/feb00/articles/synthsecrets.htm and http://en.wikipedia.org/wiki/Synthesizer

public class NewWave extends SoundWave
{

public NewWave(double frequency) {
super(frequency);
}
/**
* Replace the N items in the ring buffer with N random values between -0.5 and +0.5.
*/
public void strike(){
}
}


# Synth.java


import java.awt.Color;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;

public class Synth extends Frame implements KeyListener
{
public final int WIDTH=625;
public final int HEIGHT=400;
private boolean[] key=new boolean;
private Image img;
private Graphics g0;
private int type=0;
private Visualizer scope;
private PianoKey[] synthKeys;
private String keyboardMap = "q2we4r5ty7u8i9op-[=zxdcfvgbnjmk,.;/' ";
private String notes = "A BbB C C#D EbE F F#G AbA BbB C C#D EbE F F#G AbA BbB C C#D EbE F F#G AbA ";
public Synth(){
this.requestFocusInWindow();
this.setSize(WIDTH, HEIGHT);
new WindowListener() {
public void windowClosing(WindowEvent e) {System.exit(0);}
public void windowClosed(WindowEvent e) {}
public void windowOpened(WindowEvent e) {}
public void windowIconified(WindowEvent e) {}
public void windowDeiconified(WindowEvent e) {}
public void windowActivated(WindowEvent e) {}
public void windowDeactivated(WindowEvent e) {}
});
synthKeys = new PianoKey;
scope = new Visualizer(270, 30, 200, 500, 150);
int next = 25;
for (int i=0; i<37; i++){
char k=keyboardMap.charAt(i);
String n = notes.substring(2*i, 2*i+2);
double freq = 440.0*Math.pow(1.05956, i-24);
synthKeys[i]= new PianoKey(n, k, next, 100, freq);
if (synthKeys[i].isBlack()){
synthKeys[i].shiftLocation();
next-=PianoKey.WIDTH;
}
next+=PianoKey.WIDTH;
}

}
public void paint(Graphics g){
// make a place to draw next image off screen
img = createImage(this.WIDTH, this.HEIGHT);
g0 = img.getGraphics();
// set a background
g0.setColor(Color.LIGHT_GRAY);
g0.fillRect( 0, 0, WIDTH,HEIGHT);
g0.setColor(Color.BLACK);
g0.drawString("Use keys to play, number pad to select instrument", 30, 40);
g0.drawString("sound wave type="+type, 30, 60);
//draw the white piano keys below the black keys
for(PianoKey p:synthKeys)
if (!p.isBlack())
p.draw(g0);
for(PianoKey p:synthKeys)
if (p.isBlack())
p.draw(g0);
// draw the sound wave
g0.setColor(Color.BLUE);
scope.drawBars(g0);
// or else use scope.drawLine(g0);
g.drawImage(img, 0, 0, this);
}
// needed to avoid screen flicker on Windows machines
public void update(Graphics g){
paint(g);
requestFocusInWindow(); //make sure we get the key presses!
}
public void process(){

// check if the user has typed a key, and, if so, process it
for(int i=0; i<37; i++)
synthKeys[i].setPressed(key[Character.toUpperCase(keyboardMap.charAt(i))]);
type=0;
for(int i=0; i<37; i++)
synthKeys[i].setWave(type);
}
type=1;
for(int i=0; i<37; i++)
synthKeys[i].setWave(type);
}
type=2;
for(int i=0; i<37; i++)
synthKeys[i].setWave(type);
}

// compute the superposition of the samples
double sample=0;
for(int i=0; i<37;i++){
// combine the samples
sample+=synthKeys[i].sample();
// advance the simulation of each guitar string by one step
synthKeys[i].tic();
}

// send the result to simple audio
SimpleAudio.play(sample);
// update visual data
repaint();

}
@Override
public void keyPressed(KeyEvent e) {
int keyCode=e.getKeyCode();
if(keyCode>0 && keyCode<key.length){
key[keyCode]=true;
}

}

@Override
public void keyReleased(KeyEvent e) {
int keyCode=e.getKeyCode();
if(keyCode>0 && keyCode<key.length){
key[keyCode]=false;
}

}
@Override
public void keyTyped(KeyEvent e) {}

public static void main(String[] args)
{
Synth app= new Synth();
app.setVisible(true);
while(true){
app.process();
}
}

}


# SimpleAudio.java

import javax.sound.sampled.*;

/**
*  A simplified version of Kevin Wayne's StdAudio
*  http://introcs.cs.princeton.edu/java/stdlib/
*  Copyright (c) 2000-2011, Robert Sedgewick and Kevin Wayne,
*  and are released under the GNU General Public License, version 3 (GPLv3).
*  (see http://www.gnu.org/copyleft/gpl.html)
*
*  The audio format uses a sampling rate of 44,100 (CD quality audio), 16-bit, monaural.
*
*  For additional documentation, see <a href="http://introcs.cs.princeton.edu/15inout">Section 1.5</a> of
*  <i>Introduction to Programming in Java: An Interdisciplinary Approach</i> by Robert Sedgewick and Kevin Wayne.
*
*  and http://docs.oracle.com/javase/tutorial/sound/playing.html the Java tutorial from oracle.
*/
public final class SimpleAudio {

/**
*  The sample rate - 44,100 Hz for CD quality audio.
*/
public static final int SAMPLE_RATE = 44100;

private static final int BYTES_PER_SAMPLE = 2;                // 16-bit audio
private static final int BITS_PER_SAMPLE = 16;                // 16-bit audio
private static final double MAX_16_BIT = Short.MAX_VALUE;     // 32,767
private static final int SAMPLE_BUFFER_SIZE = 4096;

private static SourceDataLine line;   // to play the sound
private static byte[] buffer;         // our internal buffer
private static int bufferSize = 0;    // number of samples currently in internal buffer

// do not instantiate
private SimpleAudio() { }

// static initializer
static { init(); }

// open up an audio stream
private static void init() {
try {
// 44,100 samples per second, 16-bit audio, mono, signed PCM, little Endian
AudioFormat format = new AudioFormat((float) SAMPLE_RATE, BITS_PER_SAMPLE, 1, true, false);
DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);

line = (SourceDataLine) AudioSystem.getLine(info);
line.open(format, SAMPLE_BUFFER_SIZE * BYTES_PER_SAMPLE);

// the internal buffer is a fraction of the actual buffer size, this choice is arbitrary
// it gets divided because we can't expect the buffered data to line up exactly with when
// the sound card decides to push out its samples.
buffer = new byte[SAMPLE_BUFFER_SIZE * BYTES_PER_SAMPLE/3 ];
} catch (Exception e) {
System.out.println(e.getMessage());
System.exit(1);
}

// no sound gets made before this call
line.start();
}

/**
* Close standard audio.
*/
public static void close() {
line.drain();
line.stop();
}

/**
* Write one sample (between -1.0 and +1.0) to standard audio. If the sample
* is outside the range, it will be clipped.
*/
public static void play(double in) {

// clip if outside [-1, +1]
if (in < -1.0) in = -1.0;
if (in > +1.0) in = +1.0;

// convert to bytes
short s = (short) (MAX_16_BIT * in);
buffer[bufferSize++] = (byte) s;
buffer[bufferSize++] = (byte) (s >> 8);   // little Endian

// send to sound card if buffer is full
if (bufferSize >= buffer.length) {
line.write(buffer, 0, buffer.length);
bufferSize = 0;
}

}
}