Skip to content
Permalink
Browse files

initial audio support via Flash fallback (for browsers without an aud…

…io API)
  • Loading branch information...
maikmerten committed Feb 17, 2014
1 parent b5162ff commit 5a39fec2e0c2bf93af552e90d2d935df5d53f5a0
Showing with 213 additions and 43 deletions.
  1. +1 −0 Makefile
  2. +154 −43 src/AudioFeeder.js
  3. +58 −0 src/dynamicaudio.as
  4. BIN src/dynamicaudio.swf
@@ -29,6 +29,7 @@ build/demo/index.html : src/demo/index.html src/demo/demo.css src/demo/demo.js s
cp src/demo/demo.css build/demo/demo.css
cp src/demo/demo.js build/demo/demo.js
cp src/demo/motd.js build/demo/motd.js
cp src/dynamicaudio.swf build/demo/dynamicaudio.swf

test -d build/demo/lib || mkdir build/demo/lib
cp src/StreamFile.js build/demo/lib/StreamFile.js
@@ -9,21 +9,14 @@ function AudioFeeder(channels, rate) {

var AudioContext = window.AudioContext || window.webkitAudioContext;
if (!AudioContext) {
// stub it out
// use Flash fallback
console.log("No W3C Web Audio API available");
this.bufferData = function(samplesPerChannel) {};
this.close = function() {};
this.mute = function() {};
this.unmute = function() {};
this.stub = true;
return;
this.flashaudio = new DynamicAudio();
}


var context = new AudioContext(),
bufferSize = 1024,
node;


var bufferSize = 1024;

function freshBuffer() {
var buffer = [];
for (var channel = 0; channel < channels; channel++) {
@@ -33,16 +26,21 @@ function AudioFeeder(channels, rate) {
}

var buffers = [],
context,
node,
pendingBuffer = freshBuffer(),
pendingPos = 0,
muted = false;

if (context.createScriptProcessor) {
node = context.createScriptProcessor(bufferSize, 0, channels)
} else if (context.createJavaScriptNode) {
node = context.createJavaScriptNode(bufferSize, 0, channels)
} else {
throw new Error("Bad version of web audio API?");

if(AudioContext) {
context = new AudioContext;
if (context.createScriptProcessor) {
node = context.createScriptProcessor(bufferSize, 0, channels)
} else if (context.createJavaScriptNode) {
node = context.createJavaScriptNode(bufferSize, 0, channels)
} else {
throw new Error("Bad version of web audio API?");
}
}

function popNextBuffer() {
@@ -54,50 +52,74 @@ function AudioFeeder(channels, rate) {
}
}

node.onaudioprocess = function(event) {
var inputBuffer = popNextBuffer(bufferSize);
if (!muted && inputBuffer) {
for (var channel = 0; channel < channels; channel++) {
var input = inputBuffer[channel],
output = event.outputBuffer.getChannelData(channel);
for (var i = 0; i < Math.min(bufferSize, input.length); i++) {
output[i] = input[i];
if(node) {
node.onaudioprocess = function(event) {
var inputBuffer = popNextBuffer(bufferSize);
if (!muted && inputBuffer) {
for (var channel = 0; channel < channels; channel++) {
var input = inputBuffer[channel],
output = event.outputBuffer.getChannelData(channel);
for (var i = 0; i < Math.min(bufferSize, input.length); i++) {
output[i] = input[i];
}
}
}
} else {
if (!inputBuffer) {
console.log("Starved for audio!");
}
for (var channel = 0; channel < channels; channel++) {
var output = event.outputBuffer.getChannelData(channel);
for (var i = 0; i < bufferSize; i++) {
output[i] = 0;
} else {
if (!inputBuffer) {
console.log("Starved for audio!");
}
for (var channel = 0; channel < channels; channel++) {
var output = event.outputBuffer.getChannelData(channel);
for (var i = 0; i < bufferSize; i++) {
output[i] = 0;
}
}
}
}
};
node.connect(context.destination);
};
node.connect(context.destination);
}

/**
* This is horribly naive and wrong.
* Replace me with a better algo!
*/
function resample(samples) {
if (rate == context.sampleRate) {
var targetRate = context.sampleRate;
if (rate == targetRate) {
return samples;
} else {
var newSamples = [];
for (var channel = 0; channel < channels; channel++) {
var input = samples[channel],
output = new Float32Array(Math.round(input.length * context.sampleRate / rate));
output = new Float32Array(Math.round(input.length * targetRate / rate));
for (var i = 0; i < output.length; i++) {
output[i] = input[Math.floor(i * rate / context.sampleRate)];
output[i] = input[Math.floor(i * rate / targetRate)];
}
newSamples.push(output);
}
return newSamples;
}
}

/**
* Resampling, scaling and reordering for the Flash fallback.
* The Flash fallback expects 44.1 kHz, stereo
* Resampling: This is horribly naive and wrong.
* TODO: Replace me with a better algo!
* TODO: Convert mono audio to stereo
*/
function resampleFlash(samples) {
var sampleincr = rate / 44100;
var samplecount = Math.floor(samples[0].length * (44100 / rate));
var newSamples = new Array(samplecount * channels);
for(var s = 0; s < samplecount; s++) {
var idx = Math.floor(s * sampleincr);
for(var c = 0; c < channels; ++c) {
newSamples[(s * channels) + c] = Math.floor(samples[c][idx] * 32768);
}
}
return newSamples;
}


function pushSamples(samples) {
var firstChannel = samples[0],
@@ -116,7 +138,10 @@ function AudioFeeder(channels, rate) {

var self = this;
this.bufferData = function(samplesPerChannel) {
if (buffers) {
if(this.flashaudio) {
var resamples = resampleFlash(samplesPerChannel);
if(resamples.length > 0) this.flashaudio.flashElement.write(resamples.join(' '));
} else if (buffers) {
samples = resample(samplesPerChannel);
pushSamples(samples);
} else {
@@ -142,3 +167,89 @@ function AudioFeeder(channels, rate) {
buffers = null;
};
}


/** Flash fallback **/

/*
The Flash fallback is based on https://github.com/an146/dynamicaudio.js
This is the contents of the LICENSE file:
Copyright (c) 2010, Ben Firshman
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* The names of its contributors may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/


function DynamicAudio(args) {
if (this instanceof arguments.callee) {
if (typeof this.init === "function") {
this.init.apply(this, (args && args.callee) ? args : arguments);
}
} else {
return new arguments.callee(arguments);
}
}


DynamicAudio.nextId = 1;

DynamicAudio.prototype = {
nextId: null,
swf: 'dynamicaudio.swf',

flashWrapper: null,
flashElement: null,

init: function(opts) {
var self = this;
self.id = DynamicAudio.nextId++;

if (opts && typeof opts['swf'] !== 'undefined') {
self.swf = opts['swf'];
}

self.flashWrapper = document.createElement('div');
self.flashWrapper.id = 'dynamicaudio-flashwrapper-'+self.id;
// Credit to SoundManager2 for this:
var s = self.flashWrapper.style;
s['position'] = 'fixed';
s['width'] = '11px'; // must be at least 6px for flash to run fast
s['height'] = '11px';
s['bottom'] = s['left'] = '0px';
s['overflow'] = 'hidden';
self.flashElement = document.createElement('div');
self.flashElement.id = 'dynamicaudio-flashelement-'+self.id;
self.flashWrapper.appendChild(self.flashElement);

document.body.appendChild(self.flashWrapper);

var id = self.flashElement.id;

self.flashWrapper.innerHTML = "<object id='"+id+"' width='10' height='10' type='application/x-shockwave-flash' data='"+self.swf+"' style='visibility: visible;'><param name='allowscriptaccess' value='always'></object>";
self.flashElement = document.getElementById(id);
},
};

@@ -0,0 +1,58 @@
package {
import flash.display.Sprite;
import flash.events.SampleDataEvent;
import flash.external.ExternalInterface;
import flash.media.Sound;

public class dynamicaudio extends Sprite {
public var bufferSize:Number = 2048; // In samples
public var sound:Sound = null;
public var buffer:Array = [];

public function dynamicaudio() {
ExternalInterface.addCallback('write', write);
}

// Called from JavaScript to add samples to the buffer
// Note we are using a space separated string of samples instead of an
// array. Flash's stupid ExternalInterface passes every sample as XML,
// which is incredibly expensive to encode/decode
public function write(s:String):void {
if (!this.sound) {
this.sound = new Sound();
this.sound.addEventListener(
SampleDataEvent.SAMPLE_DATA,
soundGenerator
);
this.sound.play();
}

var multiplier:Number = 1/32768;
for each (var sample:String in s.split(" ")) {
this.buffer.push(Number(sample)*multiplier);
}
}

public function soundGenerator(event:SampleDataEvent):void {
var i:int;

// If we haven't got enough data, write 2048 samples of silence to
// both channels, the minimum Flash allows
if (this.buffer.length < this.bufferSize*2) {
for (i = 0; i < 4096; i++) {
event.data.writeFloat(0.0);
}
return;
}

var count:Number = Math.min(this.buffer.length, 16384);

for each (var sample:Number in this.buffer.slice(0, count)) {
event.data.writeFloat(sample);
}

this.buffer = this.buffer.slice(count, this.buffer.length);
}
}
}

BIN +1.03 KB src/dynamicaudio.swf
Binary file not shown.

5 comments on commit 5a39fec

@brion

This comment has been minimized.

Copy link

replied Feb 17, 2014

Well I'll be darned, it works in IE 11. :)

Reminds me I should confirm I know how to compile an .as to a .swf ...

@maikmerten

This comment has been minimized.

Copy link
Owner Author

replied Feb 18, 2014

@brion

This comment has been minimized.

Copy link

replied Feb 18, 2014

I managed to rebuild the .swf with the Apache Flex SDK, and it still works. It's a pain in the ass to add the Flex toolchain though so it's probably easier to include the build artifact, but I'll throw in a handy wrapper script to rebuild it. Mwahahaha!

Command line to build is something like:
mxmlc -o build/dynamicaudio.swf -file-specs src/dynamicaudio.as

Yeah, proper sync is gonna be .... interesting. :D

@brion

This comment has been minimized.

Copy link

replied Feb 18, 2014

Ok, merged. :D Thanks for the initial Flash shim -- I figured it was possible but had no idea where to go about starting. :)

@maikmerten

This comment has been minimized.

Copy link
Owner Author

replied Feb 18, 2014

Please sign in to comment.
You can’t perform that action at this time.