/* A blip, laser, explosion, 8-bit synth, based heavily on a program called SFXR by DrPetter.
 * The SFXR website is here: http://www.cyd.liu.se/~tompe573/hp/project_sfxr.html
 *
 * This code is messy, but nonetheless (C) Ben Porter 2009.
 * Feel free to use in any way you wish but keep this copyright notice intake.
 */

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Random;

import processing.core.*;
import controlP5.*;
import net.beadsproject.beads.core.*;
import net.beadsproject.beads.data.Sample;
import net.beadsproject.beads.ugens.Recorder;

@SuppressWarnings("serial")
public class Jsfxr extends PApplet {

	public ControlP5 controlP5;
	AudioContext ac;
	SoundEffect effect = null;

	/*
	 * Parameters of the next sound to be synthesized.
	 * Refer to the sfxr documentation
	 * 
	 * NB: they have to be public for controlP5 to access them
	 */
	public int SampleRate = SoundEffect.FORTYFOUR;
	public int BitDepth = SoundEffect.BITS16;
	public int GeneratorType = SoundEffect.GT_SQUARE;

	public float Volume = 100.f;

	public float AttackTime = 50.f;
	public float SustainTime = 90.f;
	public float SustainPunch = 1.f;
	public float DecayTime = 460.f;

	public float Frequency = 1350.0f;
	public float MinFrequency = 50.0f;
	public float Slide = 0.0f;
	public float Curvature = 1.0f;
	public float VibratoDepth = 0.f;
	public float VibratoSpeed = 0.f;

	public float ChangeAmount = 0.2f;
	public float ChangeSpeed = 0.93f;

	public float RepeatSpeed = 0.0f;

	public float PhaserOffset = 0.0f;
	public float PhaserSweep = 0.0f;

	/* Set up the parameter ranges */
	final float MaxAttackTime = 3000.f;
	final float MaxSustainTime = 3000.f;
	final float MaxDecayTime = 3000.f;
	final float MaxSoundTime = MaxAttackTime + MaxSustainTime + MaxDecayTime;

	// gather Parameters into an array 
	public float AllParams[] = {AttackTime,SustainTime,SustainPunch,DecayTime,Frequency,MinFrequency,Slide,Curvature,VibratoDepth,VibratoSpeed,ChangeAmount,ChangeSpeed,RepeatSpeed,PhaserOffset,PhaserSweep};
	public float ParamRanges[][] = {
			{ 0,MaxAttackTime  },
			{ 0,MaxSustainTime  },
			{ 1,5.f  },
			{ 0,MaxDecayTime  },
			{ 0,3000  }, // freq
			{ 0,500  }, //minfreq
			{ -3,3  }, // slide
			{ 0,5  }, // Curvature
			{ 0,1  }, // VibratoDepth
			{ 0,3  }, //Vibratopeed
			{ -1.f,1.f  }, // ChangeAmount
			{ 0.f,1.f  }, // ChangeSpeed
			{ 0.f,1.f  }, // RepeatSpeed
			{ 0.f,1.0f  }, // PhaserOffset
			{ -1.f,1.f  } // PhaserSweep
	}; 

	// Mean and Standard Deviation for the randomiser
	// randomiser also clips to the above ranges
	public float ParamDistributions[][] = {
			{ 10, 20 },
			{ 150, 200},
			{ 1, 2.f  },
			{ 200, 300 },
			{ 600, 400 }, // freq
			{ 17.5f, 20 }, //minfreq
			{ 0, 1 }, // slide
			{ 1, 0.2f  }, // Curvature
			{ 0, 0.1f }, // VibratoDepth
			{ 0, 0.1f  }, //Vibratopeed
			{ 0, 0.3f  }, // ChangeAmount
			{ 0.f, 1.f }, // ChangeSpeed
			{ 0.f, 0.4f }, // RepeatSpeed
			{ 0.f, 0.1f  }, // PhaserOffset
			{ 0.f, 0.1f  } // PhaserSweep
	};
	
		

	/* End parameters */

