Written by Fortune Ikechi✏️
Music players are devices or applications that allow you to listen to audio files and recordings. There are many music players available, but in this article, we’ll build a clone of the popular music streaming service, Spotify, using React and ts-audio.
You might expect that this tutorial would use the Spotify API, however, Spotify and other music databases do not provide a streamable link or URL in their response body. The Spotify API does provide a preview URL, but the duration of the songs is limited to only 30 seconds, and that isn't enough for our example. Therefore, we won't be using the Spotify API or making any requests to any music API or databases.
Instead, we’ll work with dummy data consisting of songs and image art. However, if you venture across an API with a streamable link, you can also apply the methods used in this article. You can find the complete code for this tutorial at the GitHub repo. Let’s get started!
- What is ts-audio?
- Building a Spotify clone with ts-audio
- Problem solving: Mismatched song details
- Adding styling
What is ts-audio?
ts-audio is an agnostic library that makes the AudioContext
API easier to interact with. ts-audio provides you with methods like play, pause, and more, and it allows you to create playlists. ts-audio offers the following features:
- Includes a simple API that abstracts the complexity of the
AudioContext
API - Offers cross-browser support
- Makes it easy to create an audio playlist
- Works with any language that compiles into JavaScript
Building a Spotify clone with ts-audio
Let's start by creating a new React app with the command below:
npx create-react-app ts-audio
If you’re using Yarn, run the command below:
yarn create react-app ts-audio
For the rest of the tutorial, I’ll use Yarn. Next, we install the ts-audio package as follows:
yarn add ts-audio
At its core, ts-audio has two components, Audio
and AudioPlaylist
. The components are functions that we can call with specific parameters.
Using the Audio
component
The Audio
component allows us to pass in a single song to be played. It also provides us with certain methods like play()
, pause()
, stop()
, and more:
// App.js
import Audio from 'ts-audio';
import Lazarus from './music/Lazarus.mp3';
export default function App() {
const audio = Audio({
file: Lazarus
})
const play = () => {
audio.play()
}
const pause = () => {
audio.pause()
}
const stop = () => {
audio.stop()
}
return (
<>
<button onClick={play}>Play</button>
<button onClick={pause}>Pause</button>
<button onClick={stop}>Stop</button>
</>
)
}
In the code block above, we imported the Audio
component from ts-audio and the song we want to play. We created an audio instance, set it to the imported Audio
component, and then passed the imported music to the file parameter exposed by the Audio
element. We took advantage of the methods provided to us by ts-audio, like play()
and pause()
, then passed them through functions to the buttons.
Using the AudioPlaylist
component
The AudioPlaylist
component allows us to pass in multiple songs, but they have to be in an array, otherwise ts-audio won't play them. The AudioPlaylist
component provides us with methods like play()
, pause()
, stop()
, next()
, and prev()
.
The code block below is an example of how to use the AudioPlaylist
component:
// App.js
import { AudioPlaylist } from 'ts-audio';
import Lazarus from './music/Lazarus.mp3';
import Sia from './music/Sia - Bird Set Free.mp3';
export default function App() {
const playlist = AudioPlaylist({
files: [Lazarus, Sia]
})
const play = () => {
playlist.play()
}
const pause = () => {
playlist.pause()
}
const next = () => {
playlist.next()
}
const previous = () => {
playlist.prev()
}
const stop = () => {
playlist.stop()
}
return (
<>
<button onClick={play}>Play</button>
<button onClick={pause}>Pause</button>
<button onClick={next}>Next</button>
<button onClick={prev}>Prev</button>
<button onClick={stop}>Stop</button>
</>
)
}
The music player will have the following functionalities:
- Change the artist to the current song's artist whenever we click on either next or previous
- Change the image to the current song's image
- Change the song title to the current song
In the src
folder, create two folders called images
and music
, respectively. Navigate to the images
folder and paste any photos you might need. In the music
folder, you can paste any audio files that you want to use.
In the following GitHub repos, you can get the image files used in this tutorial and obtain the audio files. Next, import songs and images into App.js
as follows:
import { AudioPlaylist } from 'ts-audio';
// Music import
import Eyes from './music/01\. Jon Bellion - Eyes To The Sky.mp3';
import Mood from './music/24kGoldn-Mood-Official-Audio-ft.-Iann-Dior.mp3';
import Audio from './music/audio.mp3';
import Broken from './music/Cant Be Broken .mp3';
import Lazarus from './music/Lazarus.mp3';
import Sia from './music/Sia - Bird Set Free.mp3';
import Nobody from './music/T-Classic-Nobody-Fine-Pass-You.mp3';
import Yosemite from './music/Yosemite.mp3';
// Pictures import
import EyesImg from './images/Eyes to the sky.jpeg';
import MoodImg from './images/mood.jpeg';
import AudioImg from './images/lana.jpeg';
import BrokenImg from './images/lil wayne.jpeg';
import LazarusImg from './images/dave.jpeg';
import SiaImg from './images/sia.jpeg';
import NobodyImg from './images/nobody.jpeg';
import YosemiteImg from './images/travis.jpeg';
export default function App() {
const songs = [
{
title: 'Eyes to the sky',
artist: 'Jon Bellion',
img_src: EyesImg,
src: Eyes,
},
{
title: 'Lazarus',
artist: 'Dave',
img_src: LazarusImg,
src: Lazarus,
},
{
title: 'Yosemite',
artist: 'Travis scott',
img_src: YosemiteImg,
src: Yosemite,
},
{
title: 'Bird set free',
artist: 'Sia',
img_src: SiaImg,
src: Sia,
},
{
title: 'Cant be broken',
artist: 'Lil wayne',
img_src: BrokenImg,
src: Broken,
},
{
title: 'Mood',
artist: '24kGoldn',
img_src: MoodImg,
src: Mood,
},
{
title: 'Nobody fine pass you',
artist: 'T-Classic',
img_src: NobodyImg,
src: Nobody,
},
{
title: 'Dark paradise',
artist: 'Lana Del Ray',
img_src: AudioImg,
src: Audio,
},
]
const playlist = AudioPlaylist({
files: songs.map((song) => song.src),
});
const handlePlay = () => {
playlist.play();
};
const handlePause = () => {
playlist.pause();
};
const handleSkip = () => {
playlist.next();
};
const handlePrevious = () => {
playlist.prev();
};
return (
<>
<button onClick={handlePlay}>Play</button>
<button onClick={handlePause}>Pause</button>
<button onClick={handleSkip}>Next</button>
<button onClick={handlePrevious}>Prev</button>
</>
);
}
In the code block above, we imported the songs and images. Next, we created a song array containing objects. Each object has a title
, artist
, img_src
for the imported images, and src
for the imported songs.
After that, we mapped through the song array to get to the song's src
, which we passed into the files parameter. Remember, we have to pass it in as an array, but then the map()
method creates a new array from calling a function. Therefore, we can pass it to the files
parameter.
We also created our methods and passed them to the various buttons. We’ll create a Player.js
file to handle the buttons while we take care of the functionality in App.js
:
// Player.js
export default function Player({ play, pause, next, prev }) {
return (
<div className="c-player--controls">
<button onClick={play}>Play</button>
<button onClick={pause}>Pause</button>
<button onClick={next}>Next</button>
<button onClick={prev}>Previous</button>
</div>
);
}
In the code block above, we created a Player.js
file, then caught the props coming from App.js
, and finally passed them into the buttons.
Creating the functionalities
To create the functionalities for our application, we import useState
to get the current index of the song. We then set the image to the current photo, the artist to the current artist, and the title to the current title:
// App.js
import React, { useState } from 'react';
import Player from './Player';
import { AudioPlaylist } from 'ts-audio';
// Music import
// Pictures import
export default function App() {
const [currentSong, setCurrentSong] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
// Songs Array
const playlist =AudioPlaylist({
files: songs.map((song) => song.src),
});
const handlePlay = () => {
playlist.play();
setIsPlaying(true);
};
const handlePause = () => {
playlist.pause();
setIsPlaying(false);
};
const handleSkip = () => {
playlist.next();
setIsPlaying(true);
setCurrentSong(
(currentSong) => (currentSong + 1 + songs.length) % songs.length
);
};
const handlePrevious = () => {
playlist.prev();
setIsPlaying(true);
setCurrentSong(
(currentSong) => (currentSong - 1 + songs.length) % songs.length
);
};
return (
<>
<div className="App">
<div className="c-player">
<div className="c-player--details">
{' '}
<div className="details-img">
{' '}
<img src={songs[currentSong].img_src} alt="img" />
</div>
<h1 className="details-title">{songs[currentSong].title}</h1>
<h2 className="details-artist">{songs[currentSong].artist}</h2>
</div>
<Player
play={handlePlay}
pause={handlePause}
isPlaying={isPlaying}
setIsPlaying={setIsPlaying}
next={handleSkip}
prev={handlePrevious}
/>
</div>
</div>
</>
);
}
We created a state event and set it to zero. When we click the next button, we set the state to the sum of the remainder of the current state, one, and the song's length, divided by the song's length:
currentSong + 1 + songs.length) % songs.length
When we click the previous button, we set the state to the remainder of the current song, minus one, plus the song's length divided by the song's length:
currentSong - 1 + songs.length) % songs.length
We also created a state event that checks if the song is playing or not, and then we passed it as props to the Player
component. Finally, we handled the functionalities for changing the image, artists, and song title.
When we start the application, everything seems to work; the images change when clicking on the next button. However, the songs playing don't match the pictures and artist names displayed on the screen. Sometimes, two or more songs are playing simultaneously.
Problem solving: Mismatched song details
When we click on the next or previous buttons, we are recalculating values and effectively causing a re-render. To stop this, we wrap the song array and the created instance of the playlist in a useMemo
Hook, as seen below:
// App.js
import React, { useState, useMemo } from 'react';
import Player from './Player';
import { AudioPlaylist } from 'ts-audio';
// Music import
// Pictures import
export default function App() {
const [currentSong, setCurrentSong] = useState(0);
const songs = useMemo(
() => [
{
title: 'Eyes to the sky',
artist: 'Jon Bellion',
img_src: EyesImg,
src: Eyes,
},
{
title: 'Lazarus',
artist: 'Dave',
img_src: LazarusImg,
src: Lazarus,
},
{
title: 'Yosemite',
artist: 'Travis scott',
img_src: YosemiteImg,
src: Yosemite,
},
{
title: 'Bird set free',
artist: 'Sia',
img_src: SiaImg,
src: Sia,
},
{
title: 'Cant be broken',
artist: 'Lil wayne',
img_src: BrokenImg,
src: Broken,
},
{
title: 'Mood',
artist: '24kGoldn',
img_src: MoodImg,
src: Mood,
},
{
title: 'Nobody fine pass you',
artist: 'T-Classic',
img_src: NobodyImg,
src: Nobody,
},
{
title: 'Dark paradise',
artist: 'Lana Del Ray',
img_src: AudioImg,
src: Audio,
},
],
[]
);
const playlist = useMemo(() => {
return AudioPlaylist({
files: songs.map((song) => song.src),
});
}, [songs]);
The useMemo
Hook effectively caches the value so that it doesn't need to be recalculated and therefore doesn't cause a re-render.
Adding styling
We’ll use icons from Font Awesome Icons in this tutorial. You can install the Font Awesome package using the commands below:
yarn add @fortawesome/fontawesome-svg-core
yarn add @fortawesome/free-solid-svg-icons
yarn add @fortawesome/react-fontawesome
Copy and paste the code below into the Player.js
file:
// Player.js
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPlay, faPause, faForward, faBackward } from '@fortawesome/free-solid-svg-icons';
export default function Player({ play, pause, next, prev, isPlaying, setIsPlaying }) {
return (
<div className="c-player--controls">
<button className="skip-btn" onClick={prev}>
<FontAwesomeIcon icon={faBackward} />
</button>
<button
className="play-btn"
onClick={() => setIsPlaying(!isPlaying ? play : pause)}
>
<FontAwesomeIcon icon={isPlaying ? faPause : faPlay} />
</button>
<button className="skip-btn" onClick={next}>
<FontAwesomeIcon icon={faForward} />
</button>
</div>
);
}
In the code block above, we get the props from the App.js
file, then handle them inside the Player.js
file. For styling, copy and paste the code below into your index.css
file:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Fira Sans', sans-serif;
}
body {
background-color: #ddd;
}
.App {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
max-width: 100vw;
}
.c-player {
display: block;
background-color: #0a54aa;
max-width: 400px;
display: block;
margin: 0px auto;
padding: 50px;
border-radius: 16px;
box-shadow: inset -6px -6px 12px rgba(0, 0, 0, 0.8),
inset 6px 6px 12px rgba(255, 255, 255, 0.4);
}
.c-player > h4 {
color: #fff;
font-size: 14px;
text-transform: uppercase;
font-weight: 500;
text-align: center;
}
.c-player > p {
color: #aaa;
font-size: 14px;
text-align: center;
font-weight: 600;
}
.c-player > p span {
font-weight: 400;
}
.c-player--details .details-img {
position: relative;
width: fit-content;
margin: 0 auto;
}
.c-player--details .details-img img {
display: block;
margin: 50px auto;
width: 100%;
max-width: 250px;
border-radius: 50%;
box-shadow: 6px 6px 12px rgba(0, 0, 0, 0.8),
-6px -6px 12px rgba(255, 255, 255, 0.4);
}
.c-player--details .details-img:after {
content: '';
display: block;
position: absolute;
top: -25px;
left: -25px;
right: -25px;
bottom: -25px;
border-radius: 50%;
border: 3px dashed rgb(255, 0, 0);
}
.c-player--details .details-title {
color: #eee;
font-size: 28px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8),
-2px -2px 4px rgba(255, 255, 255, 0.4);
text-align: center;
margin-bottom: 10px;
}
.c-player--details .details-artist {
color: #aaa;
font-size: 20px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8),
-2px -2px 4px rgba(255, 255, 255, 0.4);
text-align: center;
margin-bottom: 20px;
}
.c-player--controls {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30px;
}
.c-player--controls .play-btn {
display: flex;
margin: 0 30px;
padding: 20px;
border-radius: 50%;
box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.8),
-4px -4px 10px rgba(255, 255, 255, 0.4),
inset -4px -4px 10px rgba(0, 0, 0, 0.4),
inset 4px 4px 10px rgba(255, 255, 255, 0.4);
border: none;
outline: none;
background-color: #ff0000;
color: #fff;
font-size: 24px;
cursor: pointer;
}
.c-player--controls .skip-btn {
background: none;
border: none;
outline: none;
cursor: pointer;
color: rgb(77, 148, 59);
font-size: 18px;
}
Conclusion
In this article, we’ve learned about ts-audio, an agnostic, easy-to-use library that works with theAudioContext
API. We learned about ts-audio's methods and how it makes it easier to work with audio files. Finally, we learned how to build a working music player using ts-audio.
Full visibility into production React apps
Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your React apps — start monitoring for free.
Top comments (0)