1

I would like to be able to select a music folder and shuffle all of the songs once on load and Have a skip button + return the song file name to the visualizer so that It can visualize each song. I'm still learning array's and for loops so I'm unsure of how to go about this. I also want to keep away from extra libraries for now because everything is already provided. Heres a code snippet of what I have so far

window.onload = function() {
  
  var file = document.getElementById("file");
  var audio = document.getElementById("audio");
  
  file.onchange = function() {
    var files = this.files;
    audio.src = URL.createObjectURL(files[0]);
    audio.load();
    audio.play();
    var context = new AudioContext();
    var src = context.createMediaElementSource(audio);
    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 = 256;

    var bufferLength = analyser.frequencyBinCount;
    console.log(bufferLength);

    var dataArray = new Uint8Array(bufferLength);

    var WIDTH = canvas.width;
    var HEIGHT = canvas.height;

    var barWidth = (WIDTH / bufferLength) * 1;
    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];
        
        var r = 5;
        var g = 195;
        var b = 45;

        ctx.fillStyle = "rgb(5,195,45)"
        ctx.fillRect(x, HEIGHT - barHeight, barWidth, barHeight);

        x += barWidth + 2;
      }
    }

    audio.play();
    renderFrame();
  };
};
#file {
  position: fixed;
  top: 10px;
  left: 10px;
  z-index: 100;
}

#canvas {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
}

audio {
  position: fixed;
  left: 350px;
  top: 10px;
  width: calc(50% - 20px);
}
<div id="content">
  <input type="file" id="file" accept="audio/*" />
  <canvas id="canvas"></canvas>
  <audio id="audio" controls></audio>
</div>
  • What issue are you having with HTML and JavaScript code at Question? – guest271314 Jul 27 '17 at 00:38
  • I simply don't know how to setup arrays and for loops for this. I'm very sure that I'd have to setup some form of array for collecting all of the Audio files in the folder I've selected and It's child folders. I've also Have to modify those arrays or make a new one with the location of each song and the shuffle order. I've got the basic idea in my head I'm just unsure of how to execute it. –  Jul 27 '17 at 01:22
  • You can set `webkitdirectory` and `allowdirs` attributes at `` element to enable directory upload. Iterate all uploaded directories to get all uploaded `File` objects within the directories, set as elements of an array, see [How to upload and list directories at firefox and chrome/chromium using change and drop events](https://stackoverflow.com/q/39664662/) – guest271314 Jul 27 '17 at 01:26
  • I'm not looking for drag and drop. So I'll leave that bit out. I've got the folder selection working now. How would I go about getting each audio file in that directory and all of its child directories and then make it shuffle and play. like I said in my previous post I'm still learning how to properly setup arrays and for loops. –  Jul 27 '17 at 01:57
  • Note, code at Question call `.play()` twice – guest271314 Jul 27 '17 at 06:34

1 Answers1

2

You can set webkitdirectory and allowdirs attributes at <input type="file"> element to enable directory upload.

Recursively, or using Promise repeatedly iterate directories, including directories within directories, push all files in directories to a globally defined array of File objects, see How to upload and list directories at firefox and chrome/chromium using change and drop events; where JavaScript at Answer is modified to remove drop event handler, which is specifically not a part of requirement.

Use Array.prototype.reduce() and Promise to call a function for each File object in sequence to play the media and return a fulfilled Promise at ended event of HTMLMediaElement; for example, the anonymous function at change handler at Question, modified where necessary to achieve expected requirement of a playlist created from upload of N directories or N nested directories. Note that when calling AudioContent.createMediaElementSource() more than once with the same <audio> element as parameter an exception will be thrown

Uncaught DOMException: Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode

see remove createMediaElementSource. You can define the variables globally and reference the variable using OR || to avoid the exception.

The created Blob URL is assigned to a variable and revoked at ended event of HTMLMediaElement and before being re-assigned when function is called again, if ended event is not reached.

Included <select> element for ability to select and play any one of the uploaded files.

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) {
          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 = 256;

        var bufferLength = analyser.frequencyBinCount;
        console.log(bufferLength);

        var dataArray = new Uint8Array(bufferLength);

        var WIDTH = canvas.width;
        var HEIGHT = canvas.height;

        var barWidth = (WIDTH / bufferLength) * 1;
        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];

            var r = 5;
            var g = 195;
            var b = 45;

            ctx.fillStyle = "rgb(5,195,45)"
            ctx.fillRect(x, HEIGHT - barHeight, barWidth, barHeight);

            x += barWidth + 2;
          }
        }

        renderFrame();
      })
    }

    input.addEventListener("change", processDirectoryUpload);
    select.addEventListener("change", handleSelectedSong);
<div id="content">
  Upload directory: <input id="file" type="file" accept="audio/*" directory allowdirs webkitdirectory/><br>
  <br>Now playing: <label for="audio"></label><br>
  <br><label for="select">Select a song to play:</label><br>
  <select id="select">
    </select>
  <canvas id="canvas"></canvas>
  <audio id="audio" controls></audio>
</div>
guest271314
  • 1
  • 15
  • 104
  • 177
  • This works great but now how would I prevent it from being able to select certain file extentions like jpg and png to prevent the "DOMException: Failed to load because no supported source was found." error –  Jul 27 '17 at 13:32
  • @NickHewitt You cannot prevent a user from selecting certain file types. What you can do is use `if..else` to check if the `File` object `.type` property begins with `"audio"` at `playMusic` function at first line within resolver function of `Promise` constructor before setting `.src` property of ` – guest271314 Jul 27 '17 at 13:48