View Javadoc

1   package com.melloware.jukes.gui.view.component;
2   
3   import java.awt.Color;
4   import java.awt.Cursor;
5   import java.awt.Graphics;
6   import java.awt.Image;
7   import java.awt.event.MouseAdapter;
8   import java.awt.event.MouseEvent;
9   import java.io.BufferedReader;
10  import java.io.ByteArrayInputStream;
11  import java.io.IOException;
12  import java.io.InputStreamReader;
13  import java.util.ArrayList;
14  import java.util.Arrays;
15  import java.util.Collections;
16  import java.util.List;
17  import java.util.StringTokenizer;
18  
19  import javax.sound.sampled.SourceDataLine;
20  import javax.swing.JPanel;
21  
22  import kj.dsp.KJDigitalSignalProcessingAudioDataConsumer;
23  import kj.dsp.KJDigitalSignalProcessor;
24  import kj.dsp.KJFFT;
25  
26  import org.apache.commons.logging.Log;
27  import org.apache.commons.logging.LogFactory;
28  
29  /**
30   * Spectrum Analyzer component to display visualizations in the Jukes.
31   * <p>
32   * Copyright (c) 1999-2007 Melloware, Inc. <http://www.melloware.com>
33   * @author Emil A. Lefkof III <info@melloware.com>
34   * @version 4.0
35   */
36  @SuppressWarnings("PMD")
37  public final class SpectrumTimeAnalyzer extends JPanel implements KJDigitalSignalProcessor {
38  
39     private static final Log LOG = LogFactory.getLog(SpectrumTimeAnalyzer.class);
40  
41     public static final String SCOPE = "Scope";
42     public static final String OFF = "Off";
43     public static final String ANALYZER = "Analyzer";
44     public static final int DISPLAY_MODE_SCOPE = 0;
45     public static final int DISPLAY_MODE_SPECTRUM_ANALYSER = 1;
46     public static final int DISPLAY_MODE_OFF = 2;
47     public static final int DEFAULT_WIDTH = 256;
48     public static final int DEFAULT_HEIGHT = 128;
49     public static final int DEFAULT_FPS = 50;
50     public static final int DEFAULT_SPECTRUM_ANALYSER_FFT_SAMPLE_SIZE = 512;
51     public static final int DEFAULT_SPECTRUM_ANALYSER_BAND_COUNT = 19;
52     public static final float DEFAULT_SPECTRUM_ANALYSER_DECAY = 0.05f;
53     public static final int DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY = 20;
54     public static final float DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO = 0.4f;
55     public static final float DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO_RANGE = 0.1f;
56     public static final float MIN_SPECTRUM_ANALYSER_DECAY = 0.02f;
57     public static final float MAX_SPECTRUM_ANALYSER_DECAY = 0.08f;
58     public static final Color DEFAULT_BACKGROUND_COLOR = new Color(0, 0, 128);
59     public static final Color DEFAULT_SCOPE_COLOR = new Color(255, 192, 0);
60     public static final float DEFAULT_VU_METER_DECAY = 0.02f;
61     private transient Image bi;
62     private int displayMode = DISPLAY_MODE_SCOPE;
63     private Color scopeColor = DEFAULT_SCOPE_COLOR;
64     private Color[] spectrumAnalyserColors = getDefaultSpectrumAnalyserColors();
65     private static final KJDigitalSignalProcessingAudioDataConsumer dsp = new KJDigitalSignalProcessingAudioDataConsumer(
66              2048, DEFAULT_FPS);
67     private boolean dspStarted = false;
68     private Color peakColor = null;
69     private int[] peaks = new int[DEFAULT_SPECTRUM_ANALYSER_BAND_COUNT];
70     private int[] peaksDelay = new int[DEFAULT_SPECTRUM_ANALYSER_BAND_COUNT];
71     private int peakDelay = DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY;
72     private boolean peaksEnabled = true;
73     private int barOffset = 1;
74     private int width;
75     private int height;
76     private int height_2;
77     // -- Spectrum analyser variables.
78     private static final KJFFT fft = new KJFFT(DEFAULT_SPECTRUM_ANALYSER_FFT_SAMPLE_SIZE);
79     private float[] old_FFT;
80     private int saFFTSampleSize;
81     private int saBands;
82     private float saColorScale;
83     private float saMultiplier;
84     private float saDecay = DEFAULT_SPECTRUM_ANALYSER_DECAY;
85     private transient SourceDataLine m_line = null;
86  
87     public SpectrumTimeAnalyzer() {
88        setOpaque(false);
89        initialize();
90     }
91  
92     public boolean isPeaksEnabled() {
93        return peaksEnabled;
94     }
95  
96     public void setPeaksEnabled(boolean peaksEnabled) {
97        this.peaksEnabled = peaksEnabled;
98     }
99  
100    /**
101     * Starts DSP.
102     * @param line
103     */
104    public synchronized void startDSP(SourceDataLine line) {
105       if (displayMode == DISPLAY_MODE_OFF)
106          return;
107       if (line != null)
108          m_line = line;
109 
110       if (m_line != null) {
111          if (dspStarted == true) {
112             stopDSP();
113          }
114          dsp.start(m_line);
115          dspStarted = true;
116          LOG.debug("DSP started");
117       }
118    }
119 
120    /**
121     * Stop DSP.
122     */
123    public synchronized void stopDSP() {
124       if (dspStarted) {
125          dsp.stop();
126          dspStarted = false;
127          LOG.debug("DSP stopped");
128       }
129    }
130 
131    /**
132     * Setup DSP.
133     * @param line
134     */
135    public synchronized void setupDSP(SourceDataLine line) {
136       int channels = line.getFormat().getChannels();
137       if (channels == 1)
138          dsp.setChannelMode(KJDigitalSignalProcessingAudioDataConsumer.CHANNEL_MODE_MONO);
139       else
140          dsp.setChannelMode(KJDigitalSignalProcessingAudioDataConsumer.CHANNEL_MODE_STEREO);
141       int bits = line.getFormat().getSampleSizeInBits();
142       if (bits == 8)
143          dsp.setSampleType(KJDigitalSignalProcessingAudioDataConsumer.SAMPLE_TYPE_EIGHT_BIT);
144       else
145          dsp.setSampleType(KJDigitalSignalProcessingAudioDataConsumer.SAMPLE_TYPE_SIXTEEN_BIT);
146    }
147 
148    /**
149     * Write PCM data to DSP.
150     * @param pcmdata
151     */
152    public void writeDSP(byte[] pcmdata) {
153       if (dspStarted == true) {
154          dsp.writeAudioData(pcmdata);
155       }
156    }
157 
158    /**
159     * Return DSP.
160     * @return
161     */
162    public KJDigitalSignalProcessingAudioDataConsumer getDSP() {
163       return dsp;
164    }
165 
166    /**
167     * Set visual colors from skin.
168     * @param viscolor
169     */
170    @SuppressWarnings("unchecked")
171    public void setVisColor(String viscolor) {
172       ArrayList visColors = new ArrayList();
173       viscolor = viscolor.toLowerCase();
174       ByteArrayInputStream in = new ByteArrayInputStream(viscolor.getBytes());
175       BufferedReader bin = new BufferedReader(new InputStreamReader(in));
176       try {
177          String line = null;
178          while ((line = bin.readLine()) != null) {
179             visColors.add(getColor(line));
180          }
181          Color[] colors = new Color[visColors.size()];
182          visColors.toArray(colors);
183          Color[] specColors = new Color[15];
184          System.arraycopy(colors, 2, specColors, 0, 15);
185          List specList = Arrays.asList(specColors);
186          Collections.reverse(specList);
187          specColors = (Color[]) specList.toArray(specColors);
188          setSpectrumAnalyserColors(specColors);
189          setBackground((Color) visColors.get(0));
190          if (visColors.size() > 23)
191             setPeakColor((Color) visColors.get(23));
192          if (visColors.size() > 18)
193             setScopeColor((Color) visColors.get(18));
194       } catch (IOException ex) {
195          LOG.warn("Cannot parse viscolors", ex);
196       } finally {
197          try {
198             if (bin != null)
199                bin.close();
200          } catch (IOException e) {
201          }
202       }
203    }
204 
205    /**
206     * Set visual peak color.
207     * @param c
208     */
209    public synchronized void setPeakColor(Color c) {
210       peakColor = c;
211    }
212 
213    /**
214     * Set peak falloff delay.
215     * @param framestowait
216     */
217    public void setPeakDelay(int framestowait) {
218       int min = (int) Math
219                .round((DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO - DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO_RANGE)
220                         * DEFAULT_FPS);
221       int max = (int) Math
222                .round((DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO + DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO_RANGE)
223                         * DEFAULT_FPS);
224       if ((framestowait >= min) && (framestowait <= max)) {
225          peakDelay = framestowait;
226       } else {
227          peakDelay = (int) Math.round(DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO * DEFAULT_FPS);
228       }
229    }
230 
231    /**
232     * Return peak falloff delay
233     * @return int framestowait
234     */
235    public int getPeakDelay() {
236       return peakDelay;
237    }
238 
239    /**
240     * Convert string to color.
241     * @param linecolor
242     * @return
243     */
244    public Color getColor(String linecolor) {
245       Color color = Color.BLACK;
246       StringTokenizer st = new StringTokenizer(linecolor, ",");
247       int red = 0, green = 0, blue = 0;
248       try {
249          if (st.hasMoreTokens())
250             red = Integer.parseInt(st.nextToken().trim());
251          if (st.hasMoreTokens())
252             green = Integer.parseInt(st.nextToken().trim());
253          if (st.hasMoreTokens()) {
254             String blueStr = st.nextToken().trim();
255             if (blueStr.length() > 3)
256                blueStr = (blueStr.substring(0, 3)).trim();
257             blue = Integer.parseInt(blueStr);
258          }
259          color = new Color(red, green, blue);
260       } catch (NumberFormatException e) {
261          LOG.debug("Cannot parse viscolor : " + e.getMessage());
262       }
263       return color;
264    }
265 
266    private synchronized void computeColorScale() {
267       saColorScale = ((float) spectrumAnalyserColors.length / height) * barOffset * 1.0f;
268    }
269 
270    private synchronized void computeSAMultiplier() {
271       saMultiplier = (saFFTSampleSize / 2) / saBands;
272    }
273 
274    private synchronized void drawScope(Graphics pGrp, float[] pSample) {
275       pGrp.setColor(scopeColor);
276       int wLas = (int) (pSample[0] * (float) height_2) + height_2;
277       int wSt = 2;
278       for (int a = wSt, c = 0; c < width; a += wSt, c++) {
279          int wAs = (int) (pSample[a] * (float) height_2) + height_2;
280          pGrp.drawLine(c, wLas, c + 1, wAs);
281          wLas = wAs;
282       }
283    }
284 
285    private synchronized void drawSpectrumAnalyser(Graphics pGrp, float[] pSample, float pFrrh) {
286       float c = 0;
287       float[] wFFT = fft.calculate(pSample);
288       float wSadfrr = (saDecay * pFrrh);
289       float wBw = ((float) width / (float) saBands);
290       for (int a = 0, bd = 0; bd < saBands; a += saMultiplier, bd++) {
291          float wFs = 0;
292          // -- Average out nearest bands.
293          for (int b = 0; b < saMultiplier; b++) {
294             wFs += wFFT[a + b];
295          }
296          // -- Log filter.
297          wFs = (wFs * (float) Math.log(bd + 2));
298          if (wFs > 1.0f) {
299             wFs = 1.0f;
300          }
301          // -- Compute SA decay...
302          if (wFs >= (old_FFT[a] - wSadfrr)) {
303             old_FFT[a] = wFs;
304          } else {
305             old_FFT[a] -= wSadfrr;
306             if (old_FFT[a] < 0) {
307                old_FFT[a] = 0;
308             }
309             wFs = old_FFT[a];
310          }
311          drawSpectrumAnalyserBar(pGrp, (int) c, height, (int) wBw - 1, (int) (wFs * height), bd);
312          c += wBw;
313       }
314    }
315 
316    private synchronized void drawSpectrumAnalyserBar(Graphics pGraphics, int pX, int pY, int pWidth, int pHeight,
317             int band) {
318       float c = 0;
319       for (int a = pY; a >= pY - pHeight; a -= barOffset) {
320          c += saColorScale;
321          if (c < spectrumAnalyserColors.length) {
322             pGraphics.setColor(spectrumAnalyserColors[(int) c]);
323          }
324          pGraphics.fillRect(pX, a, pWidth, 1);
325       }
326       if ((peakColor != null) && (peaksEnabled == true)) {
327          pGraphics.setColor(peakColor);
328          if (pHeight > peaks[band]) {
329             peaks[band] = pHeight;
330             peaksDelay[band] = peakDelay;
331          } else {
332             peaksDelay[band]--;
333             if (peaksDelay[band] < 0)
334                peaks[band]--;
335             if (peaks[band] < 0)
336                peaks[band] = 0;
337          }
338          pGraphics.fillRect(pX, pY - peaks[band], pWidth, 1);
339       }
340    }
341 
342    private synchronized Image getDoubleBuffer() {
343       if (bi == null || (bi.getWidth(null) != getSize().width || bi.getHeight(null) != getSize().height)) {
344          width = getSize().width;
345          height = getSize().height;
346          height_2 = height >> 1;
347          computeColorScale();
348          bi = getGraphicsConfiguration().createCompatibleVolatileImage(width, height);
349       }
350       return bi;
351    }
352 
353    public static Color[] getDefaultSpectrumAnalyserColors() {
354       Color[] wColors = new Color[256];
355       for (int a = 0; a < 128; a++) {
356          wColors[a] = new Color(0, (a >> 1) + 192, 0);
357       }
358       for (int a = 0; a < 64; a++) {
359          wColors[a + 128] = new Color(a << 2, 255, 0);
360       }
361       for (int a = 0; a < 64; a++) {
362          wColors[a + 192] = new Color(255, 255 - (a << 2), 0);
363       }
364       return wColors;
365    }
366 
367    /**
368     * @return Returns the current display mode, DISPLAY_MODE_SCOPE or
369     *         DISPLAY_MODE_SPECTRUM_ANALYSER.
370     */
371    public synchronized int getDisplayMode() {
372       return displayMode;
373    }
374 
375    /**
376     * @return Returns the current number of bands displayed by the spectrum
377     *         analyser.
378     */
379    public synchronized int getSpectrumAnalyserBandCount() {
380       return saBands;
381    }
382 
383    /**
384     * @return Returns the decay rate of the spectrum analyser's bands.
385     */
386    public synchronized float getSpectrumAnalyserDecay() {
387       return saDecay;
388    }
389 
390    /**
391     * @return Returns the color the scope is rendered in.
392     */
393    public synchronized Color getScopeColor() {
394       return scopeColor;
395    }
396 
397    /**
398     * @return Returns the color scale used to render the spectrum analyser bars.
399     */
400    public synchronized Color[] getSpectrumAnalyserColors() {
401       return spectrumAnalyserColors;
402    }
403 
404    private void initialize() {
405       setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
406       setBackground(DEFAULT_BACKGROUND_COLOR);
407       prepareDisplayToggleListener();
408       setSpectrumAnalyserBandCount(DEFAULT_SPECTRUM_ANALYSER_BAND_COUNT);
409       setSpectrumAnalyserFFTSampleSize(DEFAULT_SPECTRUM_ANALYSER_FFT_SAMPLE_SIZE);
410       dsp.add(this);
411    }
412 
413    public void paintComponent(Graphics pGraphics) {
414       synchronized (this) {
415          if (displayMode == DISPLAY_MODE_OFF)
416             return;
417 
418          if (dspStarted) {
419             pGraphics.drawImage(getDoubleBuffer(), 0, 0, null);
420          } else {
421             super.paintComponent(pGraphics);
422          }
423       }
424    }
425 
426    private void prepareDisplayToggleListener() {
427       setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
428       addMouseListener(new MouseAdapter() {
429          public void mouseClicked(MouseEvent pEvent) {
430             if (pEvent.getButton() == MouseEvent.BUTTON1) {
431                if (displayMode + 1 > 2) {
432                   displayMode = 0;
433                } else {
434                   displayMode++;
435                }
436             }
437          }
438       });
439    }
440 
441    /*
442     * (non-Javadoc)
443     * @see kj.dsp.KJDigitalSignalProcessor#process(float[], float[], float)
444     */
445    public synchronized void process(float[] pLeft, float[] pRight, float pFrameRateRatioHint) {
446       if (displayMode == DISPLAY_MODE_OFF)
447          return;
448       final Graphics wGrp = getDoubleBuffer().getGraphics();
449       wGrp.setColor(getBackground());
450       wGrp.fillRect(0, 0, getSize().width, getSize().height);
451       switch (displayMode) {
452       case DISPLAY_MODE_SCOPE:
453          drawScope(wGrp, stereoMerge(pLeft, pRight));
454          break;
455       case DISPLAY_MODE_SPECTRUM_ANALYSER:
456          drawSpectrumAnalyser(wGrp, stereoMerge(pLeft, pRight), pFrameRateRatioHint);
457          break;
458       }
459       final Graphics graphics = getGraphics();
460       if (graphics != null)
461          graphics.drawImage(getDoubleBuffer(), 0, 0, null);
462    }
463 
464    /**
465     * Sets the current display mode.
466     * @param pMode Must be either DISPLAY_MODE_SCOPE or
467     *           DISPLAY_MODE_SPECTRUM_ANALYSER.
468     */
469    public synchronized void setDisplayMode(int pMode) {
470       displayMode = pMode;
471    }
472 
473    /**
474     * Sets the display mode based on a string value of one of the String
475     * constants OFF, ANALYZER, SCOPE.
476     * <p>
477     * @param pMode the string value of which mode to set
478     */
479    public synchronized void setDisplayMode(String pMode) {
480       if (ANALYZER.equals(pMode)) {
481          this.setDisplayMode(DISPLAY_MODE_SPECTRUM_ANALYSER);
482       } else if (SCOPE.equals(pMode)) {
483          this.setDisplayMode(DISPLAY_MODE_SCOPE);
484       } else {
485          this.setDisplayMode(DISPLAY_MODE_OFF);
486       }
487    }
488 
489    /**
490     * Sets the color of the scope.
491     * @param pColor
492     */
493    public synchronized void setScopeColor(Color pColor) {
494       scopeColor = pColor;
495    }
496 
497    /**
498     * Sets the numbers of bands rendered by the spectrum analyser.
499     * @param pCount Cannot be more than half the "FFT sample size".
500     */
501    public synchronized void setSpectrumAnalyserBandCount(int pCount) {
502       saBands = pCount;
503       peaks = new int[saBands];
504       peaksDelay = new int[saBands];
505       computeSAMultiplier();
506    }
507 
508    /**
509     * Sets the spectrum analyser band decay rate.
510     * @param pDecay Must be a number between 0.0 and 1.0 exclusive.
511     */
512    public synchronized void setSpectrumAnalyserDecay(float pDecay) {
513       if ((pDecay >= MIN_SPECTRUM_ANALYSER_DECAY) && (pDecay <= MAX_SPECTRUM_ANALYSER_DECAY)) {
514          saDecay = pDecay;
515       } else
516          saDecay = DEFAULT_SPECTRUM_ANALYSER_DECAY;
517    }
518 
519    /**
520     * Sets the spectrum analyser color scale.
521     * @param pColors Any amount of colors may be used. Must not be null.
522     */
523    public synchronized void setSpectrumAnalyserColors(Color[] pColors) {
524       spectrumAnalyserColors = pColors;
525       computeColorScale();
526    }
527 
528    /**
529     * Sets the FFT sample size to be just for calculating the spectrum analyser
530     * values. The default is 512.
531     * @param pSize Cannot be more than the size of the sample provided by the
532     *           DSP.
533     */
534    public synchronized void setSpectrumAnalyserFFTSampleSize(int pSize) {
535       saFFTSampleSize = pSize;
536       old_FFT = new float[saFFTSampleSize];
537       computeSAMultiplier();
538    }
539 
540    private float[] stereoMerge(float[] pLeft, float[] pRight) {
541       for (int a = 0; a < pLeft.length; a++) {
542          pLeft[a] = (pLeft[a] + pRight[a]) / 2.0f;
543       }
544       return pLeft;
545    }
546 
547 }