This content originally appeared on Envato Tuts+ Tutorials and was authored by Andy Leverenz
This tutorial will show you how to build a custom music player, using the Web audio API, that is uniquely branded with CSS, HTML, and JavaScript.
HTML5 and the Web Audio API are tools that allow you to own a given website’s audio playback experience.
Rather than always using the browser defaults or third-party solutions to play audio, we can tap into the supplied free APIs and deliver a more branded design to end users on the web.
With that in mind, let’s leverage JavaScript to use the Web Audio API and ultimately give the end user more control over their audio listening experience.
Our HTML Audio Player Demo
Click on the play button, listen to it go.
The final audio player features volume, play, pause, and scrubbing controls. We’ll also make it look sleek with some rich color and design, loosely based on some of the designs in this After Effects Music Player Pack on Envato Elements.
For the purposes of this tutorial our audio player is simple. It can, however, be redesigned and extended to use a track list should you need more than one audio file at a given time.
Playback in CodePen and Google Chrome
Whilst building this audio player, I noticed an issue with the CodePen demo in Google Chrome. If you play the audio track, then refresh the page, you’ll probably find the track won’t play again. It seems the combination of CodePen’s iframes, plus Google’s Autoplay policy, conspire to give us a low media engagement score and render the track unplayable.
As you’ll see, we add the recommended code to allow for playing audio based on user interaction (a click) but we’re still getting errors in this case. Rest assured that once outside the CodePen environment our audio player works just fine in Google Chrome.
1. The HTML
Let’s begin by adding the necessary HTML to make this concept a reality! We need to have an <audio>
tag element on the page to tap into the Web Audio API most efficiently. This doesn’t need to display visually by default, but it’s crucial that we have it on the page.
<div class="player"> <div class="player-track-meta"> <p>Track name</p> <p><span>Track Author</span></p> </div> <div class="player-controls"> <button class="player-play-btn" role="button" aria-label="Play" data-playing="false" > <div class="player-icon-play"> <svg xmlns="https://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>play</title><polygon class="icon-play" points="19.05 12 6 3.36 6 20.64 19.05 12"/><rect class="icon-container" width="24" height="24"/></svg> </div> <div class="player-icon-pause hidden"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>pause</title><g><rect class="icon-pause" x="6" y="3.26" width="4" height="17.48"/><rect class="icon-pause" x="14" y="3.26" width="4" height="17.48"/></g><rect class="icon-container" width="24" height="24"/></svg> </div> </button> <div class="player-timeline"> <span class="player-time player-time-current">00:00</span> <div class="player-progress"> <div class="player-progress-filled"></div> </div> <span class="player-time player-time-duration">00:00</span> </div> <div class="player-volume-container"> <input type="range" id="volume" min="0" max="1" value="1" step="0.01" class="player-volume" /> </div> </div> <audio src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/858/outfoxing.mp3" crossorigin="anonymous" ></audio> </div>
Linked inside the src attribute of the <audio>
tag on the page, you’ll find an MP3 audio track.
The core player HTML markup comprises a variety of containers, controls, and SVG icons. We leverage a range input to adjust volume and swap between a play and pause icon for better use of space.
2. Styling the Player
Your music player can be as unique as you like. I chose my own preferred style, which is quite simplistic and will allow you to add and remove your own details without too much trouble. Here’s the final CSS I ended up with.
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap'); body { background: url("https://assets.codepen.io/210284/music-bg_1.jpg") center center; background-size: cover; color: #1f2937; font-family: 'Inter', sans-serif; } .hidden { display: none; } .player { max-width: 500px; margin: 7rem auto; background: white; padding: 36px 32px 24px 32px; border-radius: 14px; box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); } .player-track-meta { text-align: center; } .player-track-meta p { margin: 0; font-size: 20px; color: #0e0e0e; font-weight: 700; } .player-track-meta span { font-size: 16px; font-weight: 400; padding: 0 2px; position: relative; top: 1px; color: #a3a3a3; } .player-controls { display: flex; align-items: center; } .player-play-btn { background: transparent; border: none; cursor: pointer; display: flex; justify-content: center; align-items: center; width: 36px; height: 36px; } .icon-container { fill: transparent; stroke: none; } .player-play-btn:hover { fill: #444444; } .player-play-btn svg { color: #0e0e0e; position: relative; left: 0.5px; width: 36px; height: 36px; display: block; } .player-play-btn:hover svg { color: #ffffff; } .player-timeline { display: flex; flex: 1; align-items: center; justify-content: space-between; padding-left: 10px; } .player-progress { display: flex; postion: relative; height: 6px; background: #a3a3a3; border-radius: 25px; margin: 0 5px; flex: 10; flex-basis: 100%; overflow: hidden; } .player-progress-filled { height: 6px; background: #0e0e0e; flex: 0; flex-basis: 0%; border-radius: 25px; } .player-time { padding: 2px 5px; } .player-volume-container { width: 15%; } .player-volume { height: 28px; -webkit-appearance: none; margin: 10px 0; width: 100%; background: transparent; } .player-volume:focus { outline: none; } .player-volume::-webkit-slider-runnable-track { width: 100%; height: 6px; cursor: pointer; animate: 0.2s; background: #0e0e0e; border-radius: 10px; } .player-volume::-webkit-slider-thumb { height: 16px; width: 16px; border-radius: 100px; border: none; background: #0e0e0e; cursor: pointer; -webkit-appearance: none; margin-top: -4px; } .player-volume:focus::-webkit-slider-runnable-track { background: #0e0e0e; } .player-volume::-moz-range-track { width: 100%; height: 6px; cursor: pointer; animate: 0.2s; background: #0e0e0e; border-radius: 10px; } .player-volume::-moz-range-thumb { height: 16px; width: 16px; border-radius: 100px; border: none; background: #0e0e0e; cursor: pointer; margin-top: -4px; } .player-volume::-ms-track { width: 100%; height: 6px; cursor: pointer; animate: 0.2s; background: #0e0e0e; border-radius: 10px; } .player-volume::-ms-fill-lower { background: #0e0e0e; border-radius: 10px; } .player-volume::-ms-fill-upper { background: #0e0e0e; border-radius: 10px; } .player-volume::-ms-thumb { margin-top: 1px; height: 15px; width: 15px; border-radius: 5px; border: none; background: #0e0e0e; cursor: pointer; } .player-volume:focus::-ms-fill-lower { background: #38bdf8; } .player-volume:focus::-ms-fill-upper { background: #38bdf8; }
In most cases, I maintained a naming convention for class names with the prefix .player-
to keep things modular. A utility class of .hidden
was added, which will come in handy when we tackle the JavaScript portion of this tutorial.
3. Making it All Work With JavaScript
To tap into the Web Audio API, we need some form of media element in the HTML. You can optionally create one with JavaScript, but I find it a little easier to render something on the page that’s not in view.
You’ll recall we added the <audio>
tag in a previous step. We’ll query for it first and also create something known as a new AudioContext()
instance.
// load sound via <audio> tag const audioElement = document.querySelector("audio") const audioCtx = new AudioContext() const track = audioCtx.createMediaElementSource(audioElement)
What’s AudioContext?
AudioContext is an interface that represents an audio-processing graph built from audio modules that are linked together (AudioNodes).
That’s a complicated way of saying that an AudioContext is a grouping of audio nodes you can interact with and manipulate. Things like volume (gain), panning, and more are options available to tweak. We'll interact more with this feature coming up when we address volume controls.
The track
variable allows us to adjust the audio element's properties directly using the AudioContext. Read more about the createMediaElementSource() method.
“An AudioContext is a grouping of audio nodes you can interact with and manipulate.”
Querying for Player Controls
What’s a custom player without custom controls? The next step is to query for all the custom controls we added in the HTML. We’ll using the Document.querySelector()
method to return each associated element assigned by a variable.
// Player controls and attributes const playButton = document.querySelector(".player-play-btn") const playIcon = playButton.querySelector(".player-icon-play") const pauseIcon = playButton.querySelector(".player-icon-pause") const progress = document.querySelector(".player-progress") const progressFilled = document.querySelector(".player-progress-filled") const playerCurrentTime = document.querySelector(".player-time-current") const playerDuration = document.querySelector(".player-time-duration") const volumeControl = document.querySelector(".player-volume")
Here we have variables for each independent control and the progress bar shown in the user interface.
Waiting Before JavaScript Fires
To properly load the audio, which can sometimes take longer than other items on the page, it probably makes sense to wait for the entire page to load before we run any JavaScript.
We’ll start with an event listener that waits for the page to load. We can wrap the entirety of our code in this block.
window.addEventListener("load", () => { // all code goes here besides variables })
We’ll start by listening for the playButton
variable’s click event to instruct our player to Play.
// Play button toggle playButton.addEventListener("click", () => { // check if context is in suspended state (autoplay policy) // By default, browsers won't allow you to autoplay audio. // You can override by finding the AudioContext state and resuming it after a user interaction like a "click" event. if (audioCtx.state === "suspended") { audioCtx.resume() } // Play or pause track depending on state if (playButton.dataset.playing === "false") { audioElement.play() playButton.dataset.playing = "true" playIcon.classList.add("hidden") pauseIcon.classList.remove("hidden") } else if (playButton.dataset.playing === "true") { audioElement.pause() playButton.dataset.playing = "false" pauseIcon.classList.add("hidden") playIcon.classList.remove("hidden") } })
A few things happen at once when the playButton
gets clicked.
- Browsers are smart enough to stop auto-playing audio from playing on the first load. Inside the
AudioContext
method, there is a state method that returns a value of “suspended”, “running”, or “closed”. In our case, we’ll be looking for “suspended”. If that‘s the state that returns, we can proceed to resume the audio with the method calledresume()
. -
We use data attributes in the HTML to denote when the button is “playing” or “paused”.
-
If the play or pause button is clicked, we can dynamically tell the
audioElement
to play or pause. -
For a better user experience, I added the ability to show and hide the play or pause icons depending on the player’s state.
Update Time Stamps and Progress
Each track you load with an AudioElement context will have its characteristics and metadata you can display in the HTML. We start by making everything zero on the first-page load and proceed to call a function that dynamically updates and formats the time as the audio gets played or paused.
We’ll additionally show a progress bar that will dynamically fill based on the amount of lapsed audio. This is handy for the end user who might want to glance at a progress bar rather than read the remaining time.
// Update progress bar and time values as audio plays audioElement.addEventListener("timeupdate", () => { progressUpdate() setTimes() })
I created two functions that are extracted elsewhere in the JavaScript file. The main thing to denote about the code above is the type of event listener we keep track of. The timeupdate
event is unique to media like Audio or Video within the Web API.
Displaying and Formatting Time
We can use the playerCurrentTime
and playerDuration
variables to display and format time. We’ll set the textContent of those tags in the HTML to match a new timestamp relative to the audioElement’s current attributes. An audioElement will have a currentTime
property and a duration
property.
Using the Date API in JavaScript, we can tap into a handy one-liner to convert the default seconds that get returned from currentTime
and duration
in a format that matches HH:MM:SS
(Hours, Minutes, Seconds).
// Display currentTime and duration properties in real-time function setTimes() { playerCurrentTime.textContent = new Date(audioElement.currentTime * 1000) .toISOString() .substr(11, 8) playerDuration.textContent = new Date(audioElement.duration * 1000) .toISOString() .substr(11, 8) }
Updating Player Progress
Updating the progress bar in our HTML is relatively simple and comes down to a percentage calculation. We’ll get the percent returned by dividing the audioElement.currentTime
by the audioElement.duration
and multiplying that by 100.
Finally, we can set some CSS via JavaScript by using the progressFilled
variable we created before and adjusting the flex-basis
property to grow or shrink depending on the change percentage.
// Update player timeline progress visually function progressUpdate() { const percent = (audioElement.currentTime / audioElement.duration) * 100 progressFilled.style.flexBasis = `${percent}%` }
Add Volume Controls
Adjusting volume taps back into the AudioContext object we used before. We’ll need to call a method named createGain()
and change the gain value to map to the volume range input within the HTML.
// Bridge the gap between gainNode and AudioContext so we can manipulate volume (gain) const gainNode = audioCtx.createGain() volumeControl.addEventListener("change", () => { gainNode.gain.value = volumeControl.value }) track.connect(gainNode).connect(audioCtx.destination)
We created a track
variable early on in this tutorial and are finally putting it to use here. Using the connect()
method, you can connect the track to the gainNode
and then to the AudioContext
. Without this line, the volume range input doesn’t know about the volume of the audio.
We’ll listen for a change event to map the volume relative to the gain.
What Happens When the Audio Ends?
We can reset the player after the audio ends so it can be ready for another listen should the end user want to start it over.
// if the track ends, reset the player audioElement.addEventListener("ended", () => { playButton.dataset.playing = "false" pauseIcon.classList.add("hidden") playIcon.classList.remove("hidden") progressFilled.style.flexBasis = "0%" audioElement.currentTime = 0 audioElement.duration = audioElement.duration })
Here we toggle the play button icon from pause to play, set the data-playing attribute to false
, reset the progress bar, and the audioElement’s currentTime
and duration
properties.
Scrubbing the Progress Bar to Skip and Rewind
Our progress bar is functional visually, but it would be more helpful if you could click anywhere on the timeline and adjust the current audio playback. We can achieve this with a series of event listeners and a new function.
// Scrub player timeline to skip forward and back on click for easier UX let mousedown = false function scrub(event) { const scrubTime = (event.offsetX / progress.offsetWidth) * audioElement.duration audioElement.currentTime = scrubTime } progress.addEventListener("click", scrub) progress.addEventListener("mousemove", (e) => mousedown && scrub(e)) progress.addEventListener("mousedown", () => (mousedown = true)) progress.addEventListener("mouseup", () => (mousedown = false))
The scrub()
function requires an event argument we listen for. In particular, the offsetX
property allows us to pinpoint where a user clicked and make calculations relative to the audioElement’s properties.
Finally, we can listen on the progress bar itself for a set of events like click, mousemove, mousedown, and mouseup to adjust the audio element’s currentTime
property.
4. Putting it All Together
The final JavaScript code is below. One thing to note is on the first-page load; I call the setTimes()
function once again so we can get real-time displayed correctly before the user even starts manipulating the audio player.
// load sound via <audio> tag const audioElement = document.querySelector("audio") const audioCtx = new AudioContext() const track = audioCtx.createMediaElementSource(audioElement) // Player controls and attributes const playButton = document.querySelector(".player-play-btn") const playIcon = playButton.querySelector(".player-icon-play") const pauseIcon = playButton.querySelector(".player-icon-pause") const progress = document.querySelector(".player-progress") const progressFilled = document.querySelector(".player-progress-filled") const playerCurrentTime = document.querySelector(".player-time-current") const playerDuration = document.querySelector(".player-time-duration") const volumeControl = document.querySelector(".player-volume") document.addEventListener("DOMContentLoaded", () => { // Set times after page load setTimes() // Update progress bar and time values as audio plays audioElement.addEventListener("timeupdate", () => { progressUpdate() setTimes() }) // Play button toggle playButton.addEventListener("click", () => { // check if context is in suspended state (autoplay policy) // By default, browsers won't allow you to autoplay audio. // You can override by finding the AudioContext state and resuming it after a user interaction like a "click" event. if (audioCtx.state === "suspended") { audioCtx.resume() } // Play or pause track depending on state if (playButton.dataset.playing === "false") { audioElement.play() playButton.dataset.playing = "true" playIcon.classList.add("hidden") pauseIcon.classList.remove("hidden") } else if (playButton.dataset.playing === "true") { audioElement.pause() playButton.dataset.playing = "false" pauseIcon.classList.add("hidden") playIcon.classList.remove("hidden") } }) // if the track ends, reset the player audioElement.addEventListener("ended", () => { playButton.dataset.playing = "false" pauseIcon.classList.add("hidden") playIcon.classList.remove("hidden") progressFilled.style.flexBasis = "0%" audioElement.currentTime = 0 audioElement.duration = audioElement.duration }) // Bridge the gap between gainNode and AudioContext so we can manipulate volume (gain) const gainNode = audioCtx.createGain() const volumeControl = document.querySelector(".player-volume") volumeControl.addEventListener("change", () => { gainNode.gain.value = volumeControl.value }) track.connect(gainNode).connect(audioCtx.destination) // Display currentTime and duration properties in real-time function setTimes() { playerCurrentTime.textContent = new Date(audioElement.currentTime * 1000) .toISOString() .substr(11, 8) playerDuration.textContent = new Date(audioElement.duration * 1000) .toISOString() .substr(11, 8) } // Update player timeline progress visually function progressUpdate() { const percent = (audioElement.currentTime / audioElement.duration) * 100 progressFilled.style.flexBasis = `${percent}%` } // Scrub player timeline to skip forward and back on click for easier UX let mousedown = false function scrub(event) { const scrubTime = (event.offsetX / progress.offsetWidth) * audioElement.duration audioElement.currentTime = scrubTime } progress.addEventListener("click", scrub) progress.addEventListener("mousemove", (e) => mousedown && scrub(e)) progress.addEventListener("mousedown", () => (mousedown = true)) progress.addEventListener("mouseup", () => (mousedown = false)) // Track credit: Outfoxing the Fox by Kevin MacLeod under Creative Commons + MDN for the link. })
Conclusion
There you have it! With a bit of JavaScript and elbow grease, you can create your very own branded music player.
From here, you might experiment with adding more controls, like skipping buttons or panning buttons. I’d also check out the AudioTracklist interface, which allows you to create playlists and extend the design as necessary.
This content originally appeared on Envato Tuts+ Tutorials and was authored by Andy Leverenz
Andy Leverenz | Sciencx (2022-09-26T15:04:03+00:00) Build a Custom HTML Music Player, Using JavaScript and the Web Audio API. Retrieved from https://www.scien.cx/2022/09/26/build-a-custom-html-music-player-using-javascript-and-the-web-audio-api/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.