diff --git a/src/edu/mit/d54/plugins/audio/AudioProcessor.java b/src/edu/mit/d54/plugins/audio/AudioProcessor.java new file mode 100644 index 0000000..e85921b --- /dev/null +++ b/src/edu/mit/d54/plugins/audio/AudioProcessor.java @@ -0,0 +1,256 @@ +package edu.mit.d54.plugins.audio; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.TargetDataLine; + +import edu.emory.mathcs.jtransforms.fft.FloatFFT_1D; + +/** + * The AudioProcessor reads data from the computer's line input to facilitate audio visualization plugins. + * A plugin should call frameUpdate on every frame to refresh the AudioProcessor with new data. The raw + * audio samples as well as FFT magnitude are available via class methods. + */ +public class AudioProcessor { + + private static final int FFT_LEN=2048; + private static final float SAMPLE_RATE=44100; + + private static TargetDataLine line; + private AudioInputStream input; + + private final int fftNumBins; + private final double fftMaxFreq; + private final boolean fftBinLog; + private final double fftScaleDecay; + private final double freqScalePower; + + private byte[] frameRaw=new byte[FFT_LEN*2]; + private int[] frameSamples=new int[FFT_LEN]; + private float[] fftMag; + private float[] fftMagBinned; + private float fftMaxValue; + + static + { + try { + line=AudioSystem.getTargetDataLine( + new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,SAMPLE_RATE,16,1,2,SAMPLE_RATE,false)); + } catch (LineUnavailableException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + /** + * Create a new AudioProcessor around the line in device. + * @param fftNumBins Number of FFT bins to provide + * @param fftMaxFreq Maximum audio frequency (in Hz) to return through FFT + * @param fftBinLog Scale the FFT bins to equal width in log frequency. Otherwise, FFT bins + * will be equal width in frequency. + * @param fftScaleDecay The exponential rate (ratio per frame) that the peak FFT amplitude is + * reduced. This should be a number between 0 and 1. + * @param freqScalePower The exponential rate to reduce effective amplitude versus frequency. + * Amplitude is multipled by (freq^^freqScalePower). Typically this is used to represent the + * deemphasize bass versus the middle and high frequencies which are perceived to be louder. + */ + public AudioProcessor(int fftNumBins, double fftMaxFreq, boolean fftBinLog, double fftScaleDecay, double freqScalePower) + { + this.fftNumBins=fftNumBins; + this.fftMaxFreq=fftMaxFreq; + this.fftBinLog=fftBinLog; + this.fftScaleDecay=fftScaleDecay; + this.freqScalePower=freqScalePower; + } + + /** + * Create a new AudioProcessor around the line in device with generally useful defaults. + * fftMaxFreq is set to 3500 Hz, fftBinLog is true, fftScaleDecay is 0.998, and freqScalePower + * is 0.125. + * @param fftNumBins Number of FFT bins to provide + */ + public AudioProcessor(int fftNumBins) + { + this(fftNumBins,3500,true,0.998,0.125); //was 0.25 + } + + /** + * @return the raw audio samples from the current frame. + */ + public int[] getFrameSamples() + { + return frameSamples; + } + + /** + * Get the FFT magnitude bins from the current frame. The size of this array is determined + * by the fftNumBins parameter. The frequency width and amplitude of the bins is affected by + * the fftMaxFreq, fftBinLog, and freqScalePower parameters. + * @return the FFT magnitude bins from the current frame. + */ + public float[] getFFTMagBins() + { + return fftMagBinned; + } + + /** + * Get the peak value seen by the FFT in the current frame, or the decaying previous higher peak. + * The rate at which the previous peak decays is determined by fftScaleDecay. + * @return the FFT peak value + */ + public float getFFTMaxValue() + { + return fftMaxValue; + } + + /** + * Open the audio channel so data can be captured. This must be called once before frameUpdate or the + * data accessors are called. + */ + public void openChannel() + { + try + { + line.open(); + } catch (LineUnavailableException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + line.start(); + line.drain(); + input=new AudioInputStream(line); + } + + /** + * Update the AudioProcessor with new sample data. This method should be called every time the + * DisplayPlugin is updated and before the audio data is accessed. + */ + public void frameUpdate() + { + try + { + while (input.available()>frameRaw.length) //flush the stream + { + byte[] flush=new byte[2]; + input.read(flush); + } + int avail=input.available(); + // System.out.println("Reading "+avail); + if (frameRaw.length>avail) + { + System.arraycopy(frameRaw, avail, frameRaw, 0, frameRaw.length-avail); + input.read(frameRaw,frameRaw.length-avail,avail); + } + else + { + input.read(frameRaw,0,frameRaw.length); + } + ByteBuffer bb=ByteBuffer.wrap(frameRaw); + bb.order(ByteOrder.LITTLE_ENDIAN); + float[] fft=new float[FFT_LEN]; + for (int i=0; i colorPalette; + boolean colorPaletteInterpolate; + + float[][] levels; + float[][] delta; + @Override + protected void loop() { + audio.frameUpdate(); + + int w=getDisplay().getWidth(); + int h=getDisplay().getHeight(); + + //shift + for (int i=0; i(); + for (String t : tok) + { + colorPalette.add(Color.decode(t.replace("#", "0x"))); + } + } + else if (knob.equals("colorPaletteInterpolate")) + colorPaletteInterpolate=Boolean.parseBoolean(getKnobValue("colorPaletteInterpolate")); + else if (knob.equals("colorPreset")) + { + switch (Integer.parseInt(value)) + { + case 1: //R/O/Y/W colors (indy) + setKnob("colorShiftRate","0"); + setKnob("colorPaletteWidth","17"); + setKnob("colorPaletteAngle","90"); + setKnob("colorPalette","#FF0000,#FF4000,#FFFF00,#FFFFFF"); + setKnob("colorPaletteInterpolate","true"); + break; + case 2: //B/magenta/light blue slow scroll + setKnob("colorShiftRate","0.1"); + setKnob("colorPaletteWidth","25"); + setKnob("colorPaletteAngle","-100"); + setKnob("colorPalette","#6666FF,#0000FF,#FF00FF,#6666FF"); + setKnob("colorPaletteInterpolate","true"); + break; + case 3: //full color fast scroll down + setKnob("colorShiftRate","1"); + setKnob("colorPaletteWidth","55"); + setKnob("colorPaletteAngle","-90"); + setKnob("colorPalette","#FF0000,#FFFF00,#00FF00,#00FFFF,#0000FF,#FF00FF,#FF0000"); + setKnob("colorPaletteInterpolate","true"); + break; + case 4: //R/W/B fixed + setKnob("colorShiftRate","0"); + setKnob("colorPaletteWidth","9"); + setKnob("colorPaletteAngle","0"); + setKnob("colorPalette","#FF0000,#FFFFFF,#0000FF"); + setKnob("colorPaletteInterpolate","false"); + break; + case 5: //red/blue slow scroll + setKnob("colorShiftRate","0.5"); + setKnob("colorPaletteWidth","90"); + setKnob("colorPaletteAngle","-90"); + setKnob("colorPalette","#FF0000,#0000FF,#FF0000"); + setKnob("colorPaletteInterpolate","true"); + break; + case 6: //R/W/B diagonal stripes scrolling + setKnob("colorShiftRate","0.2"); + setKnob("colorPaletteWidth","20"); + setKnob("colorPaletteAngle","45"); + setKnob("colorPalette","#FFFFFF,#FF0000,#FF0000,#FF0000,#FF0000,#FFFFFF,#0000FF,#0000FF,#0000FF,#0000FF"); + setKnob("colorPaletteInterpolate","false"); + } + } + } + + private int getColor(int x, int y, float scale) + { + scale=Math.min(1f, Math.max(0f, scale)); + float[] ret=new float[3]; + + double colorOffset=getTime()*colorShiftRate*getFramerate(); + double xColorPos=x+0.5+colorOffset*Math.cos(colorPaletteAngle); + double yColorPos=y+0.5+colorOffset*Math.sin(colorPaletteAngle); + double paletteAngle=colorPaletteAngle-Math.atan2(yColorPos, xColorPos); + double colorDist=Math.sqrt(xColorPos*xColorPos+yColorPos*yColorPos); + double coPalettePos=colorDist*Math.cos(paletteAngle); + + double coPalettePosMod=coPalettePos%colorPaletteWidth; + if (coPalettePos<0) + coPalettePosMod=colorPaletteWidth+coPalettePosMod; + + if (!colorPaletteInterpolate) + { + double palettePos=coPalettePosMod*(colorPalette.size())/colorPaletteWidth; + ret=colorPalette.get((int)Math.floor(palettePos)).getColorComponents(new float[3]); + } + else + { + double palettePos=coPalettePosMod*(colorPalette.size()-1)/colorPaletteWidth; + float diff=(float)(palettePos%1); + int colorA=(int)Math.floor(palettePos); + int colorB=(int)Math.ceil(palettePos); + if (colorA<0 || colorB>=colorPalette.size()) + throw new IllegalArgumentException("colorA="+colorA+" colorB="+colorB); + float[] a=colorPalette.get(colorA).getColorComponents(new float[3]); + float[] b=colorPalette.get(colorB).getColorComponents(new float[3]); + a[0]=a[0]*(1-diff)+b[0]*diff; + a[1]=a[1]*(1-diff)+b[1]*diff; + a[2]=a[2]*(1-diff)+b[2]*diff; + ret=a; + } + return new Color(ret[0]*scale,ret[1]*scale,ret[2]*scale).getRGB(); + } +} diff --git a/src/edu/mit/d54/plugins/audio/VUMeterPlugin.java b/src/edu/mit/d54/plugins/audio/VUMeterPlugin.java new file mode 100644 index 0000000..ce5c4df --- /dev/null +++ b/src/edu/mit/d54/plugins/audio/VUMeterPlugin.java @@ -0,0 +1,168 @@ +package edu.mit.d54.plugins.audio; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.util.ArrayList; +import java.util.List; + +import edu.mit.d54.Display2D; +import edu.mit.d54.DisplayPlugin; + +/** + * This plugin displays a multi-band VU meter using the audio input from AudioProcessor. The colors are + * customizable through the knob interface. It was first used for the Independence Day display on July 4, 2012. + */ +public class VUMeterPlugin extends DisplayPlugin { + private AudioProcessor audio; + + public VUMeterPlugin(Display2D display, double framerate) { + super(display, framerate); + registerKnob("amplitudeMaxRatio","30","Ratio between the maximum amplitude and the minimum visible amplitude"); + registerKnob("colorShiftRate","0","Color shift rate in pixels per frame"); + registerKnob("colorPaletteWidth","17","Width of the color palette in pixels"); + registerKnob("colorPaletteAngle","90","Angle of the color palette in degrees"); + registerKnob("colorPalette","#FF0000,#FFFF00,#FFFF00,#00FF00,#00FF00,#00FF00,#00FF00,#00FF00","Comma separated list of colors to represent the palette"); + registerKnob("colorPaletteInterpolate","false","Enable interpolation for the color palette"); + registerKnob("colorPreset","2","Set the color settings to one of the presets"); + audio=new AudioProcessor(display.getWidth()); + } + + @Override + protected void onStart() + { + audio.openChannel(); + } + + @Override + protected void onStop() + { + audio.closeChannel(); + } + + private double amplitudeMaxRatio; + private double colorShiftRate; + private double colorPaletteWidth; + private double colorPaletteAngle; + private List colorPalette; + private boolean colorPaletteInterpolate; + + private float[] fftBinsOld=new float[getDisplay().getWidth()]; + @Override + protected void loop() { + audio.frameUpdate(); + //render + Graphics2D g=getDisplay().getGraphics(); + int w=getDisplay().getWidth(); + int h=getDisplay().getHeight(); + for (int i=0; i=(int)Math.round(h-level*h)) + getDisplay().setPixelRGB(i, j, getColor(i,j,1)); + } + /* if (level>levelOld) + { + g.setColor(Color.red); + g.drawLine(i,(int)(h-levelOld*h),i,(int)(h-level*h)); + g.setColor(Color.green); + g.drawLine(i, h, i, (int)(h-levelOld*h)); + } + else + { + g.setColor(Color.green); + // g.setColor(Color.getHSBColor((float) Math.min(0.4f, 0.4f-(level-levelOld)), 1, 1)); + g.drawLine(i, h, i, (int)(h-level*h)); + }*/ + } + fftBinsOld=audio.getFFTMagBins(); + } + + @Override + protected void knobChanged(String knob, String value) + { + if (knob.equals("amplitudeMaxRatio")) + amplitudeMaxRatio=Double.parseDouble(value); + else if (knob.equals("colorShiftRate")) + colorShiftRate=Double.parseDouble(value); + else if (knob.equals("colorPaletteWidth")) + colorPaletteWidth=Double.parseDouble(value); + else if (knob.equals("colorPaletteAngle")) + colorPaletteAngle=Double.parseDouble(value)*Math.PI/180; + else if (knob.equals("colorPalette")) + { + String[] tok=value.split(","); + colorPalette=new ArrayList(); + for (String t : tok) + { + colorPalette.add(Color.decode(t.replace("#", "0x"))); + } + } + else if (knob.equals("colorPaletteInterpolate")) + colorPaletteInterpolate=Boolean.parseBoolean(getKnobValue("colorPaletteInterpolate")); + else if (knob.equals("colorPreset")) + { + switch (Integer.parseInt(value)) + { + case 1: // R/W/B horiz + setKnob("colorShiftRate","0"); + setKnob("colorPaletteWidth","9"); + setKnob("colorPaletteAngle","0"); + setKnob("colorPalette","#FF0000,#FFFFFF,#0000FF"); + setKnob("colorPaletteInterpolate","false"); + break; + case 2: // R/Y/G vert (traditional VU meter) + setKnob("colorShiftRate","0"); + setKnob("colorPaletteWidth","17"); + setKnob("colorPaletteAngle","90"); + setKnob("colorPalette","#FF0000,#FFFF00,#FFFF00,#00FF00,#00FF00,#00FF00,#00FF00"); + setKnob("colorPaletteInterpolate","false"); + break; + } + } + + + } + + private int getColor(int x, int y, float scale) + { + scale=Math.min(1f, Math.max(0f, scale)); + float[] ret=new float[3]; + + double colorOffset=getTime()*colorShiftRate*getFramerate(); + double xColorPos=x+0.5+colorOffset*Math.cos(colorPaletteAngle); + double yColorPos=y+0.5+colorOffset*Math.sin(colorPaletteAngle); + double paletteAngle=colorPaletteAngle-Math.atan2(yColorPos, xColorPos); + double colorDist=Math.sqrt(xColorPos*xColorPos+yColorPos*yColorPos); + double coPalettePos=colorDist*Math.cos(paletteAngle); + + double coPalettePosMod=coPalettePos%colorPaletteWidth; + if (coPalettePos<0) + coPalettePosMod=colorPaletteWidth+coPalettePosMod; + + if (!colorPaletteInterpolate) + { + double palettePos=coPalettePosMod*(colorPalette.size())/colorPaletteWidth; + ret=colorPalette.get((int)Math.floor(palettePos)).getColorComponents(new float[3]); + } + else + { + double palettePos=coPalettePosMod*(colorPalette.size()-1)/colorPaletteWidth; + float diff=(float)(palettePos%1); + int colorA=(int)Math.floor(palettePos); + int colorB=(int)Math.ceil(palettePos); + if (colorA<0 || colorB>=colorPalette.size()) + throw new IllegalArgumentException("colorA="+colorA+" colorB="+colorB); + float[] a=colorPalette.get(colorA).getColorComponents(new float[3]); + float[] b=colorPalette.get(colorB).getColorComponents(new float[3]); + a[0]=a[0]*(1-diff)+b[0]*diff; + a[1]=a[1]*(1-diff)+b[1]*diff; + a[2]=a[2]*(1-diff)+b[2]*diff; + ret=a; + } + return new Color(ret[0]*scale,ret[1]*scale,ret[2]*scale).getRGB(); + } +}