/*
 * Copyright (C) 2011 Jacquet Wong
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.musicg.wave.extension;

import com.musicg.dsp.FastFourierTransform;
import com.musicg.dsp.WindowFunction;
import com.musicg.wave.Wave;

/**
 * Handles the wave data in frequency-time domain.
 *
 * @author Jacquet Wong
 */
public class Spectrogram{
	
	public static final int SPECTROGRAM_DEFAULT_FFT_SAMPLE_SIZE = 1024;
	public static final int SPECTROGRAM_DEFAULT_OVERLAP_FACTOR = 0;	// 0 for no overlapping
	
	private Wave wave;
	private double[][] spectrogram;	// relative spectrogram
	private double[][] absoluteSpectrogram;	// absolute spectrogram
	private int fftSampleSize;	// number of sample in fft, the value needed to be a number to power of 2
	private int overlapFactor;	// 1/overlapFactor overlapping, e.g. 1/4=25% overlapping
	private int numFrames;	// number of frames of the spectrogram
	private int framesPerSecond;	// frame per second of the spectrogram
	private int numFrequencyUnit;	// number of y-axis unit
	private double unitFrequency;	// frequency per y-axis unit

	/**
	 * Constructor
	 * 
	 * @param wave
	 */
	public Spectrogram(Wave wave) {
		this.wave=wave;
		// default
		this.fftSampleSize=SPECTROGRAM_DEFAULT_FFT_SAMPLE_SIZE;
		this.overlapFactor=SPECTROGRAM_DEFAULT_OVERLAP_FACTOR;
		buildSpectrogram();
	}
	
	/**
	 * Constructor
	 * 
	 * @param wave
	 * @param fftSampleSize	number of sample in fft, the value needed to be a number to power of 2
	 * @param overlapFactor	1/overlapFactor overlapping, e.g. 1/4=25% overlapping, 0 for no overlapping
	 */
	public Spectrogram(Wave wave, int fftSampleSize, int overlapFactor) {
		this.wave=wave;
		
		if (Integer.bitCount(fftSampleSize)==1){
			this.fftSampleSize=fftSampleSize;
		}
		else{
			System.err.print("The input number must be a power of 2");
			this.fftSampleSize=SPECTROGRAM_DEFAULT_FFT_SAMPLE_SIZE;
		}

		this.overlapFactor=overlapFactor;
		
		buildSpectrogram();
	}
	
	/**
	 * Build spectrogram
	 */
	private void buildSpectrogram(){

		short[] amplitudes=wave.getSampleAmplitudes();
		int numSamples = amplitudes.length;
		
		int pointer=0;
		// overlapping
		if (overlapFactor>1){
			int numOverlappedSamples=numSamples*overlapFactor;
			int backSamples=fftSampleSize*(overlapFactor-1)/overlapFactor;
			int fftSampleSize_1=fftSampleSize-1;
			short[] overlapAmp= new short[numOverlappedSamples];
			pointer=0;
			for (int i=0; i<amplitudes.length; i++){
				overlapAmp[pointer++]=amplitudes[i];
				if (pointer%fftSampleSize==fftSampleSize_1){
					// overlap
					i-=backSamples;
				}
			}
			numSamples=numOverlappedSamples;
			amplitudes=overlapAmp;
		}
		// end overlapping
			
		numFrames=numSamples/fftSampleSize;
		framesPerSecond=(int)(numFrames/wave.length());	
			
		// set signals for fft
		WindowFunction window = new WindowFunction();
		window.setWindowType("Hamming");
		double[] win=window.generate(fftSampleSize);
	
		double[][] signals=new double[numFrames][];
		for(int f=0; f<numFrames; f++) {
			signals[f]=new double[fftSampleSize];
			int startSample=f*fftSampleSize;
			for (int n=0; n<fftSampleSize; n++){
				signals[f][n]=amplitudes[startSample+n]*win[n];							
			}
		}
		// end set signals for fft
			
		absoluteSpectrogram=new double[numFrames][];
		// for each frame in signals, do fft on it
		FastFourierTransform fft = new FastFourierTransform();
		for (int i=0; i<numFrames; i++){			
			absoluteSpectrogram[i]=fft.getMagnitudes(signals[i]);
		}
			
		if (absoluteSpectrogram.length>0){
				
			numFrequencyUnit=absoluteSpectrogram[0].length;
			unitFrequency=(double)wave.getWaveHeader().getSampleRate()/2/numFrequencyUnit;	// frequency could be caught within the half of nSamples according to Nyquist theory
					
			// normalization of absoultSpectrogram
			spectrogram=new double[numFrames][numFrequencyUnit];
				
			// set max and min amplitudes
			double maxAmp=Double.MIN_VALUE;
		    double minAmp=Double.MAX_VALUE;	
			for (int i=0; i<numFrames; i++){
				for (int j=0; j<numFrequencyUnit; j++){
					if (absoluteSpectrogram[i][j]>maxAmp){
						maxAmp=absoluteSpectrogram[i][j];
					}
					else if(absoluteSpectrogram[i][j]<minAmp){
						minAmp=absoluteSpectrogram[i][j];
					}
				}
			}
			// end set max and min amplitudes
				
			// normalization
			// avoiding divided by zero 
			double minValidAmp=0.00000000001F;
			if (minAmp==0){
				minAmp=minValidAmp;
			}
				
			double diff=Math.log10(maxAmp/minAmp);	// perceptual difference
			for (int i=0; i<numFrames; i++){
				for (int j=0; j<numFrequencyUnit; j++){
					if (absoluteSpectrogram[i][j]<minValidAmp){
						spectrogram[i][j]=0;
					}
					else{
						spectrogram[i][j]=(Math.log10(absoluteSpectrogram[i][j]/minAmp))/diff;
					}
				}
			}
			// end normalization
		}
	}
	
	/**
	 * Get spectrogram: spectrogram[time][frequency]=intensity
	 * 
	 * @return	logarithm normalized spectrogram
	 */
	public double[][] getNormalizedSpectrogramData(){
		return spectrogram;
	}
	
	/**
	 * Get spectrogram: spectrogram[time][frequency]=intensity
	 * 
	 * @return	absolute spectrogram
	 */
	public double[][] getAbsoluteSpectrogramData(){
		return absoluteSpectrogram;
	}

	public int getNumFrames(){
		return numFrames;
	}
	
	public int getFramesPerSecond(){
		return framesPerSecond;
	}
	
	public int getNumFrequencyUnit(){
		return numFrequencyUnit;
	}
	
	public double getUnitFrequency(){
		return unitFrequency;
	}

	public int getFftSampleSize() {
		return fftSampleSize;
	}

	public int getOverlapFactor() {
		return overlapFactor;
	}
}