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:
- a file on a users device
- a file on the web
- the Internet Archive
- the Free Music Archive
It includes basic controls:
- play / pause
- volume / mute
- progress bar
- next / previous track
- shuffle
- modifiable playlist
Notable Technologies
- No server
- No build step. Only native browser JavaScript, HTML, and CSS.
- Tailwind CSS
- Web audio API
- Web components
- JavaScript modules
- http-server for development
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
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
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!