Created
January 20, 2011 14:47
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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