< back

bird wrongs

resources: diy audio player.

One reason I decided to try and make my own audio player for my website was that I wanted it to look and behave a certain way that fit in with my website; another reason is because it has better privacy and security implications to use self-hosted scripts. Luckily, it's not too hard, and I figured it out so you don't have to!

A few things you need to set up:

  1. HTML scaffolding for the player
  2. Some styling (can be minimal, can be fancy; there's a minimal snippet I'll show that you can build off of)
  3. A folder called "playlist" that contains your audio files and a file called "playlist.json"
  4. A bit of javascript that's kind of long but not actually that scary due to my commitment to writing it in plain javascript and not in jQuery.
<div id="player-wrapper">
  <div id="song-info">
    <div id="song-image"></div>
    <div id="song-labels">
      <div id="song-name"></div>
      <div id="song-artist"></div>
      <div id="song-album"></div>
    </div>
  </div>
  <div id="playback-position"><div id="playback-marker"></div><span id="position-text"></span></div>
  <button id="play-pause" data-playing="false" role="switch" aria-checked="false">
    <span>Play</span>
  </button>
  <audio src="" id="music-player"></audio>
</div>

(I borrowed the HTML for the button from this tutorial on making a music player leveraging the YouTube API)

So that's the HTML scaffolding; pretty much everything is prominently labeled. If you wanted to add more fields, you could put them in the box with the id #song-labels.

Next, we'll need to add a little CSS to make things look right. You can adapt this a lot, of course, and put your own spin on it! I've only included the barebones stuff.

#playlist-wrapper {
	position: relative;
}

#playback-position {
  width: 100%;
  height: 1px;
  position: relative;
  background-color: black;
}

#playback-marker {
  width: 6px;
  height: 6px;
  border-radius: 100%;
  background-color: black;
  transform: translateX(-50%) translateY(-50%);
}

This will not look good! I am confident in your ability to make it look nicer, though. After all, this is Neocities. You're here because you're cool.

Then, let's go make our playlist file. We're going to use a file format called JSON that's a file containing a JavaScript object. Here's an example:

[
  {
    "name": "song name",
    "artist": "artist name",
    "album_art": "path/to/album/art.jpg",
    "file": "path/to/song_file.ogg",
    "credit": "https://credit.link"
  }
]

To add more items, add a comma after the closing curly brace, and copy the curly-braced section. As the script is currently set up, you'll want your music files and cover art images in the same folder, but you can change that up with pretty minimal edits. Playlist items are played in order from top to bottom.

Last, this is the javascript snippet you need to add. You can either paste it into a script tag or put it in a separate file; just put it right before the closing </body> tag whichever way you do it. I've commented to explain what each part does; basically, it pulls the data from the playlist.json file, loads in the first song, and watches for either play/pause events, or the event that's fired when a song ends. If the song has ended, it loads in the next song and plays it.

// setting up a bunch of elements we'll need to manipulate, the initial playlist position that we'll update, and the playlist holder
let playlistPosition = 0;
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioContext = new AudioContext();
const musicPlayer = document.getElementById('music-player');
const playPause = document.getElementById('play-pause');
const songName = document.getElementById('song-name');
const songArtist = document.getElementById('song-artist');
const songAlbum = document.getElementById('song-album');
const songImage = document.getElementById('song-image');
const marker = document.getElementById('playback-marker');
const positionText = document.getElementById('position-text');
let playlist = [];

// helper function to update the music info on the player
const updateSong = () => {
  musicPlayer.src = "/playlist/"+playlist[playlistPosition].file;
  songName.innerHTML = playlist[playlistPosition].name;
  songArtist.innerHTML = `<a href="${playlist[playlistPosition].credit}" target="_blank">${playlist[playlistPosition].artist}</a>`;
  songAlbum.innerHTML = playlist[playlistPosition].album;
  songImage.innerHTML = `<img src="/playlist/${playlist[playlistPosition].album_art}" />`;
}

// helper function to take a number of seconds and turn it into (h:)m:s format
const formatTime = (sec) => {
  const hours = Math.floor(sec / 360);
  const minutes = Math.floor((sec - (hours * 360)) / 60);
  const seconds = Math.floor(sec - (hours * 360) - (minutes * 60));
  return `${hours > 0 ? `${hours}:` : ''}${hours > 0 ? minutes.toString().padStart(2, '0') : minutes}:${seconds.toString().padStart(2, '0')}`;
}

// main initializer
const initAudio = async () => {
  // get the playlist data from the json file asynchronously
  const playlistData = await fetch('../playlist/playlist.json');
  playlist = await playlistData.json();
  
  // if there's items in the playlist (there should be!) initialize with the first song
  if (playlist.length > 0) {
    updateSong();
  }
  
  // connect the audio player to the AudioContext API we use as the controller
  const track = audioContext.createMediaElementSource(musicPlayer);
  track.connect(audioContext.destination);
  
  // handle click events on the play/pause button—toggle based on current state
  playPause.addEventListener('click', () => {
    if (audioContext.state === 'suspended') {
      audioContext.resume();
    }

    if (playPause.dataset.playing === "false") {
      musicPlayer.play();
      playPause.dataset.playing = "true";
      playPause.querySelector('span').innerHTML = "Pause";
    } else {
      musicPlayer.pause();
      playPause.dataset.playing = "false";
      playPause.querySelector('span').innerHTML = "Play";
    }
  }, false);

  // if the song ends, check if there's more songs in the playlist; if there are, load its data and start the player again. if not, change the button to the stopped state
  musicPlayer.addEventListener('ended', () => {
    playlistPosition++;
    if (playlistPosition < playlist.length) {
      updateSong();
      musicPlayer.play();
    } else {
      playPause.dataset.playing = "false";
    }
  });
  
  // every second (1000 ms), calculate the percentage we're at through the current song, format the time, and position the marker
  setInterval(() => {
    const percent = (musicPlayer.currentTime/musicPlayer.duration) * 100;
    const text = `${formatTime(musicPlayer.currentTime)} / ${formatTime(musicPlayer.duration)}`;
    positionText.innerHTML = text;
    marker.style.left = percent+"%";
  }, 1000);
};

// run the initializer
initAudio();

And there you have it! This should get you up and running with a functional (if not particularly pretty) version you can make your own. ✨