Speech to Text in Action Script 3

Action script 3 and AIR are quite interesting with many supports to desktop and mobile application, and one of them is sound support. Inspired by this post, I would like to create a small application using AS3 and AIR, which can be used to record voice and then, send it to google spech-to-text api. As a result, we will get a text-format of what we’ve said.

Here, we will use Action Script 3 and Adobe Air 3.2 in Window 7 professional. The IDE is Flash Builder 4.6.

Before starting, It’s better to summarize what we will do:

1. Create a simple GUI interface.

2. Record what we said using micro

3. Encode raw data to .flac and send it to google

4. Mp3 encoder implementation.

Part1: Create a simple GUI interface.

Click here to download full source code

After this section, we will be able to create a simple interface like this:

Interface

Figure1: Interface of our application

  1. Create a new Flex Project: From within Flash Builder, choose File -> New -> Flex Project. In the Project name textbox, choose a name, SimpleSoundApp for ex. In TheApplication Type, choose Desktop (run in Adobe Air) as we will work with Desktop application
NewProject

Figure 2: NewProject

Choose Finish, now, you should have a project with directory structure like this:

DirectoryStructure

  1. Open SimpleSoundApp.mxml and add this code to it:
<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
                       xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx"
                       width="315" height="438" title="Simple Recorder">
    <fx:Declarations>         <!-- Place non-visual elements (e.g., services, value objects) here -->
    </fx:Declarations>
    <fx:Script>                 
        <![CDATA[             
            import mx.controls.Alert;
            public function toMp3IsClicked() {
                Alert.show("toMp3IsClicked");
            }
            public function toWavIsClicked() {
                Alert.show("toMp3IsClicked");
            }
            public function toFlacIsClicked() {
                Alert.show("toMp3IsClicked");
            }
            public function recordIsClicked() {
                Alert.show("toMp3IsClicked");
                label.text = "Recording...";
                record.enabled = false;
                stop.enabled = true;
            }
            public function stopIsClicked() {
                Alert.show("toMp3IsClicked");
                label.text = "Click Record to begin.";
                record.enabled = true;
                stop.enabled = false;
                toMp3.enabled = true;
                toWav.enabled = true;
                toFlac.enabled = true;
            }
            public function sendIsClicked() {
                Alert.show("toMp3IsClicked");
            }
        ]]>
    </fx:Script>
    <s:Group x="31" y="31" width="250" height="212">
        <s:Label id="label" x="10" y="80" width="240" height="55"
                 fontSize="24" text="Click Record to begin." />
    </s:Group>
    <s:Group x="31" y="269" width="250" height="134">
        <s:Button id="record" x="10" y="30" label="Record"
                  click="recordIsClicked();" name="record" />
        <s:Button id="stop" x="90" y="30" label="Stop" click="stopIsClicked();"
                  enabled="false" name="stop" />
        <s:Button id="send" x="170" y="30" label="Send" click="sendIsClicked()"
                  name="send" />
        <s:Button id="toMp3" x="10" y="90" label="To Mp3" click="toMp3IsClicked();"
                  enabled="false" name="toMp3" />
        <s:Button id="toWav" x="88" y="90" width="70" label="To Wav"
                  click="toWavIsClicked();" enabled="false" name="toWav" />
        <s:Button id="toFlac" x="170" y="90" label="To Flac"
                  click="toFlacIsClicked();" enabled="false" name="toFlac" />
    </s:Group>
</s:WindowedApplication>

For now, we have a simple interface as in figure 1. Note that all functions we listed here are just place-holder functions. It will be discussed later. Also, label is changed based on the current state of our application and buttons are enabled only if the record is finished (after clicking Record, and then Stop).

 

Part2: Record what we said using micro

In part 1, We’ve seen how to create a simple interface in AS3. Based on this interface, we will implement two function recordIsClicked and stopIsClicked. At the end of part 2, The application should be able to record what we said, and then, replay it.

  1. First, create a new pakage com.gmail.nxhoaf, and then, create a our Recorder class.
Recorder
Figure 3: Recorder
  1. Handle for recordIsClicked and stopIsClicked

We need to implement two functions to handle recordIsClicked and stopIsClicked named startRecord and stopRecord respectively.

  • Here is the startRecord() function
