public class RMSSample {
	public float levelL;
	public float levelR;
	public float peakL;
	public float peakR;

	public RMSSample(float levL, float levR, float peakL, float peakR) {
		this.peakR = peakR;
		this.peakL = peakL;
		this.levelR = levR;
		this.levelL = levL;
	}

	/**
	 * Create a wave sample from samples.
	 * 
	 * @param sampL
	 *            the left samples
	 * @param sampR
	 *            the right samples
	 */
	public static RMSSample create(float[] sampL, float[] sampR) {
		Assert.isTrue(sampL != null);
		Assert.isTrue(sampR != null);
		Assert.isTrue(sampR.length == sampL.length);
		
		float levL = 0.0f;
		float levR = 0.0f;
		float peakL = 0.0f;
		float peakR = 0.0f;

		for (int i = 0; i < sampL.length; i++) {
			levR += (sampR[i] * sampR[i]);
			levL += (sampL[i] * sampL[i]);
			if (sampR[i] > peakR) peakR = sampR[i];
			if (sampL[i] > peakL) peakL = sampL[i];
		}
		levR = (float) Math.sqrt(levR / sampR.length);
		levL = (float) Math.sqrt(levL / sampL.length);

		return new RMSSample(levL, levR, peakL, peakR);
	}

} public class WaveCleaner {
	public static final String TITLE = "Wavecleaner"; 
	public static String workingDir = ".";
	
	private static Logger LOG = LogFactory.getLog(WaveCleaner.class);
	private static WaveCleaner application;
	private MainScreen mainFrame;
	public Minim minim;
	protected File tempDir = null;
	private AppConfiguration config;

	private WaveCleaner(){
		if( WaveCleaner.application != null ){
			throw new IllegalStateException("The application is ALREADY created!");
		}
	}
	
	/**
	 * Get the application instance.
	 * 
	 * @return the application instance.
	 */
	public static final WaveCleaner getApplication(){
		if( application == null ){
			throw new IllegalStateException("The application MUST be created at this point.");
		}
		return application;
	}
	
	public JFrame getMainScreen(){
		return mainFrame;
	}
	
	/**
	 * Looking for the user directory.
	 * 
	 * @return the working directory
	 */
	protected static File lookingForUserDirectory(){
		String userHome = System.getProperty("user.home");
		if( userHome != null ){
			LOG.info("User directory set from 'user.home'" );
			File d = new File(userHome);
			if( d.exists() && d.isDirectory() ){
				return d;
			}
		}
		
		// The following code is only when the user home
		// is not provided by the JVM. Should never be the case.
		String homePath = System.getenv("HOMEPATH");
		if( homePath != null ){
			LOG.info("User directory set from HOMEDRIVE/PATH." );
			String homeDrive = System.getenv("HOMEDRIVE");
			String homeShare = System.getenv("HOMESHARE");
			File f = new File( (homeDrive == null ? homeShare : homeDrive) + homePath);
			if( f.isDirectory() ) return f;
		}
		
		File home = new File(System.getenv("HOME"));
		if( home.isDirectory() ){
			return home;
		}
		
		return new File(".");
	}

	public static void main(String[] args) {
		LOG.debug("Program started.");
		
		// For Mac users!
        System.setProperty("apple.laf.useScreenMenuBar", "true");
        System.setProperty("com.apple.mrj.application.apple.menu.about.name", "Test");
        try {
			UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
		} catch (ClassNotFoundException|InstantiationException|IllegalAccessException|UnsupportedLookAndFeelException e) {
			System.err.println("Does not support the native look and feel");
		}
        
        application = new WaveCleaner();
        application.start();
		application.cleanUp();
		for( int i = 0; i < args.length; i++ ){
			if(args[i].charAt(0) == '-'){
				switch( args[i].charAt(1) ){
				case 's' :
					// Load the sound file
					String soundFile = args[++i];
					SwingUtilities.invokeLater( () -> {
						application.mainFrame.loadSoundFile(soundFile);
					});
					break;
					
				case 'c' :
					application.setConfigurationFile(args[i++]);
					break;
				
				case 'r' :
					SwingUtilities.invokeLater(() -> {
						application.mainFrame.onRecordSound();					
					});
					break;
				}
			}
		}
	}
	
	
	/**
	 * load the file stream.
	 * 
	 * @param f the file to load
	 * @return the stream loaded.
	 */
	public AudioRecordingStream loadFileStream(File f){
		String filename = f.getAbsolutePath();
		AudioRecordingStream stream = this.minim.loadFileStream(filename);
		return stream;
	}


	/**
	 * Retrieve an audio recorder.
	 * 
	 * @param f the file to save the sound.
	 * @param sampleRate the sample rate
	 * @return the {@link AudioRecorder}.
	 */
	public AudioRecorder getAudioRecorder(File f, float sampleRate){
		AudioInput in = this.minim.getLineIn(Minim.STEREO, (int)(sampleRate / 20), sampleRate);
		AudioRecorder stream = minim.createRecorder(in, f.getAbsolutePath());
		return stream;
	}


	/**
	 * Check for old temporary files created by other instances. If found, they are deleted.
	 * Note the deletion works only if the file is not locked (then 2 instances can run 
	 * simultaneously)
	 *  
	 */
	protected void cleanUp(){
		File files[] = tempDir.listFiles();
		for(File f : files){
			String name = f.getName();
			if( name.startsWith(AudioCache.PREFIX) && name.endsWith(AudioCache.SUFFIX)){
				boolean success = f.delete();
				LOG.info("Deletion of {} has {}.", f, (success ? "SUCCEED" : "FAILED"));
			}
		}
	}
	
	public AudioSample getAudioSample( File f ){
		String name = f.getAbsolutePath();
		return minim.loadSample(name);
	}
	
	/**
	 * Terminates the application
	 */
	public void dispose(){
		minim.dispose();
	}
	
	/**
	 * Starts the program
	 */
	public void start(){
		this.tempDir = new File( System.getProperty("java.io.tmpdir") );

		File userDir = lookingForUserDirectory();
		this.setConfigurationFile(userDir.getAbsolutePath() + File.separatorChar + ".wavecleaner");
		
		// Initialize the main screen
		mainFrame = new MainScreen();
		minim = new Minim(new ProcessingLegacy());
		mainFrame.init(this);
	}

	public AudioInput getLineIn(float sampleRate, int bufferSize){
		AudioInput input = this.minim.getLineIn(Minim.STEREO, bufferSize, sampleRate);
		return input;
	}
	
	public AudioOutput getLineOut(){
		return this.minim.getLineOut(Minim.STEREO);
	}

	public String fixAudioFilename(String filename ){
		// Add an extension if necessary
		String extension = "";
		if( filename.indexOf(".") >= 0 ){
			extension = filename.toLowerCase().substring(filename.lastIndexOf('.'));
		}
		if( !Arrays.asList(".wav", ".aiff").contains(extension) ){
			filename += ".wav"; 
		}
		
		return filename;
	}
	
	public AudioRecorder createRecorder(Recordable source, String filename){
		return this.minim.createRecorder(source, filename);
	}

	void setConfigurationFile(String fileName){
		File f = new File(fileName);
		this.config = new AppConfiguration(f);
	}
} public class AudioDocumentPlayer extends FilePlayer {
	private static Logger LOG = LogFactory.getLog(AudioDocumentPlayer.class);
	
	private static final int NB_CHANNELS = 2;
		
	float[] queue;
	int first = 0; // First positio in the queue
	int last = 0;  // Last positio in the queue

	/**
	 * Return the delay in samples. The delay is the exact number of
	 * samples in the queue. Not very tricky.
	 * 
	 * @return the delay in samples
	 */
	public int getDelay(){
		return last - first;
	}

	public AudioDocumentPlayer(AudioRecordingStream iFileStream) {
		super(iFileStream);
		this.queue = new float[100];
		this.setSampleRate(iFileStream.getFormat().getSampleRate());
	}
	
	public int playHead(){
		// TODO: Why no sample rate?
		float sampleRate = 48000 /*sampleRate() */;
		double pos = this.position() / 1000.0;
		int sample = (int)(pos * sampleRate) /*+ last - first*/;
		return sample;
	}

	public void cue(int millis){
		// We should know this... Because we are not at the same point anymore
		super.cue(millis);
		listenerManager.publish( l -> l.audioPlayed(this.playHead()) );
	}
	
	private int buffsize(){
		return this.queue.length / NB_CHANNELS;
	}
	

	/**
	 * Pop one (and only one) sample per channel. Use to get the original
	 * signal for the original file.
	 * 
	 * @param channels the STEREO channel.
	 */
	public synchronized void pop(float[] channels) {
		if (first < 0 || last < first) {
			throw new IllegalAccessError("The queue is empty!");
		}
		int pos = first * NB_CHANNELS;
		channels[0] = queue[pos];
		channels[1] = queue[pos + 1];
		
		// For debugging purposes, create a white noise
		queue[pos] =  (float)(Math.random() * 2.0 - 1.0);
		queue[pos + 1] = (float)(Math.random() * 2.0 - 1.0);
		
		first++; // We are ahead
		if (first == buffsize()) {
			// Not fully necessary because we do a modulo when pushing
			// but helps in calling the refresh
			first -= buffsize();
			last -= buffsize();
		}
		if( first % 100 == 0 ){
			listenerManager.publishOnce( l -> l.audioPlayed(this.playHead() - last + first));
		}
	}

	
	/**
	 * Push a new sample in the queue. Note the queue is a basic 
	 * list (an array) where the data is pushed in a circular manner. Then
	 * there is no need of copy (except when the queue must be expanded).
	 * 
	 * @param left the left sample.
	 * @param right the right sample.
	 */
	synchronized void push(float left, float right) {
		if ( (last - first) + 1 > buffsize()) {
			// Add 100 more samples
			int newSize = (int)(this.buffsize() * 1.25) * NB_CHANNELS;
			float[] newQueue = new float[newSize];
			// The copy below is not performant.
			for(int i = first; i < last; i++ ){
				int oldPos = (i % buffsize()) * NB_CHANNELS;
				int newPos = ((i - first) % buffsize()) * NB_CHANNELS;
				for(int ch = 0; ch < NB_CHANNELS; ch ++ ){
					newQueue[ newPos++ ] = queue[ oldPos++ ]; 
				}
			}
			
			// The queue is now from 0.
			last -= first;
			first = 0;
			queue = newQueue;
			LOG.info("Increased buffer to {} samples.", buffsize());
		}

		int pos = (last % buffsize()) * NB_CHANNELS;
		queue[pos] = left;
		queue[pos + 1] = right;
		last++;
	}

	
	protected void uGenerate(float[] channels) 
	{
		super.uGenerate(channels);
		push(channels[0], channels[1]); // Store in the original buffer
	}
	

	private ListenerManager<AudioPlayerListener> listenerManager = new ListenerManager<AudioPlayerListener>();
	
	
	/**
	 * Register a new listener. Used at the beginning for the
	 * {@link WaveFormComponent} but any object can listen.
	 * 
	 * @param listener
	 *            an audio listener.
	 */
	public void addPlayerListener(AudioPlayerListener listener) {
		this.listenerManager.add(listener);
	}

	public void removePlayerListener(AudioPlayerListener listener) {
		this.listenerManager.remove(listener);
	}
} public interface AudioPlayerListener extends EventListener {
	
	/**
	 * A sample is played (then audio is playing). The last
	 * sample played is sent.
	 * 
	 * @param sample the last sample played.
	 */
	void audioPlayed( int sample );
	
	/**
	 * The file has stopped to play.
	 */
	void audioPaused();
} public class ClickRemovalFilter extends AudioFilter {
	private static Logger LOG = LogFactory.getLog(ClickRemovalFilter.class);
	private final static int SEP = 2049;
	
	public static final String THRESHOLD = "thresold";
	public static final String CLICK_WIDTH ="clickWidth";
	public static final String REMOVED_LEFT = "removed left";
	public static final String REMOVED_RIGHT = "removed right";

	/**
	 * Performs spline interpolation given a set of control points.
	 * Code from <a href="https://gist.github.com/lecho/7627739">https://gist.github.com/</a>.
	 * 
	 */
	static class SplineInterpolator {

		private final List<Float> mX;
		private final List<Float> mY;
		private final float[] mM;

		private SplineInterpolator(List<Float> x, List<Float> y, float[] m) {
			mX = x;
			mY = y;
			mM = m;
		}

		/**
		 * Creates a monotone cubic spline from a given set of control points.
		 * 
		 * The spline is guaranteed to pass through each control point exactly. Moreover, assuming the control points are
		 * monotonic (Y is non-decreasing or non-increasing) then the interpolated values will also be monotonic.
		 * 
		 * This function uses the Fritsch-Carlson method for computing the spline parameters.
		 * http://en.wikipedia.org/wiki/Monotone_cubic_interpolation
		 * 
		 * @param x
		 *            The X component of the control points, strictly increasing.
		 * @param y
		 *            The Y component of the control points
		 * @return
		 * 
		 * @throws IllegalArgumentException
		 *             if the X or Y arrays are null, have different lengths or have fewer than 2 values.
		 */
		public static SplineInterpolator createMonotoneCubicSpline(List<Float> x, List<Float> y) {
			if (x == null || y == null || x.size() != y.size() || x.size() < 2) {
				throw new IllegalArgumentException("There must be at least two control "
						+ "points and the arrays must be of equal length.");
			}

			final int n = x.size();
			float[] d = new float[n - 1]; // could optimize this out
			float[] m = new float[n];

			// Compute slopes of secant lines between successive points.
			for (int i = 0; i < n - 1; i++) {
				float h = x.get(i + 1) - x.get(i);
				if (h <= 0f) {
					throw new IllegalArgumentException("The control points must all "
							+ "have strictly increasing X values.");
				}
				d[i] = (y.get(i + 1) - y.get(i)) / h;
			}

			// Initialize the tangents as the average of the secants.
			m[0] = d[0];
			for (int i = 1; i < n - 1; i++) {
				m[i] = (d[i - 1] + d[i]) * 0.5f;
			}
			m[n - 1] = d[n - 2];

			// Update the tangents to preserve monotonicity.
			for (int i = 0; i < n - 1; i++) {
				if (d[i] == 0f) { // successive Y values are equal
					m[i] = 0f;
					m[i + 1] = 0f;
				} else {
					float a = m[i] / d[i];
					float b = m[i + 1] / d[i];
					float h = (float) Math.hypot(a, b);
					if (h > 9f) {
						float t = 3f / h;
						m[i] = t * a * d[i];
						m[i + 1] = t * b * d[i];
					}
				}
			}
			return new SplineInterpolator(x, y, m);
		}

		/**
		 * Interpolates the value of Y = f(X) for given X. Clamps X to the domain of the spline.
		 * 
		 * @param x
		 *            The X value.
		 * @return The interpolated Y = f(X) value.
		 */
		public float interpolate(float x) {
			// Handle the boundary cases.
			final int n = mX.size();
			if (Float.isNaN(x)) {
				return x;
			}
			if (x <= mX.get(0)) {
				return mY.get(0);
			}
			if (x >= mX.get(n - 1)) {
				return mY.get(n - 1);
			}

			// Find the index 'i' of the last point with smaller X.
			// We know this will be within the spline due to the boundary tests.
			int i = 0;
			while (x >= mX.get(i + 1)) {
				i += 1;
				if (x == mX.get(i)) {
					return mY.get(i);
				}
			}

			// Perform cubic Hermite spline interpolation.
			float h = mX.get(i + 1) - mX.get(i);
			float t = (x - mX.get(i)) / h;
			return (mY.get(i) * (1 + 2 * t) + h * mM[i] * t) * (1 - t) * (1 - t)
					+ (mY.get(i + 1) * (3 - 2 * t) + h * mM[i + 1] * (t - 1)) * t * t;
		}

		// For debugging.
		@Override
		public String toString() {
			StringBuilder str = new StringBuilder();
			final int n = mX.size();
			str.append("[");
			for (int i = 0; i < n; i++) {
				if (i != 0) {
					str.append(", ");
				}
				str.append("(").append(mX.get(i));
				str.append(", ").append(mY.get(i));
				str.append(": ").append(mM[i]).append(")");
			}
			str.append("]");
			return str.toString();
		}
	}
	
	/**
	 * Create the decrackler.
	 * 
	 * @param iStream
	 *            the input stream.
	 */
	public ClickRemovalFilter() {
		super();
		this.addParameter(THRESHOLD, 0, 900, 200, 5, v -> {
			NumberFormat formatter = new DecimalFormat("0");
			return formatter.format(v);
		});
//		this.addParameter(SAMPLE_WIDTH, 0.5f, 5.0f, 1.0f, 0.1f, v -> {
//			NumberFormat formatter = new DecimalFormat("0 ms");
//			return formatter.format(v);
//		});
		this.addParameter(CLICK_WIDTH, 0.0f, 0.001f, 0.0005f, 0.00001f, v -> {
			NumberFormat formatter = new DecimalFormat("0.00 ms");
			return formatter.format(v * 1000);
		});
		this.addParameter(REMOVED_RIGHT, 0, Float.MAX_VALUE, 0, 1, v -> {
			NumberFormat formatter = new DecimalFormat("0");
			return formatter.format(v);
		});
		this.addParameter(REMOVED_LEFT, 0, Float.MAX_VALUE, 0, 1, v -> {
			NumberFormat formatter = new DecimalFormat("0");
			return formatter.format(v);
		});
	}
	
//	private int before_width = 0;
	private int after_width = 200;
	private int nbFound = 0;
//	private int sep = SEP;
	float y[][] = new float[2][];
	float ret[][] = new float[2][];
	
	public float[][] nextSamples() {
		int sampleWidth = 8192; // (int)this.getControl(SAMPLE_WIDTH) / 2;
		// int clickWidth = 5; // (int)this.getControl(CLICK_WIDTH) / 2;
		// float factor = 2.5f;
		after_width = sampleWidth;
		// int windowSize = sampleWidth * 2;
		
		float input[][] = loadSamples(sampleWidth, sampleWidth);
		for(int ch = 0; ch < 2; ch++){
			if(ret[ch] == null || ret[ch].length != sampleWidth){
				// Allocate the correct size!
				ret[ch] = new float[sampleWidth];
			}
			
			if(y[ch] == null || y[ch].length != sampleWidth * 2){
				// Allocate the correct size!
				float[] newBuf = new float[sampleWidth * 2];
				if( y[ch] != null ){
					// Copy the end of the queue...
					System.arraycopy(y[ch], 0, newBuf, 0, y[ch].length);
				}
				y[ch] = newBuf;
			}
			System.arraycopy(input[ch], 0, y[ch], 0, input[ch].length);

			// Calculate and store work done
			int nb = removeClicks(y[ch], sampleWidth * 2);
			if(nb > 0){
				this.addToControl(ch == 0 ? REMOVED_LEFT : REMOVED_RIGHT, nb);
			}
			// Save results
			System.arraycopy(y[ch], 0, ret[ch], 0, sampleWidth);
			System.arraycopy(y[ch], sampleWidth, y[ch], 0, y[ch].length - sampleWidth );
		}
		
//		before_width = after_width;
		return ret;
	}
	
	/**
	 * Remove the clicks found in the buffer.
	 * 
	 * @param buffer the buffer
	 * @param len the length of the buffer
	 */
	protected int removeClicks(float[] buffer, int len ) {
		int mClickWidth = this.getSampleControl(CLICK_WIDTH);
		int mThresholdLevel = this.getIntControl(THRESHOLD);
		int left = 0;
		float msw;
		int ww;
		int sep = SEP;
		int s2 = sep / 2;
		float[] ms_seq = new float[len];
		float[] b2 = new float[len];

		for( int i = 0; i < len; i++){
			b2[i] = buffer[i]*buffer[i];
		}
		
		//
		// Shortcut for rms - multiple passes through b2,
		// accumulating as we go.
		//
		for (int i = 0; i < len; i++) {
			ms_seq[i] = b2[i];
		}

		int i;
		for (i = 1; i < sep; i *= 2) {
			for (int j = 0; j < len - i; j++) {
				ms_seq[j] += ms_seq[j + i];
			}
		}

		// Cheat by truncating sep to next-lower power of two... 
		sep = i;

		for (i = 0; i < len - sep; i++) {
			ms_seq[i] /= sep;
		}

		
		//
		// ww runs from about 4 to mClickWidth. wrc is the reciprocal;
		// chosen so that integer roundoff doesn't clobber us.
		//
		int found = 0;
		int wrc;
		for (wrc = mClickWidth / 4; wrc >= 1; wrc /= 2) {
			ww = mClickWidth / wrc;

			for (i = 0; i < len - sep; i++) {
				msw = 0;
				for (int j = 0; j < ww; j++) {
					msw += b2[i + s2 + j];
				}
				msw /= ww;

				if (msw >= mThresholdLevel * ms_seq[i] / 10.0) {
					if (left == 0) {
						LOG.info("left is zero?");
						left = i + s2;
					}
				} else {
					if (left != 0 && (int)(i - left + s2) <= ww * 2) {
						float lv = buffer[left];
						float rv = buffer[i + ww + s2];
						LOG.info("Remove click...");

						
						if( left < 20 || (i + ww + s2 - left < 1)){
							// AUDACITY_METHOD:
							for (int j = left; j < i + ww + s2; j++) {
								float old = buffer[j];
								buffer[j] = (rv * (j - left) + lv * (i + ww + s2 - j)) / (float) (i + ww + s2 - left);
								LOG.info("AUDACITY: buff[{}] from {} to {}.", j, old, buffer[j]);
								b2[j] = buffer[j] * buffer[j];
							}	
						}
						else {
							int nb = Math.max(2, i + ww + s2 - left);
							List<Float> xArray = new ArrayList<Float>();
							List<Float> yArray = new ArrayList<Float>();
							for (int j = left - nb; j < left; j++) {
								xArray.add((float) j);
								yArray.add(buffer[j]);
							}	
							for (int j = 0; j < nb; j++){
								int x = i + ww + s2 + j;
								xArray.add((float)x);
								yArray.add(buffer[x]);
							}
							SplineInterpolator interpolator = SplineInterpolator.createMonotoneCubicSpline(xArray, yArray);
							
							// Interpolate
							float power_0 = 0;
							float power_1 = 0;
							float power_2 = 0;
							for (int j = left; j < i + ww + s2; j++) {
								float old = buffer[j];
								float v1 = (rv * (j - left) + lv * (i + ww + s2 - j)) / (float) (i + ww + s2 - left);
								float v2 = interpolator.interpolate((float)j);
								power_0 += buffer[j] * buffer[j];
								power_1 += (v1 * v1);
								power_2 += (v2 * v2);
								// LOG.info("buff[{}] from {} to v1={} and v2={}.", j, old, v1, v2);
								buffer[j] = v2;
								b2[j] = buffer[j] * buffer[j];
							}	
							LOG.info("Removed click at {} (len={}, nb={}) removed from POWER {} to {} versus {}.", left,  i + ww + s2 - left, nb, power_0, power_1, power_2);
						}
						
						found++;
						left = 0;
					} else if (left != 0) {
						left = 0;
					}
				}
			}
		}
		return found;
	}

} public class DecrackleFilter extends AudioFilter {
	private static Logger LOG = LogFactory.getLog(DecrackleFilter.class);

	public static final int WINDOW_SIZE = 2000;
	public static final String AVERAGE = "average";
	public static final String FACTOR = "factor";
	
	/**
	 * Create the decrackler.
	 * 
	 * @param iStream
	 *            the input stream.
	 */
	public DecrackleFilter() {
		super();
		this.addParameter(FACTOR, 0.01f, 1.0f, 0.5f, 0.2f, v -> {
			NumberFormat formatter = new DecimalFormat("0.0");
			return formatter.format(v);
		});
		this.addParameter(AVERAGE, 1, 10, 3, 1, v -> {
			NumberFormat formatter = new DecimalFormat("0");
			return formatter.format(v);
		});
	}

	private float[][] y = new float[2][];
	private float[][] y_hat = new float[2][];
	private float[] y_hat_tmp = null;
	
	private int old_width = 1;
	
	/**
	 * Process to the decrackling of one buffer.
	 */
	public float[][] nextSamples() {
		// Load control values
		int width = Math.max(1, (int)this.getControl(AVERAGE));
		int nmax = WINDOW_SIZE;
		
		int asize = nmax + old_width + width;
		if( y_hat_tmp == null || y_hat_tmp.length < asize ){
			// Inputs have changed or first round
			y_hat_tmp = newFloatArray(y_hat_tmp, asize);
			for (int ch = 0; ch < 2; ch++) {
				y[ch] = newFloatArray(y[ch], asize);
				y_hat[ch] = newFloatArray(y_hat[ch], asize);
			}
		}
		
		int n = nmax + old_width; 
		int[] cflag = new int[asize];
		
		int fixed = 0;
		float[][] input = loadSamples(nmax, width);
		for (int ch = 0; ch < 2; ch++) {
			Validate.isTrue(input[ch].length == nmax+width);
			System.arraycopy(input[ch], 0, y[ch], old_width, nmax+width);
			
			int scount = 0;
			double absum = 0.0;
			for (int i = old_width + 1; i < n; i++) {
				/* absum += fabs(y[ch][i]); */
				absum += Math.abs(y[ch][i] - y[ch][i-1]);
				scount++;
			}
			double scaled_factor = this.getControl(FACTOR) * absum / scount;

			for (int i = 0; i < n + width; i++) {
				double dy0 = 0;
				double dy1 = 0;
				double dy2dx = 0;

				y_hat[ch][i] = y[ch][i];
				if (i < 2) {
					/* wgt[i] = 1.0 ; */
					cflag[i] = 0;
				} else {
					dy0 = y_hat[ch][i-1] - y_hat[ch][i-2];
					dy1 = y[ch][i-0] - y_hat[ch][i-1];
					dy2dx = dy1 - dy0;

					if (dy2dx > scaled_factor) {
						// We found one!
						cflag[i-1] = 1;
						fixed++;
					} else if (dy2dx < -scaled_factor) {
						// We found one!
						cflag[i-1] = 1;
						fixed++;
					} else {
						cflag[i-1] = 0;
					}
					
				}
			}

			for (int i = old_width; i < n; i++) {
				int first, last;
				int flag = 0;
				double sum = 0.0;
				double sumwgt = 0.0;

				first = i - old_width;
				if (first < 0) {
					first = 0;
					LOG.warn("first < 0");
				}
				last = i + width;
				if (last > asize - 1) {
					last = asize - 1;
					LOG.warn("last > asize");
				}

				for (int j = first; j <= last; j++) {
					sum += y_hat[ch][j];
					sumwgt++;
				}
				flag = cflag[i - 1] | cflag[i] | cflag[i + 1];

				if (cflag[i] != 0) {
					fixed++;
					y_hat_tmp[i] = (float) (sum / sumwgt);
				} else if (flag != 0) {
					y_hat_tmp[i] = (float) ((sum / sumwgt + y_hat[ch][i]) / 2.0);
				} else {
					y_hat_tmp[i] = y_hat[ch][i];
				}
			}

			for (int i = 0; i < n + width; i++) {
				y_hat[ch][i] = y_hat_tmp[i];
			}
		

			// LOG.debug("Flagged {}% ({} times on {}), channel {}", (int)((float)fixed / asize * 100), fixed, asize, (ch == 0 ? "LEFT" : "RIGHT"));
		} // next channel

		// We MUST send back ONLY the first nmax samples!
		float ret[][] = new float[2][nmax];
		System.arraycopy(y_hat[0], old_width, ret[0], 0, nmax);
		System.arraycopy(y_hat[1], old_width, ret[1], 0, nmax);

		/*
		 * Copy the last width points of this window to the "pre-window" of
		 * the next window
		 */
		for (int i = 0; i < width; i++) {
			for (int ch = 0; ch < 2; ch++) {
				y_hat[ch][i] = y_hat[ch][i + n - width];
				y[ch][i] = y[ch][i + n - width];
			}
		}
		
		// LOG.debug("Changes applied: {}", fixed);
		old_width = width; // Now transfer the new value.
		return ret;
	}
} public class PreamplifierFilter extends AudioFilter {
	private Logger LOG = LogFactory.getLog(PreamplifierFilter.class);
	
	public static final String GAIN = "gain";
	public static final String SOURCE = "source";
	public static final String LIMITER = "limiter";
	
	/**
	 * Source for normal sound (through the filters).
	 */
	public static final int NORMAL = 0;
	
	/**
	 * Source for original sound (untouched).
	 */
	public static final int ORIGINAL = 1;
	
	/**
	 * Source minus original sound (what has been removed by the filters).
	 */
	public static final int DIFF = 2;
	
	/**
	 * Original sound on left and filtered one on right. No more stereo has
	 * the music is transferred in MONO.
	 * 
	 */
	public static final int LEFT_RIGHT = 3;
	
	private AudioDocumentPlayer player;
	private VUMeterComponent vumeter = null;
	
	public PreamplifierFilter( AudioDocumentPlayer player){
		super();
		this.addParameter(GAIN, -24.0f, +12.0f, 3.0f, 0.1f, (v) -> {
			NumberFormat formatter = new DecimalFormat("0.0 dB");
			return formatter.format(v);
		});
		this.addSelectorParameter(SOURCE, 4);
		this.addBooleanParameter(LIMITER, true);
		this.setSampleRate(48000f); // Force a default sample rate (overriden by the player)
		if( player != null ){
			this.setPlayer(player);
		}
	}
	
	public void setEnabled(boolean b){
		setControl(SOURCE, ConvertUtils.bool2flt(b) );
	}

	public boolean getEnabled(){
		return ConvertUtils.flt2bool( getControl(SOURCE) );
	}
	
	public void setPlayer(AudioDocumentPlayer player){
		Assert.notNull(player);
		Assert.isTrue( player.sampleRate() > 0 );
		LOG.debug("New player is now {}", player);
		this.player = player;
		this.setSampleRate(player.sampleRate()); // Use the player sample rate
	}

	public void setVUMeter(VUMeterComponent vumeter){
		this.vumeter = vumeter;
		this.vumeter.setSampleRate(this.sampleRate());
	}
	
	@Override
	protected void sampleRateChanged(){
		if( this.vumeter != null ){
			this.vumeter.setSampleRate(this.sampleRate());
		}
	}
	
	protected void process(MultiChannelBuffer buff) {
		if(player == null){
			throw new IllegalAccessError("'player' not set.");
		}
		
		float[] original = new float[2];
		float[] source = new float[2];
		float[] sample = new float[2];
		float dBvalue = this.getControl(GAIN);
		float mValue = (float)Math.pow(10.0, (0.05 * dBvalue));
		int mode = this.getIntControl(SOURCE);

		int len = buff.getBufferSize();
		for(int i = 0; i < len; i++ ){
			source[0] = buff.getSample(0, i);
			source[1] = buff.getSample(1, i);
				
			player.pop(original);
			
//			if( Math.abs(source[0] - original[0]) > 0.1 || Math.abs(source[1] - original[1]) > 0.1) {
//				LOG.debug("SIGNAL at {} HAS CHANGED A LOT: {} => {} (delay = {})", i, original, source, player.getDelay() );
//			}

			switch (mode) {
			case NORMAL:
				sample = source;
				break;
			case ORIGINAL:
				sample = original;
				break;
			case DIFF:
				sample[0] = source[0] - original[0];
				sample[1] = source[1] - original[1];
				break;
			case LEFT_RIGHT:
				// Channels are monaural to avoid a bad interpretation.
				sample[0] = (source[0] + source[1]) / 2.0f;
				sample[1] = (original[0] + original[1]) / 2.0f;
				break;
			}
				
			for( int ch = 0; ch < buff.getChannelCount(); ch++ ){
				sample[ch] = sample[ch] * mValue;
				buff.setSample(ch, i, sample[ch]);
			}
			

			float maxVol = Math.max(Math.abs(sample[0]), Math.abs(sample[1]));
			if( maxVol > 0.90f && ConvertUtils.flt2bool(getControl(LIMITER))){
				dBvalue = this.getControl(GAIN) / (maxVol + 0.1f);
				this.setControl(GAIN, dBvalue);
				mValue = (float)Math.pow(10.0, (0.05 * dBvalue));
			}
			if( vumeter != null ){
				vumeter.push(sample);
			}
		}
	}
	
} final public class Assert {
	public final static void isTrue( boolean value ){
		if(!value){
			throw new IllegalArgumentException("Expected value is false.");
		}
	}

	public final static void notNull( Object o ){
		if(o == null){
			throw new IllegalArgumentException("Expected value is null.");
		}
	}
	
	public final static void isEventDispatchThread(){
		if(!SwingUtilities.isEventDispatchThread()){
			throw new IllegalStateException("You must run this in the AWT Thread.");
		}
	}
	
	public final static void equals( Object a, Object b){
		if(!Objects.equals(a, b)){
			throw new IllegalArgumentException("Expected values must be equal.");
		}
	}
} public class ListenerManager<T> {
	private static Logger LOG = LogFactory.getLog(ListenerManager.class);
	private List<ListenerInfo<T>> listenerInfos = new ArrayList<>();
	
	/**
	 * We keep information on the listener
	 *
	 *
	 */
	private static class ListenerInfo<T>  {
		AtomicInteger mutex = new AtomicInteger(0);
		T listener;
		int skipped = 0;
		int calls = 0;
		
		public ListenerInfo(T listener){
			this.listener = listener;
		}
		
		/**
		 * We invoke the listener but only if a call is not already
		 * in the queue. This avoid multiple calls. Note only the
		 * first call is, in this case, used. The other calls are
		 * dimissed.
		 * 
		 * @param val the value for invocation.
		 */
		public void invoke(Consumer<T> fnct){
			calls++;
			if( mutex.getAndIncrement() < 1 ){
				// We can run the code in the SWING thread...
				// the value is now "1" (means: waiting)
				// we boost once  
				mutex.incrementAndGet();
				SwingUtilities.invokeLater( () -> {
					// LOG.debug("invoked.");
					fnct.accept(this.listener);
					mutex.decrementAndGet();
				});
			}
			else {
				// for debugging purposes
				skipped++;
				if( skipped % 1000 == 0 ){
					int ratio = (int)(100.0 * (calls - skipped) / calls);
					LOG.debug("Listener {} called {} times ({}%).", this.listener.getClass().getSimpleName(), (calls - skipped), ratio);
				}
			}
			mutex.decrementAndGet();
		}
		
		T getListener(){
			return listener;
		}
	}
	
	/**
	 * Very basic constructor
	 */
	public ListenerManager() {
	}
	
	
	/**
	 * Notify all the listeners. We use a {@link Function}
	 * to keep the code simple. Note the listener  
	 * 
	 */
	public void publishOnce(Consumer<T> function){
		for(ListenerInfo<T> infos : this.listenerInfos){
			infos.invoke(function);
		}
	}
	
	/**
	 * Force to publish.
	 * 
	 * @param function the code to call.
	 */
	public void publish(final Consumer<T> function){
		for(ListenerInfo<T> infos : this.listenerInfos){
			SwingUtilities.invokeLater( () -> {
				function.accept(infos.getListener());
			});
		}
	}

	/**
	 * Send directly on the current thread.
	 * 
	 * @param function the code to call.
	 */
	public void send(final Consumer<T> function){
		for(ListenerInfo<T> infos : this.listenerInfos){
			function.accept(infos.getListener());
		}
	}
	
	/**
	 * Add a new listener for this manager. If the listener was already there,
	 * it is replaced.
	 * 
	 * @param listener the listener to add.
	 */
	public void add(T listener ) {
		synchronized(this.listenerInfos){
			this.listenerInfos.remove(listener); // in case already there!
			ListenerInfo<T> infos = new ListenerInfo<T>(listener);
			this.listenerInfos.add(infos);
		}
	}
	
	/**
	 * Remove the specified listener.
	 * 
	 * @param listener
	 */
	public void remove(T listener ) {
		synchronized(this.listenerInfos){
			this.listenerInfos.remove(listener);
		}
	}

}
 public class LogFactory {
	public static final Logger getLog(final Class<?> clazz) {
		Logger log = LogManager.getLogger(clazz);
		return log;
	}
}

