FlexでWaveファイルを再生するライブラリを作ってみた

flash.media.SoundだとWaveファイルは再生出来ない。
でもたまにWaveファイルを再生したい時もある。
例えば、AquesTalkで生成されたのWaveデータを返すWebAPIを作って、それをFlex(Flash)で読み込んで再生する、とか。
意外とWaveじゃなきゃいけない時ってあると思う。
そこで、HTTPでWaveファイルをバイナリで取得して、Dynamic sound generationを使って音を生成するライブラリを作ってみた。
 

類似品

Flex User Groupのトピック「wavファイルを再生する方法」で書かれてるのとは違う方法。
http://www.fxug.net/modules/xhnewbb/viewtopic.php?topic_id=730
 

WaveSound.asのいいところ

いろんなフォーマットに対応してる。
外から制御出来る。
AquesTalkで出力されるWaveファイルをちゃんと再生出来る。(1ch、8000Hz、16bit固定)
 

WaveSound.asの悪いところ

重い。
FlashPlayer10以上じゃないと動かない。
 

つくったもの

jp/eth0/WaveSound.as
wave_client.mxml
 

音質

44100Hzなら問題なく再生出来る。
Dynamic sound generationでは44100Hz固定なので、44100Hz以外なら44100Hzになるように波形を引き伸ばす(縮める)。
波形そのものを変えてる訳じゃないから音は悪い。
例えば4000Hzのデータを再生しようとした場合、同じデータを約11回繰り返すから、方形波に近い波形でとんでもなく悪い。
フーリエ変換でもすればよくなるだろうけど、そこまでの気力はない。
 

対応フォーマット

データ形式

RIFF形式のWaveファイル。
FormatID=1のPCMフォーマットで確認。
多分それ以外にもいくつかはイケるんじゃないかと思う。
トルエンディアン。
 

チャンネル数

1、2。
 

ビットレート

多分なんでもイケる。(言うまでもなく8の倍数)
8bit、16bit、24bitは出来た。
 

サンプリングレート

多分なんでもイケる。
下は4000Hz、上は96000Hzまで確認。
 

対応チャンク

fmt、data。
parseWaveData()内のcaseを増やせば対応出来る。
RIFF形式について詳しくないから書けない。
 

ベンチマーク

HTTPで取得して、parseWaveData()する時間を計測。
wave_clientのデバッグメッセージのTotal部分。
1分36秒のファイルで検証。
大体こんな感じ。

1ch 8bit 4000Hz : 0.25s
2ch 16bit 44100Hz : 5.515s
2ch 24bit 96000Hz : 19.7s

 

jp/eth0/WaveSound.as