public function startRecord() :void {
    this.bytes = new ByteArray;
    mic.gain = 100;
    mic.rate = 44;
    mic.setSilenceLevel(0,4000);                    
    // Remove playback listener if any
    mic.removeEventListener(SampleDataEvent.SAMPLE_DATA, onPlaying);
    // Add record listener
    mic.addEventListener(SampleDataEvent.SAMPLE_DATA, onRecording);
}

When recording, the device buffers the microphone input, so we need to create a ByteArray object. rate defines the sample rate and impacts the quality of the input audio, here, we use 44, this means 44100 Hz. Gain is the amount by which the microphone boosts the signal. We want to want to boost the microphone to the maximum assuming that were’re more likely in a noisy place.

Then, we remove any previous listener if any, and add our new listener. Here is the code for onRecording() function:

private function onRecording(event:SampleDataEvent):void {
    while (event.data.bytesAvailable) {
        var sample:Number = event.data.readFloat();
        bytes.writeFloat(sample);
    }
}
    • To stop the recording process, just remove the event listener:
public function stopRecord() :void {
    mic.removeEventListener(SampleDataEvent.SAMPLE_DATA,onRecording);
}
  1. Playback

To playback, we read the sample stored in our ByteArray object and play it. Note that after recording, we must rewind the byte position to the beginning…

public function playback () :void {
    if (bytes.length > 0) {
        bytes.position = 0;
        var sound:Sound = new Sound();
        sound.addEventListener(SampleDataEvent.SAMPLE_DATA,onPlaying);
        sound.play();
    }
}

private function onPlaying(event:SampleDataEvent): void {
    var sample:Number;
    for (var i:int = 0; i < 8192; i++) {
        if (!bytes.bytesAvailable) return;
        sample = bytes.readFloat();
        event.data.writeFloat(sample);
        event.data.writeFloat(sample);
    }
}

Audio data is made of samples. 8,192 samples is the maximum amount of data that
can be written to a sound at one time. If you try to play more samples, you will get a
runtime error. If you provide fewer than 2,048 samples, the application will assume the sound reached its end and will stop.

Even though the microphone is monophonic in, sound data needs to
be written to both left and right output channels. If you don’t do this, the recording
playback will be twice the speed at which it was recorded because two samples will run in one stereo sample of the sound object.

Full source code of theRecorder.as class is shown below:

package com.gmail.nxhoaf { 
    import flash.events.SampleDataEvent; 
    import flash.media.Microphone; 
    import flash.media.Sound;  
    import flash.utils.ByteArray;  
    public class Recorder     { 
        private var bytes:ByteArray;     
        private var mic:Microphone;  
        public function Recorder()
        {
        }

        public function setMicrophone (mic : Microphone) {
            this.mic = mic;
        }

        public function getMicrophone () {
            return mic;
        }

        public function startRecord() :void {
            this.bytes = new ByteArray();
            mic.gain = 100;
            mic.rate = 44;
            mic.setSilenceLevel(0,4000);                    
            // Remove playback listener if any
            mic.removeEventListener(SampleDataEvent.SAMPLE_DATA, onPlaying);
            // Add record listener
            mic.addEventListener(SampleDataEvent.SAMPLE_DATA, onRecording);
        }

        public function stopRecord() :void {
            mic.removeEventListener(SampleDataEvent.SAMPLE_DATA,onRecording);
        }

        public function playback () :void {
            if (bytes.length > 0) {
                bytes.position = 0;
                var sound:Sound = new Sound();
                sound.addEventListener(SampleDataEvent.SAMPLE_DATA,onPlaying);
                sound.play();
            }
        }

        private function onRecording(event:SampleDataEvent):void {
            while (event.data.bytesAvailable) {
                var sample:Number = event.data.readFloat();
                bytes.writeFloat(sample);
            }
        }

        private function onPlaying(event:SampleDataEvent): void {
            var sample:Number;
            for (var i:int = 0; i < 8192; i++) {
                if (!bytes.bytesAvailable) return;
                sample = bytes.readFloat();
                event.data.writeFloat(sample);
                event.data.writeFloat(sample);
            }
        }
    }
}
  1. Modify SimpleSoundApp‘s interface

