package net.paulhertz.pixelaudio.example;

import processing.core.*;
import processing.data.JSONArray;
import processing.data.JSONObject;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import ddf.minim.AudioOutput;
import ddf.minim.Minim;
import ddf.minim.MultiChannelBuffer;
import net.paulhertz.pixelaudio.*;
import net.paulhertz.pixelaudio.sampler.*;
import net.paulhertz.pixelaudio.schedule.TimedLocation;

// TODO revise for PixelAudio library release.

/**
 * BigWaveSynthAudio shows how you can load the color output of WaveSynth into the pixel
 * array of a MultiGen and play the WaveSynth's audio output with a PASamplerInstrument.
 * <p>
 * WaveSynth is a class that combines an additive synthesis audio generator with 
 * a digital "color organ," an image generator that maps audio signals to colors. 
 * MultiGen is child class of PixelMapGen that allows you to combine individual
 * PixelMapGens into a composite PixelMapGen instance with one signal path traversing 
 * all the pixels of the corresponding image. This example sketch also allows you to load
 * JSON data files stored in the example sketches' data folder to reconfigure the WaveSynth. 
 * On startup, we call initWaveDataList() to create a WaveData array with two operators.</p> 
 * 
 * <p>Note the role that sampleRate plays in generating images and audio signals from a 
 * WaveSynth. Three sample rate values influence the audio output and image display: the 
 * audio output frequency, usually set just once, the audio instrument playback frequency,
 * which can vary, and the WaveSynth's local sampling rate, which can also vary. The global 
 * sampleRate variable sets the frequency of audio output. In initAudio() it also sets the
 * playback frequency of the PASamplerInstrument:</p>
 * <pre>
 *     this.audioOut = minim.getLineOut(Minim.STEREO, 1024, sampleRate);
 *     synth = new PASamplerInstrumentPool(audioBuffer, audioOut.sampleRate(), maxVoices, 1, audioOut, shortAdsr);
 * </pre>
 * <p>The local sample rate of a WvaeSynth affects the pattern in the display image and may
 * affect playback of the audio signal generated by the WaveSynth. WaveSynth.sampleRate is set 
 * in the initWaveSynth() method and can be changed with key commands '1' through '7'. 
 * <pre>
 *   synth.setSampleRate(genWidth * genHeight);
 * </pre>
 * The audio sampler instrument that plays the audio signals generated by a WaveSynth, the 
 * PASamplerInstrumentPool object "synth", uses the audioOut sampleRate for its playback
 * sample rate. It ignores the local sample rate of the WaveSynth. When you change the 
 * sampling rate of the WaveSynth, the frequency of playback will change, because the synth
 * is set up to step through its buffer at the audio sampling rate. Frequencies will be accurate
 * only when the WaveSynth sample rate, the synth playback sample rate, and the output rate are
 * all the same. This happens when you press the '7' key, which sets the WaveSynth rate to 
 * the audio output sampling rate (which we'll say is 44100 Hz). The WaveSynth then squeezes
 * its sine wave operator output into 44100 audio samples or pixels, on the screen and in its 
 * audio signal. For the WaveSyth that shows on startup, with frequencies of 768 and 192 Hz, 
 * the output frequency of a note will then be 768 Hz, right around G5 (G above middle C) on 
 * a piano keyboard.</p>
 * 
 * <p>At sampleRate = genWidth * genHeight, one cycle of a sine wave set in the WaveSynth would
 * fill an entire HilbertGen. A frequency of 768 will place 768 copies of a sine wave along
 * the signal path over the entire bitmap of a single HilbertGen. The MultiGen which is 
 * displayed in this sketch is composed of six Hilbert curves (HilbertGen objects). 768 was
 * used as a frequency because it shares factors with the HilbertGen's dimensions. This results
 * in symmetrical patterns.</p>
 * 
 * <p>The audio signals and images generated by the WaveSyth data files in the JSON folder
 * demonstrate a wide range of possibilities. Some produce low frequencies that create
 * large areas of shifting colors when animation is on. You may not be able to hear the
 * audio these example produce, except as a click from the envelope. Some JSON files produce high 
 * frequencies that appear as fine-grained images but can't be heard because the audio frequency
 * is too high. The medium frequencies can produce many different timbres. The audio signal
 * they produce tends to be very repetitive. Multiple instruments playing together can result
 * in interesting phase interference effects. The raindrops() method can be called ('d') to produce
 * random audio events that demonstrate these effects. Combine the raindrops() method with 
 * detuning ('t' command) to get a wide range of animation and audio drone effects.</p>
 * 
 * <p>For more information and sample code about the WaveSyth class, see the WaveSynthEditor and 
 * WaveSynthSequencer example sketches.</p>
 * 
 * <pre>
 * Press ' ' to turn animation on and off.
 * Press 'd' to turn raindrops on and off.
 * Press 't' to detune instruments by a small amount.
 * Press 'T' to restore tuning to instruments.
 * Press 'l' or 'L' to select long or short notes for audio events.
 * Press 'o' or 'O' to open a WaveSynth settings JSON file.
 * Press 'j' or 'J' to save current WaveSynth to a JSON file.
 * Press 'f' to print the current frame rate to the console.
 * Press '1' to set the current WaveSynth's sample rate to genWidth * genHeight.
 * Press '2' to set the current WaveSynth's sample rate to genWidth * genHeight / 2.
 * Press '3' to set the current WaveSynth's sample rate to genWidth * genHeight / 3.
 * Press '4' to set the current WaveSynth's sample rate to genWidth * genHeight / 4.
 * Press '5' to set the current WaveSynth's sample rate to genWidth * genHeight / 5.
 * Press '6' to set the current WaveSynth's sample rate to genWidth * genHeight / 6.
 * Press '7' to set the current WaveSynth's sample rate to the audio sample rate.
 * Press 'h' to show help message in console.
 * </pre>
 * See also: BigWaveSynth, WaveSynthEditor, WaveSynthSequencer, and MultiGenLookupTables.
 * For a wealth of methods to create MultiGens, see ArgosyMixer.
 * 
 */
