Skip to content

Instantly share code, notes, and snippets.

@jeremyfromearth
Created January 20, 2011 14:47
Show Gist options
  • Save jeremyfromearth/787994 to your computer and use it in GitHub Desktop.
Save jeremyfromearth/787994 to your computer and use it in GitHub Desktop.
Allows audio effects parameters to be controlled by linearly graphed values through a node based user interface.
package makemachine.examples.audio
{
import com.bit101.components.*;
import flash.display.*;
import flash.events.*;
import flash.geom.Point;
import flash.media.*;
import flash.net.URLRequest;
import flash.utils.ByteArray;
/**
* Allows audio effects parameters to be controlled by linearly graphed values through a
* node based user interface.
* For more details: http://labs.makemachine.net/2011/01/parameter-graph/
*/
[SWF( backgroundColor="0x222222", frameRate="30", width="610", height="140" )]
public class ParameterGraph extends Sprite
{
// -- constants
public static const WIDTH:int = 600;
public static const HEIGHT:int = 100;
public static const PADDING:int = 5;
/// -- dragging
protected var minX:int;
protected var maxX:int;
// -- nodes
protected var nodes:Vector.<Sprite>;
protected var selectedNode:Sprite;
protected var first:Sprite;
protected var last:Sprite;
protected var values:Vector.<Number>;
protected var resonanceNodes:Vector.<Sprite>;
protected var resonanceValues:Vector.<Number>
protected var firstRes:Sprite;
protected var lastRes:Sprite;
protected var cutoffNodes:Vector.<Sprite>
protected var cutoffValues:Vector.<Number>;
protected var firstCut:Sprite;
protected var lastCut:Sprite;
protected var distNodes:Vector.<Sprite>
protected var distValues:Vector.<Number>;
protected var firstDist:Sprite;
protected var lastDist:Sprite;
protected var volumeNodes:Vector.<Sprite>
protected var volumeValues:Vector.<Number>;
protected var firstVol:Sprite;
protected var lastVol:Sprite;
// -- ui
protected var label:Label;
protected var progressBar:ProgressBar;
protected var playButton:PushButton;
protected var playhead:Sprite;
protected var graph:Sprite;
protected var waveform:Sprite;
// -- sound
protected var input:Sound;
protected var output:Sound;
protected var length:int;
protected var playing:Boolean;
protected var channel:SoundChannel;
protected var mp3Url:String;
// -- filter
protected var filterAmp:Number;
protected var filterOutput:Number;
protected var filterCutoff:Number;
protected var filterResonance:Number;
// -- distortion
protected var distortion:Number;
public function ParameterGraph()
{
label = new Label( this, 240, 60, 'Loading...' );
progressBar = new ProgressBar( this, 240, 80 );
input = new Sound();
output = new Sound();
addEventListener( Event.ENTER_FRAME, validate );
}
// -- once the stage is valid, start loading the mp3
protected function validate( event:Event ):void
{
if( !stage ) return;
if( !stage.stageWidth ) return;
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;
if( stage.loaderInfo.parameters[ 'mp3' ] )
{
mp3Url = stage.loaderInfo.parameters[ 'mp3' ];
} else {
mp3Url = 'flying_lotus_kill_your_coworkers.mp3';
}
input.addEventListener( Event.COMPLETE, onSoundLoadComplete );
input.load( new URLRequest( mp3Url ) );
input.addEventListener( ProgressEvent.PROGRESS, onLoadProgress );
removeEventListener( Event.ENTER_FRAME, validate );
}
// ----------------------------------------------
//
// -- load events
//
// ----------------------------------------------
// -- once the mp3 is loaded, initialize the app
protected function onSoundLoadComplete( event:Event ):void
{
createUI();
initData();
renderAudio();
onCutoff();
drawGraph();
}
// -- update the loading progress bar
protected function onLoadProgress( event:ProgressEvent ):void
{
progressBar.value = event.bytesLoaded / event.bytesTotal;
}
// ----------------------------------------------
//
// -- methods
//
// ----------------------------------------------
// -- initializes models/value vectors
// -- each parameter is represented by a list of values
// -- each list is as long as the graph is wide
protected function initData():void
{
filterAmp =
filterOutput = 0;
filterCutoff = .5;
filterResonance = .5;
distortion = 0;
cutoffValues = generateValueVector( WIDTH, .5 );
resonanceValues = generateValueVector( WIDTH, .5 );
distValues = generateValueVector( WIDTH, .5 );
volumeValues = generateValueVector( WIDTH, .5 );
}
// -- clears the graph and repopulates with
protected function swapGraphs():void
{
while( graph.numChildren > 0 )
{
graph.removeChildAt( 0 );
}
for( var i:int = 0; i < nodes.length; i++ )
{
graph.addChild( nodes[i] );
}
drawGraph();
}
// ----------------------------------------------
//
// -- sound
//
// ----------------------------------------------
// -- toggles between play and stop
protected function toggle( event:Event = null ):void
{
if( playing ) {
stop();
}else {
play();
}
}
// -- stops audio playback and updates ui
protected function stop():void
{
if( playing )
{
playButton.label = 'Play';
playButton.selected = false;
playing = false;
if( channel )
{
bytes.position = 0;
channel.stop();
removeEventListener( Event.ENTER_FRAME, onPlayback );
output.removeEventListener( SampleDataEvent.SAMPLE_DATA, onSampleData );
}
}
}
// -- starts audio playback and updates ui
protected function play():void
{
if( !playing )
{
playButton.label = 'Stop';
playButton.selected = true;
playing = true;
output.addEventListener( SampleDataEvent.SAMPLE_DATA, onSampleData );
addEventListener( Event.ENTER_FRAME, onPlayback );
channel = output.play();
}
}
// -- fills a buffer with audio data
protected function onSampleData( event:SampleDataEvent ):void
{
var distortion:Number
var sample:Number;
var mappedIndex:int;
var resonance:Number;
var cutoff:Number;
var amplitude:Number;
for( var i:int = 0; i < 4096; i++ )
{
// -- map the index of the byte array position to an index within the width of the graph
// -- this gives us the index of the value vectors for each param
mappedIndex = map( bytes.position, 0, bytes.length, 0, WIDTH - 1 );
sample = bytes.readFloat();
// -- apply distortion - needs optimization...
distortion = 1 - distValues[ mappedIndex ];
if( sample < distortion ) sample = sample;
if( sample > distortion ) sample = distortion + ( sample - distortion ) / ( ( ( sample - distortion ) / ( 1 - distortion ) ) ^ 2048 );
if( sample > 1 ) sample = (distortion+1) * .5;
// -- apply filter - cheap filtering method
resonance = resonanceValues[ mappedIndex ];
cutoff = cutoffValues[ mappedIndex ];
filterAmp *= Math.min( resonance, .99 );
filterAmp += ( sample - filterOutput ) * cutoff;
filterOutput += filterAmp;
sample += filterOutput;
// -- attenuate the overall volume
amplitude = volumeValues[ mappedIndex ]
event.data.writeFloat( sample * amplitude );
if( bytes.position >= bytes.length )
{
bytes.position = 0;
}
}
}
// -- update the playhead
protected function onPlayback( event:Event ):void
{
playhead.x = PADDING + ( ( channel.position * 44.1 ) % length / length ) * WIDTH;
}
// ----------------------------------------------
//
// -- mouse events
//
// ----------------------------------------------
// -- when the graph is clicked
protected function onMouseDown( event:MouseEvent ):void
{
var node:Sprite
var index:int = nodes.indexOf( event.target );
// -- if an existing node is clicked
if( index > -1 )
{
node = nodes[ index ];
if( node == first )
{
minX = 0;
maxX = 0;
}
if( node == last )
{
minX = WIDTH;
maxX = WIDTH;
}
if( index > 0 && index < nodes.length -1 )
{
minX = nodes[index -1].x;
maxX = nodes[index + 1].x;
}
// -- create a new node
} else {
var insertIndex:int;
var minDistance:Number;
node = new Sprite();
node.x = graph.mouseX;
node.y = graph.mouseY;
node.mouseEnabled = true;
node.buttonMode = true;
node.useHandCursor = true;
var n:Sprite;
var distance:Number;
// -- finds the closest node to the left to determine where to insert a new new node
for( var i:int = 0; i < nodes.length; i++ )
{
n = nodes[i];
distance = node.x - n.x;
if( isNaN( minDistance ) )
{
insertIndex = i + 1;
minDistance = node.x - n.x;
} else {
if( distance <= minDistance && distance >= 0 )
{
insertIndex = nodes.indexOf( n ) + 1;
minDistance = node.x - n.x;
}
}
}
graph.addChild( node );
nodes.splice( insertIndex, 0, node );
minX = nodes[ insertIndex - 1].x;
maxX = nodes[ insertIndex + 1].x;
}
selectedNode = node;
addEventListener( Event.ENTER_FRAME, onNodeDrag );
stage.addEventListener(MouseEvent.MOUSE_UP, onStageMouseUp );
}
// -- updates the visuals as well as the values of the current values vector
protected function onNodeDrag( event:Event = null ):void
{
drawGraph();
updateValues();
}
// -- stop redrawing
protected function onStageMouseUp( event:Event ):void
{
removeEventListener( Event.ENTER_FRAME, onNodeDrag );
stage.removeEventListener( MouseEvent.MOUSE_UP, onStageMouseUp );
}
// ----------------------------------------------
//
// -- key events
//
// ----------------------------------------------
// -- catch key events, toggle playback on space bar, delete selected node on delete key
protected function onKeyDown( event:KeyboardEvent ):void
{
switch( event.keyCode )
{
case 8: // -- delete
deleteSelectedNode();
break;
case 32: // -- space bar
toggle();
break;
}
}
// ----------------------------------------------
//
// -- ui events
//
// ----------------------------------------------
// -- when the cutoff option is selected
protected function onCutoff( event:Event = null ):void
{
nodes = cutoffNodes;
values = cutoffValues;
first = firstCut;
last = lastCut;
selectedNode = null;
swapGraphs();
}
// -- when the resonance option is selected
protected function onResonance( event:Event = null ):void
{
nodes = resonanceNodes;
values = resonanceValues;
first = firstRes;
last = lastRes;
selectedNode = null;
swapGraphs();
}
// -- when the distortion option is selected
protected function onDistortion( event:Event = null ):void
{
nodes = distNodes;
values = distValues;
first = firstDist;
last = lastDist;
selectedNode = null;
swapGraphs();
}
// -- when the volume option is selected
protected function onVolume( event:Event = null ):void
{
nodes = volumeNodes;
values = volumeValues;
first = firstVol;
last = lastVol;
selectedNode = null;
swapGraphs();
}
// -- deletes the selected node from the list and resets the values
protected function deleteSelectedNode( event:Event = null ):void
{
if( selectedNode && selectedNode != first && selectedNode != last )
{
var index:int = nodes.indexOf( selectedNode );
selectedNode.y = HEIGHT * .5;
updateValues();
nodes.splice( index, 1 );
if( selectedNode.parent ) selectedNode.parent.removeChild( selectedNode );
selectedNode = null;
drawGraph();
}
}
// -- resets the selected graph values, kind of hackish... it's a prototype
protected function onClear( event:Event = null ):void
{
selectedNode = null;
if( nodes == cutoffNodes )
{
cutoffNodes = Vector.<Sprite>( [ firstCut, lastCut ] );
cutoffValues = generateValueVector( WIDTH, .5 );
onCutoff();
}
if( nodes == resonanceNodes )
{
resonanceNodes = Vector.<Sprite>( [ firstRes, lastRes ] );
resonanceValues = generateValueVector( WIDTH, .5 );
onResonance();
}
if( nodes == distNodes )
{
distNodes = Vector.<Sprite>( [ firstDist, lastDist ] );
distValues = generateValueVector( WIDTH, .5 );
onDistortion();
}
if( nodes == volumeNodes )
{
volumeNodes = Vector.<Sprite>( [ firstVol, lastVol ] );
volumeValues = generateValueVector( WIDTH, .5 );
onVolume();
}
while( graph.numChildren > 0 )
{
graph.removeChildAt( 0 );
}
first.y = last.y = HEIGHT * .5;
graph.addChild( first );
graph.addChild( last );
drawGraph();
}
// ----------------------------------------------
//
// -- rendering
//
// ----------------------------------------------
protected var bytes:ByteArray = new ByteArray();
protected function renderAudio():void
{
length = input.length * 44.1;
input.extract( bytes, length, 0 );
var inc:Number = WIDTH / ( length / 200 );
var n:Number = 0;
bytes.position = 0;
var i:int = 0;
var xpos:Number = 0;
var g:Graphics = waveform.graphics;
g.lineStyle( 1, 0xFFFFFF, .1 );
g.moveTo( 0, HEIGHT * .5 );
while( bytes.position < bytes.length )
{
n = ( bytes.readFloat() + bytes.readFloat() ) * .5;
if( i % 200 == 0 )
{
g.lineTo( xpos, ( HEIGHT * .5 ) + HEIGHT * .5 * n );
xpos += inc;
}
i++;
}
bytes.position = 0;
}
// -- updates arrays of normalized values
protected function updateValues():void
{
var i:int;
var dist:int;
var prev:Sprite;
var next:Sprite;
var inc:Number;
var startIndex:int;
var endIndex:int;
var value:Number = 0;
// -- only update the values between the first and the next node
if( selectedNode == first )
{
next = nodes[1];
dist = next.x - first.x;
inc = ( first.y - next.y ) / dist;
startIndex = Math.round( first.x );
value = HEIGHT - first.y;
for( i = 0; i < dist; i++ )
{
value += inc;
values[i+startIndex] = normalize( value, 0, HEIGHT );
}
return;
}
// -- only update the values between the last and second to last node
if( selectedNode == last )
{
prev = nodes[ nodes.length - 2 ];
dist = last.x - prev.x;
inc = ( prev.y - last.y ) / dist;
startIndex = Math.round( prev.x );
value = HEIGHT - prev.y;
for( i = 0; i < dist; i++ )
{
value += inc;
values[ i + startIndex ] = normalize( value, 0, HEIGHT );
}
return;
}
// -- update the values between the selected node, the one before it and the one after it
var selectedIndex:int = nodes.indexOf( selectedNode );
// -- calculate values between selected and previous node
prev = nodes[ selectedIndex - 1 ];
dist = selectedNode.x - prev.x;
inc = ( prev.y - selectedNode.y ) / dist;
startIndex = Math.round( prev.x );
value = HEIGHT - prev.y;
for( i = 0; i < dist; i++ )
{
value += inc;
values[ i + startIndex ] = normalize( value, 0, HEIGHT );
}
// -- calculate values between selected and next node
next = nodes[ selectedIndex + 1 ];
dist = next.x - selectedNode.x;
inc = ( selectedNode.y - next.y ) / dist;
startIndex = Math.round( selectedNode.x );
value = HEIGHT - selectedNode.y;
for( i = 0; i < dist; i++ )
{
value += inc;
values[ i + startIndex ] = normalize( value, 0, HEIGHT );
}
}
protected function generateValueVector( length:int, defaultValue:Number = .5 ):Vector.<Number>
{
var i:int =0;
var v:Vector.<Number> = new Vector.<Number>( length, true );
v.fixed = true;
for( i = 0; i < length; i++ )
{
v[i] = defaultValue;
}
return v;
}
protected function drawGraph():void
{
var line:Graphics = graph.graphics;
var node:Sprite = nodes[0];
var shape:Graphics = node.graphics;
drawRect( line, 0, 0, WIDTH, HEIGHT );
line.lineStyle( 1, 0x555555 );
line.moveTo( 0, HEIGHT * .5 );
line.lineTo( WIDTH, HEIGHT * .5 );
line.beginFill( 0xFFFFFF, .2 );
line.lineStyle( 2, 0xFF0655, 1, true, LineScaleMode.NONE, CapsStyle.SQUARE, JointStyle.MITER, 2 );
line.moveTo( node.x, node.y );
if( selectedNode )
{
selectedNode.y = constrain( graph.mouseY, 0, HEIGHT );
selectedNode.x = constrain( graph.mouseX, minX, maxX );
}
for( var i:int = 0; i < nodes.length; i++ )
{
node = nodes[i];
shape = node.graphics;
line.lineTo( node.x, node.y );
shape.clear();
shape.lineStyle( 0, 0, 0 );
shape.beginFill( node == selectedNode ? 0xF7FF0F : 0x00c6ff )
shape.drawCircle( 0, 0, 4 );
shape.endFill();
shape.beginFill( 0xFFFFFF )
shape.drawCircle( 0, 0, 2 );
shape.endFill();
}
line.lineStyle( 0, 0, 0 )
line.lineStyle( 0, 0, 0 );
line.lineTo( WIDTH, HEIGHT );
line.lineTo( 0, HEIGHT );
line.lineTo( 0, nodes[0].y );
line.endFill();
}
protected function drawRect( g:Graphics, xpos:int, ypos:int, w:int, h:int, color:uint = 0xFFFFFF, a:Number = .1 ):void
{
g.clear();
g.beginFill( color, a );
g.drawRect( xpos, ypos, w, h );
g.endFill();
}
// ----------------------------------------------
//
// -- ui
//
// ----------------------------------------------
protected function createUI():void
{
removeChild( label );
removeChild( progressBar );
waveform = new Sprite();
waveform.x = waveform.y = PADDING;
waveform.mouseEnabled = waveform.mouseChildren = false;
playhead = new Sprite();
playhead.x = playhead.y = PADDING;
playhead.mouseEnabled = playhead.mouseChildren = false;
drawRect( playhead.graphics, 0, 0, 2, HEIGHT, 0x00c6FF, .5 );
graph = new Sprite();
graph.x = graph.y = PADDING;
// -- creates the start and end nodes
firstCut = new Sprite();
lastCut = new Sprite();
firstRes = new Sprite();
lastRes = new Sprite();
firstDist = new Sprite();
lastDist = new Sprite();
firstVol = new Sprite();
lastVol = new Sprite();
// -- used to reference current start and end
first = firstCut;
last = lastCut;
firstCut.buttonMode =
lastCut.buttonMode =
firstRes.buttonMode =
lastRes.buttonMode =
firstDist.buttonMode =
lastDist.buttonMode =
firstVol.buttonMode =
lastVol.buttonMode = true;
firstRes.y = lastRes.y =
first.y = last.y =
firstDist.y = lastDist.y =
firstVol.y = lastVol.y = HEIGHT * .5;
first.x = 0;
last.x =
lastRes.x =
lastDist.x =
lastVol.x = WIDTH;
nodes = Vector.<Sprite>( [first, last] );
cutoffNodes = Vector.<Sprite>( [ firstCut, lastCut ] );
resonanceNodes = Vector.<Sprite>( [ firstRes, lastRes ] );
distNodes = Vector.<Sprite>( [ firstDist, lastDist ] );
volumeNodes = Vector.<Sprite>( [ firstVol, lastVol ] );
var hbox:HBox = new HBox( this, PADDING, 120 );
new Label( hbox, 0, -4, 'Parameter:' );
new RadioButton( hbox, 0, 0, 'Cutoff', true, onCutoff )
new RadioButton( hbox, 0, 0, 'Resonance', false, onResonance );
new RadioButton( hbox, 0, 0, 'Distortion', false, onDistortion );
new RadioButton( hbox, 0, 0, 'Volume', false, onVolume );
new PushButton( hbox, 0, -10, 'Clear', onClear );
new PushButton( hbox, 0, -10, 'Delete Selected', deleteSelectedNode );
playButton = new PushButton( hbox, 0, -10, 'Play', toggle );
playButton.toggle = true;
addChild( waveform );
addChild( playhead );
addChild( graph );
graph.addChild( first );
graph.addChild( last );
graph.addEventListener( MouseEvent.MOUSE_DOWN, onMouseDown );
stage.addEventListener( KeyboardEvent.KEY_DOWN, onKeyDown );
}
// ----------------------------------------------
//
// -- utils
//
// ----------------------------------------------
protected function constrain( value:Number, min:Number, max:Number ):Number
{
return Math.min( Math.max( value, min ), max );
}
public function map( value:Number, min1:Number, max1:Number, min2:Number, max2:Number ):Number
{
return interpolate( normalize( value, min1, max1 ), min2, max2 );
}
public function interpolate( n:Number, min:Number, max:Number ):Number
{
return min + ( max - min ) * n;
}
public function normalize( value:Number, min:Number, max:Number ):Number
{
return ( value - min ) / ( max - min );
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment