/* * 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; } }