package {

    import __AS3__.vec.Vector;
    
    import fl.controls.Slider;
    import fl.events.SliderEvent;
    
    import flash.display.Sprite;
    import flash.events.Event;
    import flash.events.SampleDataEvent;
    import flash.external.ExternalInterface;
    import flash.media.Sound;
    import flash.media.SoundChannel;
    import flash.net.URLRequest;
    import flash.utils.ByteArray;

    [SWF(width="640",height="360")]
    public class Granular extends Sprite
    {
        // the sound output object
        private var _output:Sound;
        private var _outputChannel:SoundChannel;
        
        [Embed(source="lynn.mp3")]
        private var SAMPLE_MP3:Class;
        
        // the mp3 sample
        private var _sound:Sound;
        // the loaded sample data
        private var _sampleData:ByteArray;
        private var _numBytes:int;
        
        // keeping track of where we are in the bytearray
        private var _bytePosition:int;
        
        // interface elements
        private var _speedSlider:Slider;
        private var _grainSizeSlider:Slider;
        private var _grainIntervalSlider:Slider;
        
        // a sine-squared amplitude envelope each grain will follow
        private var _envelope:ByteArray;

        // how quickly to move through the sample, creating new grains
        private var _speed:Number = 1;

        // how many samples long each grain is
        private var _grainSize:int;
        // how fast to move through the grain envelope (always 44100/_grainSize)
        private var _grainEnvelopeSpeed:int;

        // how often (in samples) to kick off a new grain
        private var _grainInterval:int = 1000;
        // where are we in the grain kicking off cycle
        private var _grainIntervalCounter:int = 0;
        
        // an instance pool for grain descriptions
        private var _grainPool:GrainPool;
        
        // the grains currently in use
        private var _activeGrains:Vector.<GrainDescription>;
        private var _numActiveGrains:int = 0;

        // iterators for listing and processing grains
        private var _grainCounter:int;
        private var _tempGrain:GrainDescription;
        
        public function Granular()
        {
            _speedSlider = new LabeledSlider("speed", -1, 2, 1, .01, true);
            _speedSlider.addEventListener(SliderEvent.CHANGE, _speedSliderMoved);
            _speedSlider.width = 540;
            _speedSlider.x = 50;
            _speedSlider.y = 50;
            addChild(_speedSlider);
            
            _grainSizeSlider = new LabeledSlider("grain length", 1, 5000, 1000, 1, true);
            _grainSizeSlider.addEventListener(SliderEvent.CHANGE, _grainSizeSliderMoved);
            _grainSizeSlider.width = 540;
            _grainSizeSlider.x = 50;
            _grainSizeSlider.y = 100;
            addChild(_grainSizeSlider);
            
            _grainIntervalSlider = new LabeledSlider("start interval", 1, 5000, 1000, 1, true);
            _grainIntervalSlider.addEventListener(SliderEvent.CHANGE, _grainIntervalSliderMoved);
            _grainIntervalSlider.width = 540;
            _grainIntervalSlider.x = 50;
            _grainIntervalSlider.y = 150;
            addChild(_grainIntervalSlider);
            
            _speedSlider.value = 1;
            _speed = 1;
            _grainSizeSlider.value = 1900;
            _grainSize = 1900;
            _grainEnvelopeSpeed = 44100 / _grainSize;
            _grainIntervalSlider.value = 800;
            _grainInterval = 800;
            
            
            // initialize variables
            _activeGrains = new Vector.<GrainDescription>();
            _grainPool = new GrainPool();


            _createSineSquaredEnvelope();
            _drawSignal(_envelope, 50, 320, 50, 540, 0x000066);

            var ur:URLRequest = new URLRequest("lynn.mp3");
            _sound = new SAMPLE_MP3();

            // extract the mp3 sound to a bytearray
            _sampleData = new ByteArray();
            _sound.extract(_sampleData, _sound.length*44100, 0);
            _numBytes = _sampleData.length;
            
            // store the phase as a Number for fractional speeds
            _floatLength = _sampleData.length/8;

            // display the sound visually
            _drawSignal(_sampleData, 50, 220, 50, 540, 0x006600);

            // initialize the position variables
            _floatPosition = 0;
            _bytePosition = 0;

            // start the output and listen for SampleDataEvent
            _output = new Sound();
            _output.addEventListener(SampleDataEvent.SAMPLE_DATA, _fillBuffer);
            
            ExternalInterface.addCallback("start", start);
            ExternalInterface.addCallback("stop", stop);
        }
        
        
        
        public function start():void {
            _outputChannel = _output.play(0);
        }
        public function stop():void {
            _outputChannel.stop();
        }
        
        
        // accumulators for the sample values
        private var _l:Number;
        private var _r:Number;

        // position within the current buffer
        private var _sample:int;


        // holder for grain envelope value
        private var _amplitude:Number;

        // holder for updating the current position of each grain
        private var _grainSamplePosition:int;
        
        // position and sample length as Number of samples
        private var _floatPosition:Number;
        private var _floatLength:Number;


        // a sound bytearray is ready to be prepared for output - fill it with audio data
        private function _fillBuffer(event:SampleDataEvent):void {
            
            // put some large power-of-two number of samples into the buffer
            for (_sample=0; _sample<2048; ++_sample) {
                
                // find the position in terms of left/right pairs of 4-byte floats
                _bytePosition = Math.floor(_floatPosition)*8;

                // add grains every time "_grainInterval" samples go by
                _grainIntervalCounter++;
                if (_grainIntervalCounter >= _grainInterval) {
                    _grainIntervalCounter = 0;
                    
                    // get a pooled GrainDescription instance - don't instantiate new objects
                    _tempGrain = _grainPool.getGrain();
                    
                    // start this grain at the current position
                    _tempGrain.samplePositionTimes8 = _bytePosition;
                    
                    // start this grain's envelope at the beginning
                    _tempGrain.envelopePosition = 0;
                    
                    // add to active grains
                    _activeGrains.push(_tempGrain)
                    _numActiveGrains++;
                }
                
                // reset sample values
                _l = 0;
                _r = 0;
                
                // iterate through active grains, adding each's contribution
                for (_grainCounter=0; _grainCounter<_numActiveGrains; ++_grainCounter) {
                    
                    _tempGrain = _activeGrains[_grainCounter];

                    // increment grain envelope and check for grain complete
                    _tempGrain.envelopePosition += _grainEnvelopeSpeed;
                    if (_tempGrain.envelopePosition >= 44100) {

                        // remove this grain
                        _activeGrains.splice(_grainCounter, 1);
                        _numActiveGrains--;

                        // keep iterating through active grains at this index - the new position of the next grain
                        _grainCounter--;

                        // return the instance so garbage collection never runs
                        _grainPool.returnGrain(_tempGrain);
                    }
                    else {
                        // the envelope is a stream of 4-byte floats
                        _envelope.position = _tempGrain.envelopePosition * 4;
                        _amplitude = _envelope.readFloat();

                        // update current position of this grain in the sample
                        _grainSamplePosition = _tempGrain.samplePositionTimes8;
                        // samples are left/right pairs of 4-byte floats
                        _grainSamplePosition += 8;
                        // loop position when necessary
                        if (_grainSamplePosition >= _numBytes) {
                            _grainSamplePosition -= _numBytes;
                        }
                        _tempGrain.samplePositionTimes8 = _grainSamplePosition;
                        
                        // move to the updated sample position
                        _sampleData.position = _grainSamplePosition;
                        
                        // read and scale left/right values from the sample at this grain's position
                        _l += _amplitude * _sampleData.readFloat();
                        _r += _amplitude * _sampleData.readFloat();
                    }
                }
                
                // write to the output buffer
                event.data.writeFloat(_l);
                event.data.writeFloat(_r);
                
                // update the position and loop when necessary
                _floatPosition += _speed;
                if (_floatPosition >= _floatLength) {
                    _floatPosition -= _floatLength;
                }
                // this loops when the speed is negative
                else if (_floatPosition < 0) {
                    _floatPosition += _floatLength;
                }

            }
        
        }
        
        
        private function _speedSliderMoved(event:SliderEvent):void {
            _speed = _speedSlider.value;
        }
        private function _grainSizeSliderMoved(event:SliderEvent):void {
            _grainSize = _grainSizeSlider.value;
            _grainEnvelopeSpeed = 44100 / _grainSize;
        }
        private function _grainIntervalSliderMoved(event:SliderEvent):void {
            _grainInterval = _grainIntervalSlider.value;
        }

        // sinus squared from zero to PI fades in and out nicely and reaches full amplitude in the middle
        private function _createSineSquaredEnvelope():void {
            _envelope = new ByteArray();
            var delta:Number = Math.PI / Number(44100);
            for (var i:Number=0; i<44100; ++i) {
                var val:Number = Math.sin(i*delta) * Math.sin(i*delta);
                _envelope.writeFloat(val);
            }
        }
        

        //make a graph of the bytearray data
        private function _drawSignal(bytes:ByteArray, _x:Number, _y:Number, vscale:Number, _width:Number, color:uint):void {
            graphics.lineStyle(0, color);
            var cnt:int = bytes.length/4;
            var dx:Number = _width/(bytes.length/4);
            bytes.position = 0;
            graphics.moveTo(_x, _y);
            for (var i:Number=0; i<cnt; ++i) {
                graphics.lineTo(_x + i*dx, _y - vscale*bytes.readFloat());
            }
        }
            
    }
}