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回繰り返すから、方形波に近い波形でとんでもなく悪い。
フーリエ変換でもすればよくなるだろうけど、そこまでの気力はない。
対応フォーマット
チャンネル数
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>