diff --git a/examples/inspector/inspector.html b/examples/inspector/inspector.html
index eb41c803f..9c077371d 100644
--- a/examples/inspector/inspector.html
+++ b/examples/inspector/inspector.html
@@ -14,6 +14,7 @@
+
diff --git a/lib/mp3/mp3.js b/lib/mp3/mp3.js
new file mode 100644
index 000000000..923e93b1c
--- /dev/null
+++ b/lib/mp3/mp3.js
@@ -0,0 +1 @@
+// placeholder for MP3Decoder class
diff --git a/src/flash/display/Loader.js b/src/flash/display/Loader.js
index 5f43fa8d7..a26eeac71 100644
--- a/src/flash/display/Loader.js
+++ b/src/flash/display/Loader.js
@@ -3,6 +3,7 @@ var LoaderDefinition = (function () {
var LOADER_PATH = 'flash/display/Loader.js';
var WORKER_SCRIPTS = [
'../../../lib/DataView.js/DataView.js',
+ '../../../lib/mp3/mp3.js',
'../util.js',
diff --git a/src/flash/media/Sound.js b/src/flash/media/Sound.js
index def50f64e..a29277f92 100644
--- a/src/flash/media/Sound.js
+++ b/src/flash/media/Sound.js
@@ -1,9 +1,17 @@
+var PLAY_USING_AUDIO_TAG = true;
+
var SoundDefinition = (function () {
var audioElement = null;
function getAudioDescription(soundData, onComplete) {
audioElement = audioElement || document.createElement('audio');
+ if (!audioElement.canPlayType(soundData.mimeType)) {
+ onComplete({
+ duration: 0
+ });
+ return;
+ }
audioElement.src = "data:" + soundData.mimeType + ";base64," + base64ArrayBuffer(soundData.data);
audioElement.load();
audioElement.addEventListener("loadedmetadata", function () {
@@ -23,8 +31,19 @@ var SoundDefinition = (function () {
this._id3 = new flash.media.ID3Info();
var s = this.symbol;
- if (s && s.packaged) {
- var soundData = s.packaged;
+ if (s) {
+ var soundData = {};
+ if (s.pcm) {
+ soundData.sampleRate = s.sampleRate;
+ soundData.channels = s.channels;
+ soundData.pcm = s.pcm;
+ soundData.end = s.pcm.length;
+ }
+ soundData.completed = true;
+ if (s.packaged) {
+ soundData.data = s.packaged.data.buffer;
+ soundData.mimeType = s.packaged.mimeType;
+ }
var _this = this;
getAudioDescription(soundData, function (description) {
_this._length = description.duration;
@@ -48,27 +67,68 @@ var SoundDefinition = (function () {
}
var _this = this;
- var loader = this._loader = new flash.net.URLLoader(request);
- loader.dataFormat = "binary";
+ var stream = this._stream = new flash.net.URLStream();
+ var ByteArrayClass = avm2.systemDomain.getClass("flash.utils.ByteArray");
+ var data = ByteArrayClass.createInstance();
+ var dataPosition = 0;
+ var mp3DecodingSession = null;
+ var soundData = { completed: false };
- loader.addEventListener("progress", function (event) {
+ stream.addEventListener("progress", function (event) {
console.info("PROGRESS");
+ _this._bytesLoaded = event.public$bytesLoaded;
+ _this._bytesTotal = event.public$bytesTotal;
+
+ if (!PLAY_USING_AUDIO_TAG && !mp3DecodingSession) {
+ // initialize MP3 decoding
+ mp3DecodingSession = decodeMP3(soundData, function ondurationchanged(duration) {
+ if (_this._length === 0) {
+ // once we have some data, trying to play it
+ _this._soundData = soundData;
+
+ _this._playQueue.forEach(function (item) {
+ item.channel._playSoundDataViaChannel(soundData, item.startTime);
+ });
+ }
+ // TODO estimate duration based on bytesTotal
+ _this._length = duration * 1000;
+ });
+ }
+
+ var bytesAvailable = stream.bytesAvailable;
+ stream.readBytes(data, dataPosition, bytesAvailable);
+ if (mp3DecodingSession) {
+ mp3DecodingSession.pushData(data, dataPosition, bytesAvailable);
+ }
+ dataPosition += bytesAvailable;
+
_this.dispatchEvent(event);
});
- loader.addEventListener("complete", function (event) {
+ stream.addEventListener("complete", function (event) {
_this.dispatchEvent(event);
- var soundData = _this._soundData = {
- data: loader.data.a,
- mimeType: 'audio/mpeg'
- };
- getAudioDescription(soundData, function (description) {
- _this._length = description.duration;
- });
- _this._playQueue.forEach(function (item) {
- playChannel(soundData, item.channel, item.startTime, item.soundTransform);
- });
+ soundData.data = data.a;
+ soundData.mimeType = 'audio/mpeg';
+ soundData.completed = true;
+
+ if (PLAY_USING_AUDIO_TAG) {
+ _this._soundData = soundData;
+
+ getAudioDescription(soundData, function (description) {
+ _this._length = description.duration;
+ });
+
+ _this._playQueue.forEach(function (item) {
+ item.channel._playSoundDataViaAudio(soundData, item.startTime);
+ });
+ }
+
+ if (mp3DecodingSession) {
+ mp3DecodingSession.close();
+ }
});
+
+ stream.load(request);
},
loadCompressedDataFromByteArray: function loadCompressedDataFromByteArray(bytes, bytesLength) {
throw 'Not implemented: loadCompressedDataFromByteArray';
@@ -82,22 +142,25 @@ var SoundDefinition = (function () {
startTime = startTime || 0;
var channel = new flash.media.SoundChannel();
channel._sound = this;
+ channel._soundTransform = soundTransform;
this._playQueue.push({
channel: channel,
- startTime: startTime,
- soundTransform: soundTransform
+ startTime: startTime
});
if (this._soundData) {
- playChannel(this._soundData, channel, startTime, soundTransform);
+ if (PLAY_USING_AUDIO_TAG)
+ channel._playSoundDataViaAudio(this._soundData, startTime);
+ else
+ channel._playSoundDataViaChannel(this._soundData, startTime);
}
return channel;
},
get bytesLoaded() {
- return this._loader.bytesLoaded;
+ return this._bytesLoaded;
},
get bytesTotal() {
- return this._loader.bytesTotal;
+ return this._bytesTotal;
},
get id3() {
return this._id3;
@@ -116,13 +179,59 @@ var SoundDefinition = (function () {
}
};
- function playChannel(soundData, channel, startTime, soundTransform) {
- var element = channel._element;
- element.src = "data:" + soundData.mimeType + ";base64," + base64ArrayBuffer(soundData.data);
- element.addEventListener("loadeddata", function loaded() {
- element.currentTime = startTime / 1000;
- element.play();
- });
+ // TODO send to MP3 decoding worker
+ function decodeMP3(soundData, ondurationchanged) {
+ var currentSize = 8000;
+ var pcm = new Float32Array(currentSize);
+ var position = 0;
+ var mp3Decoder = new MP3Decoder();
+ mp3Decoder.onframedata = function (frame, channels, sampleRate) {
+ if (frame.length === 0)
+ return;
+ if (!position) {
+ // first data: initializes pcm data fields
+ soundData.sampleRate = sampleRate,
+ soundData.channels = channels;
+ soundData.pcm = pcm;
+ }
+ if (position + frame.length >= currentSize) {
+ do {
+ currentSize *= 2;
+ } while (position + frame.length >= currentSize);
+ var newPcm = new Float32Array(currentSize);
+ newPcm.set(pcm);
+ pcm = soundData.pcm = newPcm;
+ }
+ pcm.set(frame, position);
+ soundData.end = position += frame.length;
+
+ var duration = position / soundData.sampleRate / soundData.channels;
+ ondurationchanged(duration);
+ };
+ return {
+ chunks: [],
+ pushData: function (data, offset, length) {
+ function decodeNext() {
+ var chunk = chunks.shift();
+ mp3Decoder.push(chunk);
+ if (chunks.length > 0)
+ setTimeout(decodeNext);
+ }
+ var chunks = this.chunks;
+ var initPush = chunks.length === 0;
+ var maxChunkLength = 8000;
+ for (var i = 0; i < length; i += maxChunkLength) {
+ var chunkOffset = offset + i;
+ var chunkLength = Math.min(length - chunkOffset, maxChunkLength);
+ var chunk = new Uint8Array(data.a, chunkOffset, chunkLength);
+ chunks.push(chunk);
+ }
+ if (initPush)
+ decodeNext();
+ },
+ close: function () {
+ }
+ };
}
var desc = Object.getOwnPropertyDescriptor;
diff --git a/src/flash/media/SoundChannel.js b/src/flash/media/SoundChannel.js
index 351f96ac5..93c2a6d8a 100644
--- a/src/flash/media/SoundChannel.js
+++ b/src/flash/media/SoundChannel.js
@@ -2,7 +2,56 @@ const SoundChannelDefinition = (function () {
return {
// ()
initialize: function () {
- this._element = document.createElement('audio');
+ this._element = null;
+ this._position = 0;
+ this._pcmData = null;
+ this._soundTransform = null;
+ },
+ _playSoundDataViaChannel: function (soundData, startTime) {
+ assert(soundData.pcm, 'no pcm data found');
+
+ var self = this;
+ var position = Math.round(startTime / 1000 * soundData.sampleRate) *
+ soundData.channels;
+ this._position = startTime;
+ this._audioChannel = createAudioChannel(soundData.sampleRate, soundData.channels);
+ this._audioChannel.ondatarequested = function (e) {
+ var end = soundData.end;
+ if (position >= end && soundData.completed) {
+ // end of buffer
+ self._audioChannel.stop();
+ return;
+ }
+ // TODO loop
+
+ var count = Math.min(end - position, e.count);
+ if (count === 0) return;
+
+ var data = e.data;
+ var source = soundData.pcm;
+ for (var j = 0; j < count; j++) {
+ data[j] = source[position++];
+ }
+
+ self._position = position / soundData.sampleRate / soundData.channels * 1000;
+ };
+ this._audioChannel.start();
+ },
+ _playSoundDataViaAudio: function (soundData, startTime) {
+ this._position = startTime;
+ var self = this;
+ var element = document.createElement('audio');
+ if (!element.canPlayType(soundData.mimeType))
+ error('\"' + soundData.mimeType + '\" type playback is not supported by the browser');
+ element.src = "data:" + soundData.mimeType + ";base64," + base64ArrayBuffer(soundData.data);
+ element.addEventListener("loadeddata", function loaded() {
+ element.currentTime = startTime / 1000;
+ element.play();
+ });
+ element.addEventListener("timeupdate", function timeupdate() {
+ self._position = element.currentTime * 1000;
+ });
+ this._element = element;
},
__glue__: {
native: {
@@ -11,16 +60,207 @@ const SoundChannelDefinition = (function () {
instance: {
// (void) -> void
stop: function stop() {
- this._element.pause();
+ if (this._element) {
+ this._element.pause();
+ }
+ if (this._audioChannel) {
+ this._audioChannel.stop();
+ }
},
"position": {
// (void) -> Number
get: function position() {
- return this._element.currentTime * 1000;
+ return this._position;
+ }
+ },
+ "soundTransform": {
+ get: function soundTransform() {
+ return this._soundTransform;
+ },
+ set: function soundTransform(val) {
+ this._soundTransform = val;
}
}
}
}
}
};
-}).call(this);
\ No newline at end of file
+}).call(this);
+
+function createAudioChannel(sampleRate, channels) {
+ if (WebAudioChannel.isSupported)
+ return new WebAudioChannel(sampleRate, channels);
+ else if (AudioDataChannel.isSupported)
+ return new AudioDataChannel(sampleRate, channels);
+ else
+ error('PCM data playback is not supported by the browser');
+}
+
+// Resample sound using linear interpolation for Web Audio due to
+// http://code.google.com/p/chromium/issues/detail?id=73062
+function AudioResampler(sourceRate, targetRate) {
+ this.sourceRate = sourceRate;
+ this.targetRate = targetRate;
+ this.tail = [];
+ this.sourceOffset = 0;
+}
+AudioResampler.prototype = {
+ ondatarequested: function (e) { },
+ getData: function (channelsData, count) {
+ var k = this.sourceRate / this.targetRate;
+
+ var offset = this.sourceOffset;
+ var needed = Math.ceil((count - 1) * k + offset) + 1;
+ var sourceData = [];
+ for (var channel = 0; channel < channelsData.length; channel++)
+ sourceData.push(new Float32Array(needed));
+ var e = { data: sourceData, count: needed };
+ this.ondatarequested(e);
+ for (var channel = 0; channel < channelsData.length; channel++) {
+ var data = channelsData[channel];
+ var source = sourceData[channel];
+ for (var j = 0; j < count; j++) {
+ var i = j * k + offset;
+ var i1 = Math.floor(i), i2 = Math.ceil(i);
+ var source_i1 = i1 < 0 ? this.tail[channel] : source[i1];
+ if (i1 === i2) {
+ data[j] = source_i1;
+ } else {
+ var alpha = i - i1;
+ data[j] = source_i1 * (1 - alpha) + source[i2] * alpha;
+ }
+ }
+ this.tail[channel] = source[needed - 1];
+ }
+ this.sourceOffset = ((count - 1) * k + offset) - (needed - 1);
+ }
+};
+
+function WebAudioChannel(sampleRate, channels) {
+ var context = WebAudioChannel.context;
+ if (!context) {
+ if (typeof AudioContext !== 'undefined')
+ context = new AudioContext();
+ else
+ context = new webkitAudioContext();
+ WebAudioChannel.context = context;
+ }
+ this.context = context;
+ this.contextSampleRate = context.sampleRate || 44100;
+
+ this.channels = channels;
+ this.sampleRate = sampleRate;
+ if (this.contextSampleRate != sampleRate) {
+ this.resampler = new AudioResampler(sampleRate, this.contextSampleRate);
+ this.resampler.ondatarequested = function (e) {
+ this.requestData(e.data, e.count);
+ }.bind(this);
+ }
+}
+WebAudioChannel.prototype = {
+ start: function () {
+ var source = this.context.createScriptProcessor ?
+ this.context.createScriptProcessor(2048, 0, this.channels) :
+ this.context.createJavaScriptNode(2048, 0, this.channels);
+ var self = this;
+ source.onaudioprocess = function(e) {
+ var channelsData = [];
+ for (var i = 0; i < self.channels; i++)
+ channelsData.push(e.outputBuffer.getChannelData(i));
+ var count = channelsData[0].length;
+ if (self.resampler) {
+ self.resampler.getData(channelsData, count);
+ } else {
+ var e = { data: channelsData, count: count };
+ self.requestData(channelsData, count);
+ }
+ };
+
+ source.connect(this.context.destination);
+ this.source = source;
+ },
+ stop: function () {
+ this.source.disconnect(this.context.destination);
+ },
+ requestData: function (channelsData, count) {
+ var channels = this.channels;
+ var buffer = new Float32Array(count * channels);
+ var e = { data: buffer, count: buffer.length };
+ this.ondatarequested(e);
+
+ for (var j = 0, p = 0; j < count; j++) {
+ for (var i = 0; i < channels; i++)
+ channelsData[i][j] = buffer[p++];
+ }
+ }
+};
+WebAudioChannel.isSupported = (function() {
+ return typeof AudioContext !== 'undefined' ||
+ typeof webkitAudioContext != 'undefined';
+})();
+
+// from https://wiki.mozilla.org/Audio_Data_API
+function AudioDataChannel(sampleRate, channels) {
+ this.sampleRate = sampleRate;
+ this.channels = channels;
+}
+AudioDataChannel.prototype = {
+ start: function () {
+ var sampleRate = this.sampleRate;
+ var channels = this.channels;
+ var self = this;
+
+ // Initialize the audio output.
+ var audio = new Audio();
+ audio.mozSetup(channels, sampleRate);
+
+ var currentWritePosition = 0;
+ var prebufferSize = sampleRate * channels / 2; // buffer 500ms
+ var tail = null, tailPosition;
+
+ // The function called with regular interval to populate
+ // the audio output buffer.
+ this.interval = setInterval(function() {
+ var written;
+ // Check if some data was not written in previous attempts.
+ if(tail) {
+ written = audio.mozWriteAudio(tail.subarray(tailPosition));
+ currentWritePosition += written;
+ tailPosition += written;
+ if(tailPosition < tail.length) {
+ // Not all the data was written, saving the tail...
+ return; // ... and exit the function.
+ }
+ tail = null;
+ }
+
+ // Check if we need add some data to the audio output.
+ var currentPosition = audio.mozCurrentSampleOffset();
+ var available = currentPosition + prebufferSize - currentWritePosition;
+ available -= available % channels; // align to channels count
+ if(available > 0) {
+ // Request some sound data from the callback function.
+ var soundData = new Float32Array(available);
+ self.requestData(soundData, available);
+
+ // Writting the data.
+ written = audio.mozWriteAudio(soundData);
+ if(written < soundData.length) {
+ // Not all the data was written, saving the tail.
+ tail = soundData;
+ tailPosition = written;
+ }
+ currentWritePosition += written;
+ }
+ }, 100);
+ },
+ stop: function () {
+ clearInterval(this.interval);
+ },
+ requestData: function (data, count) {
+ this.ondatarequested({data: data, count: count});
+ }
+};
+AudioDataChannel.isSupported = (function () {
+ return 'mozSetup' in (new Audio);
+})();
diff --git a/src/swf/sound.js b/src/swf/sound.js
index 72385f1fc..20b48f6a7 100644
--- a/src/swf/sound.js
+++ b/src/swf/sound.js
@@ -63,7 +63,7 @@ function defineSound(tag, dictionary) {
case SOUND_FORMAT_PCM_BE:
if (tag.soundSize == SOUND_SIZE_16_BIT) {
for (var i = 0, j = 0; i < pcm.length; i++, j += 2)
- pcm[i] = ((data[i] << 24) | (data[i + 1] << 16)) / 2147483648;
+ pcm[i] = ((data[j] << 24) | (data[j + 1] << 16)) / 2147483648;
packaged = packageWave(data, sampleRate, channels, 16, true);
} else {
for (var i = 0; i < pcm.length; i++)
@@ -74,7 +74,7 @@ function defineSound(tag, dictionary) {
case SOUND_FORMAT_PCM_LE:
if (tag.soundSize == SOUND_SIZE_16_BIT) {
for (var i = 0, j = 0; i < pcm.length; i++, j += 2)
- pcm[i] = ((data[i + 1] << 24) | (data[i] << 16)) / 2147483648;
+ pcm[i] = ((data[j + 1] << 24) | (data[j] << 16)) / 2147483648;
packaged = packageWave(data, sampleRate, channels, 16, false);
} else {
for (var i = 0; i < pcm.length; i++)
@@ -153,7 +153,7 @@ function SwfSoundStream_decode_PCM(data) {
function SwfSoundStream_decode_PCM_be(data) {
var pcm = new Float32Array(data.length / 2);
for (var i = 0, j = 0; i < pcm.length; i++, j += 2)
- pcm[i] = ((data[i] << 24) | (data[i + 1] << 16)) / 2147483648;
+ pcm[i] = ((data[j] << 24) | (data[j + 1] << 16)) / 2147483648;
this.currentSample += pcm.length / this.channels;
return {
pcm: pcm,
@@ -164,7 +164,7 @@ function SwfSoundStream_decode_PCM_be(data) {
function SwfSoundStream_decode_PCM_le(data) {
var pcm = new Float32Array(data.length / 2);
for (var i = 0, j = 0; i < pcm.length; i++, j += 2)
- pcm[i] = ((data[i + 1] << 24) | (data[i] << 16)) / 2147483648;
+ pcm[i] = ((data[j + 1] << 24) | (data[j] << 16)) / 2147483648;
this.currentSample += pcm.length / this.channels;
return {
pcm: pcm,