public class BigWaveSynthAudio extends PApplet {
	PixelAudio pixelaudio;			// our shiny new library
	MultiGen multigen;				// a PixelMapGen that handles multiple gens
	int rows = 3;					// rows
	int columns = 2;				// columns
	int genWidth = 512;				// width of PixelMapGen objects, for a HilbertGen must be a power of 2
	int genHeight = 512;			// height of PixelMapGen objects, for a HilbertGen must be equal to genWidth
	PixelAudioMapper mapper;		// instance of a class for reading, writing, and transcoding audio and image data 
	int mapSize;					// size of the display bitmap, audio signal, wavesynth pixel array, mapper arrays, etc. 
	ArrayList<WaveData> wdList;		// a list of audio "operators" that drive a WaveSynth
	WaveSynth wavesynth;			// a sort of additive audio synthesis color organ that also produces sound
	PImage synthImage;				// local variable that points to wavesynth's PImage instance variable

	// WaveSynth variables
	float myGamma = 1.0f;			// a non-linear brightness adjustment
	int animSteps = 240;			// number of steps in the animation
	int animStop = animSteps;		// The step at which the animation should stop (not used here)
	int step = 0;					// the current step in the animation
	String comments;				// a JSON field that provides information about the WaveSynth effects it produces
	
	/** Minim audio library */
	Minim minim;					// library that handles audio 
	AudioOutput audioOut;			// line out to sound hardware
	MultiChannelBuffer audioBuffer;	// data structure to hold audio samples
	boolean isBufferStale = false;	// flags that audioBuffer needs to be reset: i.e., after loading JSON data to wavesynth
	float sampleRate = 48000;			// a critical value for display and audio, see the setup method
	float[] audioSignal;			// the audio signal as an array of floats
	int[] rgbSignal;				// the colors in the display image, in the order the signal path visits them
	int audioLength;				// length of the audioSignal, same as the number of pixels in the display image

	// SampleInstrument setup
	int shortDuration = 2000;       // duration in milliseconds, for a comparatively short note
	int longDuration = 3000;        // duration in milliseconds, for a comparatively long note
	boolean isPlayLongNote = true;  // do we play a long note or a short note? (key command 'l' or 'L')
	int samplelen;                  // calculated sample synth note length, samples
	float defaultGain = 0.8f;
	float gain = defaultGain;
	PASamplerInstrumentPool synth;  // audio sampler instrument pool
	
	int poolSize = 24;
	boolean isUseSynth = false;     // switch between pool and synth, pool is preferred

	// ADSR and params
	ADSRParams shortAdsr;			// attack, decay, sustain, release for short note 
	ADSRParams longAdsr;			// attack, decay, sustain, release for long note 

	// file i/o
	String jsonFolder = "/JSON_data/";
	File currentDataFile;
	String currentFileName;
	JSONObject json;

	// animation
	boolean isAnimating = true;
	boolean oldIsAnimating;
	boolean isLooping = true;
	int circleColor = color(233, 220, 199, 128);
	boolean isRaining = false;

	// interaction
	int sampleX;
	int sampleY;
	int samplePos;            // position of a mouse click along the signal path, index into the audio array
	ArrayList<TimedLocation> timeLocsArray;
	int count = 0;	

	
	public static void main(String[] args) {
		PApplet.main(new String[] { BigWaveSynthAudio.class.getName() });
	}

	public void settings() {
		size(rows * genWidth, columns * genHeight);
	}

	public void setup() {
		pixelaudio = new PixelAudio(this);
		multigen = hilbertLoop3x2(genWidth, genHeight);	// See the MultiGenDemo example for details on how MultiGen works
		mapper = new PixelAudioMapper(multigen);		// initialize a PixelAudioMapper
		mapSize = mapper.getSize();						// set mapSize
		wdList = initWaveDataList();					// generate a list of WaveData objects
		wavesynth = new WaveSynth(mapper, wdList);		// create a WaveSynth object
		initWaveSynth(wavesynth);						// set wavesynth fields
		synthImage = wavesynth.mapImage;				// set the image to display
		timeLocsArray = new ArrayList<TimedLocation>(); // initialize mouse event tracking array
		initAudio();
		showHelp();
	}

	/**
	 * Generates a looping fractal signal path consisting of 6 HilbertGens,
	 * arranged 3 wide and 2 tall, to fit a 3 * genW by 2 * genH image. 
	 * This particular MultiGen configuration is used so extensively in my 
	 * sample code that I've given it a factory method in the HilbertGen class.
	 * It's written out here so you can see how it works. 
	 * 
	 * Note that genH must equal genW and both must be powers of 2. For the 
	 * image size we're using in this example, genW = image width / 3 and 
	 * genH = image height / 2.
	 * 
	 * @param genW    width of each HilbertGen 
	 * @param genH    height of each HilbertGen
	 * @return a 3 x 2 array of Hilbert curves, connected in 
	 *         a loop (3 * genWidth by 2 * genHeight pixels)
	 */
	public MultiGen hilbertLoop3x2(int genW, int genH) {
		// list of PixelMapGens that create an image using mapper
		ArrayList<PixelMapGen> genList = new ArrayList<PixelMapGen>();
		// list of x,y coordinates for placing gens from genList
		ArrayList<int[]> offsetList = new ArrayList<int[]>();
		genList.add(new HilbertGen(genW, genH, PixelMapGen.fx270));
		offsetList.add(new int[] { 0, 0 });
		genList.add(new HilbertGen(genW, genH, PixelMapGen.nada));
		offsetList.add(new int[] { genW, 0 });
		genList.add(new HilbertGen(genW, genH, PixelMapGen.fx90));
		offsetList.add(new int[] { 2 * genW, 0 });
		genList.add(new HilbertGen(genW, genH, PixelMapGen.fx90));
		offsetList.add(new int[] { 2 * genW, genH });
		genList.add(new HilbertGen(genW, genH, PixelMapGen.r180));
		offsetList.add(new int[] { genW, genH });
		genList.add(new HilbertGen(genW, genH,PixelMapGen.fx270));
		offsetList.add(new int[] { 0, genH });
		return new MultiGen(3 * genW, 2 * genH, offsetList, genList);
	}

	public ArrayList<WaveData> initWaveDataList() {
		ArrayList<WaveData> list = new ArrayList<WaveData>();
		float frequency = 768; 		// sampleRate/62.5f;
		float amplitude = 0.8f;
		float phase = 0.0f;
		float dc = 0.0f;
		float cycles = 1.0f;
		int waveColor = color(159, 190, 251);
		int steps = this.animSteps;
		WaveData wd = new WaveData(frequency, amplitude, phase, dc, cycles, waveColor, steps);
		list.add(wd);
		frequency = 192; 			// sampleRate/250.0f;
		phase = 0.0f;
		cycles = 2.0f;
		waveColor = color(209, 178, 117);
		wd = new WaveData(frequency, amplitude, phase, dc, cycles, waveColor, steps);
		list.add(wd);
		return list;
	}

	/**
	 * Sets gain, gamma, isScaleHisto, animSteps, and sampleRate instance variables 
	 * of a WaveSynth object and generates its first frame of animation.
	 * 
	 * @param synth		a WaveSynth object whose attributes will be set
	 * @return			the WaveSynth object with attributes set
	 */
	public WaveSynth initWaveSynth(WaveSynth synth) {
		synth.setGain(0.8f);
		synth.setGamma(myGamma);
		synth.setScaleHisto(false);
		synth.setAnimSteps(this.animSteps);
		synth.setSampleRate(genWidth * genHeight);
		println("--- mapImage size = " + synth.mapImage.pixels.length);
		synth.prepareAnimation();
		synth.renderFrame(0);
		return synth;
	}
	

	public void initAudio() {
		minim = new Minim(this);
		// use the getLineOut method of the Minim object to get an AudioOutput object
		// PixelAudio instruments require a STEREO AudioOutput
		this.audioOut = minim.getLineOut(Minim.STEREO, 1024, sampleRate);
		this.audioBuffer = new MultiChannelBuffer(1024, 1);
		renderSignal();
		println("----->> initialized audio buffer, length is "+ audioBuffer.getBufferSize() +(" samples."));
		// ADSR envelope slow attack and decay, fast release, good for moderately short audio events
		shortAdsr = new ADSRParams(1.0f, 0.7f, 0.01f, 0.3f, 0.1f);
		// ADSR envelope with fast attack, slow decay, very long release, good for long audio events
		longAdsr = new ADSRParams(1.0f, 0.001f, 0.2f, 0.8f, 5.0f);
		// create a pool with 24 instruments, each with 4 polyphonic voices
		synth = new PASamplerInstrumentPool(audioBuffer, audioOut.sampleRate(), poolSize, 4, audioOut, shortAdsr);
	}

	public void draw() {
		image(synthImage, 0, 0, width, height);
		if (isAnimating) stepAnimation();
		runTimeArray();								// animate audio event markers
		if (isRaining) {
			// animation slows the frame rate, so we change the threshold when animating
		    float thresh = (isAnimating) ? 0.25f : 0.05f;
		    if (random(0,1) < thresh) {
		      raindrops();							// trigger random audio events
		    }
		}
		if (isBufferStale) {
			renderSignal();
			wavesynth.renderFrame(step);
		}
	}

	/**
	 * Run the animation for audio events. 
	 */
	public void runTimeArray() {
		int currentTime = millis();
		timeLocsArray.forEach(tl -> {
			tl.setStale(tl.eventTime() < currentTime);
			if (!tl.isStale()) {
				drawCircle(tl.getX(), tl.getY());
			}
		});
		timeLocsArray.removeIf(TimedLocation::isStale);
	}

	/**
	 * Draws a circle at the location of an audio trigger (mouseDown event).
	 * @param x		x coordinate of circle
	 * @param y		y coordinate of circle
	 */
	public void drawCircle(int x, int y) {
		float size = isRaining? random(10, 30) : 60;
		fill(circleColor);
		noStroke();
		circle(x, y, size);
	}	

	/**
	 * Trigger a WaveSynth sample at a random location.
	 */
	public void raindrops() {
		int signalPos = (int) random(samplelen, mapSize - samplelen - 1);
		int[] coords = mapper.lookupImageCoord(signalPos);
		sampleX = coords[0];
		sampleY = coords[1];
		if (audioSignal == null || isBufferStale) {
			renderSignal();
			isBufferStale = false;
		}
		float panning = map(sampleX, 0, width, -0.9f, 0.9f);
		// calculate average duration centered on 2.0 seconds
		int duration = calcSampleLen(2.0f);
		int actualLen = playSample(samplePos, duration, 0.15f, shortAdsr, 1.0f, panning);
		// uncomment for debugging
		// println("--- raindrop duration "+ nf(duration / sampleRate, 0, 4) +" secs, actual length "+ nf(actualLen / sampleRate, 0, 4) +" secs");
	}
	
	/**
	 * Detunes each instrument in the polyphonic synth by a small amount. 
	 * Demonstrates how to access individual PASamplerInstrument instances in 
	 * PASamplerInstrumentPool's list of instruments. 
	 */
	public void detuneInstruments() {
		List<PASamplerInstrument> pool = synth.getInstruments();
		for (PASamplerInstrument inst : pool) {
			float pitch = inst.getPitchScale();
			float newPitch = randomShift(pitch, 0.001f);
			inst.setPitchScale(newPitch);
		}
	}
	
