AS3(Flash)でAAC/M4A/OGGのPCMデータを扱う裏技

FlashのSoundクラスではAAC形式のサウンドを扱えません(NetStreamでは扱えます)。これで何が困るかというと、extractメソッドによるPCMデータ取得ができないのです。Any Beatsではこの問題に随分と悩まされてきました。(普段M4Aを使ってる人たちから音楽ファイルが表示されないぜ的なクレームとか)

そこで、色々悩んだ結果、ブラウザのWeb Audio APIを使う方法を思いつきました。Web Audio APIをJSから叩くと、Chromeやsafari(Mac)ではAACを扱えます。ChromeではOGGも使えます。JSとAS3のデータの受け渡しが非常に重いのが難ですが。。。

以下抜粋。(JSのBase64部分はご自身で補完してください)

JS

var decodeIntervalID = 0;
var audioContext;
if (typeof AudioContext != "undefined") audioContext = new AudioContext();
else if (typeof webkitAudioContext != "undefined") audioContext = new webkitAudioContext();
if(audioContext){
	decodeSound = function(base64){
		if(decodeIntervalID > 0) clearInterval(decodeIntervalID);
		var swf = getSWF("***");
		if(!swf) return false;
		var bytes = Base64Binary.decodeArrayBuffer(base64);
		base64 = null;
		var sendFunc = function(buffer) {
			try{
				var isStereo = buffer.numberOfChannels >= 2;
				var leftArray = buffer.getChannelData(0);
				var rightArray;
				if(isStereo) rightArray = buffer.getChannelData(1);
					
				var i = 0;
				var len = leftArray.length;
				var sampleRate = buffer.sampleRate > 44100 ? 44100 : buffer.sampleRate;
				var downSamplingStep = -1;
				var downSampling = 1;
				if(buffer.sampleRate > 44100){
					downSamplingStep = (1/((buffer.sampleRate-44100)/buffer.sampleRate));
					downSampling = 44100 / buffer.sampleRate;
				}
				swf.decodeSoundStart(len);
				var nextSkip = downSamplingStep;
				var nextSkipInt = Math.floor(nextSkip);
				decodeIntervalID = setInterval(function(){
					if(i >= len){
						clearInterval(decodeIntervalID);
						swf.decodeSoundComplete(sampleRate, buffer.numberOfChannels);
						decodeIntervalID = 0;
						buffer = leftArray = rightArray = null;
						return;
					}
					var resultBuffer = new ArrayBuffer(100000 * 4);
					var result = new Float32Array(resultBuffer);
					if(isStereo){
						for(j = 0; i < len, j < 100000; i++){
							if(i == nextSkipInt){
								nextSkip += downSamplingStep;
								nextSkipInt = Math.floor(nextSkip);
								continue;
							}
							result[j++] = leftArray[i] * downSampling;
							result[j++] = rightArray[i] * downSampling;
						}
					}
					else{
						for(j = 0; i < len, j < 100000; i++){
							if(i == nextSkipInt){
								nextSkip += downSamplingStep;
								nextSkipInt = Math.floor(nextSkip);
								continue;
							}
							result[j++] = leftArray[i] * downSampling;
						}
					}
					swf.decodeSoundAdd(base64ArrayBuffer(resultBuffer));
				}, 50);
			}
			catch(e){ swf.decodeSoundError(); }
		};
		if(audioContext.decodeAudioData) {
			audioContext.decodeAudioData(bytes, sendFunc, function(e){
				swf.decodeSoundError();
			});
		}
		else{
			try{
				var b = audioContext.createBuffer(bytes, false);
				sendFunc(b);
			}
			catch(e){ alert(e);swf.decodeSoundError(); }
		}
		bytes = null;
		return true;
	};
}
function getSWF(swf_name) {
	if (navigator.appName.indexOf("Microsoft") != -1) {
		return window[swf_name]
	}
	else {
		return document[swf_name]
	}
}

AS3


ExternalInterface.addCallback("decodeSoundStart", _decodeSoundStart);
ExternalInterface.addCallback("decodeSoundAdd", _decodeSoundAdd);
ExternalInterface.addCallback("decodeSoundComplete", _decodeSoundComplete);
ExternalInterface.addCallback("decodeSoundError", _decodeSoundError);

private var _pcmBytes:ByteArray;
private function _decodeSoundStart(len:uint):void {
	if (_pcmBytes) _pcmBytes.clear();
	else _pcmBytes = new ByteArray();
	_pcmBytes.endian = Endian.LITTLE_ENDIAN;
}
private function _decodeSoundAdd(base64:String):void {
	var i:int;
	var bytes:ByteArray = Base64.decodeToByteArray(base64);
	var len:uint = bytes.length / 4;
	if (len > 0) {
		bytes.endian = Endian.LITTLE_ENDIAN;
		bytes.position = 0;
		for (i = 0; i < len; i++) {
			_pcmBytes.writeShort(bytes.readFloat() * 0x7fff);
		}
	}
}
private function _decodeSoundComplete(rate:uint, channels:uint):void {
	_pcmBytes.position = 0;
	
	var bits:uint = 16;
	
	var wave:ByteArray = new ByteArray();
	wave.endian = Endian.LITTLE_ENDIAN;
	wave.writeUTFBytes("RIFF");
	wave.writeInt( uint( _pcmBytes.length + 44 ) );
	wave.writeUTFBytes("WAVE");
	wave.writeUTFBytes("fmt ");
	wave.writeInt( uint( 16 ) );
	wave.writeShort( uint( 1 ) );
	wave.writeShort( channels );
	wave.writeInt( rate );
	wave.writeInt( uint( rate * channels * ( bits >> 3 ) ) );
	wave.writeShort( uint( channels * ( bits >> 3 ) ) );
	wave.writeShort( bits );
	wave.writeUTFBytes("data");
	wave.writeInt( _pcmBytes.length );
	wave.writeBytes( _pcmBytes );
	wave.position = 0;
	
	// use wave
}

private function _decodeSoundError():void 
{
	// error
}

このコードはAny Beatsで実際に使用しています。もっと効率の良い方法があれば教えて欲しいです><

コメントを残す

メールアドレスが公開されることはありません。

次のHTML タグと属性が使えます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>