Audio Player

Audio Player

·

8 min read

Preview.gif

I made an online audio player inspired by the recently shuttered Google Play Music.

Live app: audio-player-rho.vercel.app

Source code: github.com/jason00111/AudioPlayer

Features Overview

It can play audio from:

It includes basic controls:

  • play / pause
  • volume / mute
  • progress bar
  • next / previous track
  • shuffle
  • modifiable playlist

Notable Technologies

The Details

Here I give an instructive introduction to some parts of the app.

Audio Element

<audio id="audio" controls></audio>
const audioElement = document.getElementById('audio');
audioElement.src = url;

Custom Controls

Instead of using the browser's built-in controls, we can control the playback with our own buttons using JavaScript. Note that the controls attribute has been removed, telling the browser not to show the built-in controls.

<audio id="audio"></audio>
<button id="play">Play</button>
<button id="pause">Pause</button>
const audioElement = document.getElementById('audio');
const playButton = document.getElementById('play');
const pauseButton = document.getElementById('pause');

playButton.addEventListener('click', () => audioElement.play());
pauseButton.addEventListener('click', () => audioElement.pause());

Progress Bar

The audio element emits timeupdate events which we can use to set the width of a div.

<div id="progressBarContainer">
  <div id="progressBar" class="bg-yellow-500 h-full absolute top-0 left-0"></div>
</div>
audioElement.addEventListener('timeupdate', setProgressIndicator);

function setProgressIndicator() {
  progressBar.style.setProperty(
    'width',
    `${progressBarContainer.clientWidth * audioElement.currentTime / audioElement.duration}px`
  );
}

So far, the progress bar will indicate the progress. Next, we can allow it to be clicked on to modify the position.

progressBarContainer.addEventListener('click', handleProgressClick);

function handleProgressClick(event) {
  const box = progressBarContainer.getBoundingClientRect();

  audioElement.currentTime = ((event.x - box.left) / box.width) * audioElement.duration;
}

Another approach is to use an input element of type range.

Volume Control

The simplest way to control the volume is to set audioElement.volume.

However, I also generate "click" sounds whenever a button is clicked. I want the volume of those sounds to be controlled by the volume slider as well. So I set up a master gain using the Web Audio API.

Volume Slider

<input type="range" id="volumeSlider" value="1.0" min="0" max="1.5" step="0.01">
const audioContext = new AudioContext();
const masterGain = audioContext.createGain();
const audioSource = audioContext.createMediaElementSource(audioElement);

audioSource.connect(masterGain);
masterGain.connect(audioContext.destination);

volumeSlider.addEventListener('input', setVolume);

function setVolume() {
  masterGain.gain.value = volumeSlider.valueAsNumber;
}

Volume Icon

Volume Slider 2.gif

The icon changes as the volume is adjusted.

<icon-button id="volumeIcon" title="Mute" icon="volumeHalf"></icon-button>
function setVolume() {
  masterGain.gain.value = volumeSlider.valueAsNumber;
  setVolumeIcon(volumeSlider.valueAsNumber);
}

function setVolumeIcon(volume) {
  const normalizedVolume = volume / Number(volumeSlider.max);

  if (normalizedVolume > 0.5) {
    volumeIcon.setAttribute('icon', 'volumeFull');
  } else if (normalizedVolume > 0) {
    volumeIcon.setAttribute('icon', 'volumeHalf');
  } else {
    volumeIcon.setAttribute('icon', 'mute');
  }
}

icon-button is a custom element made using web components. I won't get into the details here.

Visualization

Visualization.gif

The visualization was created with the help an analyser. It analyses the sound and gives us the intensity of different frequency bands in real time.

<div id="visualization" class="w-full flex justify-center items-end"></div>
const audioSource = audioContext.createMediaElementSource(audioElement);
const analyser = audioContext.createAnalyser();

audioSource.connect(analyser);
analyser.connect(masterGain);

analyser.fftSize = 64;
const freqData = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(freqData);

for (let i = 0; i < analyser.frequencyBinCount; i++) {
  const band = document.createElement('div');
  const bandClasses = 'h-4 w-4 rounded-full';
  band.classList.add(...bandClasses.split(' '));
  visualization.append(band);
}

draw();

function draw() {
  analyser.getByteFrequencyData(freqData);

  visualization.childNodes.forEach((bandElement, index) => {
    setBand(bandElement, freqData[index] / 255);
  });

  requestAnimationFrame(draw);
}

function setBand(bandElement, intensity) {
  bandElement.style.setProperty('background-color', `rgba(245, 158, 11, ${intensity})`);
  bandElement.style.setProperty('height', `${intensity * visualization.clientHeight}px`);
}

There is a lot going on here! Let's break it down a little.

The for loop adds a div to the DOM for each frequency band. This is done dynamically in JavaScript so that the number of frequency bands can be easily changed. Also, having 32 identical divs in the HTML seems silly.

The code before the for loop sets up the analyser to put the frequency data in a typed array called freqData.

The draw function takes this data and sets the heights and colors of the divs which represent the bands.

requestAnimationFrame is how we make smooth animations in the browser. The standard pattern is to call requestAnimationFrame within a draw function, passing in the draw function itself as a callback. The result is that draw keeps being called, over and over, in a way which is controlled by the browser to ensure a smooth animation.

Click Sound

Audio feedback is not common on the web. I decided to include a click sound when buttons are clicked. It was partially because I wanted to experiment to see what would happen, and partially because I wanted to learn more about the Web Audio API.

Archive Searches

Searching the two archives, the Internet Archive and the Free Music Archive, was an interesting problem. Neither of them provide nice ways to get the file URL's, so I had to do some hacky-type things. These include using a CORS proxy and scraping through XML and HTML.

Possible Future Work

The ability to create and save multiple playlists is a must.

The discoverability of music and audio content using the two archive searches is not great. The simplest improvement would be to add some curated playlists. Another possibility is to recommend content based on the users history.

Add more sources of audio, like other archives.

The ability to play non-free content.

The ability to play video.


Exposing crimes is not a crime!

Don't shoot the messenger!

Free political prisoner Julian Assange!

Screen Shot 2021-02-07 at 11.55.04 PM.png


Cover photo by C D-X on Unsplash