Use URL.createObjectURL for reading local files

Somehow I overlooked this method of extracting data from a local file. Now that
we have a real URL for a local file that does not break Cross Origin Request
rules, we can go back to using an `<audio>` tag for playing our media which
greatly simplifies much of the media functionality.
This commit is contained in:
Jordan Eldredge 2015-09-21 22:40:46 -07:00
parent dea6e43f96
commit 1b8ab66845
2 changed files with 167 additions and 230 deletions

View file

@ -1,26 +1,14 @@
/* Emulate the native <audio> element with Web Audio API */
define({
_context: new(window.AudioContext || window.webkitAudioContext)(),
_source: null,
_buffer: null,
_callbacks: {
waiting: function(){},
stopWaiting: function(){},
playing: function(){},
timeupdate: function(){},
visualizerupdate: function(){},
ended: function(){}
},
_startTime: 0,
_position: 0,
_balance: 0,
_playing: false,
_loop: false,
autoPlay: false,
define([],
function() {
return function() {
this._context = new(window.AudioContext || window.webkitAudioContext)();
this.audio = document.createElement('audio');
this.visualizerUpdateCallback = function() { /* no op */};
this._balance = 0;
this.audio.setAttribute('crossOrigin', 'anonymous');
init: function() {
// The _source node has to be recreated each time it's stopped or
// paused, so we don't create it here.
this._source = this._context.createMediaElementSource(this.audio);
// Create the spliter node
this._chanSplit = this._context.createChannelSplitter(2);
@ -62,6 +50,10 @@ define({
// |
// destination
this._source.connect(this._analyser);
this._source.connect(this._chanSplit);
// Connect split channels to left / right gains
this._chanSplit.connect(this._leftGain,0);
this._chanSplit.connect(this._rightGain,1);
@ -74,187 +66,136 @@ define({
this._gainNode.connect(this._context.destination);
// Load from url
this.loadUrl = function(url) {
this.audio.src = url;
};
this.setAutoPlay = function(autoPlay) {
this.audio.autoplay = autoPlay;
};
/* Properties */
this.duration = function() {
return this.audio.duration;
};
this.timeElapsed = function() {
return this.audio.currentTime;
};
this.timeRemaining = function() {
return this.duration() - this.timeElapsed();
};
this.percentComplete = function() {
return (this.timeElapsed() / this.duration()) * 100;
};
this.channels = function() {
return this._source.channelCount;
};
this.sampleRate = function() {
return this._context.sampleRate;
};
/* Actions */
this.previous = function() {
// Implement this when we support playlists
};
this.play = function(position) {
this.audio.play();
};
this.pause = function() {
this.audio.pause();
};
this.paused = function() {
return this.audio.paused;
};
this.stop = function() {
this.seekToTime(0);
this.pause();
};
/* Actions with arguments */
this.seekToPercentComplete = function(percent) {
var seekTime = this.duration() * (percent / 100);
this.seekToTime(seekTime);
};
// From 0-1
this.setVolume = function(volume) {
this._gainNode.gain.value = volume;
};
this.getVolume = function() {
return this._gainNode.gain.value;
},
// From -100 to 100
this.setBalance = function(balance) {
var changeVal = Math.abs(balance) / 100;
// Hack for Firefox. Having either channel set to 0 seems to revert us
// to equal balance.
changeVal = changeVal - 0.00000001;
if(balance > 0) { // Right
this._leftGain.gain.value = 1 - changeVal;
this._rightGain.gain.value = 1;
}
else if(balance < 0) // Left
{
this._leftGain.gain.value = 1;
this._rightGain.gain.value = 1 - changeVal;
}
else // Center
{
this._leftGain.gain.value = 1;
this._rightGain.gain.value = 1;
}
this._balance = balance;
};
this.getBalance = function() {
return this._balance;
};
this.toggleRepeat = function() {
this.audio.loop = !this.audio.loop;
};
this.toggleShuffle = function() {
// Implement this when we support playlists
};
/* Listeners */
this.addEventListener = function(event, callback) {
if(event === 'visualizerupdate') {
this.visualizerUpdateCallback = callback;
} else {
return this.audio.addEventListener(event, callback);
}
};
this.seekToTime = function(time) {
// Make sure we are within range
time = Math.min(time, this.duration());
time = Math.max(time, 0);
this.audio.currentTime = time;
};
// There is probably a more reasonable way to do this, rather than having
// it always running.
this._draw = function() {
if(!this.paused()) {
this.visualizerUpdateCallback(this._analyser);
}
window.requestAnimationFrame(this._draw.bind(this));
};
// Kick off the animation loop
this._draw(0);
return this;
},
// Load from bufferArray
loadBuffer: function(buffer, loadedCallback) {
this.stop();
this._callbacks.waiting();
var loadAudioBuffer = function(buffer) {
this._buffer = buffer;
loadedCallback();
this._callbacks.stopWaiting();
if(this.autoPlay) {
this.play(0);
}
};
var error = function (error) {
//console.error("failed to decode:", error);
};
// Decode the target file into an arrayBuffer and pass it to loadBuffer
this._context.decodeAudioData(buffer, loadAudioBuffer.bind(this), error);
},
/* Properties */
duration: function() {
return this._buffer.duration;
},
timeElapsed: function() {
return this._position;
},
timeRemaining: function() {
return this.duration() - this.timeElapsed();
},
percentComplete: function() {
return (this.timeElapsed() / this.duration()) * 100;
},
channels: function() {
if(!this._buffer) {
return 0;
}
return this._buffer.numberOfChannels;
},
sampleRate: function() {
return this._buffer.sampleRate;
},
/* Actions */
previous: function() {
// Implement this when we support playlists
},
play: function(position) {
if(this._playing) {
// So we don't get a race condition with _position getting overwritten
this.pause();
}
if(this._buffer) {
this._source = this._context.createBufferSource();
this._source.buffer = this._buffer;
this._source.connect(this._analyser);
this._source.connect(this._chanSplit);
this._position = typeof position !== 'undefined' ? position : this._position;
this._startTime = this._context.currentTime - this._position;
this._source.start(0, this._position);
this._playing = true;
this._callbacks.playing();
}
},
pause: function() {
if(!this._playing) {
return;
}
this._silence();
this._updatePosition();
},
stop: function() {
this._silence();
this._position = 0;
},
_silence: function() {
if(this._source) {
this._source.stop(0);
this._source = null;
}
this._playing = false;
},
/* Actions with arguments */
seekToPercentComplete: function(percent) {
var seekTime = this.duration() * (percent / 100);
this.seekToTime(seekTime);
},
// From 0-1
setVolume: function(volume) {
this._gainNode.gain.value = volume;
},
getVolume: function() {
return this._gainNode.gain.value;
},
// From -100 to 100
setBalance: function(balance) {
var changeVal = Math.abs(balance) / 100;
// Hack for Firefox. Having either channel set to 0 seems to revert us
// to equal balance.
changeVal = changeVal - 0.00000001;
if(balance > 0) { // Right
this._leftGain.gain.value = 1 - changeVal;
this._rightGain.gain.value = 1;
}
else if(balance < 0) // Left
{
this._leftGain.gain.value = 1;
this._rightGain.gain.value = 1 - changeVal;
}
else // Center
{
this._leftGain.gain.value = 1;
this._rightGain.gain.value = 1;
}
this._balance = balance;
},
getBalance: function() {
return this._balance;
},
toggleRepeat: function() {
this._loop = !this._loop;
},
toggleShuffle: function() {
// Implement this when we support playlists
},
/* Listeners */
addEventListener: function(event, callback) {
this._callbacks[event] = callback;
},
seekToTime: function(time) {
// Make sure we are within range
time = Math.min(time, this.duration());
time = Math.max(time, 0);
this.play(time);
},
// There is probably a more reasonable way to do this, rather than having
// it always running.
_draw: function() {
if(this._playing) {
this._updatePosition();
this._callbacks.timeupdate();
// _updatePosition might have stopped the playing
if(this._playing) {
this._callbacks.visualizerupdate(this._analyser);
}
}
window.requestAnimationFrame(this._draw.bind(this));
},
_updatePosition: function() {
this._position = this._context.currentTime - this._startTime;
if(this._position >= this._buffer.duration && this._playing) {
// Idealy we could use _source.loop, but it makes updating the position tricky
if(this._loop) {
this.play(0);
} else {
this.stop();
this._callbacks.ended();
}
}
}
};
});

View file

@ -17,9 +17,10 @@ return {
this.fileInput = document.createElement('input');
this.fileInput.type = 'file';
this.fileInput.style.display = 'none';
this.objectURL = null;
this.windowManager = WindowManager;
this.media = Media.init();
this.media = new Media();
this.skin = Skin.init(document.getElementById('visualizer'), this.media._analyser);
this.state = '';
@ -76,14 +77,17 @@ return {
window.dispatchEvent(self.events.startWaiting);
});
this.media.addEventListener('stopWaiting', function() {
this.media.addEventListener('canplay', function() {
window.dispatchEvent(self.events.stopWaiting);
});
this.media.addEventListener('playing', function() {
this.media.addEventListener('play', function() {
self.setState('play');
});
this.media.addEventListener('loadedmetadata', function() {
self.loadMetaData();
});
this.fileInput.onchange = function(e){
self.loadFromFileReference(e.target.files[0]);
};
@ -224,27 +228,25 @@ return {
},
loadFromFileReference: function(fileReference) {
var file = new MyFile();
file.setFileReference(fileReference);
if(new RegExp("(wsz|zip)$", 'i').test(fileReference.name)) {
var file = new MyFile();
file.setFileReference(fileReference);
this.skin.setSkinByFile(file);
} else {
this.media.autoPlay = true;
this.media.setAutoPlay(true);
this.fileName = fileReference.name;
file.processBuffer(this._loadBuffer.bind(this));
if(this.objectURL) {
URL.revokeObjectURL(this.objectURL);
}
this.objectURL = URL.createObjectURL(fileReference);
this.loadFromUrl(this.objectURL, this.fileName);
}
},
// Used only for the initial load, since it must have a CORS header
loadFromUrl: function(url, fileName) {
if(!fileName) {
this.fileName = url.split("/").pop();
} else {
this.fileName = fileName;
}
var file = new MyFile();
file.setUrl(url);
file.processBuffer(this._loadBuffer.bind(this));
this.fileName = fileName ? fileName : url.split("/").pop();
this.media.loadUrl(url);
},
setSkin: function(file) {
@ -271,20 +273,14 @@ return {
this.skin.visualizer.clear();
},
/* Listeners */
_loadBuffer: function(buffer) {
function setMetaData() {
var kbps = "128";
var khz = Math.round(this.media.sampleRate() / 1000).toString();
this.skin.font.setNodeToString(document.getElementById('kbps'), kbps);
this.skin.font.setNodeToString(document.getElementById('khz'), khz);
window.dispatchEvent(this.events.channelCountUpdated);
window.dispatchEvent(this.events.titleUpdated);
window.dispatchEvent(this.events.timeUpdated);
}
// Note, this will not happen right away
this.media.loadBuffer(buffer, setMetaData.bind(this));
loadMetaData: function() {
var kbps = "128";
var khz = Math.round(this.media.sampleRate() / 1000).toString();
this.skin.font.setNodeToString(document.getElementById('kbps'), kbps);
this.skin.font.setNodeToString(document.getElementById('khz'), khz);
window.dispatchEvent(this.events.channelCountUpdated);
window.dispatchEvent(this.events.titleUpdated);
window.dispatchEvent(this.events.timeUpdated);
},
/* Helpers */