问题
I've made a visualizer in javascript that when you select a music directory your're able to select files within that directory to play and have the visualizer move to. But it would seem after loading a directory and then changing the song more than 4 times Results in less responsive movement from the visualizer. I'm unsure why this is happening. Heres an example of this happening.
Keep changing the song from the drop down box until you see it starting to slow.
window.onload = function() {
var input = document.getElementById("file");
var audio = document.getElementById("audio");
var selectLabel = document.querySelector("label[for=select]");
var audioLabel = document.querySelector("label[for=audio]");
var select = document.querySelector("select");
var context = void 0,
src = void 0,
res = [],
url = "";
function processDirectoryUpload(event) {
var webkitResult = [];
var mozResult = [];
var files;
console.log(event);
select.innerHTML = "";
// do mozilla stuff
function mozReadDirectories(entries, path) {
console.log("dir", entries, path);
return [].reduce.call(entries, function(promise, entry) {
return promise.then(function() {
return Promise.resolve(entry.getFilesAndDirectories() || entry)
.then(function(dir) {
return dir
})
})
}, Promise.resolve())
.then(function(items) {
var dir = items.filter(function(folder) {
return folder instanceof Directory
});
var files = items.filter(function(file) {
return file instanceof File
});
if (files.length) {
// console.log("files:", files, path);
mozResult = mozResult.concat.apply(mozResult, files);
}
if (dir.length) {
// console.log(dir, dir[0] instanceof Directory);
return mozReadDirectories(dir, dir[0].path || path);
} else {
if (!dir.length) {
return Promise.resolve(mozResult).then(function(complete) {
return complete
})
}
}
})
};
function handleEntries(entry) {
let file = "webkitGetAsEntry" in entry ? entry.webkitGetAsEntry() : entry
return Promise.resolve(file);
}
function handleFile(entry) {
return new Promise(function(resolve) {
if (entry.isFile) {
entry.file(function(file) {
listFile(file, entry.fullPath).then(resolve)
})
} else if (entry.isDirectory) {
var reader = entry.createReader();
reader.readEntries(webkitReadDirectories.bind(null, entry, handleFile, resolve))
} else {
var entries = [entry];
return entries.reduce(function(promise, file) {
return promise.then(function() {
return listDirectory(file)
})
}, Promise.resolve())
.then(function() {
return Promise.all(entries.map(function(file) {
return listFile(file)
})).then(resolve)
})
}
})
function webkitReadDirectories(entry, callback, resolve, entries) {
console.log(entries);
return listDirectory(entry).then(function(currentDirectory) {
console.log(`iterating ${currentDirectory.name} directory`, entry);
return entries.reduce(function(promise, directory) {
return promise.then(function() {
return callback(directory)
});
}, Promise.resolve())
}).then(resolve);
}
}
function listDirectory(entry) {
console.log(entry);
return Promise.resolve(entry);
}
function listFile(file, path) {
path = path || file.webkitRelativePath || "/" + file.name;
console.log(`reading ${file.name}, size: ${file.size}, path:${path}`);
webkitResult.push(file);
return Promise.resolve(webkitResult)
};
function processFiles(files) {
Promise.all([].map.call(files, function(file, index) {
return handleEntries(file, index).then(handleFile)
}))
.then(function() {
console.log("complete", webkitResult);
res = webkitResult;
res.reduce(function(promise, track) {
return promise.then(function() {
return playMusic(track)
})
}, displayFiles(res))
})
.catch(function(err) {
alert(err.message);
})
}
if ("getFilesAndDirectories" in event.target) {
return (event.type === "drop" ? event.dataTransfer : event.target).getFilesAndDirectories()
.then(function(dir) {
if (dir[0] instanceof Directory) {
console.log(dir)
return mozReadDirectories(dir, dir[0].path || path)
.then(function(complete) {
console.log("complete:", webkitResult);
event.target.value = null;
});
} else {
if (dir[0] instanceof File && dir[0].size > 0) {
return Promise.resolve(dir)
.then(function() {
console.log("complete:", mozResult);
res = mozResult;
res.reduce(function(promise, track) {
return promise.then(function() {
return playMusic(track)
})
}, displayFiles(res))
})
} else {
if (dir[0].size == 0) {
throw new Error("could not process '" + dir[0].name + "' directory" + " at drop event at firefox, upload folders at 'Choose folder...' input");
}
}
}
}).catch(function(err) {
alert(err)
})
}
files = event.target.files;
if (files) {
processFiles(files)
}
}
function displayFiles(files) {
select.innerHTML = "";
return Promise.all(files.map(function(file, index) {
return new Promise(function(resolve) {
if (/^audio/.test(file.type)) { /* do stuff, that is all code currently within Promise resolver function */ } else { /* proceed to next file */
resolve()
}
var option = new Option(file.name, index);
select.appendChild(option);
resolve()
})
}))
}
function handleSelectedSong(event) {
if (res.length) {
var index = select.value;
var track = res[index];
playMusic(track)
.then(function(filename) {
console.log(filename + " playback completed")
})
} else {
console.log("No songs to play")
}
}
function playMusic(file) {
return new Promise(function(resolve) {
audio.pause();
audio.onended = function() {
audio.onended = null;
if (url) URL.revokeObjectURL(url);
resolve(file.name);
}
if (url) URL.revokeObjectURL(url);
url = URL.createObjectURL(file);
audio.load();
audio.src = url;
audio.play();
audioLabel.textContent = file.name;
context = context || new AudioContext();
src = src || context.createMediaElementSource(audio);
src.disconnect(context);
var analyser = context.createAnalyser();
var canvas = document.getElementById("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var ctx = canvas.getContext("2d");
src.connect(analyser);
analyser.connect(context.destination);
analyser.fftSize = 16384;
var bufferLength = analyser.frequencyBinCount;
console.log(bufferLength);
var dataArray = new Uint8Array(bufferLength);
var WIDTH = canvas.width;
var HEIGHT = canvas.height;
var barWidth = (WIDTH / bufferLength) * 32;
var barHeight;
var x = 0;
function renderFrame() {
requestAnimationFrame(renderFrame);
x = 0;
analyser.getByteFrequencyData(dataArray);
ctx.fillStyle = "#1b1b1b";
ctx.fillRect(0, 0, WIDTH, HEIGHT);
for (var i = 0; i < bufferLength; i++) {
barHeight = dataArray[i];
ctx.fillStyle = "rgb(5,155,45)"
ctx.fillRect(x, (((HEIGHT - barHeight - 5 % barHeight) + (20 % HEIGHT - barHeight))), barWidth, barHeight + 20 % HEIGHT);
x += barWidth + 2;
}
}
renderFrame();
})
}
input.addEventListener("change", processDirectoryUpload);
select.addEventListener("change", handleSelectedSong);
}
<canvas id="canvas" width="window.innerWidth" height="window.innerHeight"></canvas>
<div id="content">
<label class="custom-file-upload">
Select Music directory <input id="file" type="file" accept="audio/*" directory allowdirs webkitdirectory/>
<p style="color: rgb(5,195,5);">Now playing:<label for="audio"></label></p>
<p style="color: rgb(5,195,5);">Select Song</p>
<select id="select">
</select>
<audio id="audio" controls></audio>
回答1:
Like noted by yuriy636, you are starting a new animation for every new song, without never stopping the previous one. So when you played 5 songs, you still have 5 visualizations rendering loop running at every frame, and 5 analyzers.
The best to do here is to refactor your code :
- create a single analyzer, only update which stream feeds it
- make the canvas animation autonomous, declare it once at first load
- since you've got only one
<canvas>
, start only one rendering animation
When using a single analyzer, your render doesn't use anything new when you change the source, it's always the same canvas, the same analyzer, the same visualization.
Here is a quick proof of concept, really dirty, but I hope you'll be able to undertstand what I did and why.
window.onload = function() {
var input = document.getElementById("file");
var audio = document.getElementById("audio");
var selectLabel = document.querySelector("label[for=select]");
var audioLabel = document.querySelector("label[for=audio]");
var select = document.querySelector("select");
var viz = null;
// removed all the IDK what it was meant for directory special handlers
function displayFiles() {
select.innerHTML = "";
// that's all synchronous, why Promises ?
res = Array.prototype.slice.call(input.files);
res.forEach(function(file, index) {
if (/^audio/.test(file.type)) {
var option = new Option(file.name, index);
select.appendChild(option);
}
});
if (res.length) {
var analyser = initAudioAnalyser();
viz = initVisualization(analyser);
// pre-select the first song ?
handleSelectedSong();
audio.pause();
}
}
function handleSelectedSong(event) {
if (res.length) {
var index = select.value;
var track = res[index];
playMusic(track)
.then(function(filename) {
console.log(filename + " playback completed")
})
viz.play();
} else {
console.log("No songs to play")
}
}
function playMusic(file) {
return new Promise(function(resolve) {
var url = audio.src;
audio.pause();
audio.onended = function() {
audio.onended = null;
// arguablily useless here since blobURIs are just pointers to real file on the user's system
if (url) URL.revokeObjectURL(url);
resolve(file.name);
}
if (url) URL.revokeObjectURL(url);
url = URL.createObjectURL(file);
// audio.load(); // would just set a 404 since you revoked the URL just before
audio.src = url;
audio.play();
audioLabel.textContent = file.name;
});
}
function initAudioAnalyser() {
var context = new AudioContext();
var analyser = context.createAnalyser();
analyser.fftSize = 16384;
var src = context.createMediaElementSource(audio);
src.connect(analyser);
src.connect(context.destination);
return analyser;
}
function initVisualization(analyser) {
var canvas = document.getElementById("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var ctx = canvas.getContext("2d");
var bufferLength = analyser.frequencyBinCount;
var dataArray = new Uint8Array(bufferLength);
var WIDTH = canvas.width;
var HEIGHT = canvas.height;
var barWidth = (WIDTH / bufferLength) * 32;
var barHeight;
var x = 0;
var paused = true;
function renderFrame() {
if (!paused) {
requestAnimationFrame(renderFrame);
} else {
return;
}
x = 0;
analyser.getByteFrequencyData(dataArray);
ctx.fillStyle = "#1b1b1b";
ctx.fillRect(0, 0, WIDTH, HEIGHT);
ctx.fillStyle = "rgb(5,155,45)"
ctx.beginPath();
for (var i = 0; i < bufferLength; i++) {
barHeight = dataArray[i];
// micro-optimisation, but concatenating all the rects in a single shape is easier for the CPU
ctx.rect(x, (((HEIGHT - barHeight - 5 % barHeight) + (20 % HEIGHT - barHeight))), barWidth, barHeight + 20 % HEIGHT);
x += barWidth + 2;
}
ctx.fill();
}
var viz = window.viz = {
play: function() {
if(paused){
paused = false;
renderFrame();
}
},
pause: function() {
paused = true;
clearTimeout(pauseTimeout);
pauseTimeout = null;
},
};
// we can even add auto pause linked to the audio element
var pauseTimeout = null;
audio.onpause = function() {
// let's really do it in 2s to keep the tear down effect
pauseTimeout = setTimeout(viz.pause, 2000);
}
audio.onplaying = function() {
clearTimeout(pauseTimeout);
// we were not playing
if(!pauseTimeout){
viz.play();
}
}
return viz;
}
input.addEventListener("change", displayFiles);
select.addEventListener("change", handleSelectedSong);
}
<canvas id="canvas" width="window.innerWidth" height="window.innerHeight"></canvas>
<div id="content">
<label class="custom-file-upload">
Select Music directory <input id="file" type="file" accept="audio/*" directory allowdirs webkitdirectory/>
<p style="color: rgb(5,195,5);">Now playing:<label for="audio"></label></p>
<p style="color: rgb(5,195,5);">Select Song</p>
<select id="select">
</select>
<audio id="audio" controls></audio>
来源:https://stackoverflow.com/questions/45360781/possible-memory-leak-or-something-else