	/**
	 * Restores the tuning of individual instruments in the polyphonic synth. 
	 */
	public void retuneInstruments() {
		List<PASamplerInstrument> pool = synth.getInstruments();
		for (PASamplerInstrument inst : pool) {
			inst.setPitchScale(1.0f);
		}
	}
	
	public float randomShift(float val, float shift) {
		val = (float) PixelAudio.gauss(val, shift);
		return val;
	}
	
	public void keyPressed() {
		float newSampleRate;
		switch (key) {
		case ' ': // turn animation on and off
			isAnimating = !isAnimating;
			println(isAnimating ? "Starting animation at frame " + step + " of " + animSteps
					: "Stopping animation at frame " + step + " of " + animSteps);
			break;
		case 'd': // turn raindrops on and off
			isRaining = !isRaining;
			println("--- isRaining = " + isRaining);
			break;
		case 't': // detune instruments by a small amount
			detuneInstruments();
			break;
		case 'T': // restore tuning to instruments
			retuneInstruments();
			break;
		case 'l': case 'L': // select long or short notes for audio events
			isPlayLongNote = !isPlayLongNote;
			println("----- isPlayLongNote = "+ isPlayLongNote);
			break;
		case 'o': case 'O': // open a WaveSynth settings JSON file
			// turn off animation while reading new settings for wavesynth
			oldIsAnimating = isAnimating;
			isAnimating = false;
			this.loadWaveData();
			isBufferStale = true;    // the audio buffer will need to be refreshed
			break;
		case 'j': case 'J': // save current WaveSynth to a JSON file
			saveWaveData();
			break;
		case 'f': // print the current frame rate to the console
			println("--->> frame rate: " + frameRate);
			break;
		case '1': // set the current WaveSynth's sample rate to genWidth * genHeight
			newSampleRate = genWidth * genHeight;
			println("--->> new sample rate for WaveSynth = genWidth * genHeight = "+ newSampleRate);
			wavesynth.setSampleRate(newSampleRate);
			isBufferStale = true;
			break;
		case '2': // set the current WaveSynth's sample rate to genWidth * genHeight / 2
			newSampleRate = genWidth * genHeight / 2;
			println("--->> new sample rate for WaveSynth = genWidth * genHeight / 2 = "+ newSampleRate);
			wavesynth.setSampleRate(newSampleRate);
			isBufferStale = true;
			break;
		case '3': // set the current WaveSynth's sample rate to genWidth * genHeight / 3
			newSampleRate = genWidth * genHeight / 3;
			println("--->> new sample rate for WaveSynth = genWidth * genHeight / 3 = "+ newSampleRate);
			wavesynth.setSampleRate(newSampleRate);
			isBufferStale = true;
			break;
		case '4': // set the current WaveSynth's sample rate to genWidth * genHeight / 4
			newSampleRate = genWidth * genHeight / 4;
			println("--->> new sample rate for WaveSynth = genWidth * genHeight / 4 = "+ newSampleRate);
			wavesynth.setSampleRate(newSampleRate);
			isBufferStale = true;
			break;
		case '5': // set the current WaveSynth's sample rate to genWidth * genHeight / 5
			newSampleRate = genWidth * genHeight / 5;
			println("--->> new sample rate for WaveSynth = genWidth * genHeight / 5 = "+ newSampleRate);
			wavesynth.setSampleRate(newSampleRate);
			isBufferStale = true;
			break;
		case '6': // set the current WaveSynth's sample rate to genWidth * genHeight / 6
			newSampleRate = genWidth * genHeight / 6;
			println("--->> new sample rate for WaveSynth = genWidth * genHeight / 6 = "+ newSampleRate);
			wavesynth.setSampleRate(newSampleRate);
			isBufferStale = true;
			break;
		case '7': // set the current WaveSynth's sample rate to the audio sample rate
			newSampleRate = this.sampleRate;
			println("--->> new sample rate for WaveSynth = this.sampleRate = "+ newSampleRate);
			wavesynth.setSampleRate(newSampleRate);
			isBufferStale = true;
			break;
		case 'h': // show help message in console
			showHelp();
			break;
		default:
			break;
		}
	}
	
