Search code examples
actionscript-3audiostreambuffervoip

Actionscript Double Buffer Audio playback by upscaling


I've been working on a way to stream mic data to a server, cycle back to clients, and play back in a packet by packet manner. So far, I have the client connectivity, intercommunication, voice sending, voice receiving, buffer storage, and a broken playback. The voice coming back plays at the proper speed without scratchy noise, but it's only ever playing a % of the voice buffer, recycling, and playing the new first %. I need the client to only play sound data it retreives once (asside from resampling for proper audio speeds) and then never again.

package Voip
{
    import flash.events.SampleDataEvent;
    import flash.events.TimerEvent;
    import flash.media.Sound;
    import flash.system.System;
    import flash.utils.ByteArray;
    import flash.utils.Timer;

    public class SoundObj
    {
        private var ID:int;
        public var sound:Sound;
        public var buf:ByteArray;
            public var _vbuf:ByteArray;

        public var _numSamples:int;
        public var _phase:Number = 0;

        public var killtimer:Timer = null;
        public var _delaytimer:Timer = new Timer(1000, 1);
        public function SoundObj(id:int)
        {
            ID = id;
            buf = new ByteArray();
            _vbuf = new ByteArray();

            sound = new Sound();
            sound.addEventListener(SampleDataEvent.SAMPLE_DATA, SoundBuffer, false, 0, true);
            sound.play();
        }
        public function receive(bytes:ByteArray):void {
            var i:int = _vbuf.position;
            _vbuf.position = _vbuf.length;
            _vbuf.writeBytes(bytes);
            _vbuf.position = i;

            _numSamples = _vbuf.length/4;
            /*var i:int = buf.position;
            buf.position = buf.length; // write to end
            buf.writeBytes(bytes);
            buf.position = i; // return to origin

            if (_delaytimer == null) {
                _delaytimer = new Timer(1000, 1);
                _delaytimer.addEventListener(TimerEvent.TIMER, finaldata);
                _delaytimer.start();
            }
            if (!_delaytimer.running) {
                // timer not running, dump buffer and reset.
                //var index:int = _vbuf.position;
                //_vbuf.position = _vbuf.length;
                //_vbuf.writeBytes(buf);
                _vbuf = buf;
                _vbuf.position = 0;
                buf = new ByteArray();
                //_vbuf.position = index;

                //sound.extract(_vbuf, int(_vbuf.length * 44.1));
                _phase = 0;
                _numSamples = _vbuf.length/4;

                // reset killtimer to silence timeout
                killtimer = new Timer(1000, 1);
                killtimer.addEventListener(TimerEvent.TIMER, killtimerEvent);
                killtimer.start();
            }*/
        }
        public function killtimerEvent(event:TimerEvent):void {
            _delaytimer = null;
        }
        // send remaining data
        public function finaldata(event:TimerEvent):void {
            if (buf.length > 0) {
                trace("adding final content");
                //var _buf:ByteArray = new ByteArray();
                //var index:int = int(_phase)*4;
                //if (index >= _vbuf.length)
                //  index = _vbuf.position;
                /*_buf.writeBytes(_vbuf, index, _vbuf.length-index);
                _buf.writeBytes(buf);
                buf = new ByteArray();*/

                //_vbuf = _buf;
                // add remaining buffer to playback
                var index:int = _vbuf.position;
                _vbuf.position = _vbuf.length;
                _vbuf.writeBytes(buf);
                _vbuf.position = index;
                // wipe buffer
                buf = new ByteArray();

                //sound.extract(_vbuf, int(_vbuf.length * 44.1));
                _phase = 0;
                //_numSamples = _vbuf.length/4;
                _numSamples = _vbuf.length/4;

                // reset killtimer to silence timeout
                killtimer = new Timer(1000, 1);
                killtimer.addEventListener(TimerEvent.TIMER, killtimerEvent);
                killtimer.start();
            }
        }
        public function SoundBuffer(event:SampleDataEvent):void {
            //try {
            //trace("[SoundBuffer:"+ID+"]");
            //sound.removeEventListener(SampleDataEvent.SAMPLE_DATA, SoundBuffer);

            // buffer 4KB of data
            for (var i:int = 0; i < 4096; i++)
            {
                var l:Number = 0;
                var r:Number = 0;
                if (_vbuf.length > int(_phase)*4) {
                    _vbuf.position = int(_phase)*4;
                    l = _vbuf.readFloat();
                    if (_vbuf.position < _vbuf.length)
                        r = _vbuf.readFloat();
                    else
                        r = l;
                }
                //if (_vbuf.position == _vbuf.length)
                    //_vbuf = new ByteArray();

                event.data.writeFloat(l);
                event.data.writeFloat(r);

                _phase += (16/44.1);
                if (_phase >= _numSamples) {
                    _phase -= _numSamples;
                }
            }
            System.gc();
        }
    }
}

The initial idea was to create a SoundObj in my scene, use obj.receive(bytes) to add data to the buffer to be played back the next time the Sound player needed new data. I've been fiddling around trying to get it to work in one way or another since. The timers were designed to determine when to buffer more data, but never really worked as desired.

Proper double buffer, proper playback.

