/*
* SpectrumTimeAnalyzer.
*
* JavaZOOM : jlgui@javazoom.net
* http://www.javazoom.net
*
*-----------------------------------------------------------------------
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Library 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 Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*----------------------------------------------------------------------
*/
package plugins.audioPlayer.javazoom.jlgui.player.amp.visual.ui;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.StringTokenizer;
import javax.sound.sampled.SourceDataLine;
import javax.swing.JPanel;
import plugins.audioPlayer.javazoom.jlgui.player.amp.skin.AbsoluteConstraints;
import kj.dsp.KJDigitalSignalProcessingAudioDataConsumer;
import kj.dsp.KJDigitalSignalProcessor;
import kj.dsp.KJFFT;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class SpectrumTimeAnalyzer extends JPanel implements KJDigitalSignalProcessor
{
private static Log log = LogFactory.getLog(SpectrumTimeAnalyzer.class);
public static final int DISPLAY_MODE_SCOPE = 0;
public static final int DISPLAY_MODE_SPECTRUM_ANALYSER = 1;
public static final int DISPLAY_MODE_OFF = 2;
public static final int DEFAULT_WIDTH = 256;
public static final int DEFAULT_HEIGHT = 128;
public static final int DEFAULT_FPS = 50;
public static final int DEFAULT_SPECTRUM_ANALYSER_FFT_SAMPLE_SIZE = 512;
public static final int DEFAULT_SPECTRUM_ANALYSER_BAND_COUNT = 19;
public static final float DEFAULT_SPECTRUM_ANALYSER_DECAY = 0.05f;
public static final int DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY = 20;
public static final float DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO = 0.4f;
public static final float DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO_RANGE = 0.1f;
public static final float MIN_SPECTRUM_ANALYSER_DECAY = 0.02f;
public static final float MAX_SPECTRUM_ANALYSER_DECAY = 0.08f;
public static final Color DEFAULT_BACKGROUND_COLOR = new Color(0, 0, 128);
public static final Color DEFAULT_SCOPE_COLOR = new Color(255, 192, 0);
public static final float DEFAULT_VU_METER_DECAY = 0.02f;
private Image bi;
private int displayMode = DISPLAY_MODE_SCOPE;
private Color scopeColor = DEFAULT_SCOPE_COLOR;
private Color[] spectrumAnalyserColors = getDefaultSpectrumAnalyserColors();
private KJDigitalSignalProcessingAudioDataConsumer dsp = null;
private boolean dspStarted = false;
private Color peakColor = null;
private int[] peaks = new int[DEFAULT_SPECTRUM_ANALYSER_BAND_COUNT];
private int[] peaksDelay = new int[DEFAULT_SPECTRUM_ANALYSER_BAND_COUNT];
private int peakDelay = DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY;
private boolean peaksEnabled = true;
private List visColors = null;
private int barOffset = 1;
private int width;
private int height;
private int height_2;
// -- Spectrum analyser variables.
private KJFFT fft;
private float[] old_FFT;
private int saFFTSampleSize;
private int saBands;
private float saColorScale;
private float saMultiplier;
private float saDecay = DEFAULT_SPECTRUM_ANALYSER_DECAY;
private float sad;
private SourceDataLine m_line = null;
// -- VU Meter
private float oldLeft;
private float oldRight;
// private float vuAverage;
// private float vuSamples;
private float vuDecay = DEFAULT_VU_METER_DECAY;
private float vuColorScale;
// -- FPS calulations.
private long lfu = 0;
private int fc = 0;
private int fps = DEFAULT_FPS;
private boolean showFPS = false;
private AbsoluteConstraints constraints = null;
// private Runnable PAINT_SYNCHRONIZER = new AWTPaintSynchronizer();
public SpectrumTimeAnalyzer()
{
setOpaque(false);
initialize();
}
public void setConstraints(AbsoluteConstraints cnts)
{
constraints = cnts;
}
public AbsoluteConstraints getConstraints()
{
return constraints;
}
public boolean isPeaksEnabled()
{
return peaksEnabled;
}
public void setPeaksEnabled(boolean peaksEnabled)
{
this.peaksEnabled = peaksEnabled;
}
public int getFps()
{
return fps;
}
public void setFps(int fps)
{
this.fps = fps;
}
/**
* Starts DSP.
* @param line
*/
public void startDSP(SourceDataLine line)
{
if (displayMode == DISPLAY_MODE_OFF) return;
if (line != null) m_line = line;
if (dsp == null)
{
dsp = new KJDigitalSignalProcessingAudioDataConsumer(2048, fps);
dsp.add(this);
}
if ((dsp != null) && (m_line != null))
{
if (dspStarted == true)
{
stopDSP();
}
dsp.start(m_line);
dspStarted = true;
log.debug("DSP started");
}
}
/**
* Stop DSP.
*/
public void stopDSP()
{
if (dsp != null)
{
dsp.stop();
dspStarted = false;
log.debug("DSP stopped");
}
}
/**
* Close DSP
*/
public void closeDSP()
{
if (dsp != null)
{
stopDSP();
dsp = null;
log.debug("DSP closed");
}
}
/**
* Setup DSP.
* @param line
*/
public void setupDSP(SourceDataLine line)
{
if (dsp != null)
{
int channels = line.getFormat().getChannels();
if (channels == 1) dsp.setChannelMode(KJDigitalSignalProcessingAudioDataConsumer.CHANNEL_MODE_MONO);
else dsp.setChannelMode(KJDigitalSignalProcessingAudioDataConsumer.CHANNEL_MODE_STEREO);
int bits = line.getFormat().getSampleSizeInBits();
if (bits == 8) dsp.setSampleType(KJDigitalSignalProcessingAudioDataConsumer.SAMPLE_TYPE_EIGHT_BIT);
else dsp.setSampleType(KJDigitalSignalProcessingAudioDataConsumer.SAMPLE_TYPE_SIXTEEN_BIT);
}
}
/**
* Write PCM data to DSP.
* @param pcmdata
*/
public void writeDSP(byte[] pcmdata)
{
if ((dsp != null) && (dspStarted == true)) dsp.writeAudioData(pcmdata);
}
/**
* Return DSP.
* @return
*/
public KJDigitalSignalProcessingAudioDataConsumer getDSP()
{
return dsp;
}
/**
* Set visual colors from skin.
* @param viscolor
*/
public void setVisColor(String viscolor)
{
ArrayList visColors = new ArrayList();
viscolor = viscolor.toLowerCase();
ByteArrayInputStream in = new ByteArrayInputStream(viscolor.getBytes());
BufferedReader bin = new BufferedReader(new InputStreamReader(in));
try
{
String line = null;
while ((line = bin.readLine()) != null)
{
visColors.add(getColor(line));
}
Color[] colors = new Color[visColors.size()];
visColors.toArray(colors);
Color[] specColors = new Color[15];
System.arraycopy(colors, 2, specColors, 0, 15);
List specList = Arrays.asList(specColors);
Collections.reverse(specList);
specColors = (Color[]) specList.toArray(specColors);
setSpectrumAnalyserColors(specColors);
setBackground((Color) visColors.get(0));
if (visColors.size()>23) setPeakColor((Color) visColors.get(23));
if (visColors.size()>18) setScopeColor((Color) visColors.get(18));
}
catch (IOException ex)
{
log.warn("Cannot parse viscolors", ex);
}
finally
{
try
{
if (bin != null) bin.close();
}
catch (IOException e)
{
}
}
}
/**
* Set visual peak color.
* @param c
*/
public void setPeakColor(Color c)
{
peakColor = c;
}
/**
* Set peak falloff delay.
* @param framestowait
*/
public void setPeakDelay(int framestowait)
{
int min = (int) Math.round((DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO - DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO_RANGE) * fps);
int max = (int) Math.round((DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO + DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO_RANGE) * fps);
if ((framestowait >= min) && (framestowait <= max))
{
peakDelay = framestowait;
}
else
{
peakDelay = (int) Math.round(DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO * fps);
}
}
/**
* Return peak falloff delay
* @return int framestowait
*/
public int getPeakDelay()
{
return peakDelay;
}
/**
* Convert string to color.
* @param linecolor
* @return
*/
public Color getColor(String linecolor)
{
Color color = Color.BLACK;
StringTokenizer st = new StringTokenizer(linecolor, ",");
int red = 0, green = 0, blue = 0;
try
{
if (st.hasMoreTokens()) red = Integer.parseInt(st.nextToken().trim());
if (st.hasMoreTokens()) green = Integer.parseInt(st.nextToken().trim());
if (st.hasMoreTokens())
{
String blueStr = st.nextToken().trim();
if (blueStr.length() > 3) blueStr = (blueStr.substring(0, 3)).trim();
blue = Integer.parseInt(blueStr);
}
color = new Color(red, green, blue);
}
catch (NumberFormatException e)
{
log.debug("Cannot parse viscolor : "+e.getMessage());
}
return color;
}
private void computeColorScale()
{
saColorScale = ((float) spectrumAnalyserColors.length / height) * barOffset * 1.0f;
vuColorScale = ((float) spectrumAnalyserColors.length / (width - 32)) * 2.0f;
}
private void computeSAMultiplier()
{
saMultiplier = (saFFTSampleSize / 2) / saBands;
}
private void drawScope(Graphics pGrp, float[] pSample)
{
pGrp.setColor(scopeColor);
int wLas = (int) (pSample[0] * (float) height_2) + height_2;
int wSt = 2;
for (int a = wSt, c = 0; c < width; a += wSt, c++)
{
int wAs = (int) (pSample[a] * (float) height_2) + height_2;
pGrp.drawLine(c, wLas, c + 1, wAs);
wLas = wAs;
}
}
private void drawSpectrumAnalyser(Graphics pGrp, float[] pSample, float pFrrh)
{
float c = 0;
float[] wFFT = fft.calculate(pSample);
float wSadfrr = (saDecay * pFrrh);
float wBw = ((float) width / (float) saBands);
for (int a = 0, bd = 0; bd < saBands; a += saMultiplier, bd++)
{
float wFs = 0;
// -- Average out nearest bands.
for (int b = 0; b < saMultiplier; b++)
{
wFs += wFFT[a + b];
}
// -- Log filter.
wFs = (wFs * (float) Math.log(bd + 2));
if (wFs > 1.0f)
{
wFs = 1.0f;
}
// -- Compute SA decay...
if (wFs >= (old_FFT[a] - wSadfrr))
{
old_FFT[a] = wFs;
}
else
{
old_FFT[a] -= wSadfrr;
if (old_FFT[a] < 0)
{
old_FFT[a] = 0;
}
wFs = old_FFT[a];
}
drawSpectrumAnalyserBar(pGrp, (int) c, height, (int) wBw - 1, (int) (wFs * height), bd);
c += wBw;
}
}
private void drawVUMeter(Graphics pGrp, float[] pLeft, float[] pRight, float pFrrh)
{
if (displayMode == DISPLAY_MODE_OFF) return;
float wLeft = 0.0f;
float wRight = 0.0f;
float wSadfrr = (vuDecay * pFrrh);
for (int a = 0; a < pLeft.length; a++)
{
wLeft += Math.abs(pLeft[a]);
wRight += Math.abs(pRight[a]);
}
wLeft = ((wLeft * 2.0f) / (float) pLeft.length);
wRight = ((wRight * 2.0f) / (float) pRight.length);
if (wLeft > 1.0f)
{
wLeft = 1.0f;
}
if (wRight > 1.0f)
{
wRight = 1.0f;
}
// vuAverage += ( ( wLeft + wRight ) / 2.0f );
// vuSamples++;
//
// if ( vuSamples > 128 ) {
// vuSamples /= 2.0f;
// vuAverage /= 2.0f;
// }
if (wLeft >= (oldLeft - wSadfrr))
{
oldLeft = wLeft;
}
else
{
oldLeft -= wSadfrr;
if (oldLeft < 0)
{
oldLeft = 0;
}
}
if (wRight >= (oldRight - wSadfrr))
{
oldRight = wRight;
}
else
{
oldRight -= wSadfrr;
if (oldRight < 0)
{
oldRight = 0;
}
}
int wHeight = (height >> 1) - 24;
drawVolumeMeterBar(pGrp, 16, 16, (int) (oldLeft * (float) (width - 32)), wHeight);
// drawVolumeMeterBar( pGrp, 16, wHeight + 22, (int)( ( vuAverage / vuSamples ) * (float)( width - 32 ) ), 4 );
drawVolumeMeterBar(pGrp, 16, wHeight + 32, (int) (oldRight * (float) (width - 32)), wHeight);
// pGrp.fillRect( 16, 16, (int)( oldLeft * (float)( width - 32 ) ), wHeight );
// pGrp.fillRect( 16, 64, (int)( oldRight * (float)( width - 32 ) ), wHeight );
}
private void drawSpectrumAnalyserBar(Graphics pGraphics, int pX, int pY, int pWidth, int pHeight, int band)
{
float c = 0;
for (int a = pY; a >= pY - pHeight; a -= barOffset)
{
c += saColorScale;
if (c < spectrumAnalyserColors.length)
{
pGraphics.setColor(spectrumAnalyserColors[(int) c]);
}
pGraphics.fillRect(pX, a, pWidth, 1);
}
if ((peakColor != null) && (peaksEnabled == true))
{
pGraphics.setColor(peakColor);
if (pHeight > peaks[band])
{
peaks[band] = pHeight;
peaksDelay[band] = peakDelay;
}
else
{
peaksDelay[band]--;
if (peaksDelay[band] < 0) peaks[band]--;
if (peaks[band] < 0) peaks[band] = 0;
}
pGraphics.fillRect(pX, pY - peaks[band], pWidth, 1);
}
}
private void drawVolumeMeterBar(Graphics pGraphics, int pX, int pY, int pWidth, int pHeight)
{
float c = 0;
for (int a = pX; a <= pX + pWidth; a += 2)
{
c += vuColorScale;
if (c < 256.0f)
{
pGraphics.setColor(spectrumAnalyserColors[(int) c]);
}
pGraphics.fillRect(a, pY, 1, pHeight);
}
}
private synchronized Image getDoubleBuffer()
{
if (bi == null || (bi.getWidth(null) != getSize().width || bi.getHeight(null) != getSize().height))
{
width = getSize().width;
height = getSize().height;
height_2 = height >> 1;
computeColorScale();
bi = getGraphicsConfiguration().createCompatibleVolatileImage(width, height);
}
return bi;
}
public static Color[] getDefaultSpectrumAnalyserColors()
{
Color[] wColors = new Color[256];
for (int a = 0; a < 128; a++)
{
wColors[a] = new Color(0, (a >> 1) + 192, 0);
}
for (int a = 0; a < 64; a++)
{
wColors[a + 128] = new Color(a << 2, 255, 0);
}
for (int a = 0; a < 64; a++)
{
wColors[a + 192] = new Color(255, 255 - (a << 2), 0);
}
return wColors;
}
/**
* @return Returns the current display mode, DISPLAY_MODE_SCOPE or DISPLAY_MODE_SPECTRUM_ANALYSER.
*/
public int getDisplayMode()
{
return displayMode;
}
/**
* @return Returns the current number of bands displayed by the spectrum analyser.
*/
public int getSpectrumAnalyserBandCount()
{
return saBands;
}
/**
* @return Returns the decay rate of the spectrum analyser's bands.
*/
public float getSpectrumAnalyserDecay()
{
return saDecay;
}
/**
* @return Returns the color the scope is rendered in.
*/
public Color getScopeColor()
{
return scopeColor;
}
/**
* @return Returns the color scale used to render the spectrum analyser bars.
*/
public Color[] getSpectrumAnalyserColors()
{
return spectrumAnalyserColors;
}
private void initialize()
{
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
setBackground(DEFAULT_BACKGROUND_COLOR);
prepareDisplayToggleListener();
setSpectrumAnalyserBandCount(DEFAULT_SPECTRUM_ANALYSER_BAND_COUNT);
setSpectrumAnalyserFFTSampleSize(DEFAULT_SPECTRUM_ANALYSER_FFT_SAMPLE_SIZE);
}
/**
* @return Returns 'true' if "Frames Per Second" are being calculated and displayed.
*/
public boolean isShowingFPS()
{
return showFPS;
}
public void paintComponent(Graphics pGraphics)
{
if (displayMode == DISPLAY_MODE_OFF) return;
if (dspStarted)
{
pGraphics.drawImage(getDoubleBuffer(), 0, 0, null);
}
else
{
super.paintComponent(pGraphics);
}
}
private void prepareDisplayToggleListener()
{
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
addMouseListener(new MouseAdapter()
{
public void mouseClicked(MouseEvent pEvent)
{
if (pEvent.getButton() == MouseEvent.BUTTON1)
{
if (displayMode + 1 > 1)
{
displayMode = 0;
}
else
{
displayMode++;
}
}
}
});
}
/* (non-Javadoc)
* @see kj.dsp.KJDigitalSignalProcessor#process(float[], float[], float)
*/
public synchronized void process(float[] pLeft, float[] pRight, float pFrameRateRatioHint)
{
if (displayMode == DISPLAY_MODE_OFF) return;
Graphics wGrp = getDoubleBuffer().getGraphics();
wGrp.setColor(getBackground());
wGrp.fillRect(0, 0, getSize().width, getSize().height);
switch (displayMode)
{
case DISPLAY_MODE_SCOPE:
drawScope(wGrp, stereoMerge(pLeft, pRight));
break;
case DISPLAY_MODE_SPECTRUM_ANALYSER:
drawSpectrumAnalyser(wGrp, stereoMerge(pLeft, pRight), pFrameRateRatioHint);
break;
case DISPLAY_MODE_OFF:
drawVUMeter(wGrp, pLeft, pRight, pFrameRateRatioHint);
break;
}
// -- Show FPS if necessary.
if (showFPS)
{
// -- Calculate FPS.
if (System.currentTimeMillis() >= lfu + 1000)
{
lfu = System.currentTimeMillis();
fps = fc;
fc = 0;
}
fc++;
wGrp.setColor(Color.yellow);
wGrp.drawString("FPS: " + fps + " (FRRH: " + pFrameRateRatioHint + ")", 0, height - 1);
}
if (getGraphics() != null) getGraphics().drawImage(getDoubleBuffer(), 0, 0, null);
// repaint();
// try {
// EventQueue.invokeLater( new AWTPaintSynchronizer() );
// } catch ( Exception pEx ) {
// // -- Ignore exception.
// pEx.printStackTrace();
// }
}
/**
* Sets the current display mode.
*
* @param pMode Must be either DISPLAY_MODE_SCOPE or DISPLAY_MODE_SPECTRUM_ANALYSER.
*/
public synchronized void setDisplayMode(int pMode)
{
displayMode = pMode;
}
/**
* Sets the color of the scope.
*
* @param pColor
*/
public synchronized void setScopeColor(Color pColor)
{
scopeColor = pColor;
}
/**
* When 'true' is passed as a parameter, will overlay the "Frames Per Seconds"
* achieved by the component.
*
* @param pState
*/
public synchronized void setShowFPS(boolean pState)
{
showFPS = pState;
}
/**
* Sets the numbers of bands rendered by the spectrum analyser.
*
* @param pCount Cannot be more than half the "FFT sample size".
*/
public synchronized void setSpectrumAnalyserBandCount(int pCount)
{
saBands = pCount;
peaks = new int[saBands];
peaksDelay = new int[saBands];
computeSAMultiplier();
}
/**
* Sets the spectrum analyser band decay rate.
*
* @param pDecay Must be a number between 0.0 and 1.0 exclusive.
*/
public synchronized void setSpectrumAnalyserDecay(float pDecay)
{
if ((pDecay >= MIN_SPECTRUM_ANALYSER_DECAY) && (pDecay <= MAX_SPECTRUM_ANALYSER_DECAY))
{
saDecay = pDecay;
}
else saDecay = DEFAULT_SPECTRUM_ANALYSER_DECAY;
}
/**
* Sets the spectrum analyser color scale.
*
* @param pColors Any amount of colors may be used. Must not be null.
*/
public synchronized void setSpectrumAnalyserColors(Color[] pColors)
{
spectrumAnalyserColors = pColors;
computeColorScale();
}
/**
* Sets the FFT sample size to be just for calculating the spectrum analyser
* values. The default is 512.
*
* @param pSize Cannot be more than the size of the sample provided by the DSP.
*/
public synchronized void setSpectrumAnalyserFFTSampleSize(int pSize)
{
saFFTSampleSize = pSize;
fft = new KJFFT(saFFTSampleSize);
old_FFT = new float[saFFTSampleSize];
computeSAMultiplier();
}
private float[] stereoMerge(float[] pLeft, float[] pRight)
{
for (int a = 0; a < pLeft.length; a++)
{
pLeft[a] = (pLeft[a] + pRight[a]) / 2.0f;
}
return pLeft;
}
/*public void update(Graphics pGraphics)
{
// -- Prevent AWT from clearing background.
paint(pGraphics);
}*/
}