/* 
 * Copyright (c) 2008 MUSIC TECHNOLOGY GROUP (MTG)
 *                         UNIVERSITAT POMPEU FABRA 
 * 
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of 
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 */
/*! \file peakDetection.c
 * \brief peak detection algorithm and functions
 */

#include "sms.h"

/*! \brief function used for the parabolic interpolation of the spectral peaks
 *
 * it performs the interpolation in a log scale and
 * stores the location in pFDiffFromMax and
 *
 * \param fMaxVal        value of max bin 
 * \param fLeftBinVal    value for left bin
 * \param fRightBinVal   value for right bin
 * \param pFDiffFromMax location of the tip as the difference from the top bin 
 *  \return the peak height 
 */
static sfloat PeakInterpolation(sfloat fMaxVal, sfloat fLeftBinVal, 
                                sfloat fRightBinVal, sfloat *pFDiffFromMax)
{
    /* get the location of the tip of the parabola */
    *pFDiffFromMax = (.5 * (fLeftBinVal - fRightBinVal) /
                      (fLeftBinVal - (2*fMaxVal) + fRightBinVal));
    /* return the value at the tip */
    return fMaxVal - (.25 * (fLeftBinVal - fRightBinVal) * *pFDiffFromMax);
}

/*! \brief detect the next local maximum in the spectrum
 *
 * stores the value in pFMaxVal 
 *                                      
 * \todo export this to sms.h and wrap in pysms
 *
 * \param pFMagSpectrum  magnitude spectrum 
 * \param iHighBinBound  highest bin to search
 * \param pICurrentLoc   current bin location
 * \param pFMaxVal       value of the maximum found
 * \param fMinPeakMag    minimum magnitude to accept a peak
 * \return the bin location of the maximum  
 */
static int FindNextMax(sfloat *pFMagSpectrum, int iHighBinBound, 
                       int *pICurrentLoc, sfloat *pFMaxVal, sfloat fMinPeakMag)
{
    int iCurrentBin = *pICurrentLoc;
    sfloat fPrevVal = pFMagSpectrum[iCurrentBin - 1];
    sfloat fCurrentVal = pFMagSpectrum[iCurrentBin];
    sfloat fNextVal = (iCurrentBin >= iHighBinBound) ? 0 : pFMagSpectrum[iCurrentBin + 1];
  
    /* try to find a local maximum */
    while(iCurrentBin <= iHighBinBound)
    {
        if(fCurrentVal > fMinPeakMag &&
           fCurrentVal >= fPrevVal &&
           fCurrentVal >= fNextVal)
            break;
        iCurrentBin++;
        fPrevVal = fCurrentVal;
        fCurrentVal = fNextVal;
        fNextVal = pFMagSpectrum[1+iCurrentBin];
    }

    /* save the current location, value of maximum and return */
    /*    location of max */
    *pICurrentLoc = iCurrentBin + 1;
    *pFMaxVal = fCurrentVal;
    return iCurrentBin;
}

/*! \brief function to detect the next spectral peak 
 *                           
 * \param pFMagSpectrum    magnitude spectrum 
 * \param iHighestBin       highest bin to search
 * \param pICurrentLoc       current bin location
 * \param pFPeakMag        magnitude value of peak
 * \param pFPeakLoc        location of peak
 * \param fMinPeakMag  minimum magnitude to accept a peak
 * \return 1 if found, 0 if not  
 */