	/* Some Funky Presets */
	final int presetTypes[] = {
			0,0,0,0,3,3,1,0,0,0,1,0};
	final float presetParameters[][] = {
			{ 45.0f,105.0f,1.0f,315.0f,690.0f,27.5f,0.0f,1.0f,0.0f,0.0f,0.49f,0.965f,0.0f,0.0f,0.0f  },
			{ 45.0f,105.0f,2.0f,405.0f,1230.0f,27.5f,0.0f,1.0f,0.0f,0.0f,0.8299999f,0.96f,0.0f,0.0f,0.0f  },
			{ 0.0f,255.0f,1.02f,90.0f,1185.0f,25.0f,-2.1f,0.77500004f,0.0f,0.0f,0.00999999f,0.0f,0.0f,0.0f,0.0f  },
			{ 0.0f,450.0f,1.02f,480.0f,1575.0f,40.0f,-0.93000007f,1.075f,0.099999994f,1.41f,-0.28000003f,0.945f,0.78499997f,0.0f,0.0f  },
			{ 0.0f,585.0f,3.24f,480.0f,1410.0f,40.0f,-1.5600001f,0.975f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,  },
			{ 0.0f,585.0f,4.62f,1230.0f,1410.0f,17.5f,-2.13f,0.975f,0.87f,1.3349999f,0.0f,0.0f,0.78f,0.0f,0.0f  },
			{ 0.0f,405.0f,1.0f,150.0f,600.0f,17.5f,0.4499998f,1.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,  },
			{ 0.0f,60.0f,1.0f,240.0f,765.0f,17.5f,0.21000004f,1.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f  },
			{ 0.0f,585.0f,1.0f,495.0f,795.0f,17.5f,0.17999983f,1.0f,0.0f,0.0f,0.38f,0.96999997f,0.885f,0.0f,0.0f  },
			{ 0.0f,60.0f,1.0f,240.0f,765.0f,17.5f,0.21000004f,1.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f  },
			{ 0.0f,45.0f,1.0f,45.0f,255.0f,17.5f,0.029999971f,1.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f  },
			{ 0.0f,105.0f,1.0f,30.0f,840.0f,17.5f,0.029999971f,1.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f  }
	};

	/*******************/
	/*******************/
	/*******************/
	/*******************/
	public void setup() {
		size(325,330);
		background(color(0xC0,0xB0,0x90));

		// load gui
		controlP5 = new ControlP5(this);
		controlP5.load("gui.xml");
		controlP5.controller("presetlabel").setColorValue(0);
		controlP5.controller("titlelabel").setColorValue(0);
		controlP5.lock();

		// create and start the AC
		ac = new AudioContext(512);
		ac.start();
	}

	/**
	 * This is where all the action is.
	 */
	public void play()
	{
		if (effect!=null) effect.remove();
		// create effect based on the parameters
		effect = new SoundEffect(
				GeneratorType,
				Volume,

				AttackTime,
				SustainTime,
				SustainPunch,
				DecayTime,

				Frequency,
				MinFrequency,
				Slide,
				Curvature,
				VibratoDepth,
				VibratoSpeed,

				ChangeAmount,
				ChangeSpeed,

				RepeatSpeed,

				PhaserOffset,
				PhaserSweep);	 
		if (effect!=null)
			// play the effect
			effect.play(ac, BitDepth, SampleRate);
	}
	
	/**
	 * Render sfx to a sound file
	 */
	public void save2aif()
	{
		ac.stop();		
		String filepath = selectInput();		
		if (filepath!=null)
		{
			filepath += ".aif";
			print ("Saving sound effect.");
			if (effect!=null) effect.remove();
			// create effect based on the parameters
			effect = new SoundEffect(
					GeneratorType,
					Volume,

					AttackTime,
					SustainTime,
					SustainPunch,
					DecayTime,

					Frequency,
					MinFrequency,
					Slide,
					Curvature,
					VibratoDepth,
					VibratoSpeed,

					ChangeAmount,
					ChangeSpeed,

					RepeatSpeed,

					PhaserOffset,
					PhaserSweep);
			effect.build(ac);

			Sample sample = new Sample(ac.getAudioFormat(), effect.lengthInSamples());
			Recorder rec = new Recorder(ac,sample);
			// start recording
			rec.addInput(effect.ugen());
			ac.out.addDependent(rec);
			ac.runForNSecondsNonRealTime(effect.lengthInMs()/1000.f);
			ac.out.removeDependent(rec);
			try {
				sample.write(filepath);
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}	
			println ("Done");
		}
		ac.start();
	}
	
