/**
 * sommesinus.java	02/01/10 - 05/01/11 - 09/08/22 - 02/09/22
 * auteur : Jean-Paul Quelen
 *
 * fait la somme de fonctions sinusoïdales de fréquence f, 2f, 3f...
 * trace la courbe correspondante
 * et génère le son.
 */

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.nio.*;
import java.util.*;
import javax.sound.sampled.*;
import javax.swing.*;

public class sommesinus extends JFrame implements ActionListener {
	static final long serialVersionUID = 220902L;
	double frequence, duree, pas;
	double[] harmoniques, phases;
	JButton ok, plus, moins, son, enregistrer;
	static SourceDataLine auline;
	JTextField tfrequence, tduree, tharmoniques, tphases;
	dessin graphe;
	int zoom = 1;
	int nh = -1;
	int frequenceech = 44100;
	short[] valechantillons;
	short[][] sinusoides;
	double pis180 = Math.PI / 180.0;
	String erreur;

	public sommesinus(String titre) {
		super (titre);
		setFont (new Font ("sans-serif", Font.PLAIN, 10));
		setLayout (new BorderLayout());
		JPanel p = new JPanel();
		p.setBackground (Color.LIGHT_GRAY);
		add (p, BorderLayout.NORTH);
		p.add (new JLabel ("Fréquence :"));
		p.add (tfrequence = new JTextField ("440.0", 10));
		p.add (new JLabel ("Durée (en s) :"));
		p.add (tduree = new JTextField ("1.0", 10));
		p.add (new JLabel ("Amplitudes :"));
		p.add (tharmoniques = new JTextField ("0.6 0.4", 10));
		p.add (new JLabel ("Phases en ° :"));
		p.add (tphases = new JTextField ("0.0 0.0", 10));
		p.add (plus = new JButton ("+"));
		plus.addActionListener (this);
		p.add (moins = new JButton ("-"));
		moins.addActionListener (this);
		p.add (ok = new JButton ("ok"));
		ok.addActionListener (this);
		p.add (son = new JButton ("son"));
		son.addActionListener (this);
		p.add (enregistrer = new JButton ("enregistrer"));
		enregistrer.addActionListener (this);
		add (graphe = new dessin(), BorderLayout.CENTER);
		enregistrer.setVisible (false);
	}

	private double parametre (JTextField tf, double d) {
		double dd = d;
		try {
			dd = Double.parseDouble (tf.getText());
		}
		catch (NumberFormatException e) {}
		if (dd > 0.0)
			d = dd;
		tf.setText (Double.toString (d));
		return d;
	}