package jp.eth0
{
	import flash.media.Sound;
	import flash.net.URLLoader;
	import flash.net.URLLoaderDataFormat;
	import flash.net.URLRequest;
	import flash.utils.ByteArray;
	import flash.utils.Endian;
	import flash.events.SampleDataEvent;
	import flash.events.EventDispatcher;
	import flash.events.Event;
	import flash.events.HTTPStatusEvent;
	import flash.events.IOErrorEvent;
	import flash.events.ProgressEvent;
	import flash.events.SecurityErrorEvent;
	import mx.utils.ObjectUtil;
	import mx.utils.StringUtil;
	import mx.controls.Alert;

	public class WaveSound extends EventDispatcher
	{
		/* variable */

		private var _sound:Sound;
		private var _loader:URLLoader;
		private var _data:Object;
		private var _volume:Number = 1;
		private var _buffer:ByteArray;
		[Bindable] public var buffer_size:uint = 4096;
		[Bindable] public var position:uint = 0;
		[Bindable] public var duration:uint = 0;


		/* constant */

		public static var LOAD_COMPLETE:String = 'load_complete';
		public static var FORMAT_ERROR:String = 'format_error';
		public static var SOUND_COMPLETE:String = 'sound_complete';
		public static var SAMPLE_DATA:String = 'sample_data';


		/* function */

		public function WaveSound(url:String = null)
		{
			// Sound
			_sound = new Sound();

			// URLLoader
			_loader = new URLLoader();
			_loader.dataFormat = URLLoaderDataFormat.BINARY;

			// EventListener
			_loader.addEventListener(Event.COMPLETE, loadWaveCompleteHandler);
			_loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, dispatchEvent);
			_loader.addEventListener(IOErrorEvent.IO_ERROR, dispatchEvent);
			_loader.addEventListener(ProgressEvent.PROGRESS, dispatchEvent);
			_loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, dispatchEvent);

			// load
			if (url!=null) {
				load(url);
			}
		}

		public function load(url:String):void
		{
			_loader.load(new URLRequest(url));
		}

		public function play():void
		{
			if (_sound.hasEventListener(SampleDataEvent.SAMPLE_DATA)==false) {
				_sound.addEventListener(SampleDataEvent.SAMPLE_DATA, sampleDataHandler);
			}
			_sound.play();
		}

		public function stop():void
		{
			if (_sound.hasEventListener(SampleDataEvent.SAMPLE_DATA)) {
				_sound.removeEventListener(SampleDataEvent.SAMPLE_DATA, sampleDataHandler);
			}
		}


		/* handler */

		private function loadWaveCompleteHandler(event:Event):void
		{
			_data = WaveSound.parseWaveData(ByteArray(URLLoader(event.target).data));
			if (_data!=null && _data['fmt']!=null && _data['data']!=null) {
				_data['info'] = new Object();
				_data['info']['chloop'] = _data['fmt']['channel']==1 ? 2 : 1;
				_data['info']['duration'] = _data['data']['chunksize'] / (_data['fmt']['blockalign'] / _data['fmt']['channel']) / (_data['fmt']['samplerate'] / 44100);
				duration = _data['info']['duration'];
				dispatchEvent(new Event(WaveSound.LOAD_COMPLETE));
			} else {
				dispatchEvent(new Event(WaveSound.FORMAT_ERROR));
			}
		}

		private function sampleDataHandler(event:SampleDataEvent):void
		{
			_buffer = new ByteArray();
			var buf_p:uint = 0;
			var local_p:uint = 0;
			position -= position%_data['fmt']['blockalign'];
			while (buf_p<buffer_size && position+buf_p<_data['info']['duration']) {
				local_p = (position + buf_p) * (_data['fmt']['samplerate'] / 44100);
				local_p -= local_p%_data['fmt']['blockalign'];
				for (var i:int=0; i<_data['info']['chloop']; i++) {
					_buffer.writeFloat(_data['data']['data'][local_p] * _volume);
				}
				buf_p++;
			}
			position += buf_p;
			if (0<_buffer.length) {
				event.data.writeBytes(_buffer);
				dispatchEvent(new Event(WaveSound.SAMPLE_DATA));
			}
			if (_buffer.length==0 || _data['info']['duration'] <= position) {
				stop();
				dispatchEvent(new Event(WaveSound.SOUND_COMPLETE));
			}
		}


		/* setter getter */

		public function get data():Object
		{
			return _data;
		}

		public function get buffer():ByteArray
		{
			return _buffer;
		}

		[Bindable] public function set volume(value:Number):void
		{
			if (value<0) {
				value = 0;
			}
			if (1<value) {
				value = 1;
			}
			_volume = value;
		}

		public function get volume():Number
		{
			return _volume;
		}


		/* static */

		public static function parseWaveData(data:ByteArray):Object
		{
			data.endian = Endian.LITTLE_ENDIAN;
			data.position = 0;
			var result:Object = new Object();
			result['header'] = new Object();
			var riff:String = data.readUTFBytes(4);
			result['header']['filesize'] = data.readUnsignedInt();
			var filetype:String = data.readUTFBytes(4);
			if (riff=='RIFF' && filetype=='WAVE') {
				while (data.position+4 <= data.length) {
					var type:String = data.readUTFBytes(4);
					switch (type) {
					case 'fmt ':
						result['fmt'] = new Object();
						result['fmt']['chunksize'] = data.readUnsignedInt();
						result['fmt']['formatid'] = data.readUnsignedShort();
						result['fmt']['channel'] = data.readUnsignedShort();
						result['fmt']['samplerate'] = data.readUnsignedInt();
						result['fmt']['byterate'] = data.readUnsignedInt();
						result['fmt']['blockalign'] = data.readUnsignedShort();
						result['fmt']['bitspersample'] = data.readUnsignedShort();
						if (16 < result['fmt']['chunksize']) {
							result['fmt']['extrasize'] = data.readUnsignedShort();
							result['fmt']['extra'] = data.readUTFBytes(result['fmt']['extrasize']);
						}
						break;
/*
					case 'fact':
						result['fact'] = new Object();
						result['fact']['chunksize'] = data.readUnsignedInt();
						result['fact']['data'] = data.readUTFBytes(result['fact']['chunksize']);
						break;
*/
					case 'data':
						result['data'] = new Object();
						result['data']['chunksize'] = data.readUnsignedInt();
						result['data']['data'] = new Array();
						var i:int;
						switch (result['fmt']['bitspersample']) {
						case 8:
							for (i=0; i<result['data']['chunksize']; i++) {
								result['data']['data'].push(data.readByte() / 128);
							}
							break;
						case 16:
							for (i=0; i<result['data']['chunksize']/2; i++) {
								result['data']['data'].push(data.readShort() / 32768);
							}
							break;
						default:
							var bit:uint = result['fmt']['bitspersample'];
							var byte:uint = result['fmt']['bitspersample'] / 8;
							var minus_value:Number = 1 << (bit - 1);
							for (i=0; i<result['data']['chunksize']/byte; i++) {
								var j:int;
								var value:Number = 0;
								for (j=0; j<byte; j++) {
									value += data.readUnsignedByte() << (8 * j);
								}
								if (minus_value <= value) {
									value -= minus_value + minus_value;
								}
								value /= minus_value + minus_value;
								result['data']['data'].push(value);
							}
							break;
						}
						break;
/*
					case 'LIST':
						result['list'] = new Object();
						result['list']['chunksize'] = data.readUnsignedInt();
						result['list']['info'] = data.readUTFBytes(4);
						while (result['list']['chunksize'] <= data.length) {
							var fourcc:String = StringUtil.trim(data.readUTFBytes(4));
							Alert.show(fourcc);
							result['list'][fourcc] = new Object();
							result['list'][fourcc]['chunksize'] = data.readUnsignedInt();
							result['list'][fourcc]['data'] = new ByteArray();
							result['list'][fourcc]['data'].writeBytes(data, data.position, uint(result['list'][fourcc]['chunksize']));
							data.position += result['list'][fourcc]['chunksize'];
							result['list'][fourcc]['data'].position = 0;
						}
						break;
*/
					default:
						break;
					}
					if (data.position%2==1) {
						data.position++;
					}
				}
			}
			return result;
		}
	}
}

 