import java.io.File;
import java.io.IOException;
import javax.sound.sampled.AudioFormat;
import org.apache.logging.log4j.Logger;
import com.oxande.wavecleaner.RMSSample;
import com.oxande.wavecleaner.WaveCleaner;
import com.oxande.wavecleaner.filters.AudioDocumentPlayer;
import com.oxande.wavecleaner.filters.AudioPlayerListener;
import com.oxande.wavecleaner.filters.ClickRemovalFilter;
import com.oxande.wavecleaner.filters.DecrackleFilter;
import com.oxande.wavecleaner.filters.PreamplifierFilter;
import com.oxande.wavecleaner.util.Assert;
import com.oxande.wavecleaner.util.ListenerManager;
import com.oxande.wavecleaner.util.logging.LogFactory;
import ddf.minim.AudioOutput;
import ddf.minim.AudioRecorder;
import ddf.minim.MultiChannelBuffer;
import ddf.minim.SignalSplitter;
import ddf.minim.spi.AudioRecordingStream;
import ddf.minim.ugens.Summer;
		}

		int streamDuration = stream.getMillisecondLength();
		totalSamples = (int) ((streamDuration * 0.001 * sampleRate)) + 1;
		int nbChunks = (int) (totalSamples / bufferSize) + 1;
		this.cache = new AudioCache(bufferSize, nbChunks);
		new Thread(() -> loadWaveSamples()).start();
	}

	/**
	 * Returns the audio file (a reference to the).
	 * 
	 * @return the file
	 */
	public File getFile() {
		File f = new File(this.fileName);
		return f;
	}

	public AudioFormat getFormat() {
		return stream.getFormat();
	}

	/**
	 * Returns the number of chunks. A chunk contains the audio levels for about
	 * 0.2 seconds of music.
	 * 
	 * @return the number of chunks.
	 */
	protected int getNumberOfChunks() {
		int totalChunks = (totalSamples / bufferSize) + 1;
		return totalChunks;
	}

	public int getNumberOfSamples() {
		return totalSamples;
	}

	/**
	 * Get the audio samples in a chunk. Use a mapped memory to load the samples
	 * inside the chunk.
	 * 
	 * @param chunk
	 *            the chunk. The size of a chunk is depending of the window size
	 *            (basically a chunk is about 20 milliseconds).
	 * @return the array of samples (always stereo expected).
	 */
	public float[][] getAudioSamples(int chunk) {
		float[][] samples = null;
		samples = this.cache.getSamples(chunk);
		return samples;
	}

	ListenerManager<AudioChangedListener> listenerManager = new ListenerManager<AudioChangedListener>();

	public void addChangedAudioListener(AudioChangedListener listener) {
		listenerManager.add(listener);
	}

	public void removeChangedAudioListener(AudioChangedListener listener) {
		listenerManager.remove(listener);
	}

	void addAudioChangedListener(AudioChangedListener listener) {

	}

	/**
	 * Load the samples of the audio file. This method should be called in a
	 * different thread.
	 * 
	 * @throws IOException
	 * 
	 */
	public synchronized void loadWaveSamples() {
		int nbChunks = this.getNumberOfChunks();
		stream.play();
		MultiChannelBuffer buf = new MultiChannelBuffer(bufferSize, stream.getFormat().getChannels());
		this.samples = new RMSSample[nbChunks];
		for (int i = 0; i < nbChunks; i++) {
			stream.read(buf);
			float left[] = buf.getChannel(leftChannel);
			float right[] = buf.getChannel(rightChannel);
			samples[i] = RMSSample.create(left, right);
			try {
				this.cache.saveSamples(i, left, right);
			} catch (IOException e) {
				LOG.error("Can not save into the cache.");
			}
			listenerManager.publishOnce(l -> l.audioChanged());
		}
		stream.pause();
		listenerManager.publish(l -> l.audioChanged());
	}

	/**
	 * Stop to play. Basically a mute but we unpatch the line out!
	 * 
	 */
	public synchronized void stop() {
		Assert.notNull(this.documentPlayer);
		if( this.documentPlayer.isPlaying() || this.documentPlayer.isLooping()){
			this.documentPlayer.pause();
			// lineOut.removeListener(this);
			// this.documentPlayer.unpatch(decrackFilter);
			// decrackFilter.unpatch(clickFilter);
			// clickFilter.unpatch(preamplifer);
			preamplifer.unpatch(lineOut);
			LOG.info("PLAYER STOPPED.");
		}
		else {
			LOG.info("Player already stopped!");
		}
		// for (AudioDocumentListener listener : listeners) {
		// listener.audioPaused();
		// }
	}

	// The 2 following methods are for compatibility because
	// the audio player can not be accessed directly.
	public void addAudioPlayerListener(AudioPlayerListener listener) {