Making Waves

In a previous post, “Digging into the Microphone in Flash Player 10.1”, reader David Law asked in the comments how it would be possible to save .wav files to the server. I wasn’t sure right offhand, but thought I’d spend my lunch hour yesterday looking into it. Well, after reading this quick tutorial on Adobe Developer Connection, it turns out it’s actually quite simple. Almost embarrassingly simple, but still I thought I’d post it in case anyone else was looking to do the same thing.

The trick is to just convert your sound bytes into a wav formatted ByteArray object which can then be saved to the server with a quick bit of AMFPHP. You’ll notice in that Adobe article that there is included a WAVWriter class – that is all that’s necessary to convert your uncompressed ByteArray to a .wav file. Once you have that, the below AMFPHP class can easily save that .wav file to your server:

data;
		$result = file_put_contents($dir . $fileName, $data);
		
		return $result;
    }
}

?>

You may notice that the above class just saves the file to a directory within your AMFPHP services directory. You’ll more than likely not want to do that, so you can just create a different path. Or you may want to save the .wav directly in a database as a blob, in which case this old post may help you out. You may also want to name the .wav file on the client side and pass that along as an additional parameter rather than giving it a random name on the server side. In any case, this was just a quick example. You can do what you want from here.

Below is a quick .swf example (requires the 10.1 Flash Player) that will allow you to save the .wav file directly to your local machine to try it out (once you begin recording, you only have 3 seconds to say something into your mic, so make it quick. Didn’t want to go crazy on bandwidth).

