Synthesizer

<< Base Converter | LabTrailIndex | Chapter 1-9 Practice >>

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)

See a Demonstration of the working 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(){
		//your code here

	}
	/**
	 * @return whether or not the buffer is empty (size equals zero)
	 */
	public boolean isEmpty(){
		//your code here
	}
	/**
	 * @return whether or not the buffer full  (size equals capacity)
	 */
	public boolean isFull(){
		//your code here
	}
	/**
	 * 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;
		}
		//your code here
	}
	/**
	 * 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;
		}

		//your code here
	}
	/**
	 * returns (but does not delete) the item from the front
	 * @return the item from the front
	 */
	public double peek(){
		//your code here
	}
	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.
 *
 * @author  (your name)
 * @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;
		newest=n-1;
	}

	public void add(double sample){
		int value = (int)(height*2.0*(sample+0.5));
		oldest=(oldest+1)%n;
		newest=(newest+1)%n;
		values[newest]=value;
	}
	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(){
		//your code here
	}
}

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(){
		// your code here
	}
}

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[68836];
	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.addKeyListener(this);
    		this.requestFocusInWindow();
    		this.setSize(WIDTH, HEIGHT);
    		this.addWindowListener(
    		           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[37];
    		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))]);
        if (key[KeyEvent.VK_NUMPAD0]){ //change sound wave
        	   	type=0;
        	   	for(int i=0; i<37; i++)
        	   		synthKeys[i].setWave(type);
        }
        if (key[KeyEvent.VK_NUMPAD1]){ //change sound wave
	   		type=1;
	   		for(int i=0; i<37; i++)
	   		synthKeys[i].setWave(type);
        }
        if (key[KeyEvent.VK_NUMPAD2]){ //change sound wave
	   		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
        scope.add(sample);
        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;
        }

    }
}