After implementing all back-end functions, we go to the front-end now, and modify a little bit the source, Alert.show(…) placeholder will be replaced with an more appropriate functions. Here is the new version of our SimpleSoundApp.mxml

<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
                       xmlns:s="library://ns.adobe.com/flex/spark"
                       xmlns:mx="library://ns.adobe.com/flex/mx"
                       width="315" height="438" title="Simple Recorder">
    <fx:Declarations>
        <!-- Place non-visual elements (e.g., services, value objects) here -->
    </fx:Declarations>
    <fx:Script>
        <![CDATA[
            import com.gmail.nxhoaf.Recorder;
            import com.gmail.nxhoaf.SoundFormat;

            import mx.controls.Alert;
            var recorder : Recorder = new Recorder();

            public function toMp3IsClicked() {
                Alert.show("toMp3IsClicked");
            }

            public function toWavIsClicked() {
                Alert.show("toWavIsClicked");
            }

            public function playbackIsClicked() {
                recorder.playback();
            }

            public function recordIsClicked() {
                var mic : Microphone = Microphone.getMicrophone();
                if (mic == null) {
                    return;
                }
                recorder.setMicrophone(mic);
                recorder.startRecord();
                label.text = "Recording...";
                record.enabled = false;
                stop.enabled = true;
            }

            public function stopIsClicked() {
                recorder.stopRecord();
                label.text = "Click Record to begin.";
                record.enabled = true;
                stop.enabled = false;
                toMp3.enabled = true;
                toWav.enabled = true;
                playback.enabled = true;
            }

            public function sendIsClicked() {
                Alert.show("sendIsClicked");        
            }
        ]]>
    </fx:Script>
    <s:Group x="31" y="31" width="250" height="212">
        <s:Label id="label" x="10" y="80" width="240" height="55" fontSize="24"
                 text="Click Record to begin."/>
    </s:Group>
    <s:Group x="31" y="269" width="250" height="134">
        <s:Button id="record" x="10" y="30" label="Record" click="recordIsClicked();" name="record"/>
        <s:Button id="stop" x="90" y="30" label="Stop" click="stopIsClicked();" enabled="false"
                  name="stop"/>
        <s:Button id="send" x="170" y="30" label="Send" click="sendIsClicked()" name="send"/>
        <s:Button id="toMp3" x="10" y="90" label="To Mp3" click="toMp3IsClicked();" enabled="false"
                  name="toMp3"/>
        <s:Button id="toWav" x="88" y="90" width="70" label="To Wav" click="toWavIsClicked();"
                  enabled="false" name="toWav"/>
        <s:Button id="playback" x="170" y="90" label="Play! " click="playbackIsClicked();"
                  enabled="false" name="playback"/>
    </s:Group>
</s:WindowedApplication>

That is, we’re now able to record sound, and play it back. In the next part, We’ll examine how to encode what we said to .mp3, .wav, and then, we will send it to google speech api to get out text-form of what we said…

 

Part 3: Encode raw data to .flac and send it to Google

So far, we’ve created a small application with a simple interface and some methods(record, playback..). Now, we’ll try to send what we say to Google. As a result, we should have the result in text-format. For example, when you say “Hello”, you will get a Json data structure telling you that your utterance is “Hello”.

In this part, we will:

  • Encode audio stream to .flac format. In fact, the audio file sent to Google can be .flac or Speex, but here, we choose .flac
  • Send .flac format to Google.

1. Encode audio stream to .flac format

First, download this lib which is used to encode the audio stream as .flac file. In the downloaded file, copy flac.swc and put it into libs folder of the project:

2. Send .flac format to Google.

The sending code is put in encodeToFlacAndSend() function:

