Package com.talixa.specan.widgets

Source Code of com.talixa.specan.widgets.SpectrumAnalyzerWidget

package com.talixa.specan.widgets;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.io.File;
import java.io.IOException;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineUnavailableException;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.SwingWorker;

import com.talixa.audio.riff.exceptions.RiffFormatException;
import com.talixa.audio.wav.WaveFile;
import com.talixa.specan.SpectrumAnalyzer;
import com.talixa.specan.dsp.SharedDSPFunctions;
import com.talixa.specan.fft.Complex;
import com.talixa.specan.fft.FFT;
import com.talixa.specan.fft.FrequencyTools;
import com.talixa.specan.input.LineInputReader;

@SuppressWarnings("serial")
public class SpectrumAnalyzerWidget extends JPanel implements MouseListener, MouseMotionListener 
 
  private static final int RESOLUTION = 512;      // length/width of widget
  private static final int ZOOM = 1024;        // magnification factor for spectrum
  private static final int FFT_LEN = RESOLUTION*2;   // Length of FFT buffer ***MUST be a power of 2*** 
  private static final int shiftValue = 800;      // number of bytes to shift for a refresh rate of 1/10 second
 
  // Frequency data
  private Complex amplitudeData[] = new Complex[FFT_LEN];          // amplitude data from PCM
  private Complex frequencyData[];                    // corresponding frequency data
  private double scaledAmpFreq[];                      // amp/freq data
  private double[][] waterfallData = new double[RESOLUTION/2][RESOLUTION];// waterfall data   
 
  private WaveFile waveFile;      // wave file to analyze
  private JLabel status;        // status label below widget
  private SpectrumAnalyzerWidget parent = this;
 
  // loop audio?
  private boolean loop = false;
  private boolean soundOn = true;
  private boolean useLineIn = false;
 
  // things to display
  private boolean waterfall = false;    // true if waterfall to display
  private boolean scope = false;      // true if scope to display
  private String mode = "Spectrum";    // current display mode
  private int scopeCenter;        // center point for oscilloscope view
  private int waterfallRows;        // number of rows of waterfall to display
  private static int scopeZoom = 1;    // zoom factor for oscope
 
  // data for label
  private int clickedFreq = 0;      // last clicked frequency
  private int maxFreq = 0;        // maximum frequency in current spectrum display
  private int freqSpan = 0;        // frequency span for clicked points
 
  // for drawing span line
  private int startX = 0;          // click point x
  private int startY = 0;          // click point y
  private int currX = 0;          // current mouse position
  private int currY = 0;          // current mouse position
 
  // points for previous line
  private int lastX1 = 0;     
  private int lastX2 = 0;
  private int lastY1 = 0;
  private int lastY2 = 0;
 
  // colors
  private static final Color BACKGROUND = Color.BLACK;
  private static final Color SPECTRUM = Color.GREEN;
 
  public SpectrumAnalyzerWidget(JLabel status) {
    this.status = status; 
    this.addMouseListener(this);
    this.addMouseMotionListener(this);   
  }
 
  private Clip clip;  // playing audio clip
  public void setWave(String fileName) throws IOException, RiffFormatException {
    useLineIn = false;
    waveFile = SpectrumAnalyzer.readWaveFile(fileName);
    if (waveFile == null) {
      return;
    }
   
    // play audio file
    try
      if (soundOn) {
          File audioFile = new File(fileName)
          AudioInputStream stream = AudioSystem.getAudioInputStream(audioFile);
          AudioFormat format = stream.getFormat();
          DataLine.Info info = new DataLine.Info(Clip.class, format);       
          if (clip != null) {
            clip.close();
          }
          clip = (Clip) AudioSystem.getLine(info);
          clip.open(stream);                       
      }
    }
    catch (Exception e) {
      // ignore errors
    }
    this.playing = true;
 
 
  public void useLineIn() {
    this.useLineIn = true;
  }
 
  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    this.draw(g);
  }     
 
  //*********************************************************
  //        DRAW DATA ON SCREEN
  //*********************************************************
  private void draw(Graphics g) { 
    // reset background
    g.setColor(BACKGROUND);
    g.fillRect(0, 0, RESOLUTION, RESOLUTION);
   
    // display status   
    String opts;
    if (loop && soundOn) {
      opts = "Loop, Sound - ";
    } else if (loop) {
      opts = "Loop - ";
    } else if (soundOn) {
      opts = "Sound - ";
    } else {
      opts = "";
    }   
    String stat = String.format("Cursor: %4d - Span: %4d - Max: %4d - %s%s", clickedFreq, freqSpan, maxFreq, opts, mode);   
    status.setText(stat);
   
    // if nothing to display, return
    if (scaledAmpFreq == null) {
      return;
    }
   
    // display oscope data
    if (scope) {
      g.setColor(Color.GREEN);
     
      int lastSample = (amplitudeData.length/2)/scopeZoom;
     
      for(int i = 1; i < lastSample; ++i) {
        int currentDataPoint = (int)(amplitudeData[i].re()*100)+(scopeCenter);
        int previousDataPoint = (int)(amplitudeData[i-1].re()*100)+(scopeCenter);
        int previousX = (i-1) * scopeZoom;
        int currentX = i * scopeZoom;       
        g.drawLine(previousX, previousDataPoint, currentX, currentDataPoint);
     
    }
   
    // display waterfall data
    if (waterfall) {
      for(int row = 0; row < waterfallRows; ++row) {
        for(int col = 0; col < RESOLUTION-1; ++col) {
          if (waterfallData[row] != null) {             
            // This implementation for color works very well well.            
            int re = (int)(waterfallData[row][col] * 100000);
            int gr = (int)(waterfallData[row][col] * 10000);
            int bl = (int)(waterfallData[row][col] * 1000);         
            g.setColor(new Color(re > 255 ? 255 : re,gr > 255 ? 255 : gr,bl > 255 ? 255 : bl));
            g.drawRect(col, row, 1, 1);
          }
        }
      }     
    }   
   
    // always display spec an
    for(int i = 0; i < scaledAmpFreq.length; ++i) {
      g.setColor(SPECTRUM);
      g.drawLine(i, RESOLUTION, i , RESOLUTION-(int)(scaledAmpFreq[i]*ZOOM));
     
      // if one click, display line from click to current mouse
      // else if previous clicks, display line from click 1 to click 2
      if (startX > 0 && startY > 0) {
        g.setColor(Color.RED);
        g.drawLine(startX, startY, currX, currY);
      } else if (lastX1 > 0 && lastY1 > 0) {
        g.setColor(Color.RED);
        g.drawLine(lastX1, lastY1, lastX2, lastY2);
      }
    }   
  }     
 
  //***********************************************************
  //            GUI THREAD
  //***********************************************************
  private static SwingWorker<Void, Void> worker = null;
  public void runSpectrumAnalysis() {   
    // if already running, kill thread
    if (worker != null) {       
      worker.cancel(true);
      while(!worker.isDone()) {
        try {
          Thread.sleep(100);
        } catch (InterruptedException e) {
          // DO NOTHING
        }
      }
      waterfallData = new double[RESOLUTION/2][RESOLUTION]// waterfall data
    }
   
    // create worker thread and run transform
    worker = new SwingWorker<Void, Void>() {
      protected Void doInBackground() {
        boolean terminated = false;
        do {
          try {
            if (!useLineIn) {
              if (clip != null) {
                while (clip.isRunning()) {
                  Thread.sleep(50);
                }
                clip.setFramePosition(0);
                clip.start();
              }
              short[] samples = SharedDSPFunctions.extractWaveFileData(waveFile);
              runTransform(samples);
            } else {             
              try {
                if (clip != null) {
                  clip.stop();
                }
                LineInputReader reader = new LineInputReader(FFT_LEN);
                reader.startRecording();
                while(!terminated) {
                  runTransform(reader.getNextFrame());
                }
                reader.stopRecording();
              } catch (LineUnavailableException e) {
                JOptionPane.showMessageDialog(parent, e.getMessage());
              }
            }
          } catch (InterruptedException e) {
            terminated = true;
          }
        } while (loop && !terminated);       
        return null;
      }
    };
   
    worker.execute();
  }   
 
  //*****************************************************
  //          FFT CODE
  //***************************************************** 
  // iterate through data and calculate FFT data
  private void runTransform(short[] samples) throws InterruptedException
    int numberSamples = samples.length;
    for(int baseAddress = 0; (baseAddress)+FFT_LEN <= numberSamples; baseAddress+=shiftValue) {     
      for (int offset = 0; offset < FFT_LEN; offset++) {       
        short signedSample = samples[baseAddress+offset];               
        double sample = ((double) signedSample) / Short.MAX_VALUE;
        amplitudeData[offset] = new Complex(sample,0);
      }
                       
      while (!playing) {         
        Thread.sleep(100);         
      }
      long startTime = System.currentTimeMillis();
      runFFT();
      long endTime = System.currentTimeMillis();
      repaint();
      // 1/10 of a second = realtime for an 8K sampled signal w/ a shift of 1600 bytes (2 bits / sample, 800 samples)       
      // subtract the time to run the FFT so that the audio stays relatively synced
      Thread.sleep(100 - (endTime - startTime));                           
    }     
  }   
 
  // Run the FFT against the amplitude data
  private void runFFT() {
    frequencyData = FFT.fft(amplitudeData);

    // move waterfall data
    for(int i = (RESOLUTION/2)-1; i > 0; --i) {
      waterfallData[i] = waterfallData[i-1];
    }     
   
    scaledAmpFreq = new double[FFT_LEN/2];
   
    double fmax=-99999.9;
    double fmin= 99999.9;   
   
    // Use FFT_LEN/2 since the data is mirrored within the array.
    for(int i=1;i < FFT_LEN/2-1;i++) {
      double re = frequencyData[i].re();
        double im = frequencyData[i].im();
        //get amplitude and scale to range 0 - RESOLUTION
        scaledAmpFreq[i]=FrequencyTools.amplitudeScaled(re,im,FFT_LEN,RESOLUTION);
        if (scaledAmpFreq[i] > fmax) {
            fmax = scaledAmpFreq[i];
            maxFreq = SharedDSPFunctions.getFreqBySampleNumber(FFT_LEN, i);
        } else if (scaledAmpFreq[i] < fmin){
            fmin = scaledAmpFreq[i];
        }       
    }         
   
    // set top row of waterfall
    waterfallData[0] = scaledAmpFreq;   
  }
 
  //**********************************************************
  //        EXTERNAL INTERFACE OPTIONS
  //********************************************************** 
  // state goes from spectrum -> waterfall -> scope -> all -> back to spectrum
  public void toggleSecondView() {   
    if (waterfall && !scope) {
      waterfall = false;
      scope = true;
      mode = "Spectrum + Scope";
      scopeCenter = RESOLUTION/4;
    } else if (scope && !waterfall) {
      waterfall = true;
      scope = true;
      mode = "Display All";
      scopeCenter = RESOLUTION/2;
      waterfallRows = RESOLUTION/4;
    } else if (scope && waterfall){
      waterfall = false;
      scope = false;
      mode = "Spectrum Only";
    } else {
      waterfall = true;
      scope = false;
      mode = "Spectrum + Waterfall";
      waterfallRows = RESOLUTION/2;
    }
    this.repaint();
  }
 
  public void toggleLoop() {
    loop = !loop;
    this.repaint();
  }
 
  private boolean playing = true
  public void play() {
    playing = true;
    if (clip != null) {
      clip.start();
    }
  }
 
  public void stop() {
    playing = false;
    if (clip != null) {
      clip.stop();
    }
  }
 
  private float gain;
  public void toggleSound() {
    if (clip != null) {     
      FloatControl volume = (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN);
      volume.getValue();
      if (soundOn) {
        gain = volume.getValue();
        volume.setValue(-80);
      } else if (!soundOn) {
        volume.setValue(gain);
      }
    }
    soundOn = !soundOn;
    this.repaint();
  }
 
  public void increaseZoom() {
    scopeZoom *= 2;
  }
 
  public void decreaseZoom() {
    if (scopeZoom > 1) {
      scopeZoom /= 2;
    }
  }
 
  public boolean isScopeDisplayed() {
    return this.scope;
  }

  //***************************************************
  //      ALL BELOW CODE HANDLES MOUSE EVENTS
  //***************************************************
  @Override
  public void mouseDragged(MouseEvent e) {
    // do nothing
  }

  @Override
  public void mouseMoved(MouseEvent e) {
    clickedFreq = SharedDSPFunctions.getFreqBySampleNumber(FFT_LEN, e.getX());
    currX = e.getX();
    currY = e.getY();
    repaint();
  }

  @Override
  public void mouseClicked(MouseEvent e) {   
    // DO NOTHING
  }

  private int startSpan;
  @Override
  public void mousePressed(MouseEvent e) {
    if (e.getButton() == 1) { 
      // left button = freq span
      if (startX == 0) {
        startX = e.getX();
        startY = e.getY();
        startSpan = SharedDSPFunctions.getFreqBySampleNumber(FFT_LEN, e.getX());
      } else {
        freqSpan = Math.abs(SharedDSPFunctions.getFreqBySampleNumber(FFT_LEN, e.getX()) - startSpan);       
       
        // persist current line
        lastX1 = startX;
        lastY1 = startY;
        lastX2 = e.getX();
        lastY2 = e.getY();
       
        // reset start
        startX = 0;
        startY = 0;
      }
    } else {
      // right button = clear
      startX = 0;
      startY = 0;
      lastX1 = 0;
      lastY1 = 0;
      lastX2 = 0;
      lastY2 = 0;
      freqSpan = 0;
    }
    repaint();
  }

  @Override
  public void mouseReleased(MouseEvent e) {
    // do nothing
  }

  @Override
  public void mouseEntered(MouseEvent e) {
    // DO NOTHING
  }

  @Override
  public void mouseExited(MouseEvent e) {
    // DO NOTHING 
 
}
TOP

Related Classes of com.talixa.specan.widgets.SpectrumAnalyzerWidget

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.