	public void actionPerformed (ActionEvent evt) {
		if ((evt.getSource() == ok) || (evt.getSource() == son)) {
			frequence = parametre (tfrequence, frequence);
			duree = parametre (tduree, duree);

// lectures des harmoniques

			StringTokenizer st = new StringTokenizer (tharmoniques.getText(), " ", false);
			nh = st.countTokens();
			if (nh == 0)
				nh = 1;
			harmoniques = new double [nh];
			int index = 0;
			while (st.hasMoreTokens()) {
				String s = st.nextToken();
				try {
					harmoniques [index ++] = Double.parseDouble (s);
				}
				catch (NumberFormatException e) {}
			}
			String s = "";
			index = 0;
			while (index < harmoniques.length)
				s += harmoniques [index ++] + " ";
			tharmoniques.setText (s);

// lectures des phases

			st = new StringTokenizer (tphases.getText(), " ", false);
			phases = new double [nh];
			index = 0;
			while ((st.hasMoreTokens()) && (index < nh)) {
				s = st.nextToken();
				try {
					phases [index ++] = Double.parseDouble (s);
				}
				catch (NumberFormatException e) {}
			}
			s = "";
			index = 0;
			while (index < phases.length)
				s += phases [index ++] + " ";
			tphases.setText (s);

// Initialisation de l'audio

			AudioFormat format = new AudioFormat (44100.0f, 16, 1, true, false);
			auline = null;
			DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
			try {
				auline = (SourceDataLine) AudioSystem.getLine (info);
				auline.open (format);
			}
			catch (LineUnavailableException e) {}
			catch (Exception e) {}
 
			auline.start();

// Calcul des échantillons

			double t = 0.0;
			pas = 2.0 * Math.PI * frequence / 44100.0;
			int nechantillons = (int) (44100.0 * duree);
			valechantillons = new short [nechantillons]; // pour le graphique
			sinusoides = new short [nechantillons] [harmoniques.length];

			int nBytesRead = 2 * nechantillons;
			byte[] abData = new byte [nBytesRead]; // pour l'audio

			for (int i = 0; i < nechantillons; i ++) {
				double sint = 0.0;
				int k = 1;
				for (int j = 0; j < harmoniques.length; j ++) {
					double d = Math.sin (k * t + phases [j]* pis180) * harmoniques [j];
					short sd = (short) (d * 32768.0);
					if (d > 1.0)
						sd = Short.MAX_VALUE;
					else if (d < -1.0)
						sd = Short.MIN_VALUE;
					sinusoides [i] [j] = sd; 
					sint += d;
					k ++;
				}
				short ssint = (short) (sint * 32768.0);
				if (sint > 1.0)
					ssint = Short.MAX_VALUE;
				else if (sint < -1.0)
					ssint = Short.MIN_VALUE;
				valechantillons [i] = ssint;
				t += pas;
				abData [2 * i] = (byte) (255 & ssint);
				abData [2 * i + 1] = (byte) (ssint / 256);
			}

// lance le graphique

			graphe.repaint();

// emission du son

			if ((evt.getSource() == son) && (nBytesRead >= 0))
				auline.write(abData, 0, nBytesRead);
		} else if (evt.getSource() == plus && zoom < Integer.MAX_VALUE / 2)
			zoom *= 2;
		else if (evt.getSource() == moins && zoom > 1)
			zoom /= 2;
		else if (evt.getSource() == enregistrer) {

			FileDialog fd = new FileDialog (new Frame(), "Enregistrer un fichier son .wav", FileDialog.SAVE);
			fd.setFile ("*.wav");
			fd.setVisible (true);
			erreur = null;
			String nomf = fd.getFile();
			String dir = fd.getDirectory();
			try {
				FileOutputStream outs = new FileOutputStream (dir + nomf);
				ecrstr (outs, "RIFF");
				ecriture4 (outs, 44 + valechantillons.length * 2 - 8); // taille du fichier - 8
				ecrstr (outs, "WAVEfmt ");
				ecriture4 (outs, 16); // taille du bloc - 8
				ecriture2 (outs, (short) 1); // format PCM
				ecriture2 (outs, (short) 1); // 1 canal
				ecriture4 (outs, frequenceech); // fréquence
				ecriture4 (outs, frequenceech * 2); // octets par seconde
				ecriture2 (outs, (short) 2); // 1 canal * 2 octets
				ecriture2 (outs, (short) 16); // 16 bits par échantillon
				ecrstr (outs, "data");
				ecriture4 (outs, valechantillons.length * 2); // taille des données

// recherche du max pour normalisation

/*				double dmax = 0.0;
				for (int i = 0; i < valechantillons.length; i++) {
					double vec = valechantillons [i];
					if ( Math.abs (vec) > dmax)
						dmax = vec;
				}*/

// normalisation et écriture

//				if (dmax != 0.0)
					for (int i = 0; i < valechantillons.length; i ++) {
						short vec = valechantillons [i];
						ecriture2 (outs, vec);
					}
/*				else
					for (int i = 0; i < valechantillons.length; i ++)
						ecriture2 (outs, (short) 0);*/
				outs.close();
			}
			catch (java.io.IOException e) {
				erreur = "erreur : " + e + " " + dir + nomf;
			}
			catch (java.lang.SecurityException e) {
				erreur = "erreur : " + e;
			}

		}
		graphe.repaint();
	} // fin actionPerformed

// écriture d'une chaîne de caractères

	private boolean ecrstr (FileOutputStream outs, String s) {
		try {
			outs.write (s.getBytes());
		}
		catch (java.io.IOException e) {
			erreur = "problème d'écriture";
			return false;
		}
		return true;
	}

// écriture de 2 octets en "little endian" d'un "short"

	private boolean ecriture2 (FileOutputStream outs, short n) {
		try {
			byte b = (byte) n; //L
			outs.write (b);
			b = (byte) (n >> 8); //H
			outs.write (b);
		}
		catch (java.io.IOException e) {
			erreur = "problème d'écriture";
		 return false;
		}
		return true;
	}

// écriture de 4 octets en "little endian" d'un "int"

	private boolean ecriture4 (FileOutputStream outs, int n) {
		try {
			byte b = (byte) n;
			outs.write (b);
			b = (byte) (n >> 8);
			outs.write (b);
			b = (byte) (n >> 16);
			outs.write (b);
			b = (byte) (n >> 24);
			outs.write (b);
		}
		catch (java.io.IOException e) {
			erreur = "problème d'écriture";
			return false;
		}
		return true;
	}

