package com.mmcvey; import java.awt.Color; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.DataLine; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.TargetDataLine; import com.meapsoft.FFT; public class SoundAnalyser { private static final float SAMPLE_RATE = 48000f; private static final float MAX_HUE_CHANGE = 0.0005f; // 0.0005 private static final float MAX_BRIGHTNESS_CHANGE = 0.0020f;// 0.00020f private final int fftSize; // power of two private final int sampleSize; // power of two, smaller or equal to FFT_SIZE private final int numBuffers; private final float[] historyHue; private final double[] historyLoudness; private int changeCooldown; private int changeLock = 0; private boolean canChange = true; private float shiftAmount; private float shift = 0.0f; private float colorWeight; private float saturation = 1.0f; private float currentHue = 0.0f; private float currentBrightness = 1.0f; private int color = 0; private TargetDataLine line; public SoundAnalyser() { fftSize = 4096; sampleSize = 256; numBuffers = fftSize / sampleSize * 2; historyHue = new float[10]; historyLoudness = new double[(int) (0.5 / (sampleSize / SAMPLE_RATE / 2))]; changeCooldown = (int) (0.5 / (sampleSize / SAMPLE_RATE / 2)); shiftAmount = 0.0001f; colorWeight = 2.5f; } public SoundAnalyser(int fftSize, int sampleSize) { this.fftSize = fftSize; this.sampleSize = sampleSize; numBuffers = this.fftSize / this.sampleSize * 2; historyHue = new float[30]; historyLoudness = new double[(int) (1 / (this.sampleSize / SAMPLE_RATE / 2))]; changeCooldown = (int) (0.5 / (this.sampleSize / SAMPLE_RATE / 2)); shiftAmount = 0.00001f; colorWeight = 1.5f; } public void start() { AudioFormat format = new AudioFormat(SAMPLE_RATE, 16, // sample size in bits 1, // mono true, // signed false); DataLine.Info info = new DataLine.Info(TargetDataLine.class, format); // format is an AudioFormat object if (!AudioSystem.isLineSupported(info)) { // Handle the error ... } // Obtain and open the line. try { line = (TargetDataLine) AudioSystem.getLine(info); line.open(format); // output(line); new Thread(() -> run()).start(); } catch (LineUnavailableException ex) { // Handle the error ... } } public void stop() { line.close(); } private synchronized void run() { System.out.println(line); FFT fft = new FFT(fftSize); int numBytesRead; final byte[][] data = new byte[numBuffers][sampleSize]; final double[] dataD = new double[fftSize]; final double[] dataCurrent = new double[sampleSize / 2]; double[] dataIm = new double[fftSize]; // Begin audio capture. line.start(); // long time = System.nanoTime(); while (line.isOpen()) { //System.out.println(running); // Read the next chunk of data from the TargetDataLine. // System.out.println((System.nanoTime() - time) / 1.0E9); // time = System.nanoTime(); numBytesRead = line.read(data[0], 0, sampleSize); if (numBytesRead != sampleSize) { return; // thow something? } for (int j = 0; j < sampleSize; j += 2) { for (int i = 0; i < numBuffers; i++) { dataD[j / 2 + i * sampleSize / 2] = (((short) data[i][j + 1]) << 8 | data[i][j]) / (Short.MAX_VALUE * 1.0f); } dataCurrent[j / 2] = dataD[j / 2]; } byte[] temp; temp = data[0]; for (int i = 0; i < numBuffers - 1; i++) { data[i] = data[i + 1]; } data[numBuffers - 1] = temp; // System.out.println(data[0] + " + " + data[1] + " = " + dataD[0]); dataIm = new double[fftSize]; fft.fft(dataD, dataIm); updateColor(toMagnitudes(dataD, dataIm), dataCurrent); // System.out.println(dataD[170]); } } private static double[] toMagnitudes(final double[] realPart, final double[] imaginaryPart) { final double[] powers = new double[realPart.length / 2]; for (int i = 0; i < powers.length; i++) { powers[i] = Math.sqrt(realPart[i] * realPart[i] + imaginaryPart[i] * imaginaryPart[i]); } return powers; } /** * only above a certain level? * * @param mags * @return */ private synchronized int updateColor(double[] mags, double[] data) { double loudness = 0.0; for (double d : data) { loudness += Math.abs(d); } loudness = Math.log(loudness); double maxLoud = loudness; for (int i = historyLoudness.length - 2; i >= 0; i--) { if (historyLoudness[i] > maxLoud) maxLoud = historyLoudness[i]; // aveLoud += historyLoudness[i]; historyLoudness[i + 1] = historyLoudness[i]; } // aveLoud += loudness; historyLoudness[0] = loudness; if (loudness < 0.5) { float bright = (float) ((loudness + 0.2) / 0.7f); currentBrightness += (bright - currentBrightness) / 128; if (currentBrightness < 0.0f) { currentBrightness = 0.0f; } //System.out.println("Dimming"); } else { // aveLoud /= (float) historyLoudness.length; float brightness = (float) (1.0f - (maxLoud - loudness) / maxLoud * 0.0f); if (brightness > 1.0f) { //currentBrightness = 1.0f; brightness = 1.0f; } // if (brightness < 0.75f) { // brightness = 0.75f; // } if (brightness > currentBrightness) { if (brightness - currentBrightness > MAX_BRIGHTNESS_CHANGE * 4) { currentBrightness += MAX_BRIGHTNESS_CHANGE * 4; } else { currentBrightness = brightness; } } else { if (currentBrightness - brightness > MAX_BRIGHTNESS_CHANGE) { currentBrightness -= MAX_BRIGHTNESS_CHANGE; } else { currentBrightness = brightness; } } if (currentBrightness < 0.8f) System.out.println("brightness is " + brightness + " and currentBrightness is " + currentBrightness); } double frequencyGap = SAMPLE_RATE / (double) fftSize; final int maxF = 10000; double total = 0.0; int num = 0; for (int i = 1; i < mags.length; i++) { if (i * frequencyGap > maxF) { break; } num++; total += mags[i]; } double ave = total / (double) num; double dev = 0.0; for (int i = 1; i < mags.length; i++) { if (i * frequencyGap > maxF) { break; } dev = (mags[i] > dev) ? mags[i] - dev : dev - mags[i]; } double sd = dev / (double) num; double aveF = 0.0; double total1SD = 0.0; @SuppressWarnings("unused") int numSD = 0; for (int i = 1; i < mags.length; i++) { if (i * frequencyGap > maxF) { break; } if (mags[i] > ave + 2000.0 * sd) { // System.out.println((double) i * frequencyGap); aveF += mags[i] * (double) i * frequencyGap * 500.0; total1SD += mags[i] * 500.0; numSD++; } else if (mags[i] > ave + 1000.0 * sd) { aveF += mags[i] * (double) i * frequencyGap * 10.0; total1SD += mags[i] * 10.0; } else if (mags[i] > ave + 100.0 * sd) { aveF += mags[i] * (double) i * frequencyGap * 1.0; total1SD += mags[i] * 1.0; } } aveF /= total1SD; /* * System.out.printf( * "total: %.3f ave: %.5f sd: %.5f AveF: %.3f totSD: %.3f numSD: %d birghtness: %.3f loudness: %.3f\n" * , total, ave, sd, aveF, total1SD, numSD, currentBrightness, loudness); */ float hue = (float) (aveF / (float) maxF * colorWeight); float aveHue = 0.0f; for (int i = historyHue.length - 2; i >= 0; i--) { aveHue += historyHue[i]; historyHue[i + 1] = historyHue[i]; } aveHue += hue; historyHue[0] = hue; aveHue /= (float) historyHue.length; if (aveHue - currentHue > MAX_HUE_CHANGE) { if (canChange && aveHue - currentHue > 0.33f) { currentHue = aveHue; canChange = false; changeLock = changeCooldown; } else { currentHue += MAX_HUE_CHANGE; if (!canChange) { if (changeLock > 0) { changeLock--; } else { canChange = true; } } } } else if (aveHue - currentHue < -MAX_HUE_CHANGE) { if (canChange && aveHue - currentHue < -0.33f) { currentHue = aveHue; canChange = false; changeLock = changeCooldown; } else { currentHue -= MAX_HUE_CHANGE; if (!canChange) { if (changeLock > 0) { changeLock--; } else { canChange = true; } } } } else { currentHue = aveHue; } shift += shiftAmount; if (shift > 1.0) { shift -= 1.0f; } // System.out.println(brightness); color = Color.HSBtoRGB(currentHue + shift, 1.0f, currentBrightness); return color; } public int getBlue() { return (color) & 0xff; } public int getGreen() { return (color >> 8) & 0xff; } public int getRed() { return (color >> 16) & 0xff; } public double getLoudness() { return historyLoudness[0]; } public double getBrightness() { return currentBrightness; } public void setShif(float shift) { shiftAmount = shift; } public float getShift() { return shift; } public void setColorWight(float colorWeight) { this.colorWeight = colorWeight; } public void setChangeCooldown(double seconds) { changeCooldown = (int) (seconds / (sampleSize / SAMPLE_RATE / 2)); } }