static int FindNextPeak(sfloat *pFMagSpectrum, int iHighestBin, 
                        int *pICurrentLoc, sfloat *pFPeakMag, 
                        sfloat *pFPeakLoc, sfloat fMinPeakMag)
{
    int iPeakBin = 0;    /* location of the local peak */
    sfloat fPeakMag = 0; /* value of local peak */
  
    /* keep trying to find a good peak while inside the freq range */
    while((iPeakBin = FindNextMax(pFMagSpectrum, iHighestBin, 
                                  pICurrentLoc, &fPeakMag, fMinPeakMag)) 
          <= iHighestBin)
    {
        /* get the neighboring samples */
        sfloat fDiffFromMax = 0;
        sfloat fLeftBinVal = pFMagSpectrum[iPeakBin - 1];
        sfloat fRightBinVal = pFMagSpectrum[iPeakBin + 1];
        if(fLeftBinVal <= 0 || fRightBinVal <= 0) //ahah! there you are!
            continue;
        /* interpolate the spectral samples to obtain
           a more accurate magnitude and freq */
        *pFPeakMag = PeakInterpolation(fPeakMag, fLeftBinVal,
                                       fRightBinVal, &fDiffFromMax);
        *pFPeakLoc = iPeakBin + fDiffFromMax;
        return 1;
    }
    /* if it does not find a peak return 0 */
    return 0;
}

/*! \brief get the corresponding phase value for a given peak
 *
 * performs linear interpolation for a more accurate phase

 * \param pPhaseSpectrum     phase spectrum
 * \param fPeakLoc                 location of peak
 * \return the phase value                             
 */
static sfloat GetPhaseVal(sfloat *pPhaseSpectrum, sfloat fPeakLoc)
{
    int bin = (int)fPeakLoc;
    sfloat fFraction = fPeakLoc - bin,
           fLeftPha = pPhaseSpectrum[bin],
           fRightPha = pPhaseSpectrum[bin+1];
  
    /* check for phase wrapping */
    if(fLeftPha - fRightPha > 1.5 * PI)
        fRightPha += TWO_PI;
    else if(fRightPha - fLeftPha > 1.5 * PI)
        fLeftPha += TWO_PI;
  
    /* return interpolated phase */
    return fLeftPha + fFraction * (fRightPha - fLeftPha);
}

/*! \brief find the prominent spectral peaks
 * 
 * uses a dB spectrum
 *
 * \param sizeSpec size of magnitude spectrum
 * \param pMag pointer to power spectrum
 * \param pPhase pointer to phase spectrum
 * \param pSpectralPeaks pointer to array of peaks
 * \param pAnalParams peak detection parameters
 * \return the number of peaks found
 */
int sms_detectPeaks(int sizeSpec, sfloat *pMag, sfloat *pPhase,
                    SMS_Peak *pSpectralPeaks, SMS_AnalParams *pAnalParams)
{
    int sizeFft = sizeSpec << 1;
    sfloat fInvSizeFft = 1.0 / sizeFft;
    int iFirstBin = MAX(1, sizeFft * pAnalParams->fLowestFreq / pAnalParams->iSamplingRate);
    int iHighestBin = MIN(sizeSpec-1, 
                          sizeFft * pAnalParams->fHighestFreq / pAnalParams->iSamplingRate);
    int iPeak = 0;

    /* clear peak structure */
    for(iPeak = 0; iPeak < pAnalParams->maxPeaks; iPeak++)
    {
        pSpectralPeaks[iPeak].fFreq = 0.0;
        pSpectralPeaks[iPeak].fMag = 0.0;
        pSpectralPeaks[iPeak].fPhase = 0.0;
    }

    /* set starting search values */
    int iCurrentLoc = iFirstBin;
    sfloat fPeakMag = 0.0;  /* magnitude of peak */
    sfloat fPeakLoc = 0.0;  /* location of peak */

    /* find peaks */
    iPeak = 0;
    while((iPeak < pAnalParams->maxPeaks) &&
          (FindNextPeak(pMag, iHighestBin, &iCurrentLoc, &fPeakMag, 
                        &fPeakLoc, pAnalParams->fMinPeakMag) == 1))
    {
        /* store peak values */
        pSpectralPeaks[iPeak].fFreq = pAnalParams->iSamplingRate * fPeakLoc * fInvSizeFft;
        pSpectralPeaks[iPeak].fMag = fPeakMag;
        pSpectralPeaks[iPeak].fPhase = GetPhaseVal(pPhase, fPeakLoc);
        iPeak++;
    }

    /* return the number of peaks found */
    return iPeak;
}