package ch.fhnw.tvver;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import ch.fhnw.ether.audio.IAudioRenderTarget;
import ch.fhnw.ether.audio.fx.FFT;
import ch.fhnw.ether.media.AbstractRenderCommand;
import ch.fhnw.ether.media.RenderCommandException;

/**
 * Pitch Detection with the Harmonic Product Spectrum Algorithm
 */
public class HpsPitchDetection extends AbstractRenderCommand<IAudioRenderTarget> {

	private FFT fft;
	private OnSetDetection osd;
	private int harmonics;
	private Map<Integer, Long> pitchHistory;
	private PitchDetectionResult lastResult;

	public HpsPitchDetection(FFT fft, OnSetDetection osd, int harmonics) {
		this.fft = fft;
		this.osd = osd;
		this.harmonics = harmonics;
		this.pitchHistory = new HashMap<>();
	}

	@Override
	protected void run(IAudioRenderTarget target) throws RenderCommandException {
		float[] spectrum = this.fft.power();
		if (this.osd.tone) {
			this.lastResult = this.detectPitch(spectrum, this.harmonics);
			if (this.lastResult.isPitched()) {
				System.out.println(String.format("%5d%20f%20f", this.lastResult.getMidiNote(),
						this.lastResult.getFreq(), this.lastResult.getAmplitude()));
			}
		}
	}

	private PitchDetectionResult detectPitch(float[] spectrum, int harmonics) {
		List<List<Float>> downsamples = new ArrayList<>();
		for (int index = 1; index <= harmonics; index++) {
			downsamples.add(this.downsample(spectrum, index));
		}

		List<Float> hps = new ArrayList<>();

		for (int index = 0; index < downsamples.get(harmonics - 1).size(); index++) {
			float product = 1;
			for (int downsampleIdx = 0; downsampleIdx < harmonics; downsampleIdx++) {
				product *= downsamples.get(downsampleIdx).get(index);
			}
			hps.add(product);
		}

		float maxProduct = 0;
		int maxIndex = 0;

		for (int index = 0; index < hps.size() - 1; index++) {
			if (hps.get(index) > maxProduct) {
				maxProduct = hps.get(index);
				maxIndex = index;
			}
		}

		float amplitude = spectrum[maxIndex];
		float fundamentalFreq = this.fft.idx2f(maxIndex);
		int midiNote = this.calcMidiNote(fundamentalFreq);

		long now = System.currentTimeMillis();
		long lastPlayed = this.pitchHistory.getOrDefault(midiNote, 0L);
		this.pitchHistory.put(midiNote, now);

		boolean isPitched = true;
		isPitched &= fundamentalFreq > 50.0f;
		isPitched &= now - lastPlayed > 100;
		isPitched &= amplitude > 100;
		return new PitchDetectionResult(fundamentalFreq, amplitude, midiNote, isPitched);

		// Octave errors are common (detection is sometimes an octave too high).
		// To correct, apply this rule: if the second peak amplitude below
		// initially chosen pitch is approximately 1/2 of the chosen pitch AND
		// the ratio of amplitudes is above a threshold (e.g., 0.2 for 5
		// harmonics), THEN select the lower octave peak as the pitch for the
		// current frame.
		// Due to noise, frequencies below about 50 Hz should not be searched
		// for a pitch.
		// TODO Octave correction

	}

	/**
	 * decreases the sampling rate of x by keeping every nth sample starting
	 * with the first sample.
	 */
	private ArrayList<Float> downsample(float[] samples, int n) {
		ArrayList<Float> downsampled = new ArrayList<>();
		for (int index = n - 1; index < samples.length; index += n) {
			downsampled.add(samples[index]);
		}
		return downsampled;
	}

	private int calcMidiNote(float freq) {
		return (int) Math.ceil(69 + 12 * (Math.log(freq / 440) / Math.log(2)));
	}
	
	public PitchDetectionResult getLastResult(){
		return this.lastResult;
	}

}

@@ -479,37 +479,8 @@
 ft;%0A
-%09private OnSetDetection osd;%0A
 %09pri
@@ -622,28 +622,8 @@
 fft,
- OnSetDetection osd,
  int
@@ -658,26 +658,8 @@
 ft;%0A
-%09%09this.osd = osd;%0A
 %09%09th
@@ -818,16 +818,49 @@
 tion %7B%0A%09
+%7D%0A%0A%09public void detectPitch() %7B%0A%09
 %09float%5B%5D
@@ -892,374 +892,8 @@
 r();
-%0A%09%09if (this.osd.tone) %7B%0A%09%09%09this.lastResult = this.detectPitch(spectrum, this.harmonics);%0A%09%09%09if (this.lastResult.isPitched()) %7B%0A%09%09%09%09System.out.println(String.format(%22%255d%2520f%2520f%22, this.lastResult.getMidiNote(),%0A%09%09%09%09%09%09this.lastResult.getFreq(), this.lastResult.getAmplitude()));%0A%09%09%09%7D%0A%09%09%7D%0A%09%7D%0A%0A%09private PitchDetectionResult detectPitch(float%5B%5D spectrum, int harmonics) %7B
 %0A%09%09L
@@ -973,16 +973,21 @@
 ndex %3C= 
+this.
 harmonic
@@ -1145,16 +1145,21 @@
 les.get(
+this.
 harmonic
@@ -1253,16 +1253,21 @@
 leIdx %3C 
+this.
 harmonic
@@ -1732,49 +1732,8 @@
 );%0A%0A
-%09%09long now = System.currentTimeMillis();%0A
 %09%09lo
@@ -1832,14 +1832,85 @@
 te, 
-now
+System.currentTimeMillis());%0A%09%09long now = this.pitchHistory.get(midiNote
 );%0A
+%09%09
 %0A%09%09b
@@ -1968,17 +1968,17 @@
 Freq %3E 5
-0
+5
 .0f;%0A%09%09i
@@ -2047,22 +2047,34 @@
  100;%0A%09%09
-return
+this.lastResult = 
  new Pit
@@ -3114,17 +3114,16 @@
 )));%0A%09%7D%0A
-%09
 %0A%09public
@@ -3159,16 +3159,17 @@
 Result()
+ 
 %7B%0A%09%09retu
@@ -3190,12 +3190,72 @@
 sult;%0A%09%7D
+%0A%09%0A%09public void clearResult() %7B%0A%09%09this.lastResult = null;%0A%09%7D
 %0A%0A%7D%0A

Remove on set detection from hps