Add ogg and wav support, fail gracefully on bad decode

ogg.js and vorbis.js are intentionally missing pending fixes to their compile.
master
William Toohey 10 years ago
parent 7dd22e6d90
commit 46662028cb
  1. 19
      gulpfile.js
  2. 36
      lib/workers/audio-worker.js
  3. 11
      src/js/HuesEditor.js
  4. 2
      src/js/HuesSettings.js
  5. 5
      src/js/ResourcePack.js
  6. 184
      src/js/SoundManager.js
  7. 2
      src/js/audio/aurora.js
  8. 0
      src/js/audio/mp3.js

@ -9,7 +9,7 @@ var order = require("gulp-order");
var del = require('del');
var jshint = require('gulp-jshint');
gulp.task('default', ['css', 'mp3', 'minify'], function() {
gulp.task('default', ['css', 'audio', 'minify'], function() {
});
@ -31,10 +31,15 @@ gulp.task('css', function(){
.pipe(gulp.dest('css'));
});
gulp.task("mp3", function () {
return gulp.src("src/js/mp3/*.js")
gulp.task("audio", function () {
gulp.src(["src/js/audio/aurora.js", "src/js/audio/mp3.js"])
.pipe(concat("audio-min.js"))
.pipe(uglify())
.pipe(gulp.dest("lib"));
gulp.src(["src/js/audio/ogg.js", "src/js/audio/vorbis.js"])
.pipe(concat("oggvorbis.js"))
.pipe(uglify())
.pipe(concat("mp3-min.js"))
.pipe(gulp.dest("lib"));
});
@ -63,7 +68,8 @@ gulp.task('clean', function() {
return del([
'lib/hues-min.js',
'lib/hues-min.map',
'lib/mp3-min.js',
'lib/audio-min.js',
'lib/oggvorbis.js',
'css',
'release']);
});
@ -72,7 +78,8 @@ gulp.task('release', ['default', 'lint'], function() {
gulp.src([
'css/hues-min.css',
'lib/hues-min.js',
'lib/mp3-min.js',
'lib/audio-min.js',
'lib/oggvorbis.js',
'fonts/**/*',
'img/**/*',
'index.html',

@ -1,39 +1,53 @@
// Use mp3.js and aurora.js for dev
// Use mp3-min.js for release
//importScripts('aurora.js');
//importScripts('mp3.js');
importScripts('../mp3-min.js');
importScripts('../audio-min.js');
// Flash value
var LAME_DELAY_START = 2258;
var LAME_DELAY_END = 0;
var deinterleaveAndTrim = function(buffer, asset) {
// because MP3 is bad, we nuke silence
var channels = asset.format.channelsPerFrame,
len = buffer.length / channels;
len = buffer.length / channels,
newLen, start;
// because MP3 is bad, we nuke silence
if(asset.format.formatID == "mp3") {
newLen = len - LAME_DELAY_START - LAME_DELAY_END;
result = new Float32Array(newLen * channels);
start = LAME_DELAY_START;
} else {
newLen = len;
start = 0;
}
var result = new Float32Array(newLen * channels);
for(var sample = 0; sample < newLen; sample++) {
for(var channel = 0; channel < channels; channel++) {
result[channel*newLen + sample] = buffer[(sample+LAME_DELAY_START)*channels + channel];
result[channel*newLen + sample] = buffer[(sample+start)*channels + channel];
}
}
return result;
}
self.addEventListener('message', function(e) {
if(!e.data.ogg) {
importScripts('../oggvorbis.js');
}
// To see if things are working, we can ping the worker
if(e.data.ping) {
self.postMessage({ping: true});
return;
}
var arrayBuffer = e.data;
var arrayBuffer = e.data.buffer;
var asset = AV.Asset.fromBuffer(arrayBuffer);
// Any errors are thrown up the chain to our Promises
// On error we still want to restore the audio file
asset.on("error", function(error) {
self.postMessage({arrayBuffer : arrayBuffer,
error: error},
[arrayBuffer]);
});
asset.decodeToBuffer(function(buffer) {
var fixedBuffer = deinterleaveAndTrim(buffer, asset);

@ -278,6 +278,15 @@ HuesEditor.prototype.loadAudio = function(editor) {
// load audio
this.blobToArrayBuffer(file)
.then(buffer => {
// Is this buffer even decodable?
let testSong = {test: buffer};
return this.core.soundManager.loadBuffer(testSong, "test")
// keep the buffer moving through the chain
// remember it's been passed through to a worker, so we update the reference
.then(() => {
return testSong.test;
});
}).then(buffer => {
this.song[editor._sound] = buffer;
// Save filename for XML export
let noExt = file.name.replace(/\.[^/.]+$/, "");
@ -827,7 +836,7 @@ HuesEditor.prototype.uiCreateSingleEditor = function(title, soundName, rhythmNam
let fileInput = document.createElement("input");
fileInput.type ="file";
fileInput.accept="audio/mp3";
fileInput.accept="audio/mp3|audio/wav|audio/ogg";
fileInput.multiple = false;
fileInput.onchange = this.loadAudio.bind(this, container);
let load = this.createButton("Load " + title.replace(/&nbsp;/g,""), rightHeader);

@ -27,7 +27,7 @@
- Go to the HTML and edit the `defaults` object instead!
*/
HuesSettings.prototype.defaultSettings = {
// Location relative to root - where do the mp3/zip workers live
// Location relative to root - where do the audio/zip workers live
// This is required because Web Workers need an absolute path
workersPath : "lib/workers/",
// Debugging var, for loading zips or not

@ -57,7 +57,7 @@ function Respack() {
this.loadedFromURL = false;
}
Respack.prototype.audioExtensions = new RegExp("\\.(mp3|ogg)$", "i");
Respack.prototype.audioExtensions = new RegExp("\\.(mp3|ogg|wav)$", "i");
Respack.prototype.imageExtensions = new RegExp("\\.(png|gif|jpg|jpeg)$", "i");
Respack.prototype.animRegex = new RegExp("(.*?)_\\d+$");
@ -225,6 +225,9 @@ Respack.prototype.parseSong = function(file) {
case "ogg":
mime = "audio/ogg";
break;
case "wav":
mime = "audio/wav";
break;
default:
mime = "application/octet-stream";
}

@ -32,6 +32,7 @@ function SoundManager(core) {
/* Lower level audio and timing info */
this.context = null; // Audio context, Web Audio API
this.oggSupport = false;
this.buildSource = null;
this.loopSource = null;
this.buildup = null;
@ -68,8 +69,8 @@ SoundManager.prototype.init = function() {
window.AudioContext = window.AudioContext || window.webkitAudioContext;
this.context = new window.AudioContext();
// These don't always exist
this.context.suspend = this.context.suspend || Promise.resolve();
this.context.resume = this.context.resume || Promise.resolve();
this.context.suspend = this.context.suspend || Promise.resolve;
this.context.resume = this.context.resume || Promise.resolve;
this.gainNode = this.context.createGain();
this.gainNode.connect(this.context.destination);
} catch(e) {
@ -77,28 +78,39 @@ SoundManager.prototype.init = function() {
return;
}
resolve();
}).then(response => {
}).then(() => {
// check for .ogg support - if not, we'll have to load the ogg decoder
return new Promise((resolve, reject) => {
// See if our MP3 decoder is working
let mp3Worker;
this.context.decodeAudioData(miniOgg, success => {
this.oggSupport = true;
resolve();
}, error => {
this.oggSupport = false;
resolve();
});
});
}).then(() => {
return new Promise((resolve, reject) => {
// See if our audio decoder is working
let audioWorker;
try {
mp3Worker = this.createWorker();
audioWorker = this.createWorker();
} catch(e) {
console.log(e);
reject(Error("MP3 Worker cannot be started - correct path set in defaults?"));
reject(Error("Audio Worker cannot be started - correct path set in defaults?"));
return;
}
let pingListener = event => {
mp3Worker.removeEventListener('message', pingListener);
mp3Worker.terminate();
audioWorker.terminate();
resolve();
};
mp3Worker.addEventListener('message', pingListener, false);
mp3Worker.addEventListener('error', () => {
reject(Error("MP3 Worker cannot be started - correct path set in defaults?"));
audioWorker.addEventListener('message', pingListener, false);
audioWorker.addEventListener('error', () => {
reject(Error("Audio Worker cannot be started - correct path set in defaults?"));
}, false);
mp3Worker.postMessage({ping:true});
audioWorker.postMessage({ping:true, ogg:this.oggSupport});
});
}).then(response => {
}).then(() => {
return new Promise((resolve, reject) => {
// iOS and other some mobile browsers - unlock the context as
// it starts in a suspended state
@ -158,7 +170,10 @@ SoundManager.prototype.playSong = function(song, playBuild, forcePlay) {
// To prevent race condition if you press "next" twice fast
if(song != this.song) {
// Stop processing - silently ignored in the catch below
throw Error("Song not playable - ignoring!");
throw {
name: "SoundManagerRace",
message: "Song not playable - ignoring!"
};
}
this.buildup = buffers.buildup;
@ -179,10 +194,14 @@ SoundManager.prototype.playSong = function(song, playBuild, forcePlay) {
}).then(() => {
this.playing = true;
}).catch(error => {
if(error.name == "SoundManagerRace") {
// Just to ignore it if the song was invalid
// Log it in case it's something weird
console.log(error);
console.log("SoundManager couldn't play song", error);
return;
} else {
throw error;
}
});
return p;
};
@ -313,27 +332,53 @@ SoundManager.prototype.loadSong = function(song) {
};
SoundManager.prototype.loadBuffer = function(song, soundName) {
let buffer = song[soundName];
// Is this an ogg file?
let view = new Uint8Array(buffer);
// Signature for ogg file: OggS
if(this.oggSupport && view[0] == 0x4F && view[1] == 0x67 && view[2] == 0x67 && view[3] == 0x53) {
// As we don't control decodeAudioData, we cannot do fast transfers and must copy
let backup = buffer.slice(0);
return new Promise((resolve, reject) => {
let mp3Worker = this.createWorker();
this.context.decodeAudioData(buffer, result => {
resolve(result);
}, error => {
reject(Error("decodeAudioData failed to load track"));
});
}).then(result => {
// restore copied buffer
song[soundName] = backup;
return result;
});
} else { // Use our JS decoder
return new Promise((resolve, reject) => {
let audioWorker = this.createWorker();
mp3Worker.addEventListener('error', () => {
reject(Error("MP3 Worker failed to convert track"));
audioWorker.addEventListener('error', () => {
reject(Error("Audio Worker failed to convert track"));
}, false);
mp3Worker.addEventListener('message', e => {
audioWorker.addEventListener('message', e => {
let decoded = e.data;
mp3Worker.terminate();
audioWorker.terminate();
// restore transferred buffer
song[soundName] = decoded.arrayBuffer;
if(decoded.error) {
reject(new Error(decoded.error));
return;
}
// Convert to real audio buffer
let audio = this.audioBufFromRaw(decoded.rawAudio);
resolve(audio);
}, false);
// transfer the buffer to save time
mp3Worker.postMessage(song[soundName], [song[soundName]]);
audioWorker.postMessage({buffer: buffer, ogg: this.oggSupport}, [buffer]);
});
}
};
// Converts continuous PCM array to Web Audio API friendly format
@ -342,20 +387,16 @@ SoundManager.prototype.audioBufFromRaw = function(raw) {
let channels = raw.channels;
let samples = buffer.length/channels;
let audioBuf = this.context.createBuffer(channels, samples, raw.sampleRate);
//let audioBuf = this.context.createBuffer(1, buffer.length, raw.sampleRate);
//audioBuf.copyToChannel(buffer, 0, 0);
for(let i = 0; i < channels; i++) {
//console.log("Making buffer at offset",i*samples,"and length",samples,".Original buffer is",channels,"channels and",buffer.length,"elements");
// Offset is in bytes, length is in elements
let channel = new Float32Array(buffer.buffer , i * samples * 4, samples);
//console.log(channel);
audioBuf.copyToChannel(channel, i, 0);
}
return audioBuf;
};
SoundManager.prototype.createWorker = function() {
return new Worker(this.core.settings.defaults.workersPath + 'mp3-worker.js');
return new Worker(this.core.settings.defaults.workersPath + 'audio-worker.js');
};
SoundManager.prototype.initVisualiser = function(bars) {
@ -516,6 +557,95 @@ SoundManager.prototype.fadeOut = function(callback) {
setTimeout(callback, 2000);
};
let miniOggRaw =
"T2dnUwACAAAAAAAAAADFYgAAAAAAAMLKRdwBHgF2b3JiaXMAAAAAAUSsAAAA" +
"AAAAgLsAAAAAAAC4AU9nZ1MAAAAAAAAAAAAAxWIAAAEAAACcKCV2Dzv/////" +
"////////////MgN2b3JiaXMrAAAAWGlwaC5PcmcgbGliVm9yYmlzIEkgMjAx" +
"MjAyMDMgKE9tbmlwcmVzZW50KQAAAAABBXZvcmJpcx9CQ1YBAAABABhjVClG" +
"mVLSSokZc5QxRplikkqJpYQWQkidcxRTqTnXnGusubUghBAaU1ApBZlSjlJp" +
"GWOQKQWZUhBLSSV0EjonnWMQW0nB1phri0G2HIQNmlJMKcSUUopCCBlTjCnF" +
"lFJKQgcldA465hxTjkooQbicc6u1lpZji6l0kkrnJGRMQkgphZJKB6VTTkJI" +
"NZbWUikdc1JSakHoIIQQQrYghA2C0JBVAAABAMBAEBqyCgBQAAAQiqEYigKE" +
"hqwCADIAAASgKI7iKI4jOZJjSRYQGrIKAAACABAAAMBwFEmRFMmxJEvSLEvT" +
"RFFVfdU2VVX2dV3XdV3XdSA0ZBUAAAEAQEinmaUaIMIMZBgIDVkFACAAAABG" +
"KMIQA0JDVgEAAAEAAGIoOYgmtOZ8c46DZjloKsXmdHAi1eZJbirm5pxzzjkn" +
"m3PGOOecc4pyZjFoJrTmnHMSg2YpaCa05pxznsTmQWuqtOacc8Y5p4NxRhjn" +
"nHOatOZBajbW5pxzFrSmOWouxeaccyLl5kltLtXmnHPOOeecc84555xzqhen" +
"c3BOOOecc6L25lpuQhfnnHM+Gad7c0I455xzzjnnnHPOOeecc4LQkFUAABAA" +
"AEEYNoZxpyBIn6OBGEWIacikB92jwyRoDHIKqUejo5FS6iCUVMZJKZ0gNGQV" +
"AAAIAAAhhBRSSCGFFFJIIYUUUoghhhhiyCmnnIIKKqmkoooyyiyzzDLLLLPM" +
"Muuws8467DDEEEMMrbQSS0211VhjrbnnnGsO0lpprbXWSimllFJKKQgNWQUA" +
"gAAAEAgZZJBBRiGFFFKIIaaccsopqKACQkNWAQCAAAACAAAAPMlzREd0REd0" +
"REd0REd0RMdzPEeUREmUREm0TMvUTE8VVdWVXVvWZd32bWEXdt33dd/3dePX" +
"hWFZlmVZlmVZlmVZlmVZlmVZgtCQVQAACAAAgBBCCCGFFFJIIaUYY8wx56CT" +
"UEIgNGQVAAAIACAAAADAURzFcSRHciTJkixJkzRLszzN0zxN9ERRFE3TVEVX" +
"dEXdtEXZlE3XdE3ZdFVZtV1Ztm3Z1m1flm3f933f933f933f933f93UdCA1Z" +
"BQBIAADoSI6kSIqkSI7jOJIkAaEhqwAAGQAAAQAoiqM4juNIkiRJlqRJnuVZ" +
"omZqpmd6qqgCoSGrAABAAAABAAAAAAAomuIppuIpouI5oiNKomVaoqZqriib" +
"suu6ruu6ruu6ruu6ruu6ruu6ruu6ruu6ruu6ruu6ruu6ruu6LhAasgoAkAAA" +
"0JEcyZEcSZEUSZEcyQFCQ1YBADIAAAIAcAzHkBTJsSxL0zzN0zxN9ERP9ExP" +
"FV3RBUJDVgEAgAAAAgAAAAAAMCTDUixHczRJlFRLtVRNtVRLFVVPVVVVVVVV" +
"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVNU3TNE0gNGQlAAAEAMBijcHl" +
"ICElJeXeEMIQk54xJiG1XiEEkZLeMQYVg54yogxy3kLjEIMeCA1ZEQBEAQAA" +
"xiDHEHPIOUepkxI556h0lBrnHKWOUmcpxZhizSiV2FKsjXOOUketo5RiLC12" +
"lFKNqcYCAAACHAAAAiyEQkNWBABRAACEMUgppBRijDmnnEOMKeeYc4Yx5hxz" +
"jjnnoHRSKuecdE5KxBhzjjmnnHNSOieVc05KJ6EAAIAABwCAAAuh0JAVAUCc" +
"AIBBkjxP8jRRlDRPFEVTdF1RNF3X8jzV9ExTVT3RVFVTVW3ZVFVZljzPND3T" +
"VFXPNFXVVFVZNlVVlkVV1W3TdXXbdFXdlm3b911bFnZRVW3dVF3bN1XX9l3Z" +
"9n1Z1nVj8jxV9UzTdT3TdGXVdW1bdV1d90xTlk3XlWXTdW3blWVdd2XZ9zXT" +
"dF3TVWXZdF3ZdmVXt11Z9n3TdYXflWVfV2VZGHZd94Vb15XldF3dV2VXN1ZZ" +
"9n1b14Xh1nVhmTxPVT3TdF3PNF1XdV1fV13X1jXTlGXTdW3ZVF1ZdmXZ911X" +
"1nXPNGXZdF3bNl1Xll1Z9n1XlnXddF1fV2VZ+FVX9nVZ15Xh1m3hN13X91VZ" +
"9oVXlnXh1nVhuXVdGD5V9X1TdoXhdGXf14XfWW5dOJbRdX1hlW3hWGVZOX7h" +
"WJbd95VldF1fWG3ZGFZZFoZf+J3l9n3jeHVdGW7d58y67wzH76T7ytPVbWOZ" +
"fd1ZZl93juEYOr/w46mqr5uuKwynLAu/7evGs/u+soyu6/uqLAu/KtvCseu+" +
"8/y+sCyj7PrCasvCsNq2Mdy+biy/cBzLa+vKMeu+UbZ1fF94CsPzdHVdeWZd" +
"x/Z1dONHOH7KAACAAQcAgAATykChISsCgDgBAI8kiaJkWaIoWZYoiqbouqJo" +
"uq6kaaapaZ5pWppnmqZpqrIpmq4saZppWp5mmpqnmaZomq5rmqasiqYpy6Zq" +
"yrJpmrLsurJtu65s26JpyrJpmrJsmqYsu7Kr267s6rqkWaapeZ5pap5nmqZq" +
"yrJpmq6reZ5qep5oqp4oqqpqqqqtqqosW55nmproqaYniqpqqqatmqoqy6aq" +
"2rJpqrZsqqptu6rs+rJt67ppqrJtqqYtm6pq267s6rIs27ovaZppap5nmprn" +
"maZpmrJsmqorW56nmp4oqqrmiaZqqqosm6aqypbnmaoniqrqiZ5rmqoqy6Zq" +
"2qppmrZsqqotm6Yqy65t+77ryrJuqqpsm6pq66ZqyrJsy77vyqruiqYpy6aq" +
"2rJpqrIt27Lvy7Ks+6JpyrJpqrJtqqouy7JtG7Ns+7pomrJtqqYtm6oq27It" +
"+7os27rvyq5vq6qs67It+7ru+q5w67owvLJs+6qs+ror27pv6zLb9n1E05Rl" +
"UzVt21RVWXZl2fZl2/Z90TRtW1VVWzZN1bZlWfZ9WbZtYTRN2TZVVdZN1bRt" +
"WZZtYbZl4XZl2bdlW/Z115V1X9d949dl3ea6su3Lsq37qqv6tu77wnDrrvAK" +
"AAAYcAAACDChDBQashIAiAIAAIxhjDEIjVLOOQehUco55yBkzkEIIZXMOQgh" +
"lJI5B6GUlDLnIJSSUgihlJRaCyGUlFJrBQAAFDgAAATYoCmxOEChISsBgFQA" +
"AIPjWJbnmaJq2rJjSZ4niqqpqrbtSJbniaJpqqptW54niqapqq7r65rniaJp" +
"qqrr6rpomqapqq7ruroumqKpqqrrurKum6aqqq4ru7Ls66aqqqrryq4s+8Kq" +
"uq4ry7Jt68Kwqq7ryrJs27Zv3Lqu677v+8KRreu6LvzCMQxHAQDgCQ4AQAU2" +
"rI5wUjQWWGjISgAgAwCAMAYhgxBCBiGEkFJKIaWUEgAAMOAAABBgQhkoNGRF" +
"ABAnAAAYQymklFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSCmllFJKKaWU" +
"UkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSqmklFJKKaWU" +
"UkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWU" +
"UkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWU" +
"UkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWU" +
"UkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWU" +
"UkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimVUkoppZRS" +
"SimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRS" +
"SimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFIKAJCKcACQejCh" +
"DBQashIASAUAAIxRSinGnIMQMeYYY9BJKClizDnGHJSSUuUchBBSaS23yjkI" +
"IaTUUm2Zc1JaizHmGDPnpKQUW805h1JSi7HmmmvupLRWa64151paqzXXnHPN" +
"ubQWa64515xzyzHXnHPOOecYc84555xzzgUA4DQ4AIAe2LA6wknRWGChISsB" +
"gFQAAAIZpRhzzjnoEFKMOecchBAihRhzzjkIIVSMOeccdBBCqBhzzDkIIYSQ" +
"OecchBBCCCFzDjroIIQQQgcdhBBCCKGUzkEIIYQQSighhBBCCCGEEDoIIYQQ" +
"QgghhBBCCCGEUkoIIYQQQgmhlFAAAGCBAwBAgA2rI5wUjQUWGrISAAACAIAc" +
"lqBSzoRBjkGPDUHKUTMNQkw50ZliTmozFVOQORCddBIZakHZXjILAACAIAAg" +
"wAQQGCAo+EIIiDEAAEGIzBAJhVWwwKAMGhzmAcADRIREAJCYoEi7uIAuA1zQ" +
"xV0HQghCEIJYHEABCTg44YYn3vCEG5ygU1TqIAAAAAAADADgAQDgoAAiIpqr" +
"sLjAyNDY4OjwCAAAAAAAFgD4AAA4PoCIiOYqLC4wMjQ2ODo8AgAAAAAAAAAA" +
"gICAAAAAAABAAAAAgIBPZ2dTAAQBAAAAAAAAAMViAAACAAAA22A/JwIBAQAK";
// write the bytes of the string to an ArrayBuffer
let miniOggBin = atob(miniOggRaw);
let miniOgg = new ArrayBuffer(miniOggBin.length);
let view = new Uint8Array(miniOgg);
for (var i = 0; i < miniOggBin.length; i++) {
view[i] = miniOggBin.charCodeAt(i);
}
window.SoundManager = SoundManager;
})(window, document);

@ -1730,6 +1730,8 @@ Demuxer = (function(_super) {
formats = [];
Demuxer.test = function() {return formats};
Demuxer.register = function(demuxer) {
return formats.push(demuxer);
};
Loading…
Cancel
Save