public function encodeToFlacAndSend() : void {    
    var flacCodec:Object;
    flacCodec = (new cmodule.flac.CLibInit).init();
    bytes.position = 0;
    var rawData: ByteArray = new ByteArray();
    var flacData : ByteArray = new ByteArray();
    rawData = convert32to16(bytes);
    flacData.endian = Endian.LITTLE_ENDIAN;
    flacCodec.encode(    encodingCompleteHandler, 
        encodingProgressHandler, 
        rawData, 
        flacData, 
        rawData.length, 
        30);            
    function encodingCompleteHandler(event:*):void {
        trace("FLACCodec.encodingCompleteHandler(event):", event);
        //Alert.show(flacData.length.toString());            
        var PATH:String = "https://www.google.com/speech-api/v1/recognize?xjerr=1&client=chromium&lang=en-US";
        var urlRequest:URLRequest = new URLRequest(PATH);
        var urlLoader:URLLoader = new URLLoader();
        urlRequest.contentType = "audio/x-flac; rate=44000";
        urlRequest.data = flacData;
        urlRequest.method = URLRequestMethod.POST;

        urlLoader.dataFormat = URLLoaderDataFormat.TEXT;// default
        urlLoader.addEventListener(Event.COMPLETE, urlLoader_complete);
        urlLoader.addEventListener(ErrorEvent.ERROR, urlLoader_error);
        urlLoader.load(urlRequest);

        function urlLoader_complete(evt:Event):void {
            Alert.show(urlLoader.data);

        }
        function urlLoader_error(evt:ErrorEvent): void {
            Alert.show("*** speech to text *** " + evt.toString());
        }
    }

    function encodingProgressHandler(progress:int):void {
        trace("FLACCodec.encodingProgressHandler(event):", progress);;
    }
}

For now, SimpleSoundApp looks like this:

<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
                       xmlns:s="library://ns.adobe.com/flex/spark"
                       xmlns:mx="library://ns.adobe.com/flex/mx"
                       width="315" height="438"
                       title="Simple Recorder">
    <fx:Declarations>
        <!-- Place non-visual elements (e.g., services, value objects) here -->
    </fx:Declarations>
    <fx:Script >
        <![CDATA[
            import mx.controls.Alert;
            import com.gmail.nxhoaf.Recorder;
            var recorder : Recorder = new Recorder();
            public function toMp3IsClicked() {
                Alert.show("toMp3IsClicked");
            }
            public function toWavIsClicked() {
                Alert.show("toWavIsClicked");
            }
            public function playbackIsClicked() {
                recorder.playback();
            }
            public function recordIsClicked() {
                var mic : Microphone = Microphone.getMicrophone();
                if (mic == null) {
                    return;
                }
                recorder.setMicrophone(mic);
                recorder.startRecord();
                label.text = "Recording...";
                record.enabled = false;
                stop.enabled = true;
            }
            public function stopIsClicked() {
                recorder.stopRecord();
                label.text = "Click Record to begin.";
                record.enabled = true;
                stop.enabled = false;
                toMp3.enabled = true;
                toWav.enabled = true;
                playback.enabled = true;
            }
            public function sendIsClicked() {
                recorder.encodeToFlacAndSend();
            }
        ]]>
    </fx:Script>
    <s:Group x="31" y="31" width="250" height="212">
        <s:Label id="label" x="10" y="80" width="240" height="55" fontSize="24" text="Click Record to begin."/>
    </s:Group>
    <s:Group x="31" y="269" width="250" height="134">
        <s:Button id="record" name="record" x="10" y="30" label="Record" click="recordIsClicked();"/>
        <s:Button id="stop" name="stop" x="90" y="30" label="Stop" enabled="false" click="stopIsClicked();"/>
        <s:Button x="170" y="30" id="send" name="send" label="Send" click="sendIsClicked()"/>

        <s:Button id = "toMp3" name="toMp3" x="10" y="90" label="To Mp3" enabled="false" click="toMp3IsClicked();"/>
        <s:Button id="toWav" x="88" y="90" width="70" label="To Wav" enabled="false" name="toWav" click="toWavIsClicked();"/>
        <s:Button x="170" y="90" name="playback" id="playback" label="Play! " enabled="false" click="playbackIsClicked();"/>
    </s:Group>
</s:WindowedApplication>

And here is our Recorder.as:

package com.gmail.nxhoaf
{
    import cmodule.flac.CLibInit;

    import flash.events.ErrorEvent;
    import flash.events.Event;
    import flash.events.SampleDataEvent;
    import flash.media.Microphone;
    import flash.media.Sound;
    import flash.net.FileReference;
    import flash.net.URLLoader;
    import flash.net.URLLoaderDataFormat;
    import flash.net.URLRequest;
    import flash.net.URLRequestMethod;
    import flash.utils.ByteArray;
    import flash.utils.Endian;

    import mx.controls.Alert;

    public class Recorder
    {
        private var bytes:ByteArray;
        private var mic:Microphone;

        private static const FLOAT_MAX_VALUE:Number = 1.0;
        private static const SHORT_MAX_VALUE:int = 0x7fff;
        public function Recorder()
        {
        }

        public function setMicrophone (mic : Microphone) {
            this.mic = mic;
        }

        public function getMicrophone () {
            return mic;
        }

        public function startRecord() :void {
            this.bytes = new ByteArray();
            mic.gain = 100;
            mic.rate = 44;
            mic.setSilenceLevel(0,4000);
            // Remove playback listener if any
            mic.removeEventListener(SampleDataEvent.SAMPLE_DATA, onPlaying);
            // Add record listener
            mic.addEventListener(SampleDataEvent.SAMPLE_DATA, onRecording);
        }

        public function stopRecord() :void {
            mic.removeEventListener(SampleDataEvent.SAMPLE_DATA,onRecording);
        }

        public function playback () :void {
            if (bytes.length > 0) {
                bytes.position = 0;
                var sound:Sound = new Sound();
                sound.addEventListener(SampleDataEvent.SAMPLE_DATA,onPlaying);
                sound.play();
            }
        }

        private function onRecording(event:SampleDataEvent):void {
            while (event.data.bytesAvailable) {
                var sample:Number = event.data.readFloat();
                bytes.writeFloat(sample);
            }
        }

        private function onPlaying(event:SampleDataEvent): void {
            var sample:Number;
            for (var i:int = 0; i < 8192; i++) {
                if (!bytes.bytesAvailable) return;
                sample = bytes.readFloat();
                event.data.writeFloat(sample);
                event.data.writeFloat(sample);
            }
        }

        public function encodeToFlacAndSend() : void {
            var flacCodec:Object;
            flacCodec = (new cmodule.flac.CLibInit).init();
            bytes.position = 0;
            var rawData: ByteArray = new ByteArray();
            var flacData : ByteArray = new ByteArray();
            rawData = convert32to16(bytes);
            flacData.endian = Endian.LITTLE_ENDIAN;
            flacCodec.encode( encodingCompleteHandler,
                encodingProgressHandler,
                rawData,
                flacData,
                rawData.length,
                30);
            function encodingCompleteHandler(event:*):void {
                trace("FLACCodec.encodingCompleteHandler(event):", event);
                //Alert.show(flacData.length.toString());
                var PATH:String = "https://www.google.com/speech-api/v1/recognize?xjerr=1&client=chromium&lang=en-US";
                var urlRequest:URLRequest = new URLRequest(PATH);
                var urlLoader:URLLoader = new URLLoader();
                urlRequest.contentType = "audio/x-flac; rate=44000";
                urlRequest.data = flacData;
                urlRequest.method = URLRequestMethod.POST;

                urlLoader.dataFormat = URLLoaderDataFormat.TEXT;// default
                urlLoader.addEventListener(Event.COMPLETE, urlLoader_complete);
                urlLoader.addEventListener(ErrorEvent.ERROR, urlLoader_error);
                urlLoader.load(urlRequest);

                function urlLoader_complete(evt:Event):void {
                    Alert.show(urlLoader.data);

                }
                function urlLoader_error(evt:ErrorEvent): void {
                    Alert.show("*** speech to text *** " + evt.toString());
                }
            }

            function encodingProgressHandler(progress:int):void {
                trace("FLACCodec.encodingProgressHandler(event):", progress);;
            }
        }

        /**
         * Converts an (raw) audio stream from 32-bit (signed, floating point)
         * to 16-bit (signed integer).
         *
         * @param source The audio stream to convert.
         */
        private function convert32to16(source:ByteArray):ByteArray {
            trace("BitrateConvertor.convert32to16(source)", source.length);

            var result:ByteArray = new ByteArray();
            result.endian = Endian.LITTLE_ENDIAN;

            while( source.bytesAvailable ) {
                var sample:Number = source.readFloat() * SHORT_MAX_VALUE;

                // Make sure we don't overflow.
                if (sample < -SHORT_MAX_VALUE) sample = -SHORT_MAX_VALUE;
                else if (sample > SHORT_MAX_VALUE) sample = SHORT_MAX_VALUE;

                result.writeShort(sample);
            }

            trace(" - result.length:", result.length);
            result.position = 0;
            return result;
        }

    }
}

