Make Music Player Using JavaScript
Tags: javascript, html, css, codenewbie
Published: 2021-09-13 12:19:55

## Giới thiệu

Trong bài hướng dẫn này chúng ta sẽ được luyện tập cách sử dụng lớp (class) trong JavasScript. Với những bạn mới học thì chúng ta sẽ tiếp cận thêm nhiều thứ hay ho. Cùng theo dõi bài viết này nhé 😄🔥

> Source: [here](https://homiedev.com)

## Thực hành

Đầu tiên chúng ta sẽ xem thử demo Music Player này hoạt động như thế nào nhé 👇:

Xem hướng dẫn tại [Homiedev](https://homiedev.com/make-music-player-using-classes-in-javascript/)

Các bạn đã xem qua video chúng ta sẽ thấy Music Player của chúng ta sẽ có những chức năng chính sau:

  \#Nút play/pause song

  \#Nút set progress song (tua bài hát)

  \#Chức năng chọn bài kế tiếp hoặc trước đó

  \#Chọn bài hát trong danh sách phát

Chúng ta sẽ cùng đi xây dựng các chức năng chính này nhé 😄😄.

Để bắt đầu thì chúng ta sẽ tạo HTML nhé:

*Trong div container*:

Chúng ta sẽ tạo một box chứa info song và các button play/pause với class là *music-content*.

```html
<div class="music-content">
  <button class="play-list">Playlist <i class="fas fa-list"></i></button>
  <section class="music-content-box">
    <div class="thumbnail-song"><img src="image/MINHANH.jpg" alt="" /></div>
    <div class="content-wrapper">
      <div class="info-song">
        <p class="song-name">INI - Eluveitie</p>
        <p class="author">Eluvi</p>
      </div>
    </div>
  </section>
  <audio id="audio"></audio>
  <div class="bar-song">
    <span class="current-time">00:00</span>
    <div class="progress"><div class="progress-bar"></div></div>
    <span class="duration-time">03:22</span>
  </div>
  <div class="song-footer">
    <button class="back"><i class="fas fa-step-backward"></i></button>
    <button class="play-song"><i class="fas fa-play"></i></button>
    <button class="forward"><i class="fas fa-step-forward"></i></button>
  </div>
</div>
```

Dưới div *music-content*, ta tạo một div chứa danh sách các bài hát:

```html
<div class="playlist-box">
  <div class="header">
    <button class="button go-home">
      <i class="fas fa-chevron-left"></i>
    </button>
    <div class="text"><p>Playlist</p></div>
  </div>
  <div class="list-song"></div>
</div>
```

Xong phần html, đến phần css mình sẽ show các thuộc tính quan trọng liên quan đến phần javascript. Các bạn có thể tự design hoặc toàn bộ sorce mình sẽ để ở đầu bài viết.

Chúng ta sẽ ẩn phần *playlist-box* đi, mình muốn khi click vào button thì div này mới hiện lên.

```css
.playlist-box {
    ...
    opacity: 0;
    visibility: hidden;
    transform: scale(1.1);
}
.playlist-box.active {
    opacity: 1;
    visibility: visible;
    transform: scale(1);
}
```

https://codesandbox.io/s/make-music-player-with-vanilla-javascript-wh7yt?file=/index.html

Tiếp theo ta sẽ đến phần JavaScript nhé 😁😁

👉 Đầu tiên ta cùng tạo một danh sách các bài hát nhé.

```javascript
const listMusic = [
    { song: "When I'm Gone", author: 'Eminem' },
    { song: 'Mockingbird', author: 'Eminem' },
    { song: 'Ghetto Gospel', author: 'Tupac' },
    { song: 'Still Love You', author: 'Tupac' },
];
```
Mình đã import các bài hát cùng với tên như ở trên vào folder *music*

Sau khi tạo xong, ta sẽ thêm một class có tên là *UI*. Mình sẽ dùng cú pháp trong ECMAScript 2015 để tạo class. Class này chứa các method để ta tác động lên DOM mà mình sẽ khởi tạo ngay sau đây.

```javascript
class UI {
  constructor() {
    this.songIndex = 0;
  }
  // method
}
```

Trong class này chứa một *constructor* chứa vị trí bài hát trong danh sách là `this.songIndex = 0`.

Tiếp tục, ta sẽ chuyển đến phần method:

👉 Method đầu tiên ta cần tạo là hàm để xử lí nhiệm vụ *show playlist*, nó sẽ hiện danh sách bài hát khi click vào button tương ứng. Tương tự ta sẽ có method để *hide playlist*.

```javascript
class UI {
    constructor() {
        this.songIndex = 0;
    }

    // show playlist
    showPlayListBox() {
        playListBox.classList.add('active');
    }
    // hide playlist
    hidePlayListBox() {
        playListBox.classList.remove('active');
    }
}
```

👉 Method tiếp theo, ta dùng để *load info song* khi trang của chúng ta load xong.

```javascript
const audio = document.querySelector('#audio');
class UI {
  // load detail song when page loaded
  loadSong(music) {
    audio.src = `music/${music.song}.mp3`;

    this.getDuration(audio).then((time) => {
      thumbnailSong.src = `image/${music.song}.jpg`;
      nameSong.textContent = music.song;
      author.textContent = music.author;
      timeSong.textContent = time;
      thumbnailSong.classList.add('rotate-ani');
    });
  }
}
```

Đầu vào là object, ví dụ: `{ song: "When I'm Gone", author: 'Eminem' }`.

Tuy nhiên, khi làm tới method này, ta gặp phải một vấn đề đó là khi get time của bài hát *console.log* sẽ hiển thị giá trị *NaN*. 

Chúng ta sẽ lấy time của bài hát theo cách này: 

```javascript
const audio = document.querySelector('#audio');
const time = audio.duration; // NaN
```

Ta nhận được giá trị *NaN* đó là bởi vì *audio* cần thời gian để load và nó chưa sẵn sàng khi ta gọi. Để giải quyết vấn đề này ta sẽ dùng event *loadedmetadata* nó sẽ đượcc kích hoạt khi metadata đã được tải.

Mình sẽ tạo một method để get duration là *getDuration(music)*. Method này sẽ trả về một *Promise* chứa kết quả là time của bài hát.

```javascript
class UI {
  getDuration(music) {
    return new Promise(function (resolve) {
      music.addEventListener('loadedmetadata', function () {
        const time = formatTime(music.duration);

        resolve(time);
      });
    });
  }
}
```

> Vì giá trị trả về của duration là seconds (giây) nên mình sẽ forrmat lại định dạng nhờ *formatTime(seconds)*.

Chi tiết hàm *formatTime*:

```javascript
function formatTime(sec_num) {
    let hours = Math.floor(sec_num / 3600);
    let minutes = Math.floor((sec_num - hours * 3600) / 60);
    let seconds = Math.floor(sec_num - hours * 3600 - minutes * 60);

    hours = hours < 10 ? (hours > 0 ? '0' + hours : 0) : hours;

    if (minutes < 10) {
        minutes = '0' + minutes;
    }
    if (seconds < 10) {
        seconds = '0' + seconds;
    }
    return (hours !== 0 ? hours + ':' : '') + minutes + ':' + seconds;
}
```
Như vậy là chúng ta đã hiểu về method *loadSong()*. Ngoài ra trong method này mình còn add thêm animation *rotate* cho thumbnail của song.

Mặc định animation mình sẽ làm nó ngừng chuyển động bằng `animation-play-state: paused;`. Khi click play animation sẽ chuyển động tiếp 😄.

```css
.thumbnail-song img.rotate-ani {
    animation: rotate 5s linear infinite;
    animation-play-state: paused;
}
@keyframes rotate {
    0% {
        transform: rotate(0);
    }
    100% {
        transform: rotate(360deg);
    }
}
```

Như vậy là chúng ta đã xong method *loadSong()*, cũng khá dài dòng vì mình muốn giải thích cho các bạn hiểu rõ 😁

⚡ Cùng chuyển tới method tiếp theo nhé. Method này có nhiệm vụ thêm danh sách các bài hát vào DOM 👉.

```javascript
class UI {
  // set list song
  async setSongs() {
    songs.innerHTML = '';

    for (let i = 0; i < listMusic.length; i++) {
      const music = new Audio(`music/${listMusic[i].song}.mp3`);
      const time = await this.getDuration(music);

      songs.insertAdjacentHTML(
        'beforeend',
        `<div class="song-info">
          <div class="left">
            <span class="name-song">${listMusic[i].song}</span>
            <span class="author">${listMusic[i].author}</span>
          </div>
          <div class="right">
            <span class="minutes">${time}</span>
          </div>
        </div>`
      );
    }
  }
}
```

Giải thích một chút, lý do ở method này mình dùng [async/await](https://developer.mozilla.org/vi/docs/Web/JavaScript/Reference/Statements/async_function) vì để lấy thời gian của các bài hát trong danh sách có sẵn mình đã dùng một *Promise* và trong *for...loop* lúc này là bất đồng bộ. Để xử lí vấn đề này mình đã tạo một *async function*. Các bạn có thể đọc thêm bài viết giải thích cách dùng async/await trong các vòng lặp [tại đây](https://zellwk.com/blog/async-await-in-loops/)

Sau khi lấy được thời lượng của mỗi bài hát ta sẽ thêm nó vào trong div *songs* chứa danh sách các bài hát.

😁🖐 Mọi chuyện đơn giản hơn khi ta xử lí vấn đề ở hai method trên, Tiếp theo ta sẽ xử lí cho button play. Khi click vào nhạc sẽ phát, đồng hơn các thông tin của bài hát sẽ hiện ra.

```javascript
const musicContent = document.querySelector('.music-content');
const thumbnailSong = document.querySelector('.thumbnail-song img');
const btnPlay = document.querySelector('.play-song');

class UI {
  // play song
  playSong() {
    musicContent.classList.add('playing');
    thumbnailSong.style.animationPlayState = 'running';
    btnPlay.querySelector('.fas').classList.remove('fa-play');
    btnPlay.querySelector('.fas').classList.add('fa-pause');

    audio.play();
  }
}
```

Khi nút play được kích hoạt, mình cho animation tiếp tục hoạt động. Và sửa icon button play thành icon button pause.

Mình thêm một class cho *musicContent* để kiểm tra trang thái play or pause của bài hát. Tí nữa mình sẽ giải thích ^^

Tương tự với button pause:

```javascript
// pause song
pauseSong() {
  musicContent.classList.remove('playing');
  thumbnailSong.style.animationPlayState = 'paused';
  btnPlay.querySelector('.fas').classList.add('fa-play');
  btnPlay.querySelector('.fas').classList.remove('fa-pause');

  audio.pause();
}
```

👉 Method tiếp theo sẽ thực hiện chuyển bài hát tiếp theo khi click vào button tương ứng. method này khá đơn giản nên mình sẽ không giải thích nhiều nữa.

```javascript
// next song
nextSong() {
  this.songIndex++;

  if (this.songIndex > listMusic.length - 1) {
    this.songIndex = 0;
  }

  this.loadSong(listMusic[this.songIndex]);
}
// prev song
prevSong() {
  this.songIndex--;

  if (this.songIndex < 0) {
    this.songIndex = listMusic.length - 1;
  }

  this.loadSong(listMusic[this.songIndex]);
}
```

🔥 Chúng ta đã đi được gần hết các method để xử lí những yêu cầu mà chúng ta đặt ra, Method tiếp theo cũng khá hay đấy, hãy tiếp tục theo dõi nhé 😁

👉 Method `updateProgress(e)`:

Mình sẽ update lại width sau mỗi giây bài hát cho thanh bar progress và hiển thị thời gian hiện tại của bài hát.

```javascript
const progressBar = document.querySelector('.progress-bar');
const currentTimeDisplay = document.querySelector('.current-time');
// update progress
class UI {
  // update progress
  updateProgress(e) {
    const { currentTime, duration } = e.srcElement;
    const percentWidth = (currentTime / duration) * 100;
    progressBar.style.width = `${percentWidth}%`;
    const time = formatTime(currentTime);

    currentTimeDisplay.textContent = time;
  }
}
```

👉 Method *setProgress(e)*.

Ở method này ta giả sử người dùng muốn tua đoạn nhạc đến đoạn mong muốn. Ta sẽ update lại thanh progress đồng thời set lại currentTime tương ứng.

Ta sẽ tìm vị trí của click chuột thông qua `const width = e.offsetX;` ta sẽ tìm được width của thanh progress bar.

Sau đó, set lại width cho thanh *progressBar* và update lại thời gian hiện tại của bài hát.

```javascript
const progressBar = document.querySelector('.progress-bar');
const audio = document.querySelector('#audio');
class UI {
  // set progress
  setProgress(e) {
    const width = e.offsetX;
    const progress = e.currentTarget;
    const progressBarWidth = (width / progress.clientWidth) * 100;
    progressBar.style.width = `${progressBarWidth}%`;

    let { duration } = audio;
    audio.currentTime = (width * duration) / progress.clientWidth;
  }
}
```

👉 Method chọn bài hát trong playlist.

Ở method này mình sẽ tìm bài hát tương ứng trong *listMusic* khi ta click chuột vào bài hát trong danh sách phát.

```javascript
class UI {
  // select song in playlist
  selectSong(e) {
    const target = e.target;

    const nameSong = target.querySelector('.name-song').textContent;
    const song = listMusic.find((audio) => audio.song === nameSong);

    this.loadSong(song);
    this.playSong();

    this.hidePlayListBox();
  }
}
```

🔥🔥🔥 Như vậy là ta đã hoàn thành xong class *UI* và các method của nó. Công việc tiếp theo là mang đi sử dụng thôi ^^.

Mình sẽ tạo một event sẽ được chạy khi page load xong.

```javascript
document.addEventListener('DOMContentLoaded', eventListeners);
function eventListeners() {
  ...
}
```

Trong *eventListeners*, ta cùng thêm các event và method để sử dụng, Mình sẽ tạo một đối tượng của class *UI*.

```javascript
  const ui = new UI();
```

Đầu tiên, load bài hát và danh sách bài hát:

```javascript
function eventListeners() {
  const ui = new UI();

  // load song
  ui.loadSong(listMusic[ui.songIndex]);
  // handle set list song
  ui.setSongs();
}
```

Xử lí open/close danh sách phát:

```javascript
// handle show playlist
btnPlayList.addEventListener('click', function () {
  ui.showPlayListBox();
});
// handle hide playlist
btnHome.addEventListener('click', function () {
  ui.hidePlayListBox();
});
```

Xử lí play/pause song:

Mình sẽ kiểm tra *musicConent* chứa hay không class *playing* để thực hiện chuyển đổi button. Đây là lí do mình add class *playing* ở method *playSong() và pauseSong()*.

```javascript
// play/pause song
btnPlay.addEventListener('click', function () {
  if (musicContent.classList.contains('playing')) {
    ui.pauseSong();
  } else {
    ui.playSong();
  }
});
```

Xử lí cho thanh progress:

```javascript
// update progress
audio.addEventListener('timeupdate', function (e) {
  ui.updateProgress(e);
});
// set progress
progress.addEventListener('click', function (e) {
  ui.setProgress(e);
});
```

Xử lí cho button next hoặc lùi bài hát:

```javascript
// previous song
btnBack.addEventListener('click', function () {
  ui.prevSong();
  ui.playSong();
});
// forward song
btnForward.addEventListener('click', function () {
  ui.nextSong();
  ui.playSong();
});
```

Chọn bài hát trong danh sách phát:

```javascript
// select song
songs.addEventListener('click', function (e) {
  ui.selectSong(e);
});
```

Cuối cùng là xử lí khi bài hát kết thúc:

```javascript
// end song
audio.addEventListener('ended', function () {
  ui.nextSong();
  ui.playSong();
});
```

Xem toàn bộ source: [Làm Music Player sử dụng JavaScript
](https://www.homiedev.com/make-music-player-using-classes-in-javascript/) 

## Kết luận

Hi vọng với bài hướng dẫn này các bạn sẽ học thêm được những kiến thức bổ ích 😁😁. 

Chúng ta sẽ gặp lại nhau trong những bài viết hay ho sắp tới nhé 🔥.

Các bạn có thể đọc bài viết gốc tại website của mình: [Homiedev.com](https://www.homiedev.com/)