	/**
	 * Render sfx to a sound file
	 */
	public void save2sfx()
	{	
		String filepath = selectInput();		
		if (filepath!=null)
		{
			filepath += ".jsfxr";
			println ("Saving as " + filepath);
			if (effect!=null) effect.remove();
			// create effect based on the parameters
			effect = new SoundEffect(
					GeneratorType,
					Volume,

					AttackTime,
					SustainTime,
					SustainPunch,
					DecayTime,

					Frequency,
					MinFrequency,
					Slide,
					Curvature,
					VibratoDepth,
					VibratoSpeed,

					ChangeAmount,
					ChangeSpeed,

					RepeatSpeed,

					PhaserOffset,
					PhaserSweep);
			effect.build(ac);
			try {
				effect.write(new PrintStream(new FileOutputStream(filepath)));
			} catch (FileNotFoundException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}						
		}
	}

	public void load()
	{
		String filepath = selectInput("Load .jsfxr file.");		
		if (filepath!=null)
		{
			try {
				effect = SoundEffect.read(new File(filepath));		
				
				// update the params in the gui				
				GeneratorType = effect.GeneratorType;
				Volume = effect.Volume;
				AttackTime = effect.AttackTime;
				SustainTime = effect.SustainTime;
				SustainPunch = effect.SustainPunch;
				DecayTime = effect.DecayTime;
				Frequency = effect.Frequency;
				MinFrequency = effect.MinFrequency;
				Slide = effect.Slide;
				Curvature = effect.Curvature;
				VibratoDepth = effect.VibratoDepth;
				VibratoSpeed = effect.VibratoSpeed;
				ChangeAmount = effect.ChangeAmount;
				ChangeSpeed = effect.ChangeSpeed;
				RepeatSpeed = effect.RepeatSpeed;
				PhaserOffset = effect.PhaserOffset;
				PhaserSweep = effect.PhaserSweep;
				updateGT();
				updateParams();
				effect.build(ac);
				
			} catch (Exception e) {
				// TODO Auto-generated catch block
				effect = null;
				e.printStackTrace();
			}			
		}
	}
	
	public void draw()
	{
		background(color(0xC0,0xB0,0x90));
		// gui draws itself automatically
	}

	public void controlEvent(ControlEvent theEvent) {
		if (theEvent!=null && theEvent.isController())
		{
			int i = theEvent.controller().id();
			if (i>=0 && i<presetTypes.length)
			{
				loadPreset(i);    
			}
		}
	}
	public void setgenerator(int val)
	{
		GeneratorType = val;
	}

	public void setbitdepth(int val)
	{
		BitDepth = val;  
	}

	public void setsamplerate(int val)
	{
		SampleRate = val; 
	}

	// given an array of parameters, set the global parameters appropriately
	public void setParameters(float[] p)
	{
		AttackTime = p[0];
		SustainTime = p[1];
		SustainPunch = p[2];
		DecayTime = p[3];
		Frequency = p[4];
		MinFrequency = p[5];
		Slide = p[6];
		Curvature = p[7];
		VibratoDepth = p[8];
		VibratoSpeed = p[9];
		ChangeAmount = p[10];
		ChangeSpeed = p[11];
		RepeatSpeed = p[12];
		PhaserOffset = p[13];
		PhaserSweep = p[14];  
	}