	private boolean ecrdouble (FileOutputStream outs, double d) {
		try {
			ByteBuffer bb =ByteBuffer.allocate (8);
			bb.order (ByteOrder.LITTLE_ENDIAN);
			bb.putDouble (d);
			outs.write (bb.array());
		}
		catch (java.io.IOException e) {
			erreur = "problème d'écriture";
			return false;
		}
		return true;
	}		

// lecture de 2 octets en "little endian" conversionen "short"

	private short lectureint2 (FileInputStream ins) {
		try {
			short s = (short) ins.read(); //L
			s += (short) ins.read() << 8; //H
			return s;

/* autre méthode...
			ByteBuffer bb = ByteBuffer.allocate (2);
			byte b = (byte)ins.read();
			bb.put ((byte)ins.read()); //H
			bb.put (b); //L
			return bb.getShort (0);
 */

		}
		catch (java.io.IOException e) {
			erreur = "problème de lecture";
			return 0;
		}
	}

	public static void main (String args[]) {
		int xdim = 1200;
		int ydim = 600;
		sommesinus f = new sommesinus ("sommesinus");
		f.enregistrer.setVisible (true);
		f.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
		f.setSize (xdim, ydim);
		f.setVisible (true);
	}

// fin class sommesinus

////////////////////////////////////////////////////////////////////////////////
public class dessin extends Panel implements ActionListener {
	static final long serialVersionUID = 220902L;
	int nhd = -1;
	int nad = -1;
	Button[] lb;
	boolean[] lboo;
	
	public dessin() {
		setBackground (Color.WHITE);
	}

	public void paint (Graphics g) {
		if (valechantillons != null) {
			int w = getWidth();
			int margew = 10;
			int h = getHeight();
			int margeh = 10;
			int hm = h - margeh;
			double dhs2 = hm / 2.0;
			if (nh != nhd) {
				for (int i = 0; i < nhd; i ++) {
					removeAll();
					lb[i].removeActionListener (this);
				}
				nhd = nh;
				lb = new Button [nh];
				lboo = new boolean [nh];
				for (int i = 0; i < nh; i ++) {
					add (lb [i] = new Button (Integer.toString (i)));
					lb[i].addActionListener (this);
					lb[i].setBackground (Color.WHITE);
//					lboo [i] = false;
				}
				validate(); // pour mettre en place les boutons
			}

// trace les sinusoides

			g.setColor (Color.BLUE);
			for (int j = 0; j < harmoniques.length; j ++) {
				if (lboo [j]) {
					int index = 0;
					int yhl0 = (int) dhs2;
					for (int i = margew; i < w; i ++) {
						double y;
						if (index < valechantillons.length)
							y = sinusoides [index] [j];
						else
							y = 0.0;
						int yhl = (int) (dhs2 * (1.0 - y / 32768.0));
						g.drawLine (i - 1, yhl0, i, yhl);
						yhl0 = yhl;
						index += zoom;
					}
				}
			}

// trace les axes

			g.setColor (Color.BLACK);
			g.drawLine (margew, hm / 2, w, hm / 2);
			g.drawLine (margew, 0, margew, hm);

// trace la courbe

			int index = 0;
			int yhl0 = (int) dhs2;
			for (int i = margew; i < w; i ++) {
				double y;
				if (index < valechantillons.length)
					y = valechantillons [index];
				else
					y = 0.0;
				int yhl = (int) (dhs2 * (1.0 - y / 32768.0));
				g.drawLine (i - 1, yhl0, i, yhl);
				yhl0 = yhl;
				index += zoom;
			}

// f(x)=...

			String s = "f(t) = ";
			int k = 2;
			for (int j = 0; j < harmoniques.length; j ++) {
				if (j > 0)
					s += " + ";
				s += harmoniques [j] + " sin (" + k + " ¶ f t + " + (phases [j] / 180.0) + " ¶)";
				k += 2;
			}
			g.drawString (s, margew, h -1);
		}
	} // fin paint

	public void actionPerformed (ActionEvent evt) {
		Button bevt = (Button) evt.getSource();
		for (int i = 0; i < lb.length; i ++) {
			if (bevt == lb [i])
				lboo [i] = !lboo [i];
			if (lboo [i])
				lb[i].setBackground (Color.BLUE);
			else
				lb[i].setBackground (Color.WHITE);
		}
		repaint();
	}
} // class dessin

}
