// Interference.java (C) 2002 by Paul Falstad, www.falstad.com

import java.io.InputStream;
import java.awt.*;
import java.applet.Applet;
import java.applet.AudioClip;
import java.util.Vector;
import java.util.Hashtable;
import java.util.Enumeration;
import java.awt.image.MemoryImageSource;
import java.lang.Math;
import java.awt.event.*;
import javax.sound.sampled.*;
import java.text.NumberFormat;

class InterferenceCanvas extends Canvas {
    InterferenceFrame pg;
    InterferenceCanvas(InterferenceFrame p) {
	pg = p;
    }
    public Dimension getPreferredSize() {
	return new Dimension(300,400);
    }
    public void update(Graphics g) {
	pg.updateInterference(g);
    }
    public void paint(Graphics g) {
	pg.updateInterference(g);
    }
};

class InterferenceLayout implements LayoutManager {
    public InterferenceLayout() {}
    public void addLayoutComponent(String name, Component c) {}
    public void removeLayoutComponent(Component c) {}
    public Dimension preferredLayoutSize(Container target) {
	return new Dimension(500, 500);
    }
    public Dimension minimumLayoutSize(Container target) {
	return new Dimension(100,100);
    }
    public void layoutContainer(Container target) {
	int barwidth = 0;
	int i;
	for (i = 1; i < target.getComponentCount(); i++) {
	    Component m = target.getComponent(i);
	    if (m.isVisible()) {
		Dimension d = m.getPreferredSize();
		if (d.width > barwidth)
		    barwidth = d.width;
	    }
	}
	Insets insets = target.insets();
	int targetw = target.size().width - insets.left - insets.right;
	int cw = targetw-barwidth;
	int targeth = target.size().height - (insets.top+insets.bottom);
	target.getComponent(0).move(insets.left, insets.top);
	target.getComponent(0).resize(cw, targeth);
	cw += insets.left;
	int h = insets.top;
	for (i = 1; i < target.getComponentCount(); i++) {
	    Component m = target.getComponent(i);
	    if (m.isVisible()) {
		Dimension d = m.getPreferredSize();
		if (m instanceof Scrollbar)
		    d.width = barwidth;
		if (m instanceof Label) {
		    h += d.height/5;
		    d.width = barwidth;
		}
		m.move(cw, h);
		m.resize(d.width, d.height);
		h += d.height;
	    }
	}
    }
};


public class Interference extends Applet {
    InterferenceFrame ff;
    void destroyFrame() {
	if (ff != null)
	    ff.dispose();
	ff = null;
    }
    public void init() {
	ff = new InterferenceFrame(this);
	ff.init();
    }
    public void destroy() {
	if (ff != null)
	    ff.dispose();
	ff = null;
    }
};

class InterferenceFrame extends Frame
  implements ComponentListener, ActionListener, ItemListener,
	     AdjustmentListener,
             MouseMotionListener, MouseListener, Runnable {
    
    Thread engine = null;

    Dimension winSize;
    Image dbimage;
    
    public String getAppletInfo() {
	return "Interference by Paul Falstad";
    }

    InterferenceFrame(Interference a) {
	super("Interference Applet");
	applet = a;
    }
    Interference applet;
    Checkbox soundCheck;
    Checkbox stereoCheck;
    Checkbox metricCheck;
    Scrollbar freqBar;
    Scrollbar phaseBar;
    Scrollbar brightnessBar;
    Scrollbar speakerSepBar;
    Scrollbar scaleBar;
    Scrollbar balanceBar;
    static final double pi = 3.14159265358979323846;
    int dragX, dragY, measureX, measureY;
    boolean dragging;
    boolean java2present;

    InterferenceCanvas cv;
    NumberFormat nf;

    public void init() {
	nf = NumberFormat.getInstance();
	nf.setMaximumFractionDigits(1);

	java2present = true;
	if (System.getProperty("java.version").indexOf("1.1") == 0)
	    java2present = false;
	setLayout(new InterferenceLayout());
	cv = new InterferenceCanvas(this);
	cv.addComponentListener(this);
	cv.addMouseMotionListener(this);
	cv.addMouseListener(this);
	add(cv);

	add(soundCheck = new Checkbox("Sound"));
	soundCheck.addItemListener(this);

	add(stereoCheck = new Checkbox("Stereo"));
	stereoCheck.addItemListener(this);

	add(metricCheck = new Checkbox("Metric Units", true));
	metricCheck.addItemListener(this);

	add(new Label("Speaker Separation", Label.CENTER));
	add(speakerSepBar = new Scrollbar(Scrollbar.HORIZONTAL, 68, 1, 1, 600));
	speakerSepBar.addAdjustmentListener(this);

	add(new Label("Playing Frequency", Label.CENTER));
	add(freqBar = new Scrollbar(Scrollbar.HORIZONTAL, 750, 1, 0, 1100));
	freqBar.addAdjustmentListener(this);

	add(new Label("Phase Difference", Label.CENTER));
	add(phaseBar = new Scrollbar(Scrollbar.HORIZONTAL, 50, 1, 0, 100));
	phaseBar.addAdjustmentListener(this);

	add(new Label("Balance", Label.CENTER));
	add(balanceBar = new Scrollbar(Scrollbar.HORIZONTAL, 50, 1, 0, 100));
	balanceBar.addAdjustmentListener(this);

	add(new Label("Brightness", Label.CENTER));
	add(brightnessBar = new Scrollbar(Scrollbar.HORIZONTAL, 280,
					  1, 1, 1000));
	brightnessBar.addAdjustmentListener(this);

	add(new Label("View Scale", Label.CENTER));
	add(scaleBar = new Scrollbar(Scrollbar.HORIZONTAL, 200, 1, 100, 1000));
	scaleBar.addAdjustmentListener(this);
	balanceBar.disable();
	phaseBar.disable();
	add(new Label("http://www.falstad.com", Label.CENTER));
	reinit();
	cv.setBackground(Color.black);
	cv.setForeground(Color.lightGray);
	resize(550, 415);
	handleResize();
	show();
    }

    void reinit() {
    }
    
    void handleResize() {
        Dimension d = winSize = cv.getSize();
	if (winSize.width == 0)
	    return;
	dbimage = createImage(d.width, d.height);
    }

    int getFreq() {
	return (int) (27.5*java.lang.Math.exp(freqBar.getValue()*.004158883084));
    }

    void doPlay() {
	if (!soundCheck.getState())
	    return;
	final int rate = 44100;
	playSampleCount = precalcSize/2;

	int i;
	byte b[] = new byte[precalcSize];
	double mult = 126;
	double k = getFreq()*2*pi/rate;
	int pd = rate/getFreq();
	double phase = phaseBar.getValue() * 2*pi/100.;
	double multR = balanceBar.getValue()/100.;
	int cycles = (int) (precalcSize/2 *k / (2*pi));
	blockAdder = 2*(int) (cycles*2*pi/k);
	if (!stereoCheck.getState()) {
	    phase = 0;
	    multR = .5;
	}
	double multL = 1-multR;
	double multMax = (multL < multR) ? multR : multL;
	multL /= multMax;
	multR /= multMax;
	for (i = 0; i != playSampleCount; i++) {
	    double q1 = multL*java.lang.Math.sin(i*k);
	    double q2 = multR*java.lang.Math.sin(i*k-phase);
	    b[i*2] = (byte) (q1*mult);
	    b[i*2+1] = (byte) (q2*mult);
	}
	//System.out.print(b[0] + " " + b[2] + " " + b[blockAdder-2] + " " + b[blockAdder] + " " + b[blockAdder+2] + "\n");
	//System.out.print(cycles + " " + blockAdder + "\n");
	AudioFormat format = new AudioFormat(rate, 8, 2, true, false);
	DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
	if (line == null) {
	    try {
		line = (SourceDataLine) AudioSystem.getLine(info);
		line.open(format, bufferSize);
	    } catch (Exception e) {
		e.printStackTrace();
	    }
	    line.start();
	}
	lineBuffer = b;
	start();
	cv.repaint();
    }

    final int bufferSize = 8192;
    final int precalcSize = 16384;
    SourceDataLine line = null;
    byte lineBuffer[];
    int playSampleCount;
    int blockAdder;
    
    void centerString(Graphics g, String s, int y) {
	FontMetrics fm = g.getFontMetrics();
	g.setColor(Color.black);
	int w = fm.stringWidth(s);
	g.fillRect((winSize.width-8-w)/2, y-fm.getAscent(),
		   w+8, fm.getAscent()+fm.getDescent());
	g.setColor(Color.white);
        g.drawString(s, (winSize.width-w)/2, y);
    }

    public void paint(Graphics g) {
	cv.repaint();
    }

    final int gridSize = 100;

    public void updateInterference(Graphics realg) {
	if (!java2present) {
	    centerString(realg, "Need java2 for this applet.", 100);
	    return;
	}
	Graphics g = dbimage.getGraphics();
	if (winSize == null || winSize.width == 0)
	    return;
	g.setColor(cv.getBackground());
	g.fillRect(0, 0, winSize.width, winSize.height);
	g.setColor(cv.getForeground());

	int x, y;
	double k = getFreq()*2*pi/34500.;
	double mult = brightnessBar.getValue()/100.;
	double phase = phaseBar.getValue() * 2*pi/100.;
	//System.out.print(k + " " + phase + "\n");
	int speakerSep = speakerSepBar.getValue();
	int scale = scaleBar.getValue();
	double scaler = scale/(double) gridSize;
	double multR = balanceBar.getValue()/100.;
	if (!stereoCheck.getState()) {
	    multR = .5;
	    phase = 0;
	}
	double multL = 1-multR;
	for (x = 0; x != gridSize; x++)
	    for (y = 0; y != gridSize; y++) {
		int x1 = x*winSize.width/gridSize;
		int y1 = y*winSize.height/gridSize;
		int x2 = (x+1)*winSize.width/gridSize;
		int y2 = (y+1)*winSize.height/gridSize;
		double xx = (x-gridSize/2)*scaler;
		double yy = y*scaler;
		double xx1 = xx+speakerSep/2.;
		double r1 = java.lang.Math.sqrt(xx1*xx1+yy*yy);
		double xx2 = xx-speakerSep/2.;
		double r2 = java.lang.Math.sqrt(xx2*xx2+yy*yy);
		double r1s = multL/r1;
		double r2s = multR/r2;
		r1 *= k;
		r2 *= k;
		double q1 = r1s*java.lang.Math.sin(r1)+
		    r2s*java.lang.Math.sin(r2+phase);
		double q2 = r1s*java.lang.Math.cos(r1)+
		    r2s*java.lang.Math.cos(r2+phase);
		double q = (q1*q1+q2*q2);
		q = java.lang.Math.log(q)/mult+5;
		if (q > 2)
		    q = 2;
		if (q < 0)
		    q = 0;
		int col = 0;
		if (r1s > .1 || r2s > .1)
		    col = 0xFF0000FF;
		else if (q < 1) {
		    int val = (int) (q * 255);
		    col = (255<<24) | (val<<8);
		} else {
		    int val = (int) ((q-1) * 255);
		    col = (255<<24) | (255<<8) | (val * 0x10001);
		}
		g.setColor(new Color(col));
		g.fillRect(x1, y1, x2-x1, y2-y1);
	    }
	int f = getFreq();
	g.setColor(Color.white);
	centerString(g, "Frequency = " + getFreq() + " Hz", winSize.height-100);
	centerString(g, "Wavelength = " + convertUnits(34500/getFreq()),
		     winSize.height-80);
	centerString(g, "Speaker separation = " + convertUnits(speakerSep),
		     winSize.height-60);
	centerString(g, "Phase difference = " + (int) (phase*180/pi) + "\u00b0",
		     winSize.height-40);
	if (dragging) {
	    g.setColor(Color.blue);
	    g.drawLine(dragX, dragY, measureX, measureY);
	    int xdist = measureX-dragX;
	    int ydist = measureY-dragY;
	    double xx = xdist*scaler*gridSize/winSize.width;
	    double yy = ydist*scaler*gridSize/winSize.height;
	    int cm = (int) java.lang.Math.sqrt(xx*xx+yy*yy);
	    g.setColor(Color.white);
	    centerString(g, "Path length = " + convertUnits(cm), winSize.height-20);
	}

	realg.drawImage(dbimage, 0, 0, this);
    }

    String convertUnits(int x) {
	if (metricCheck.getState())
	    return x + " cm";
	return nf.format(x/2.54) + "\"";
    }

    public void componentHidden(ComponentEvent e){}
    public void componentMoved(ComponentEvent e){}
    public void componentShown(ComponentEvent e) {
	cv.repaint();
    }

    public void componentResized(ComponentEvent e) {
	handleResize();
	cv.repaint(100);
    }
    public void actionPerformed(ActionEvent e) {
    }
    public void adjustmentValueChanged(AdjustmentEvent e) {
	System.out.print(((Scrollbar) e.getSource()).getValue() + "\n");
	if (e.getSource() == freqBar || e.getSource() == phaseBar ||
	    e.getSource() == balanceBar)
	    doPlay();
	cv.repaint();
    }
    public void mouseDragged(MouseEvent e) {
	dragging = true;
	measureX = e.getX();
	measureY = e.getY();
	cv.repaint();
    }
    public void mouseMoved(MouseEvent e) {
	if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) != 0)
	    return;
	cv.repaint();
    }
    public void mouseClicked(MouseEvent e) {
    }
    public void mouseEntered(MouseEvent e) {
    }
    public void mouseExited(MouseEvent e) {
    }
    public void mousePressed(MouseEvent e) {
	if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) == 0)
	    return;
	dragging = true;
	measureX = dragX = e.getX();
	measureY = dragY = e.getY();
    }

    public void mouseReleased(MouseEvent e) {
	if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) == 0)
	    return;
	dragging = false;
	cv.repaint();
    }

    public void itemStateChanged(ItemEvent e) {
	if (e.getItemSelectable() == soundCheck || e.getItemSelectable() ==
	    stereoCheck) {
	    doPlay();
	    if (!stereoCheck.getState()) {
		balanceBar.disable();
		phaseBar.disable();
	    } else {
		balanceBar.enable();
		phaseBar.enable();
	    }
	}
	cv.repaint();
    }

    public boolean handleEvent(Event ev) {
        if (ev.id == Event.WINDOW_DESTROY) {
            applet.destroyFrame();
	    soundCheck.setState(false);
            return true;
        }
        return super.handleEvent(ev);
    }
  
    public void start() {
	if (engine == null) {
	    engine = new Thread(this);
	    engine.start();
	}
    }

    public void stop() {
	if (engine != null && engine.isAlive()) {
	    engine.stop();
	}
	engine = null;
    }

    public void run() {
	try {
	    int offset = 0;
	    while (true) {
		if (!soundCheck.getState())
		    break;
		byte q[] = new byte[bufferSize];
		int i;
		int len = (bufferSize+offset > blockAdder)
		    ? blockAdder-offset : bufferSize;
		for (i = 0; i != len; i++)
		    q[i] = lineBuffer[i+offset];
		line.write(q, 0, len);
		offset += len;
		if (offset >= blockAdder)
		    offset = 0;
	    }
	} catch (Exception e) {
	    e.printStackTrace();
	}
	engine = null;
    }
}