wave_client.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" applicationComplete="init()">
	<mx:Script>
		<![CDATA[
			import flash.events.*;
			import mx.controls.Alert;
			import mx.utils.ObjectUtil;
			import jp.eth0.WaveSound;


			/* var */

			[Bindable] private var wave:WaveSound;
			[Bindable] private var loaded:Boolean = false;
			private var startTime:Number;


			/* init */

			private function init():void
			{
				startTime = (new Date()).getTime();
				debug("[init]");
				debug("StartTime : " + startTime);
				wave = new WaveSound();
				wave.addEventListener(WaveSound.LOAD_COMPLETE, loadCompleteHandler);
				wave.addEventListener(WaveSound.SOUND_COMPLETE, soundCompleteHandler);
				wave.addEventListener(WaveSound.SAMPLE_DATA, sampleDataHandler)
				wave.load("test_mono_8bit_4000hz.wav");
			}

			private function loadCompleteHandler(event:Event):void
			{
				loaded = true;
				var endtime:Number = (new Date()).getTime();
				debug("[loadCompleteHandler]");
				debug("EndTime : " + endtime);
				debug("Total : " + ((endtime - startTime) / 1000));
				debug(wave.data['fmt']);
			}

			private function soundCompleteHandler(event:Event):void
			{
				debug("[soundCompleteHandler]");
			}

			private function sampleDataHandler(event:Event):void
			{
				if (wave.position%50==0) {
					buffer_area.text = '';
					wave.buffer.position = 0;
					while (wave.buffer.position+4 <= wave.buffer.length) {
						buffer_area.text += wave.buffer.readFloat() + "\n";
						wave.buffer.position += 12;
					}
				}
			}

			private function debug(str:*):void
			{
				debug_area.text += ((str is String) ? str : ObjectUtil.toString(str)) + "\n";
			}
		]]>
	</mx:Script>

	<mx:VBox width="100%" height="100%">
		<mx:HBox width="100%">
			<mx:HSlider width="100%" value="{wave.position}" maximum="{wave.duration}" dataTipPrecision="0" change="{wave.position=event.value;}" enabled="{loaded}"/>
			<mx:Label text="{wave.position} / {wave.duration}"/>
		</mx:HBox>
		<mx:HBox>
			<mx:Button label="Play" click="{wave.play(); debug('manual start');}" enabled="{loaded}"/>
			<mx:Button label="Stop" click="{wave.stop(); debug('manual stop');}" enabled="{loaded}"/>
			<mx:HSlider value="{wave.volume}" maximum="1" dataTipPrecision="0" change="{wave.volume=event.value; debug('change volume : '+event.value);}" enabled="{loaded}"/>
			<mx:Label text="{wave.volume}"/>
		</mx:HBox>
		<mx:HBox width="100%" height="100%">
			<mx:TextArea id="debug_area" width="100%" height="100%"/>
			<mx:TextArea id="buffer_area" width="100%" height="100%"/>
		</mx:HBox>
	</mx:VBox>

</mx:Application>

 

コンパイル

target-playerを指定する。

mxmlc -target-player=10.0.12 wave_client.mxml