	public void showHelp() {
		println(" * Press ' ' to turn animation on and off.");
		println(" * Press 'd' to turn raindrops on and off.");
		println(" * Press 't' to detune instruments by a small amount.");
		println(" * Press 'T' to restore tuning to instruments.");
		println(" * Press 'l' or 'L' to select long or short notes for audio events.");
		println(" * Press 'o' or 'O' to open a WaveSynth settings JSON file.");
		println(" * Press 'j' or 'J' to save current WaveSynth to a JSON file.");
		println(" * Press 'f' to print the current frame rate to the console.");
		println(" * Press '1' to set the current WaveSynth's sample rate to genWidth * genHeight.");
		println(" * Press '2' to set the current WaveSynth's sample rate to genWidth * genHeight / 2.");
		println(" * Press '3' to set the current WaveSynth's sample rate to genWidth * genHeight / 3.");
		println(" * Press '4' to set the current WaveSynth's sample rate to genWidth * genHeight / 4.");
		println(" * Press '5' to set the current WaveSynth's sample rate to genWidth * genHeight / 5.");
		println(" * Press '6' to set the current WaveSynth's sample rate to genWidth * genHeight / 6.");
		println(" * Press '7' to set the current WaveSynth's sample rate to the audio sample rate.");
		println(" * Press 'h' to show help message in console.");
	}

	public void stepAnimation() {
		step += 1;
		step %= animSteps;
		wavesynth.renderFrame(step);
	}
	
	public void mouseClicked() {
		sampleX = mouseX;
		sampleY = mouseY;
		samplePos = mapper.lookupSignalPos(sampleX, sampleY);
		println("---> samplePos = "+ samplePos);
		float panning = map(sampleX, 0, width, -0.875f, 0.875f);
		if (audioSignal == null || isBufferStale) {
			renderSignal();
			synth.setBuffer(audioBuffer);
			isBufferStale = false;
		}
		if (isPlayLongNote) playLongNote(panning);
		else playShortNote(panning);
		// playSample(samplePos, calcSampleLen(), 0.3f, adsr, 1.0f, panning);
	}

	/**
	 * Triggers an audio event with a 3-second initial duration followed by a 5-second long release. 
	 */
	public void playLongNote(float panning) {
		int samples = Math.round(longDuration * sampleRate / 1000);
		int totalLen = playSample(samplePos, samples, 0.3f, longAdsr, 1.0f, panning);
		println("----- samples = "+ samples +" at sample rate "+ sampleRate 
				+", actual duration "+ totalLen +" samples = "+ nf(totalLen/sampleRate, 0, 4) +" seconds.");
	}
	
	/**
	 * Triggers an audio event with a 3-second initial duration followed by a 5-second long release. 
	 */
	public void playShortNote(float panning) {
		int samples = Math.round(shortDuration * sampleRate / 1000);
		int totalLen = playSample(samplePos, samples, 0.3f, shortAdsr, 1.0f, panning);
		println("----- samples = "+ samples +" at sample rate "+ sampleRate 
				+", actual duration "+ totalLen +" samples = "+ nf(totalLen/sampleRate, 0, 4) +" seconds.");
	}

		
	/**
	 * Calls WaveSynth to render a audio sample array derived from the same math that creates the image.
	 */
	public void renderSignal() {
		this.audioSignal = wavesynth.renderAudioRaw(step);			// get the signal "as is" from WaveSynth
		audioSignal = WaveSynth.normalize(audioSignal, 0.9f);		// normalize samples to the range (-0.9f, 0.9f) 
		audioLength = audioSignal.length;
		audioBuffer.setBufferSize(audioLength);
		audioBuffer.setChannel(0, audioSignal);						// copy audioSignal to channel 0 of audioBuffer
		// println("--->> copied audio signal to audio buffer");
	}	

	/**
	 * Plays an audio sample with  with a custom envelope, pitch and stereo pan.
	 * 
	 * @param samplePos    position of the sample in the audio buffer
	 * @param samplelen    length of the sample (will be adjusted)
	 * @param amplitude    amplitude of the sample on playback
	 * @param env          an ADSR envelope for the sample
	 * @param pitch        pitch scaling as deviation from default (1.0), where 0.5 = octave lower, 2.0 = oactave higher 
	 * @param pan          position of sound in the stereo audio field (-1.0 = left, 0.0 = center, 1.0 = right)
	 * @return
	 */
	public int playSample(int samplePos, int samplelen, float amplitude, ADSRParams env, float pitch, float pan) {
		samplelen = synth.playSample(samplePos, samplelen, amplitude, env, pitch, pan);
		// samplelen = synth.playSample(samplePos, samplelen, amplitude);
		int durationMS = (int)(samplelen/sampleRate * 1000);
		timeLocsArray.add(new TimedLocation(sampleX, sampleY, durationMS + millis()));
		// return the length of the sample
		return samplelen;
	}
	
	/**
	 * @return a length in samples with some Gaussian variation
	 */
	public int calcSampleLen(float timeInSecs) {
		float timeInSamples = timeInSecs * sampleRate;
		float vary = 0; 
		// skip the fairly rare negative numbers
		while (vary <= 0) {
			vary = (float) PixelAudio.gauss(1.0, 0.0625);
		}
		samplelen = (int)(abs(vary * timeInSamples));
		// println("---- calcSampleLen samplelen = "+ samplelen +" samples at "+ sampleRate +"Hz sample rate");
		return samplelen;
	}

	
	//-------------------------------------------//
	//               JSON FILE I/O               //
	//-------------------------------------------//
	

	// select a file of WaveData objects in JSON format to open
	public void loadWaveData() {
		File folderToStartFrom = new File(dataPath("") + jsonFolder + "//*.json");
		selectInput("Select a file to open", "fileSelectedOpen", folderToStartFrom);
	}

	public void fileSelectedOpen(File selection) {
		if (selection == null) {
			println("Window was closed or the user hit cancel.");
			isAnimating = oldIsAnimating;
			return;
		}
		currentDataFile = selection;
		println("User selected " + selection.getAbsolutePath());
		currentFileName = selection.getAbsolutePath();
		json = loadJSONObject(currentFileName);
		setWaveSynthFromJSON(json, wavesynth);
		surface.setTitle(currentFileName);
		isAnimating = oldIsAnimating;
	}

	public void setWaveSynthFromJSON(JSONObject json, WaveSynth synth) {
		// set animation globals and WaveSynth properties
		animSteps = (json.isNull("steps")) ? 240 : json.getInt("steps");
		synth.setAnimSteps(animSteps);
		animStop = (json.isNull("stop")) ? this.animSteps : json.getInt("stop");
		synth.setStop(animStop);
		myGamma = (json.isNull("gamma")) ? 1.0f : json.getFloat("gamma");
		synth.setGamma(myGamma);
		comments = (json.isNull("comments")) ? "" : json.getString("comments");
		synth.setComments(comments);
		synth.setGain(json.isNull("blendFactor") ? 0.5f : json.getFloat("blendFactor"));
		synth.setVideoFilename((json.isNull("filename")) ? "wavesynth.mp4" : json.getString("filename"));
		synth.setScaleHisto((json.isNull("scaleHisto")) ? false : json.getBoolean("scaleHisto"));
		if (synth.isScaleHisto()) {
			synth.setHistoHigh((json.isNull("histoHigh")) ? 255 : json.getInt("histoHigh"));
			synth.setHistoLow((json.isNull("histoLow")) ? 0 : json.getInt("histoLow"));
		}
		// now load the JSON wavedata into ArrayList<WaveData> waveDataList
		JSONArray waveDataArray = json.getJSONArray("waves");
		int datalen = waveDataArray.size();
		ArrayList<WaveData> waveDataList = new ArrayList<WaveData>(datalen);
		for (int i = 0; i < datalen; i++) {
			// load fields common to both old and new format
			JSONObject waveElement = waveDataArray.getJSONObject(i);
			float f = waveElement.getFloat("freq");
			float a = waveElement.getFloat("amp");
			float p = waveElement.getFloat("phase");
			// float pInc = waveElement.getFloat("phaseInc");
			float dc = 0.0f;
			if (!waveElement.isNull("dc")) {
				dc = waveElement.getFloat("dc");
			}
			JSONObject rgbColor = waveElement.getJSONObject("color");
			int c = color(rgbColor.getInt("r"), rgbColor.getInt("g"), rgbColor.getInt("b"));
			float cycles;
			cycles = waveElement.getFloat("cycles");
			// frequency, amplitude, phase, dc, cycles, color, steps
			WaveData wd = new WaveData(f, a, p, dc, cycles, c, animSteps);
			waveDataList.add(wd);
		}
		synth.setWaveDataList(waveDataList);
		synth.prepareAnimation();
		synth.renderFrame(0);
		printWaveData(synth);
	}

	/**
	 * Outputs fields from current waveAnimal and it waveDataList
	 */
	public void printWaveData(WaveSynth synth) {
		java.nio.file.Path path = java.nio.file.Paths.get(currentFileName);
		String fname = path.getFileName().toString();
		println("\n--------=====>>> Current WaveSynth instance for file " + fname + " <<<=====--------\n");
		println("Animation steps: " + synth.getAnimSteps());
		// println("Stop frame: "+ waveAnimal.getAnimSteps());
		println("gain: " + synth.getGain());
		println("gamma: " + synth.getGamma());
		if (synth.isScaleHisto()) {
			println("scaleHisto: " + synth.isScaleHisto());
			println("histoLow: " + synth.getHistoLow());
			println("histoHigh: " + synth.getHistoHigh());
		}
		println(fname);
		println("video filename: " + synth.getVideoFilename());
		// println("WaveData list for: "+ videoFilename);
		for (int i = 0; i < synth.waveDataList.size(); i++) {
			WaveData wd = synth.waveDataList.get(i);
			println("  " + (i + 1) + ":: " + wd.toString());
		}
		println("comments: " + synth.getComments() +"\n");
	}
	
	public void saveWaveData() {
		selectOutput("Select a file to write to:", "fileSelectedWrite");
	}

	public void fileSelectedWrite(File selection) {
		if (selection == null) {
			println("Window was closed or the user hit cancel.");
			return;
		}
		WaveSynth synth = this.wavesynth;
		println("User selected " + selection.getAbsolutePath());
		// Do we have a .json at the end?
		if (selection.getName().length() < 5
				|| selection.getName().indexOf(".json") != selection.getName().length() - 5) {
			// problem missing ".json"
			currentFileName = selection.getAbsolutePath() + ".json"; // very rough approach...
		} else {
			currentFileName = selection.getAbsolutePath();
		}
		// put WaveData objects into an array
		JSONArray waveDataArray = new JSONArray();
		JSONObject waveElement;
		WaveData wd;
		for (int i = 0; i < synth.waveDataList.size(); i++) {
			wd = synth.waveDataList.get(i);
			waveElement = new JSONObject();
			waveElement.setInt("index", i);
			waveElement.setFloat("freq", wd.freq);
			waveElement.setFloat("amp", wd.amp);
			waveElement.setFloat("phase", wd.phase);
			waveElement.setFloat("phaseInc", wd.phaseInc);
			waveElement.setFloat("cycles", wd.phaseCycles);
			waveElement.setFloat("dc", wd.dc);
			// BADSR settings
			int[] rgb = PixelAudioMapper.rgbComponents(wd.waveColor);
			JSONObject rgbColor = new JSONObject();
			rgbColor.setInt("r", rgb[0]);
			rgbColor.setInt("g", rgb[1]);
			rgbColor.setInt("b", rgb[2]);
			waveElement.setJSONObject("color", rgbColor);
			// append wave data to array
			waveDataArray.append(waveElement);
		}
		// put the array into an object that tracks other state variables
		JSONObject stateData = new JSONObject();
		stateData.setInt("steps", synth.animSteps);
		stateData.setInt("stop", animStop);
		stateData.setFloat("blendFactor", synth.gain);
		stateData.setInt("dataFormat", 2);
		if (!selection.exists())
			stateData.setString("comments", "---");
		else
			stateData.setString("comments", synth.comments);
		// String videoName = selection.getName(); 
		String videoName = synth.videoFilename;
		if (videoName == null || videoName.equals("")) {
			videoName = selection.getName();
			if (videoName.indexOf(".json") != -1) {
				videoName = videoName.substring(0, videoName.indexOf(".json")) + ".mp4";
			} else {
				videoName += ".mp4";
			}
		}
		println("----->>> video name is " + videoName);
		synth.videoFilename = videoName; // ???
		stateData.setString("filename", videoName);
		stateData.setFloat("gamma", synth.gamma);
		stateData.setBoolean("scaleHisto", synth.isScaleHisto);
		stateData.setFloat("histoHigh", synth.histoHigh);
		stateData.setFloat("histoLow", synth.histoLow);
		stateData.setJSONArray("waves", waveDataArray);
		saveJSONObject(stateData, currentFileName);
		currentDataFile = new File(currentFileName);
		surface.setTitle(currentFileName);
	}
	
}
