2017-07-27 67 views
1

我在javascript中製作了一個可視化工具,當您選擇一個音樂目錄時,您可以選擇該目錄中的文件進行播放並將可視化工具移至。但是,在加載一個目錄並且然後改變歌曲超過4次之後似乎會導致可視化器的響應速度變慢。我不確定爲什麼會發生這種情況。這裏有一個例子。可能的內存泄漏或其他?

繼續從下拉框中更改歌曲,直到看到它開始變慢。

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>

+0

我不是在分析告訴爲什麼會發生JavaScript性能經歷,但我做了一些分析(DevTools'性能選項卡),它看起來像一首歌后,在瀏覽器每次花費時間而改變越來越多的時間腳本比閒置:https://pastebin.com/RV97UraD希望它可以幫助你。 – yuriy636

+0

是的,我也注意到了。 – Nickh90

回答

2

像由yuriy636注意到,你開始爲每一個新的歌曲一個新的動畫,而無需從不停歇的前一個。 所以當你播放5首歌曲時,你仍然有5個可視化渲染循環運行在每一幀,5個分析儀。

在這裏做的最好的是重構代碼:

  • 創建一個單人分析儀,僅流進它
  • 使畫布動畫自主更新,在第一負載
  • 一次聲明它因爲你只有一個<canvas>了,開始只有一個渲染動畫

當使用單人分析儀,渲染,當你常不使用任何新的東西e來源,它始終是相同的畫布,相同的分析器,相同的可視化。

這是一個概念的快速證明,非常骯髒,但我希望你能夠承擔我所做的事情和原因。

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>

+0

我誤解了你所說的話「當使用單個分析器時,渲染器在更改源代碼時不會使用任何新東西,它始終是同一個畫布,同一個分析器,同一個可視化對象。」我以爲你的意思是不管是哪一個歌曲被選中它在畫布上始終具有相同的視覺外觀。並感到困惑。任何人都可以這樣做。 – Nickh90

+0

但我注意到了一個小問題。如果在2秒鐘之前快速按下播放/暫停,則會開始執行與每次放慢之前相同的操作。您可以通過再次暫停並等待2秒來修復它。 – Nickh90

+0

@NickHewitt你是對的,不知道我做了什麼。重新引入原來的問題沒有很好的理由......現在應該修復。 – Kaiido