mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-22 09:37:17 +00:00
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:
parent
dea6e43f96
commit
1b8ab66845
2 changed files with 167 additions and 230 deletions
343
js/media.js
343
js/media.js
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
|||
54
js/winamp.js
54
js/winamp.js
|
|
@ -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 */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue