A real-time spectrum analyzer.
Released under the MIT License
Copyright (c) 2010 Gerald T. Beauregard
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CON
/**
* Copyright j2e ( http://wonderfl.net/user/j2e )
* MIT License ( http://www.opensource.org/licenses/mit-license.php )
* Downloaded from: http://wonderfl.net/c/kV2n
*/
package
{
import __AS3__.vec.Vector;
import flash.display.Sprite;
import flash.events.*;
import flash.media.Microphone;
import flash.text.*;
import flash.utils.*;
/**
* A real-time spectrum analyzer.
*
* Released under the MIT License
*
* Copyright (c) 2010 Gerald T. Beauregard
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
[SWF(width='600', height='400', frameRate='30', backgroundColor='0xFFFFFF')]
public class SpectrumAnalyzer extends Sprite
{
private const SAMPLE_RATE:Number = 22050; // Actual microphone sample rate (Hz)
private const LOGN:uint = 11; // Log2 FFT length
private const N:uint = 1 << LOGN; // FFT Length
private const BUF_LEN:uint = N; // Length of buffer for mic audio
private const UPDATE_PERIOD:int = 50; // Period of spectrum updates (ms)
private var m_fft:FFT; // FFT object
private var m_tempRe:Vector.<Number>; // Temporary buffer - real part
private var m_tempIm:Vector.<Number>; // Temporary buffer - imaginary part
private var m_mag:Vector.<Number>; // Magnitudes (at each of the frequencies below)
private var m_freq:Vector.<Number>; // Frequencies (for each of the magnitudes above)
private var m_win:Vector.<Number>; // Analysis window (Hanning)
private var m_mic:Microphone; // Microphone object
private var m_writePos:uint = 0; // Position to write new audio from mic
private var m_buf:Vector.<Number> = null; // Buffer for mic audio
private var m_tickTextAdded:Boolean = false;
private var m_timer:Timer; // Timer for updating spectrum
/**
*
*/
public function SpectrumAnalyzer()
{
var i:uint;
// Set up the FFT
m_fft = new FFT();
m_fft.init(LOGN);
m_tempRe = new Vector.<Number>(N);
m_tempIm = new Vector.<Number>(N);
m_mag = new Vector.<Number>(N/2);
//m_smoothMag = new Vector.<Number>(N/2);
// Vector with frequencies for each bin number. Used
// in the graphing code (not in the analysis itself).
m_freq = new Vector.<Number>(N/2);
for ( i = 0; i < N/2; i++ )
m_freq[i] = i*SAMPLE_RATE/N;
// Hanning analysis window
m_win = new Vector.<Number>(N);
for ( i = 0; i < N; i++ )
m_win[i] = (4.0/N) * 0.5*(1-Math.cos(2*Math.PI*i/N));
// Create a buffer for the input audio
m_buf = new Vector.<Number>(BUF_LEN);
for ( i = 0; i < BUF_LEN; i++ )
m_buf[i] = 0.0;
// Set up microphone input
m_mic = Microphone.getMicrophone();
m_mic.setLoopBack(true);
m_mic.rate = SAMPLE_RATE/1000;
m_mic.setSilenceLevel(0.0); // Have the mic run non-stop, regardless of the input level
m_mic.addEventListener( SampleDataEvent.SAMPLE_DATA, onMicSampleData );
// Set up a timer to do periodic updates of the spectrum
m_timer = new Timer(UPDATE_PERIOD);
m_timer.addEventListener(TimerEvent.TIMER, updateSpectrum);
m_timer.start();
}
/**
* Called whether new microphone input data is available. See this call
* above:
* m_mic.addEventListener( SampleDataEvent.SAMPLE_DATA, onMicSampleData );
*/
private function onMicSampleData( event:SampleDataEvent ):void
{
// Get number of available input samples
var len:uint = event.data.length/4;
// Read the input data and stuff it into
// the circular buffer
for ( var i:uint = 0; i < len; i++ )
{
m_buf[m_writePos] = event.data.readFloat();
m_writePos = (m_writePos+1)%BUF_LEN;
}
}
/**
* Called at regular intervals to update the spectrum
*/
private function updateSpectrum( event:Event ):void
{
// Copy data from circular microphone audio
// buffer into temporary buffer for FFT, while
// applying Hanning window.
var i:int;
var pos:uint = m_writePos;
for ( i = 0; i < N; i++ )
{
m_tempRe[i] = m_win[i]*m_buf[pos];
pos = (pos+1)%BUF_LEN;
}
// Zero out the imaginary component
for ( i = 0; i < N; i++ )
m_tempIm[i] = 0.0;
// Do FFT and get magnitude spectrum
m_fft.run( m_tempRe, m_tempIm );
for ( i = 0; i < N/2; i++ )
{
var re:Number = m_tempRe[i];
var im:Number = m_tempIm[i];
m_mag[i] = Math.sqrt(re*re + im*im);
}
// Convert to dB magnitude
const SCALE:Number = 20/Math.LN10;
for ( i = 0; i < N/2; i++ )
{
// 20 log10(mag) => 20/ln(10) ln(mag)
// Addition of MIN_VALUE prevents log from returning minus infinity if mag is zero
m_mag[i] = SCALE*Math.log( m_mag[i] + Number.MIN_VALUE );
}
// Draw the graph
drawSpectrum( m_mag, m_freq );
}
/**
* Draw a graph of the spectrum
*/
private function drawSpectrum(
mag:Vector.<Number>,
freq:Vector.<Number> ):void
{
// Basic constants
const MIN_FREQ:Number = 0; // Minimum frequency (Hz) on horizontal axis.
const MAX_FREQ:Number = 4000; // Maximum frequency (Hz) on horizontal axis.
const FREQ_STEP:Number = 500; // Interval between ticks (Hz) on horizontal axis.
const MAX_DB:Number = -0.0; // Maximum dB magnitude on vertical axis.
const MIN_DB:Number = -60.0; // Minimum dB magnitude on vertical axis.
const DB_STEP:Number = 10; // Interval between ticks (dB) on vertical axis.
const TOP:Number = 50; // Top of graph
const LEFT:Number = 60; // Left edge of graph
const HEIGHT:Number = 300; // Height of graph
const WIDTH:Number = 500; // Width of graph
const TICK_LEN:Number = 10; // Length of tick in pixels
const LABEL_X:String = "Frequency (Hz)"; // Label for X axis
const LABEL_Y:String = "dB"; // Label for Y axis
// Derived constants
const BOTTOM:Number = TOP+HEIGHT; // Bottom of graph
const DBTOPIXEL:Number = HEIGHT/(MAX_DB-MIN_DB); // Pixels/tick
const FREQTOPIXEL:Number = WIDTH/(MAX_FREQ-MIN_FREQ);// Pixels/Hz
//-----------------------
var i:uint;
var numPoints:uint;
numPoints = mag.length;
if ( mag.length != freq.length )
trace( "mag.length != freq.length" );
graphics.clear();
// Draw a rectangular box marking the boundaries of the graph
graphics.lineStyle( 1, 0x000000 );
graphics.drawRect( LEFT, TOP, WIDTH, HEIGHT );
graphics.moveTo(LEFT, TOP+HEIGHT);
//--------------------------------------------
// Tick marks on the vertical axis
var y:Number;
var x:Number;
for ( var dBTick:Number = MIN_DB; dBTick <= MAX_DB; dBTick += DB_STEP )
{
y = BOTTOM - DBTOPIXEL*(dBTick-MIN_DB);
graphics.moveTo( LEFT-TICK_LEN/2, y );
graphics.lineTo( LEFT+TICK_LEN/2, y );
if ( m_tickTextAdded == false )
{
// Numbers on the tick marks
var t:TextField = new TextField();
t.text = int(dBTick).toString();
t.width = 0;
t.height = 20;
t.x = LEFT-20;
t.y = y - t.textHeight/2;
t.autoSize = TextFieldAutoSize.CENTER;
addChild(t);
}
}
// Label for vertical axis
if ( m_tickTextAdded == false )
{
t = new TextField();
t.text = LABEL_Y;
t.x = LEFT-50;
t.y = TOP + HEIGHT/2 - t.textHeight/2;
t.height = 20;
t.width = 50;
//t.rotation = -90;
addChild(t);
}
//--------------------------------------------
// Tick marks on the horizontal axis
for ( var f:Number = MIN_FREQ; f <= MAX_FREQ; f += FREQ_STEP )
{
x = LEFT + FREQTOPIXEL*(f-MIN_FREQ);
graphics.moveTo( x, BOTTOM - TICK_LEN/2 );
graphics.lineTo( x, BOTTOM + TICK_LEN/2 );
if ( m_tickTextAdded == false )
{
t = new TextField();
t.text = int(f).toString();
t.width = 0;
t.x = x;
t.y = BOTTOM+7;
t.autoSize = TextFieldAutoSize.CENTER;
addChild(t);
}
}
// Label for horizontal axis
if ( m_tickTextAdded == false )
{
t = new TextField();
t.text = LABEL_X;
t.width = 0;
t.x = LEFT+WIDTH/2;
t.y = BOTTOM+30;
t.autoSize = TextFieldAutoSize.CENTER;
addChild(t);
}
m_tickTextAdded = true;
// -------------------------------------------------
// The line in the graph
// Ignore points that are too far to the left
for ( i = 0; i < numPoints && freq[i] < MIN_FREQ; i++ )
{
}
// For all remaining points within range of x-axis
var firstPoint:Boolean = true;
for ( /**/; i < numPoints && freq[i] <= MAX_FREQ; i++ )
{
// Compute horizontal position
x = LEFT + FREQTOPIXEL*(freq[i]-MIN_FREQ);
// Compute vertical position of point
// and clip at top/bottom.
y = BOTTOM - DBTOPIXEL*(mag[i]-MIN_DB);
if ( y < TOP )
y = TOP;
else if ( y > BOTTOM )
y = BOTTOM;
// If it's the first point
if ( firstPoint )
{
// Move to the point
graphics.moveTo(x,y);
firstPoint = false;
}
else
{
// Otherwise, draw line from the previous point
graphics.lineTo(x,y);
}
}
}
}
}
/**
* Performs an in-place complex FFT.
*
* Released under the MIT License
*
* Copyright (c) 2010 Gerald T. Beauregard
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
class FFT
{
public static const FORWARD:Boolean = false;
public static const INVERSE:Boolean = true;
private var m_logN:uint = 0; // log2 of FFT size
private var m_N:uint = 0; // FFT size
private var m_invN:Number; // Inverse of FFT length
// Bit-reverse lookup table
private var m_bitRevLen:uint; // Length of the bit reverse table
private var m_bitRevFrom:Vector.<uint>; // "From" indices for swap
private var m_bitRevTo:Vector.<uint>; // "To" indices for swap
/**
*
*/
public function FFT()
{
}
/**
* Initialize class to perform FFT of specified size.
*
* @param logN Log2 of FFT length. e.g. for 512 pt FFT, logN = 9.
*/
public function init(
logN:uint ):void
{
m_logN = logN
m_N = 1 << m_logN;
m_invN = 1.0/m_N;
// Create the bit-reverse table
m_bitRevFrom = new Vector.<uint>;
m_bitRevTo = new Vector.<uint>;
for ( var k:uint = 0; k < m_N; k++ )
{
var kRev:uint = BitReverse(k,logN);
if (kRev > k)
{
m_bitRevFrom.push(k);
m_bitRevTo.push(kRev);
}
}
m_bitRevLen = m_bitRevFrom.length;
}
/**
* Performs in-place complex FFT.
*
* @param xRe Real part of input/output
* @param xIm Imaginary part of input/output
* @param inverse If true (INVERSE), do an inverse FFT
*/
public function run(
xRe:Vector.<Number>,
xIm:Vector.<Number>,
inverse:Boolean = false ):void
{
var numFlies:uint = m_N >> 1; // Number of butterflies per sub-FFT
var span:uint = m_N >> 1; // Width of the butterfly
var spacing:uint = m_N; // Distance between start of sub-FFTs
var wIndexStep:uint = 1; // Increment for twiddle table index
// For each stage of the FFT
for ( var stage:uint = 0; stage < m_logN; ++stage )
{
// Compute a multiplier factor for the "twiddle factors".
// The twiddle factors are complex unit vectors spaced at
// regular angular intervals. The angle by which the twiddle
// factor advances depends on the FFT stage. In many FFT
// implementations the twiddle factors are cached, but because
// vector lookup is relatively slow in ActionScript, it's just
// as fast to compute them on the fly.
var wAngleInc:Number = wIndexStep * 2.0*Math.PI/m_N;
if ( inverse )
wAngleInc *= -1;
var wMulRe:Number = Math.cos(wAngleInc);
var wMulIm:Number = Math.sin(wAngleInc);
for ( var start:uint = 0; start < m_N; start += spacing )
{
var top:uint = start;
var bottom:uint = top + span;
var wRe:Number = 1.0;
var wIm:Number = 0.0;
// For each butterfly in this stage
for ( var flyCount:uint = 0; flyCount < numFlies; ++flyCount )
{
// Get the top & bottom values
var xTopRe:Number = xRe[top];
var xTopIm:Number = xIm[top];
var xBotRe:Number = xRe[bottom];
var xBotIm:Number = xIm[bottom];
// Top branch of butterfly has addition
xRe[top] = xTopRe + xBotRe;
xIm[top] = xTopIm + xBotIm;
// Bottom branch of butterly has subtraction,
// followed by multiplication by twiddle factor
xBotRe = xTopRe - xBotRe;
xBotIm = xTopIm - xBotIm;
xRe[bottom] = xBotRe*wRe - xBotIm*wIm;
xIm[bottom] = xBotRe*wIm + xBotIm*wRe;
// Update indices to the top & bottom of the butterfly
++top;
++bottom;
// Update the twiddle factor, via complex multiply
// by unit vector with the appropriate angle
// (wRe + j wIm) = (wRe + j wIm) x (wMulRe + j wMulIm)
var tRe:Number = wRe;
wRe = wRe*wMulRe - wIm*wMulIm;
wIm = tRe*wMulIm + wIm*wMulRe
}
}
numFlies >>= 1; // Divide by 2 by right shift
span >>= 1;
spacing >>= 1;
wIndexStep <<= 1; // Multiply by 2 by left shift
}
// The algorithm leaves the result in a scrambled order.
// Do bit-reversal to unscramble the result.
for ( var k:uint = 0; k < m_bitRevLen; k++ )
{
var brFr:uint = m_bitRevFrom[k];
var brTo:uint = m_bitRevTo[k];
var tempRe:Number = xRe[brTo];
var tempIm:Number = xIm[brTo];
xRe[brTo] = xRe[brFr];
xIm[brTo] = xIm[brFr];
xRe[brFr] = tempRe;
xIm[brFr] = tempIm;
}
// Divide everything by n for inverse
if ( inverse )
{
for ( k = 0; k < m_N; k++ )
{
xRe[k] *= m_invN;
xIm[k] *= m_invN;
}
}
}
/**
* Do bit reversal of specified number of places of an int
* For example, 1101 bit-reversed is 1011
*
* @param x Number to be bit-reverse.
* @param numBits Number of bits in the number.
*/
private function BitReverse(
x:uint,
numBits:uint):uint
{
var y:uint = 0;
for ( var i:uint = 0; i < numBits; i++)
{
y <<= 1;
y |= x & 0x0001;
x >>= 1;
}
return y;
}
}