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!
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:
AudioContext
APILet’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.
Audio
componentThe 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.
AudioPlaylist
componentThe 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:
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.
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.
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.
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; }
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
3 Replies to "Build a Spotify clone with React and ts-audio"
Why did you use a library ts-audio that has 21 weekly downloads? Isn’t that a little risky? Or it’s just my paranoia and there’s nothing to be afraid of 😀
Oftentimes, we should use highly downloaded packages. It’s always risky using 21-weekly-download package.
Voy a practicar con esto