	public float[] getCurrentParameters()
	{
		float p[] = new float[AllParams.length];

		p[0] = AttackTime;
		p[1] = SustainTime;
		p[2] = SustainPunch;
		p[3] =   DecayTime;
		p[4] =   Frequency;
		p[5] =   MinFrequency;
		p[6] =   Slide;
		p[7] =   Curvature;
		p[8] =   VibratoDepth;
		p[9] =   VibratoSpeed;
		p[10] =   ChangeAmount;
		p[11] =   ChangeSpeed ;
		p[12] =   RepeatSpeed;
		p[13] =   PhaserOffset;
		p[14] =   PhaserSweep;  
		return p;
	}

	// 1. high mutation, 0. no mutation
	public void mutateParameters(float[] p, float val)
	{
		for(int i=0;i<p.length;i++)
		{
			float minp = ParamRanges[i][0];
			float maxp = ParamRanges[i][1];
			float range = maxp - minp;
			float dr = range*val;
			p[i] += (dr/2)*(float)(1-2*Math.random());
			if (p[i]<minp) p[i] = minp;
			if (p[i]>maxp) p[i] = maxp;        
		}
	}

	public void mutate()
	{
		// get the current set of params
		float p[] = getCurrentParameters();
		mutateParameters(p,.05f);
		setParameters(p);
		updateParams();
		play();
	}

	public void randomsound()
	{
		Random r = new Random();
		
		float p[] = new float[AllParams.length];
		for(int i=0;i<p.length;i++)
		{
			float val = (float) (r.nextGaussian()*ParamDistributions[i][1] + ParamDistributions[i][0]); 
			p[i] = Math.min(ParamRanges[i][1], Math.max(ParamRanges[i][0], val));			
		}
		
		setParameters(p);
		updateParams();
		
		// (Cumulative) probabilities of each generator type appearing
		final double GeneratorProbabilities[] = 
		{
				0.3, // GT_SQUARE
				0.5, // GT_SAW
				0.85, // GT_SINE
				1.1, // GT_NOISE (>1)
		};
		
		// choose a random generatortype
		double gt = random(1);
		int gtype = 0;
		for(;gtype<4;gtype++)
		{
			if (gt < GeneratorProbabilities[gtype])
			{
				GeneratorType = gtype;
				break;
			}
		}		
		GeneratorType = Math.min(GeneratorType,SoundEffect.GT_NOISE);
		updateGT();
		play();  
	}

	public void updateParams()
	{
		controlP5.controller("AttackTime").setValue(AttackTime);
		controlP5.controller("SustainTime").setValue(SustainTime);
		controlP5.controller("SustainPunch").setValue(SustainPunch);
		controlP5.controller("DecayTime").setValue(DecayTime);
		controlP5.controller("Frequency").setValue(Frequency);
		controlP5.controller("MinFrequency").setValue(MinFrequency);
		controlP5.controller("Slide").setValue(Slide);
		controlP5.controller("Curvature").setValue(Curvature);
		controlP5.controller("VibratoDepth").setValue(VibratoDepth);
		controlP5.controller("VibratoSpeed").setValue(VibratoSpeed);
		controlP5.controller("ChangeAmount").setValue(ChangeAmount);
		controlP5.controller("ChangeSpeed").setValue(ChangeSpeed) ;
		controlP5.controller("RepeatSpeed").setValue(RepeatSpeed);
		controlP5.controller("PhaserOffset").setValue(PhaserOffset);
		controlP5.controller("PhaserSweep").setValue(PhaserSweep); 
	}

	public void updateGT()
	{
		Radio r = ((Radio)(controlP5.controller("setgenerator")));
		switch (GeneratorType)
		{
		case SoundEffect.GT_SQUARE: 
			r.activate("Square"); 
			break;
		case SoundEffect.GT_SAW:
			r.activate("Saw"); 
			break;
		case SoundEffect.GT_SINE: 
			r.activate("Sine"); 
			break;
		case SoundEffect.GT_NOISE: 
			r.activate("Noise"); 
			break;
		} 
	}

	public void loadPreset(int i)
	{
		GeneratorType = presetTypes[i];
		updateGT();

		setParameters(presetParameters[i]);
		updateParams();
		play();  
	}
}
