javascriptgoogle-chromeaudiomedia

Toggling between elements - Javascript - Digital Audio Workstation


I'm currently working on coding a digital audio workstation however I'm encountering an issue with my elements. I'm trying to implement a function in which if on a row, a dropzone is playing an audio, clicking on another dropzone of that same row would simultaneously stop playing the other dropzone and start playing the one I just clicked on. However what I am encountering is that I have to click two times on a dropzone for it to play (one time to stop/pause the other dropzone and one time to start playing the dropzone) which I do not get why Please find my code below and thank you so much in advance for all the answers and help!

const dropzones = document.getElementsByClassName("loop-cell");
const rowPlayingAudio = {};

Array.from(dropzones).forEach((dropzone, index) => {
  let audio = null;
  let isPlaying = false;

  dropzone.addEventListener("dragover", (e) => {
    e.preventDefault();
    dropzone.style.borderColor = "blue";
  });

  dropzone.addEventListener("dragleave", () => {
    if (!audio) {
      dropzone.style.borderColor = "#fda0d3";
    } else {
      dropzone.style.borderColor = "#a2d0f1";
    }
  });

  dropzone.addEventListener("drop", (e) => {
    e.preventDefault();
    dropzone.style.borderColor = "#a2d0f1";

    const files = Array.from(e.dataTransfer.files).filter((file) =>
      file.type.includes("audio/")
    );

    files.forEach((file) => {
      const reader = new FileReader();
      reader.onload = function(e) {
        const audioSrc = e.target.result;
        audio = new Audio(audioSrc);
        audio.loop = true;
        isPlaying = false;
      };
      reader.readAsDataURL(file);
    });
  });

  dropzone.addEventListener("click", () => {
    if (audio) {
      const rowIndex = Math.floor(index / 8);

      if (
        rowPlayingAudio[rowIndex] &&
        rowPlayingAudio[rowIndex].audio !== audio
      ) {
        rowPlayingAudio[rowIndex].audio.pause();
        rowPlayingAudio[rowIndex].audio.currentTime = 0;
        rowPlayingAudio[rowIndex].isPlaying = false;
        rowPlayingAudio[rowIndex] = null;
      }

      if (isPlaying) {
        audio.pause();
        audio.currentTime = 0;
        isPlaying = false;
        rowPlayingAudio[rowIndex] = null;
      } else {
        audio.play();
        isPlaying = true;
        rowPlayingAudio[rowIndex] = {
          audio,
          dropzone,
          isPlaying,
          index
        };
      }

      audio.onended = () => {
        setLaunchpadLight(index, 0);
        isPlaying = false;
        rowPlayingAudio[rowIndex] = null;
      };
    }
  });
});
.loop-grid {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  gap: 7px;
}

.loop-cell {
  background: #fec9e6;
  padding: 10px;
  text-align: center;
  border: 4px solid #fda0d3;
  cursor: pointer;
  transition: background-color 0.1s;
}
<!DOCTYPE html>
<html>
<body>
  <h1>LaunchPad 2 Looper</h1>
  <main>
    <div class="loop-grid">
      <div id="dropzone0" class="loop-cell">Keys</div>
      <div id="dropzone1" class="loop-cell">Keys</div>
      <div id="dropzone2" class="loop-cell">Keys</div>
      <div id="dropzone3" class="loop-cell">Keys</div>
      <div id="dropzone4" class="loop-cell">Keys</div>
      <div id="dropzone5" class="loop-cell">Keys</div>
      <div id="dropzone6" class="loop-cell">Keys</div>
      <div id="dropzone7" class="loop-cell">Keys</div>
      <div id="dropzone8" class="loop-cell">Bass</div>
      <div id="dropzone9" class="loop-cell">Bass</div>
      <div id="dropzone10" class="loop-cell">Bass</div>
      <div id="dropzone11" class="loop-cell">Bass</div>
      <div id="dropzone12" class="loop-cell">Bass</div>
      <div id="dropzone13" class="loop-cell">Bass</div>
      <div id="dropzone14" class="loop-cell">Bass</div>
      <div id="dropzone15" class="loop-cell">Bass</div>
    </div>
  </main>
</body>

</html>

I tried deleting any potential duplicates but I don't get where the issue could be coming from


Solution

  • Great Minimal Reproducible Example.

    I fixed the issue. Changes made:

    1. Handling state of play (rowPlayingAudio) using the built in events play, ended, pause. I am even toggling a class playing on the playing element.
    2. I am attaching those events soon after creation of audio
    3. Now the click handler is much simpler, knowing status will be handled using the events.

    const dropzones = document.querySelectorAll(".loop-cell");
    const rowPlayingAudio = {};
    
    function setLaunchpadLight() {
      // dummy
    }
    
    Array.from(dropzones).forEach((dropzone, index) => {
      let audio = null;
      let isPlaying = false;
      const rowIndex = Math.floor(index / 8);
    
      dropzone.addEventListener("dragover", (e) => {
        e.preventDefault();
        dropzone.style.borderColor = "blue";
      });
    
      dropzone.addEventListener("dragleave", () => {
        if (!audio) {
          dropzone.style.borderColor = "#fda0d3";
        } else {
          dropzone.style.borderColor = "#a2d0f1";
        }
      });
    
      dropzone.addEventListener("drop", (e) => {
        e.preventDefault();
        dropzone.style.borderColor = "#a2d0f1";
    
        const files = Array.from(e.dataTransfer.files).filter((file) =>
          file.type.includes("audio/")
        );
    
        files.forEach((file) => {
          const reader = new FileReader();
          reader.onload = function(e) {
            const audioSrc = e.target.result;
            audio = new Audio(audioSrc);
            audio.loop = true;
            isPlaying = false;
    
            function onStopForAnyReason(ev) {
              dropzone.classList.remove("playing")
              setLaunchpadLight(index, 0);
              isPlaying = false;
              audio.currentTime = 0;
              delete rowPlayingAudio[rowIndex];
            }
    
            audio.onended = onStopForAnyReason
            audio.onpause = onStopForAnyReason
            audio.onplay = () => {
              dropzone.classList.add("playing")
              isPlaying = true;
              rowPlayingAudio[rowIndex] = {
                audio,
                dropzone,
                isPlaying,
                index
              };
            }
          };
          reader.readAsDataURL(file);
        });
      });
    
      dropzone.addEventListener("click", () => {
        if (audio) {
    
          if (rowPlayingAudio[rowIndex]) {
            rowPlayingAudio[rowIndex].audio.pause();
          }
    
          if (isPlaying) {
            audio.pause();
          } else {
            audio.play();
          }
          isPlaying = !isPlaying
    
        }
      });
    });
    .loop-grid {
      display: grid;
      grid-template-columns: repeat(8, 1fr);
      gap: 7px;
    }
    
    .loop-cell {
      background: #fec9e6;
      padding: 10px;
      text-align: center;
      border: 4px solid #fda0d3;
      cursor: pointer;
      transition: background-color 0.1s;
    }
    
    .loop-cell.playing {
      background: green;
    }
    <h1>LaunchPad 2 Looper</h1>
    <main>
      <div class="loop-grid">
        <div id="dropzone0" class="loop-cell">Keys</div>
        <div id="dropzone1" class="loop-cell">Keys</div>
        <div id="dropzone2" class="loop-cell">Keys</div>
        <div id="dropzone3" class="loop-cell">Keys</div>
        <div id="dropzone4" class="loop-cell">Keys</div>
        <div id="dropzone5" class="loop-cell">Keys</div>
        <div id="dropzone6" class="loop-cell">Keys</div>
        <div id="dropzone7" class="loop-cell">Keys</div>
        <div id="dropzone8" class="loop-cell">Bass</div>
        <div id="dropzone9" class="loop-cell">Bass</div>
        <div id="dropzone10" class="loop-cell">Bass</div>
        <div id="dropzone11" class="loop-cell">Bass</div>
        <div id="dropzone12" class="loop-cell">Bass</div>
        <div id="dropzone13" class="loop-cell">Bass</div>
        <div id="dropzone14" class="loop-cell">Bass</div>
        <div id="dropzone15" class="loop-cell">Bass</div>
      </div>
    </main>