package VoipOnline
{
    import flash.events.SampleDataEvent;
    import flash.events.TimerEvent;
    import flash.media.Sound;
    import flash.system.System;
    import flash.utils.ByteArray;
    import flash.utils.Timer;

    import flashx.textLayout.formats.Float;

    public class SoundObj
    {
        public var ID:int;
        public var sound:Sound;
        internal var _readBuf:ByteArray;
        internal var _writeBuf:ByteArray;

        internal var n:Number;
        internal var _phase:Number;
        internal var _numSamples:int;

        internal var myTimer:Timer;
        internal var bytes:int;

        public function SoundObj(id:int)
        {
            ID = id;
            _readBuf = new ByteArray();
            _writeBuf = new ByteArray();

            bytes = 0;

            myTimer = new Timer(10000, 0);
            myTimer.addEventListener(TimerEvent.TIMER, timerHandler);
            myTimer.start();

            sound = new Sound();
            sound.addEventListener(SampleDataEvent.SAMPLE_DATA, SoundBuffer);
            sound.play();

        }

        public function receive(bytes:ByteArray):void 
        {
            var i:int = _writeBuf.position;
            _writeBuf.position = _writeBuf.length;
            _writeBuf.writeBytes(bytes);
            _writeBuf.position = i;

            this.bytes += bytes.length;
        }

        private function timerHandler(e:TimerEvent):void{
            trace((bytes/10) + " bytes per second.");
            bytes = 0;
        }

        public function SoundBuffer(event:SampleDataEvent):void 
        {
            //trace((_readBuf.length/8)+" in buffer, and "+(_writeBuf.length/8)+" waiting.");
            for (var i:int = 0; i < 4096; i++)
            {
                var l:Number = 0; // silence
                var r:Number = 0; // silence
                if (_readBuf.length > int(_phase)*8) {
                    _readBuf.position = int(_phase)*8;
                    l = _readBuf.readFloat();
                    if (_readBuf.position < _readBuf.length)
                        r = _readBuf.readFloat();
                    else {
                        r = l;
                        Buffer();
                    }
                } else {
                    Buffer();
                }
                event.data.writeFloat(l);
                event.data.writeFloat(r);

                _phase += 0.181;
            }
        }
        private function Buffer():void {
            // needs new data

            // snip 4096 bytes
            var buf:ByteArray = new ByteArray();
            var len:int = (_writeBuf.length >= 4096 ? 4096 : _writeBuf.length);
            buf.writeBytes(_writeBuf, 0, len);

            // remove snippet
            var tmp:ByteArray = new ByteArray();
            tmp.writeBytes(_writeBuf, len, _writeBuf.length-len);
            _writeBuf = tmp;

            // plug in snippet
            _readBuf = buf;
            _writeBuf = new ByteArray();
            _readBuf.position = 0;
            _phase = 0;
        }
    }
}

These code snippets are based on this mic setup:

mic = Microphone.getMicrophone();
mic.addEventListener(SampleDataEvent.SAMPLE_DATA, this.micParse); // raw mic data stream handler
mic.codec = SoundCodec.SPEEX;
mic.setUseEchoSuppression(true);
mic.gain = 100;
mic.rate = 44;
mic.setSilenceLevel(voicelimit.value, 1);

After considerable testing, this seems to provide the best results so far. Little grainy, but it IS compressed and filterred. Some of the issues I'm having seem to be the fault of the server. I'm only receiving ~30% of the bytes I'm sending out. That being said, the code above works. You simply adjust the _phase increment to modify speed. (0.181 == 16/44/2) Credit will go where credit is due, even if his sample didn't quite solve the issues at hand, it was still a considerable step forward.


Solution

  • I have prepared some sample data and fed it into your example and got only noise sound. I have simplified your class to only two buffers one for receiving samples and second one for providing them. Hope this will work:

    package  {
        import flash.events.*;
        import flash.media.*;
        import flash.utils.*;
    
        public class SoundObj
        {
            private var ID:int;
            public var sound:Sound;
            public var _readBuf:ByteArray;
            public var _writeBuf:ByteArray;
    
            public function SoundObj(id:int)
            {
                ID = id;
                _readBuf = new ByteArray();
                _writeBuf = new ByteArray();
    
                sound = new Sound();
                sound.addEventListener(SampleDataEvent.SAMPLE_DATA, SoundBuffer);
                sound.play();
            }
    
            public function receive(bytes:ByteArray):void
            {
                var i:int = _writeBuf.position;
                _writeBuf.position = _writeBuf.length;
                _writeBuf.writeBytes(bytes);
                _writeBuf.position = i;
                sound.play();
            }
    
            public function SoundBuffer(event:SampleDataEvent):void
            {
                for (var i:int = 0; i < 8192; i++)
                {
                    if (_readBuf.position < _readBuf.length)
                        event.data.writeFloat(_readBuf.readFloat());
                    else
                    {
                        if (_writeBuf.length >= 81920)
                        {
                            _readBuf = _writeBuf;
                            _writeBuf = new ByteArray();
                        }
    
                        if (_readBuf.position < _readBuf.length)
                            event.data.writeFloat(_readBuf.readFloat());
                        else
                        {
                            //event.data.writeFloat( 0 );
                        }
                    }
                }
            }
        }
    }
    
        // microphone sample parsing with rate change
        function micParse(event:SampleDataEvent):void 
        {
            var soundBytes:ByteArray = new ByteArray();
    
            var i:uint = 0;
            var n:Number = event.data.bytesAvailable * 44 / mic.rate * 2; // *2 for stereo
            var f:Number = 0;
            while(event.data.bytesAvailable) 
            { 
                i++;
                var sample:Number = event.data.readFloat(); 
                for (; f <= i; f+= mic.rate / 2 / 44)
                {
                    soundBytes.writeFloat(sample); 
                }
            }       
            snd.receive(soundBytes);
        }