This’s how I sent our recorded audio to Google. The next part will discuss how to encode our audio stream as .mp3 and .wav.

 

Part 4: Implementation of wav and mp3 encoder.

In this part, we’ll try to save an audio stream captured from microphone to .mp3 or .wav format. Here is what we will do:

  1. Save audio stream to .wav format

  2. Save audio stream to .mp3 format.

1. Save audio stream to .wav format

First, download the audio_sampler.zip here. Extract and then copy all the com folder to the src folder in our project. You should end up with something like this:

WAVWriter.as

Next, we’ll convert an audio input stream captured from microphone to .wav. Here is the encode function:

private function encodeToWav(bytes:ByteArray) : void {
    var wav:WAVWriter = new WAVWriter();
    wav.numOfChannels = 1;
    wav.sampleBitRate = 16;
    wav.samplingRate =     44100;

    bytes.position = 0;
    var wavData : ByteArray = new ByteArray();
    wavData.endian = Endian.BIG_ENDIAN;
    wav.processSamples(wavData,bytes,44100,1);            
    //wavData.position = 0;    
    (new FileReference()).save(wavData, ".wav");
}

The first thing is to create a new WAVWriter. We set numOfChannels to 1, it means that mono audio will be used. Then, we set the BitRate to 16, and Sample Rate to 44100 (for more info about Bit Rate and Sample Rate, see Bit Rate and Sample Rate). Wav. Also note that Big Endian is used.

Then, the audio stream is encoded as wav data and then, saved as .wav file to the external memory

For the interface, we’ll implement the toWavIsClicked():

public function toWavIsClicked() {
   recorder.saveAs(SoundFormat.WAV);
}

SoundFormat is just a class describing some sound extensions:

public class SoundFormat {
    public static const MP3:String = "mp3";
    public static const WAV:String = "wav";
    public static const FLAC:String = "flac";
}

For now, you can save your audio stream to .wav extension as in below:

saveToWav

2. Save audio stream to .mp3 format

In order to encode the audio stream as .mp3, download this lib. In the downloaded file, go to the bin folder and copy shineMP3_alchemy.swc into your libs folder:

mp3 encoder

Then, modify the Recorder.as a little bit :

private function encodeToMp3(bytes:ByteArray) : void {
    var wav:WAVWriter = new WAVWriter();
    wav.numOfChannels = 1;
    wav.sampleBitRate = 16;
    wav.samplingRate = 44100;

    bytes.position = 0;
    var wavData : ByteArray = new ByteArray();
    wavData.endian = Endian.BIG_ENDIAN;
    wav.processSamples(wavData,bytes,44100,1);            
    wavData.position = 0;

    mp3Encoder = new ShineMP3Encoder(wavData);
    mp3Encoder.addEventListener(Event.COMPLETE, mp3EncodeComplete);
    mp3Encoder.addEventListener(ProgressEvent.PROGRESS, mp3EncodeProgress);
    mp3Encoder.addEventListener(ErrorEvent.ERROR, mp3EncodeError);
    mp3Encoder.start();

    function mp3EncodeProgress(event : ProgressEvent) : void {
    }

    function mp3EncodeError(event : ErrorEvent) : void {
        Alert.show(event.toString());
    }

    function mp3EncodeComplete(event : Event) : void {
        mp3Encoder.saveAs();
    }
}

As the third-party lib accepts only .wav input format, we first convert the audio stream to .wav file. Then, use we convert wav back to mp3 file. As soon as the conversion finished, mp3EncodeComplete is called to save the encoded audio stream to .mp3.

public function toMp3IsClicked() {
    recorder.saveAs(SoundFormat.MP3);
}

mp3 encoder

 

Disclaim

This project is one part of my previous work.At the time of this writing, it worked perfectly. Now, I haven’t worked on it anymore. Thus, I can no longer support you on this.

Thank you!

Advertisements
This entry was posted in ActionScript3 and tagged , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s