In case Flash no longer exists; a copy of this site is included in the Flashpoint archive's "ultimate" collection.

Dead Code Preservation :: Archived AS3 works from wonderfl.net

Effects Parameter Graph

Allows audio effects parameters to be controlled by linearly graphed values through a node based interface.

Click anywhere in waveform to create new nodes
Once created nodes can be dragged to new locations
Press delete key to remove selected node
Pres space bar to toggle playback
Filter is most effective with Resonance parameter set near max
Get Adobe Flash player
by makemachine 25 Jan 2011
/**
 * Copyright makemachine ( http://wonderfl.net/user/makemachine )
 * MIT License ( http://www.opensource.org/licenses/mit-license.php )
 * Downloaded from: http://wonderfl.net/c/vmMG
 */

package
{
    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 = 455;
        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 = 'http://labs.makemachine.net/wp-content/uploads/2011/01/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 vbox:VBox = new VBox( this, PADDING, 110 );
            vbox.spacing = 10;
            var hbox:HBox = new HBox( vbox );
            
            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 );
            hbox.draw();
            
            hbox = new HBox( vbox );
            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 );
        }
    }
}