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
}
}