[kml_flashembed publishmethod=”static” fversion=”10.1.0″ movie=”http://blog.onebyonedesign.com/wp-content/uploads/2010/04/recorder.swf” width=”400″ height=”200″ targetclass=”flashmovie”]

Get Adobe Flash player

[/kml_flashembed]

Again, this saves the file locally rather than to a server, but if you look through the code, you’ll see a commented area that would send the file to the AMFPHP class above rather than to the user’s machine via FileReference. Also, if you want to compile this yourself, you’ll need the WAVWriter class from the Adobe tutorial as well as the great MinimalComps from Bit-101 (of course you could just use your own buttons easily enough, but those components are freakin’ awesome for quick prototyping and if you don’t already have them, I recommend you get them).

package {
	
	import com.bit101.components.PushButton;
	import com.bit101.components.Style;
	import flash.display.Sprite;
	import flash.events.Event;
	import com.adobe.audio.format.WAVWriter;
	import flash.events.MouseEvent;
	import flash.events.StatusEvent;
	import flash.events.TimerEvent;
	import flash.net.FileReference;
	import flash.net.NetConnection;
	import flash.net.Responder;
	import flash.text.AntiAliasType;
	import flash.text.TextField;
	import flash.text.TextFieldAutoSize;
	import flash.text.TextFormat;
	import flash.utils.Timer;
			
	import flash.events.SampleDataEvent;
	import flash.media.Microphone;
	import flash.utils.ByteArray;
	
	/**
	 * Record .wav file through mic then send it to users machine or server
	 * @author Devon O.
	 */
	[SWF(width='400', height='200', backgroundColor='#FFFFFF', frameRate='31')]
	public class Main extends Sprite {
		
		public static const GATEWAY_URL:String = "path/to/gateway.php";
		
		private var _startButton:PushButton;
		private var _stopButton:PushButton;
		private var _saveButton:PushButton;
		
		private var _recordingMessage:TextField;
		
		private var _timer:Timer;
		private var _mic:Microphone;
		private var _recordingComplete:Boolean = false;
		private var _soundRecording:ByteArray;
		
		private var _fileRef:FileReference = new FileReference();
		
		public function Main():void {
			if (stage) init();
			else addEventListener(Event.ADDED_TO_STAGE, init);
		}
		
		private function init(e:Event = null):void {
			removeEventListener(Event.ADDED_TO_STAGE, init);
			// entry point
			
			initMicrophone();
			initMessage();
			initUI();
		}
		
		private function initMicrophone():void {
			_mic = Microphone.getMicrophone();
			_mic.rate = 44;
			_mic.gain = 60;
			_mic.addEventListener(StatusEvent.STATUS, onmicStatus);
		}
		
		private function onmicStatus(event:StatusEvent):void {
			_mic.removeEventListener(StatusEvent.STATUS, onmicStatus);
			
			if (event.code == "Microphone.Muted") {
				_recordingMessage.text = "You must allow access\nto Mic to record.";
				_mic.removeEventListener(SampleDataEvent.SAMPLE_DATA, gotMicData);
			} else {
				_recordingMessage.text = "Recording...";
				_timer = new Timer(3000, 1);
				_timer.addEventListener(TimerEvent.TIMER_COMPLETE, timesUp);
				_timer.start();
			}
		}
		
		private function timesUp(event:TimerEvent):void {
			_timer.removeEventListener(TimerEvent.TIMER_COMPLETE, timesUp);
			stopRecording();
		}
		
		private function startRecording(event:MouseEvent):void {			
			_startButton.enabled = false;
			_recordingComplete = true;
			_soundRecording = new ByteArray();
			_mic.addEventListener(SampleDataEvent.SAMPLE_DATA, gotMicData);
		}
			
		private function stopRecording():void{
			_recordingMessage.text = "Recording complete.\nClick 'Save' to export .wav.";
			_saveButton.enabled = true;
			_mic.removeEventListener(SampleDataEvent.SAMPLE_DATA, gotMicData);
		}
			
		private function gotMicData(micData:SampleDataEvent):void {
			_soundRecording.writeBytes(micData.data);
		}
		
		private function saveRecording(event:MouseEvent):void {
			if (_soundRecording == null || _soundRecording.length <= 0) return;
			
			_saveButton.enabled = false;
			
			var wavWriter:WAVWriter = new WAVWriter();
				
			_soundRecording.position = 0;
				
			wavWriter.numOfChannels = 1;
			wavWriter.sampleBitRate = 16;
			wavWriter.samplingRate = 44100;
			
			var wavBytes:ByteArray = new ByteArray();
				
			wavWriter.processSamples(wavBytes, _soundRecording, 44100, 1); // convert our ByteArray to a WAV file.
			
			/* Use something like this to send the .wav to the server rather than the local machine:

			var resp:Responder = new Responder(successHandler, errorHandler);
			var gateway:NetConnection = new NetConnection();
			gateway.connect(GATEWAY_URL);
			gateway.call("Wave.saveWav", resp, wavBytes);
			
			*/
			
			// or use this to save it locally
			_fileRef.save(wavBytes, "sound.wav");
		}
		
		private function successHandler(o:Object):void {
			trace(o);
			if (!o) trace("wav file not saved");
		}
		
		private function errorHandler(o:Object):void {
			for (var prop:String in o) trace(prop + " = " + o[prop]);
		}
		
		private function initMessage():void {
			_recordingMessage = new TextField();
			_recordingMessage.selectable = false;
			_recordingMessage.mouseEnabled = false;
			_recordingMessage.autoSize = TextFieldAutoSize.LEFT;
			_recordingMessage.antiAliasType = AntiAliasType.ADVANCED;
			_recordingMessage.defaultTextFormat = new TextFormat("_sans", 24, 0x660000);
			_recordingMessage.text = "Click 'Record' to begin...";
			_recordingMessage.x = 10;
			_recordingMessage.y = 100;
			_recordingMessage.multiline = true;
			addChild(_recordingMessage);
		}
		
		private function initUI():void {
			Style.BUTTON_FACE = 0x000000;
			_startButton = new PushButton(this, 10, 10, "Record", startRecording);
			_saveButton = new PushButton(this, _startButton.x + _startButton.width + 5, 10, "Save", saveRecording);
			_saveButton.enabled = false;
		}
	}
}

Of course, saving .wav files is all good and well, but you'll probably want to actually be able to play them back. As you know (or should), Flash doesn't natively allow for dynamically loading and playing .wav files. Needless to say, there is a way around that though. The general idea is this: the .wav file must be loaded in as a stream of bytes. This byte stream is then added to the library of a dynamically generated .swf file. The .wav can then be extracted from the library of that .swf and played as a Sound object using ApplicationDomain. Thankfully, I didn't have to bother doing all this myself. A bit of Googling turned up this perfectly working example. I rearranged the code a little bit (added a real package, added some events, and added a playWav() method) which you can download in zipped format here. All credit goes to the original author though. Here's a quick usage example:

wp = new WavPlayer();
wp.addEventListener(ProgressEvent.PROGRESS, onProgress);
wp.addEventListener(Event.COMPLETE, onComplete);
wp.Load("mysound.wav");
		
private function onProgress(event:ProgressEvent):void {
	trace((event.bytesLoaded / event.bytesTotal) * 100);
}
		
private function onComplete(event:Event):void {
	wp.playWav();
}

Bear in mind that this WavPlayer works by loading in raw ones and zeroes which can be extremely hazardous if you don't know exactly where those ones and zeroes came from. I strongly suggest you use this only on .wav files you've created yourself or know for a fact don't have any monkey business going on inside.

Hope all that helps some folks out. Have fun...

Date: