audio タグ(要素)を Javascript を使って操作

JavaScript で音声ファイルを再生するための基本的な操作方法について。

更新日:2023年05月27日

作成日:2023年5月13日

関連ページ

Javascript で audio を操作

JavaScript を使って音声データ(audio 要素)を操作することができます。

参考サイト

audio 要素(及び video 要素)は HTMLMediaElement インターフェイスを継承していてるので、そのプロパティやメソッド、イベントを利用できます。

また、audio 要素は HTMLMediaElement から派生した HTMLAudioElement インターフェースによって実装されていて audio 要素を生成するコンストラクタが用意されています。

以下は、JavaScript を使って作成した、ボタンをクリックすると音声データを再生、一時停止、終了するシンプルな音声プレーヤーです。

Play ボタンをクリックすると音声データが再生されます。

音声プレーヤーの HTML は audio 要素と button 要素で構成され、audio 要素には controls 属性を指定せず非表示にしています(HTML に記述せずに JavaScript で生成することもできます)。

HTML
<div class="audio-player">
  <audio src="sample.mp3"></audio>
  <div class="controls">
    <button class="play" type="button">Play</button>
    <button class="pause" type="button">Pause</button>
    <button class="stop" type="button">Stop</button>
  </div>
</div>

JavaScript では各要素を取得して、ボタン要素にイベントリスナーを登録しています。

再生、停止、先頭に戻す処理は audio 要素の play() と pause() メソッド、及び currentTime プロパティを使用しています。

JavaScript
// audio 要素と各ボタン要素を取得
const audio = document.querySelector('audio');
const playBtn =  document.querySelector('.play');
const pauseBtn =  document.querySelector('.pause');
const stopBtn =  document.querySelector('.stop');

// 各ボタン要素の click イベントにリスナー関数を登録
playBtn.addEventListener('click', playAudio, false );
pauseBtn.addEventListener('click', pauseAudio, false );
stopBtn.addEventListener('click', stopAudio, false );

// Play ボタンのリスナー関数
function playAudio() {
  // play() メソッドで音声を再生
  audio.play();
}

// Pause ボタンのリスナー関数
function pauseAudio() {
  // pause() メソッドで停止
  audio.pause();
}

// Stop ボタンのリスナー関数
function stopAudio() {
  // 停止して再生位置を先頭に戻す
  audio.pause();
  audio.currentTime = 0;
}

JavaScript で audio 要素を操作する場合は、上記のように HTMLMediaElement のプロパティやメソッド、イベントなどを利用します。

また、HTMLAudioElement の Audio() コンストラクタを使って audio 要素を生成することもできます。

Audio() コンストラクタ

HTML に audio 要素を記述せずに、HTMLAudioElement のコンストラクタで生成することもできます。

以下が audio 要素を生成する Audio() コンストラクタの構文です。

const myAudio = new Audio([URLString]);

引数

音声データへの URL(src プロパティに設定される値)。省略して後から src プロパティに設定可能。

説明

Audio() は、新しい HTMLAudioElement オブジェクト(audio 要素)を作成して返します。

作成されたオブジェクトの preload プロパティは auto(事前に音声データを読み込む)に設定され、src プロパティは引数に指定された URL に設定されるか、URL が指定されていない場合は null に設定されます。

引数に URL が指定されている場合、ブラウザーは新しいオブジェクトを返す前に、メディアリソースの非同期的な読み込みを開始します。

前述の例の場合、HTML に audio 要素を記述せずに以下のように生成して使用することもできます。

const audio = new Audio('sample.mp3');

以下はコンストラクタで audio 要素を生成し、controls プロパティに true を設定して、id が audio-target の要素に追加(ドキュメントに挿入)する例です。

const audio = new Audio();
// src プロパティに音声データへのパスを指定
audio.src = 'sample.mp3';
// controls プロパティに true を設定
audio.controls = true;
// id が audio-target の要素に audio 要素を追加(ドキュメント上に表示)
document.getElementById('audio-target').appendChild(audio);

controls プロパティに true を設定しているので、以下のようにコントロール(音声プレイヤー)が表示されます。

※ 生成された audio 要素はドキュメントに挿入して使用するか、ドキュメントに挿入せずにオフスクリーンでオーディオの管理と再生に使用できます。但し、ドキュメントに挿入しない場合は document.querySelector('audio') などで audio 要素として取得することはできません。

document.createElement() で生成

他の HTML 要素同様、ドキュメントの createElement() メソッドを使って audio 要素を生成することもできます。

const audio = document.createElement('audio');
audio.src = 'sample.mp3';
audio.preload = 'auto';

メモリー管理(ガベージコレクション)

Audio() コンストラクターを使用して作成されたオーディオ要素へのすべての参照が削除された場合に、再生が現在進行中の場合、要素自体は JavaScript ランタイムのガベージ コレクション メカニズムによってメモリから削除されません。

代わりに、オーディオは再生を続け、オブジェクトは再生が終了するか pause() の呼び出しなどで一時停止されるまでメモリに残ります(再生が終了するか一時停止すると、オブジェクトはガベージコレクションの対象となります)。

MDN Audio() constructor(Memory usage and management)

Select 要素で再生

以下はセレクトボックス(Select 要素)で選択した音声を再生する例です。

この例では option 要素の value 属性に音声データの URL を設定して、選択された際にその値を audio 要素の src 属性に設定して再生します。

HTML
<select name="sound">
  <option value="">音声を選択</option>
  <option value="birds.mp3">鳥の鳴き声</option>
  <option value="wave.mp3">波の音</option>
  <option value="stream.mp3">小川のせせらぎ</option>
  <option value="">なし(無音)</option>
</select>

コンストラクタで audio 要素を生成し、src 以外の必要なプロパティを設定して、select 要素の change イベントにリスナーを設定します。

リスナーでは、まず音声データが再生中であれば停止し、value 属性の値が空でなければ src 属性に設定して再生します。value 属性の値が空の場合は停止したままになります。

以下では value の値を e.currentTarget.value で取得していますが、soundSelect.value でも同じです。

また、念の為アンロードされる前に再生中のものがあれば停止するようにしていますが、効果があるかどうかは不明です。

JavaScript
document.addEventListener('DOMContentLoaded', () => {
  // audio 要素を生成
  const audio = new Audio();
  // プロパティを設定
  audio.volume = 0.3;
  // audio.loop = true; // ループ再生する場合

  // select 要素を取得
  const soundSelect = document.querySelector('select[name="sound"]');

  // select 要素の change イベントにリスナーを設定
  soundSelect.addEventListener('change', (e) => {
    if(!audio.paused) {
      //再生中の場合は停止
      audio.pause();
    }
    // select 要素の value が空でなければその値を src に設定して再生
    if(e.currentTarget.value !== ''){
      audio.src = e.currentTarget.value;
      audio.play();
    }
  });

  //アンロードされる前に再生中のものがあれば停止(効果があるかは不明)
  window.addEventListener('beforeunload', (e) => {
    if(!audio.paused) {
      audio.pause();
    }
  });

});

audio のプロパティ

audio 要素では以下のような HTMLMediaElement のプロパティを利用できます。

HTMLMediaElement のプロパティ(一部抜粋)
プロパティ 説明
autoplay HTML の autoplay 属性の値を反映し、論理値で指定します。初期値:false
controls HTML の controls 属性を反映し、コントロールを表示するかどうかを論理値で指定します。初期値:false
controlsList HTML の controlslist 属性に該当します(HTML の属性の list のエルは小文字で、プロパティは大文字の L )。nodownload、nofullscreen、noremoteplayback を含む DOMTokenList を返します。読み取り専用。Chrome などでは audio.controlsList = 'nodownload' のように設定できますが、「読み取り専用」とあるので setAttribute() を使った方が良いのかも知れません。
currentTime 現在の再生時刻を秒単位で示します。値を設定・変更すると指定された時刻にシーク(移動)します。(例)audio.currentTime = 5

iPhone ではロード前に currentTime に0以外の値を設定すると正常に動作しません。currentTime に0以外の値を設定する場合は、preload に metadata を設定するか、loadeddata イベントを使って設定します。

duration 再生時間を秒単位で示す倍精度浮動小数点値(読み取り専用)。メディアデータがない(指定されたデータが見つからなかったり、まだ、読み込まれていない)場合は NaN を返し、ライブメディアストリームなど再生時間が不明な場合は +Infinity を返します。メタ情報の読み込みが完了したタイミング(loadedmetadata イベント)で取得すると良いようです。
ended 再生を終了したかどうかを示す真偽値を返します(読み取り専用)。再生が終了した場合に true になります。
error 最新のエラーの MediaError オブジェクトです(読み取り専用)。エラーが発生していない場合は null。要素が error イベントを受け取ったら、このプロパティを調べることでエラーの詳細を調べられます。
loop 繰り返し(ループ)再生するかどうかを論理値で指定します。初期値:false
muted 音声がミュートされているかどうかを論理値で指定します。初期値:false
paused 音声が一時停止中であるか否かを論理値で返します(読み取り専用)。
playbackRate メディアが再生されるレートを設定します。(例)audio.playbackRate = 1.5
preload 音声ファイルを事前に読み込むかどうかを設定します。以下のいずれかを指定可能。
  • none: 事前に音声データを読み込まない。
  • metadata: 音声データのメタデータ (再生時間などのメタ情報) のみを読み込む。
  • auto: 事前に音声データを読み込む。
readyState メディアの準備状態を示すプロパティです(読み取り専用)。以下が定数とその値。
  • HAVE_NOTHING(0)
  • HAVE_METADATA(1)
  • HAVE_CURRENT_DATA(2)
  • HAVE_FUTURE_DATA(3)
  • HAVE_ENOUGH_DATA(4)
src 再生する動画への URL を指定(参照)します。
volume 音量を指定(または参照)します。値は無音 0.0 から最大 1.0 の間(double)で指定できます。初期値: 1.0

audio のメソッド

audio 要素では以下のようなメソッドを利用できます。

HTMLMediaElement のメソッド(一部抜粋)
メソッド 説明
load() 音声データを先頭にリセットし、最適なソースを選択してデータを読み込むプロセスを開始します。先読みされるデータの量は、要素の preload 属性の値によって決まります。
pause() 再生を一時停止します。
play() 再生を開始します。play() メソッドは再生が開始されたときに解決または拒否される Promise を返します。
fastSeek() 低い精度で素早く指定時刻にシークします(※ Chrome ではサポートされていないのでエラー)。代わりに currentTime プロパティを設定すればその時刻にシークします。

audio のイベント

audio 要素では以下のようなイベントを利用できます。

これらのイベントを受け取るには、addEventListener() を使用するか、「onイベント名」プロパティ(onevent ハンドラー)にイベントリスナーを設定します。

HTMLMediaElement のイベント(一部抜粋)
イベント 発生するタイミング
abort リソースを完全に読み込めなかったとき(エラーが原因でない場合)
canplay 再生できる状態になったとき(途中でバッファリングのために停止する可能性あり)
canplaythrough 最後まで再生できる状態になったとき(最後まで再生するのに十分なデータが読み込まれた)
ended メディアの終わりに達した(またはそれ以上利用できるデータがない)とき
error エラー(ネットワーク接続の問題など)によりリソースが読み込めなかったとき
loadeddata メディアの1フレーム目の読み込みが終了したとき
loadedmetadata リソースのメタデータ(再生時間などのメタ情報)が読み込まれたとき
loadstart リソースの読み込みを開始したとき
pause 再生を一時停止したとき。pause() メソッドが呼び出されたとき
play 再生を開始したとき。play() メソッド、または autoplay 属性の結果として、paused プロパティが true から false に変更されたとき
progress ブラウザーがリソースを読み込む際に発生
ratechange 再生速度(再生レート)が変更されたとき
seeked シーク動作が完了したとき(現在の再生位置が変更され、論理属性の seeking が false に変更されたとき)。
seeking シーク動作が開始されたとき
stalled リソースのデータを読み込もうとしたがデータが得られなかったとき
suspend リソースの読み込みが中断されたとき
timeupdate 再生時刻を表す currentTime プロパティの値が更新されたとき
volumechange 音量を変更したとき。または mute 属性を変更したとき。
waiting 一時的なリソースのデータ不足で再生が停止したとき(データの読み込みが遅く、再生を続けられないとき)
指定した範囲を繰り返し再生

以下は timeupdate イベントを使って指定した範囲(30〜35秒)を繰り返し再生する例です。

この例では、js-loop クラスを指定した audio 要素に data-start-time と data-end-time(カスタム属性)で指定された範囲で繰り返し再生します。

HTML
<audio class="js-loop" data-start-time="30" data-end-time="35" controls src="sample.mp3"></audio>

JavaScript ではカスタム属性に指定された値を dataset プロパティで取得して、それらが設定されていれば timeupdate を監視します。

currentTime が data-end-time の値より大きくなれば、currentTime を data-start-time の値に設定して再生位置を戻しています。

timeupdate イベントの頻度はシステムの稼働状況に依存するので、指定した正確な範囲を繰り返すことはできません。

また、iOS(iPhone)では音声データをロードする前に currentTime に0以外の値を設定すると正しく動作しないため、preload に metadata を設定しています(iPhone の対策)。

document.addEventListener('DOMContentLoaded', () => {

  const jsLoopAudios = document.querySelectorAll('audio.js-loop');
  jsLoopAudios.forEach((audio) => {
    // iPhone 対策(この指定がないと正常に動作しない)
    audio.preload = 'metadata';

    // カスタム属性の値を dataset プロパティで取得して数値に変換
    const startTime = parseInt(audio.dataset.startTime);
    const endTime = parseInt(audio.dataset.endTime);

    // カスタム属性の値が設定されていれば
    if(startTime && endTime) {
      // 開始時刻を startTime に設定
      audio.currentTime = startTime;
      // timeupdate を監視
      audio.addEventListener('timeupdate',loop, false);
    }

    // リスナー関数
    function loop() {
      if( audio.currentTime >= endTime ) {
        // 現在の再生時刻が指定された終了時刻を超えたら開始時刻を設定
        audio.currentTime = startTime;
      }
    }
  });
});

preload に metadata を設定したくない場合は、以下のように最初に currentTime に値を設定する際は loadeddata イベントで設定すれば iPhone でも動作します。

※ この場合、 iPhone で見るとシークバーの位置は startTime で指定した位置ではなく、0 の位置に表示されますが、再生ボタンをタップすると指定した再生位置から再生されます。

document.addEventListener('DOMContentLoaded', () => {
  const jsLoopAudios = document.querySelectorAll('audio.js-loop');
  jsLoopAudios.forEach((audio) => {
    const startTime = parseInt(audio.dataset.startTime);
    const endTime = parseInt(audio.dataset.endTime);

    if(startTime && endTime) {
      // iOS バグのため loadeddata で currentTime(開始時刻)を設定
      audio.addEventListener('loadeddata', ()=> {
        audio.currentTime = startTime;
      });
      audio.addEventListener('timeupdate',loop, false);
    }

    function loop() {
      if( audio.currentTime >= endTime ) {
        audio.currentTime = startTime;
      }
    }
  });
});

再生と停止のトグル

以下は1つのボタンで再生と停止を行うシンプルな音声プレーヤーの例です。

Play ボタンをクリックすると音声データが再生され、ボタンのラベルが Pause に変わります。

Pause をクリックすると再生を一時停止し、ボタンのラベルが Play に変わります。

Stop をクリックすると再生中であれば再生を停止し、再生位置を先頭に戻します。

Play ボタンをクリックすると音声データが再生されます。

この例では audio 要素と2つのボタンを audio-player クラスを指定した div 要素で囲み、1つの音声プレーヤーとしています。

audio 要素には音声データの URL を src 属性に指定し、controls 属性はせずデフォルトのコントロールは非表示にしておきます。

ボタン要素にはそれぞれクラスを指定して識別できるようにしています。

HTML
<div class="audio-player">
  <audio src="sample.mp3"></audio>
  <div class="controls">
    <button class="toggle" type="button">Play</button>
    <button class="stop" type="button">Stop</button>
  </div>
</div>

JavaScript では audio-player クラスを指定した要素(音声プレーヤー)を全て取得し、forEach() でそれぞれについて、.audio-player を基点として querySelector() で audio や button を取得して処理します。

トグルボタンのクリックイベントでは、現在再生中かどうかを audio 要素の paused プロパティで判定して、停止中であれば再生し、再生中であれば停止し、ボタンのラベルを変更します。

ストップボタンのクリックイベントでも、同様に現在再生中かどうかを audio 要素の paused プロパティで判定して処理を分岐します。

再生終了時に発火する ended イベントでは、検知したらトグルボタンのラベルを Play に変更します。

JavaScript
document.addEventListener('DOMContentLoaded', () => {

 const audioPlayers = document.querySelectorAll('.audio-player');

 audioPlayers.forEach((audioPlayer) => {
   // .audio-player を基点として audio 要素を取得
   const audio = audioPlayer.querySelector('audio');
   // audio 要素は非表示(デフォルトなので省略可能)
   audio.controls = false;
   // ボリュームを調整(必要に応じて。初期値は 1.0)
   audio.volume = .3;
   // .audio-player を基点としてボタンを取得
   const toggleBtn = audioPlayer.querySelector('.toggle');
   const stopBtn =  audioPlayer.querySelector('.stop');

   // ボタンのクリックイベントのリスナー
   toggleBtn.addEventListener('click', togglePlayPause, false);
   stopBtn.addEventListener('click', stopAudio, false);
   // audio 要素の再生終了時に発行される ended イベントのリスナー
   audio.addEventListener('ended', audioEnded, false);

   // トグルボタンのリスナー
   function togglePlayPause() {
     // 一時停止中かどうかで処理を分岐
     if (audio.paused) {
      // 一時停止中の場合は再生
       audio.play();
       toggleBtn.textContent = 'Pause';
     } else {
      // 一時停止中でない(再生中)の場合は一時停止
       audio.pause();
       toggleBtn.textContent = 'Play';
     }
   }

   // ストップボタンのリスナー
   function stopAudio() {
     if (audio.paused) {
       audio.currentTime = 0;
     } else {
       audio.pause();
       audio.currentTime = 0;
       toggleBtn.textContent = 'Play';
     }
   }

   // 再生終了時にはトグルボタンのラベルを Play に変更
   function audioEnded() {
     toggleBtn.textContent = 'Play';
   }
 });
});
play() メソッドが返す Promise

play() メソッドはメディアが正常に再生を開始すると解決(resolve)され、自動再生が拒否されたりソースが見つからない場合など、再生が開始できないと却下(reject)される Promise を返します。

この Promise を監視することで実際の再生状態を判定することができます。

以下は非同期関数(Async Function)と try...catch 構文を使って play() メソッドが返す Promise を監視するように書き換えたものです。

前述の例の場合、何らかの理由で再生できなくてもボタンのラベルは Play から Pause に変更されてしまいますが、以下の場合、実際に再生されなければラベルは変更されません。

この例では音声データを再生する非同期関数 playAudio() を定義し、トグルボタンのリスナーで呼び出しています。その他は前述の例と同じです。

document.addEventListener('DOMContentLoaded', () => {

 const audioPlayers = document.querySelectorAll('.audio-player');

 audioPlayers.forEach((audioPlayer) => {
  const audio = audioPlayer.querySelector('audio');
   audio.controls = false;
   audio.volume = .3;
   const toggleBtn = audioPlayer.querySelector('.toggle');
   const stopBtn =  audioPlayer.querySelector('.stop');

   // トグルボタンの click イベントにリスナーを登録
   toggleBtn.addEventListener('click', togglePlayPause, false);
   // Stop ボタンの click イベントにリスナーを登録
   stopBtn.addEventListener('click', stopAudio, false);
   // 再生終了時の ended イベントにリスナーを登録
   audio.addEventListener('ended', audioEnded, false);

   // 音声データを再生する非同期関数(Async Function)
   async function playAudio() {
     try {
       // await を指定して Promise が確定するまで待ちます
       await audio.play();
       // Promise が解決されたらボタンのテキストを変更
       toggleBtn.textContent = 'Pause';
     } catch (err) {
       // 再生が開始されない場合(Promise が拒否された場合)の処理
       console.warn(err)
     }
   }

   // トグルボタンの click イベントのリスナー
   function togglePlayPause() {
     if (audio.paused) {
       // 定義した関数を呼び出す
       playAudio();
     } else {
       audio.pause();
       toggleBtn.textContent = 'Play';
     }
   }

   // Stop ボタンの click イベントのリスナー
   function stopAudio() {
     if (audio.paused) {
       audio.currentTime = 0;
     } else {
       audio.pause();
       audio.currentTime = 0;
       toggleBtn.textContent = 'Play';
     }
   }

   // ended イベントのリスナー
   function audioEnded() {
     toggleBtn.textContent = 'Play';
   }
 });
});

MDN: HTMLMediaElement.play()

メディアおよびウェブ音声 API の自動再生ガイド:play() メソッド

play/pause イベント

前述の例では、再生・停止のボタンの click イベントでボタンのラベルを変更しましたが、以下は play と pause イベントを使ってラベルを変更する例です。

この例では、audio の状態がわかりやすいように audio 要素を HTML に記述して controls 属性を指定して表示するようにしています。

<div class="audio-player">
  <div class="controls">
    <button class="toggle" type="button">Play</button>
  </div>
  <audio controls src="sample.mp3"></audio>
</div>

Play ボタンをクリックすると音声データが再生されます。

この方法の場合、audio 要素自体のコントロールをクリックしても、play 及び pause イベントが発生するので、再生・停止のボタンのラベルもそれに合わせて変更されます。

document.addEventListener('DOMContentLoaded', () => {

  const audioPlayers = document.querySelectorAll('.audio-player');

  audioPlayers.forEach((audioPlayer) => {
    const audio = audioPlayer.querySelector('audio')
    audio.volume = .3;
    const toggleBtn = audioPlayer.querySelector('.toggle');

    // 音声データを再生する非同期関数
    async function playAudio() {
      try {
        await audio.play();
      } catch (err) {
        console.warn(err)
      }
    }

    toggleBtn.addEventListener('click', () => {
      if (audio.paused) {
        // 上記で定義した関数を呼び出す
        playAudio();
      } else {
        audio.pause();
      }
    });

    // pause イベントでボタンのラベルを変更
    audio.addEventListener('pause', () => {
      toggleBtn.textContent = 'Play';
    });

    // play イベントでボタンのラベルを変更
    audio.addEventListener('play', () => {
      toggleBtn.textContent = 'Pause';
    });

    // ended イベントでボタンのラベルを変更
    audio.addEventListener('ended', () => {
      toggleBtn.textContent = 'Play';
    });

  });
});

1つだけ再生(他を停止)

同じページに複数の音声データがある場合、同時に再生することができてしまいます。

以下は再生ボタンをクリックして音声データを再生する際に、他に再生中の音声データがあれば停止して現在クリックされた音声データのみを再生する例です。

addEventListener でドキュメントの play イベントにリスナーを設定し、第3引数(useCapture)を true に設定します。リスナーでは全ての audio 要素を取得して、play イベントが発生した要素が自身でなければ pause() メソッドで停止させます。

対象となるのはドキュメントに挿入されている audio 要素になります。new Audio() で生成しただけでドキュメントに挿入されていない audio は対象外です。

document.addEventListener('DOMContentLoaded', () => {

  // ドキュメントの play イベント
  document.addEventListener('play', (e) => {
    // 全ての audio 要素を取得
    const audios = document.querySelectorAll('audio');
    // それぞれの audio 要素で以下を実行
    audios.forEach((audio) => {
      // play イベントが発生した要素が自身でなければ停止
      if(audio !== e.target) {
        audio.pause();
        // audio.currentTime = 0; //先頭に戻す場合(もし必要であれば)
      }
    });
  }, true); // addEventListener()の第3引数(useCapture)を true に

}, false);

音声データの長さ duration

音声データの長さは duration プロパティで取得することができます。

但し、メタデータ(再生時間などのメタ情報)の読み込みが完了したタイミング以降でないと取得できないので、loadedmetadata イベントなどを利用して取得します(loadstart ではまだ早すぎます)。

<audio src="sample.mp3" controls></audio>
//対象の audio 要素を取得
const audio = document.querySelector('audio');

// この時点では duration は取得できず、duration の値は NaN
console.log(audio.duration);  // NaN

// audio 要素の loadedmetadata イベントで duration を取得
audio.addEventListener('loadedmetadata', () => {
  //再生時間を表示
  console.log(audio.duration);  // 例:75.456
});

再生位置(currentTime)の取得

現在の再生位置は currentTime プロパティで取得することができます。

以下は再生ボタン及び停止ボタンをクリックする際に、その時の再生位置を取得してコンソールに出力する例です。

<audio src="sample.mp3" controls></audio>

play イベントは再生ボタンをクリックするなどで play() メソッドが呼び出されたり、autoplay 属性の結果として、paused プロパティが true から false に変更されたときに発生します。

pause イベントは pause() メソッドが呼び出されたときに発生します。

//対象の audio 要素を取得
const audio = document.querySelector('audio');

// audio 要素の play イベントで currentTime を取得
audio.addEventListener('play', () => {
  //再生位置を表示
  console.log('Play at: ' + audio.currentTime);  // 例:0 (初回再生時)
});

// audio 要素の pause イベントで currentTime を取得
audio.addEventListener('pause', () => {
  //再生位置を表示
  console.log('Pause at: ' + audio.currentTime);  // 例:1.033983 (停止をクリックした時点)
});
残り再生時間の表示

以下は、およその残りの再生時間を表示する例です。

残りの再生時間は音声データの長さから現在の再生位置を差し引いた値になります。取得される値は倍精度浮動小数点値なので、Math.floor() を使って小数点以下を切り捨てして表示しています。

残り再生時間:

HTML では .audio-player にカスタムデータ属性 data-audio-src を設定して、その値に audio 要素の src プロパティに設定する URL を指定しています。

残り再生時間を表示する部分以外は、再生と停止のトグル の例とほぼ同じです。

<div class="audio-player">
  <audio src="sample.mp3"></audio>
  <div class="controls">
    <button class="toggle" type="button">Play</button>
    <button class="stop" type="button">Stop</button>
  </div>
  <p class="time-remain">残り再生時間:<span></span> 秒</p>
</div>

この例では、timeupdate イベントを使って残り再生時間を表示しています。timeupdate イベントは currentTime プロパティの値が更新されたときに発生するイベントで、イベントの頻度はシステムの負荷に依存します。

残り再生時間を表示する以外の部分は 再生と停止のトグル の例とほぼ同じです。

document.addEventListener('DOMContentLoaded', () => {

 const audioPlayers = document.querySelectorAll('.audio-player');

 audioPlayers.forEach((audioPlayer) => {
   const audio = audioPlayer.querySelector('audio');
   const toggleBtn = audioPlayer.querySelector('.toggle');
   const stopBtn =  audioPlayer.querySelector('.stop');

   // 残りの再生時間を表示する span 要素
   const timeRemain = audioPlayer.querySelector('p.time-remain span');

   // 音声データの長さ
   let duration;
   // audio 要素の loadedmetadata イベントで duration を取得
   audio.addEventListener('loadedmetadata', () => {
     // 残りの再生時間の初期値として音声データの長さ(duration)を表示
     duration = audio.duration;
     // 小数点以下は切り捨てて表示
     timeRemain.textContent = Math.floor(duration);
   });

   // timeupdate イベントに残りの再生時間を表示する関数を登録
   audio.addEventListener('timeupdate', printTimeRemain, false);

   // 残りの再生時間を表示する関数
   function printTimeRemain() {
     //現在の再生位置を取得
     const cTime = audio.currentTime;
     // 音声データの長さから現在の再生位置を差し引いた値が残りの再生時間
     timeRemain.textContent = Math.floor(duration - cTime) ;
   }

   toggleBtn.addEventListener('click', togglePlayPause, false);
   stopBtn.addEventListener('click', stopAudio, false);
   audio.addEventListener('ended', audioEnded, false);

   async function playAudio() {
     try {
       await audio.play();
       toggleBtn.textContent = 'Pause';
     } catch (err) {
       console.warn(err)
     }
   }
   function togglePlayPause() {
     if (audio.paused) {
       playAudio();
     } else {
       audio.pause();
       toggleBtn.textContent = 'Play';
     }
   }
   function stopAudio() {
     if (audio.paused) {
       audio.currentTime = 0;
     } else {
       audio.pause();
       audio.currentTime = 0;
       toggleBtn.textContent = 'Play';
     }
   }
   function audioEnded() {
     toggleBtn.textContent = 'Play';
   }
 });
});

再生位置(currentTime)の変更

currentTime プロパティに値(秒数)を設定することで、再生位置を変更することができます。

<audio src="sample.mp3" controls></audio>
document.addEventListener('DOMContentLoaded', () => {
  const audio = document.querySelector('audio');
  // 再生位置を変更
  audio.currentTime = 25;
});

上記を設定すると以下のように再生位置が変更され、再生ボタンをクリックするとその時点から再生されます。※ 但し、iPhone では最初から再生されてしまい、シークバーも動きません。

iPhone の対策

currentTime プロパティに値を設定すると指定された時刻にシーク(移動)するはずですが、iPhone では初期状態(ロード前)で値に 0 以外を設定すると正しく動作しません。

前述の例の場合、iPhone 以外では currentTime プロパティに指定した時刻から音声を再生することができますが、iPhone ではシークバー上は指定した時刻になっていますが、最初(0 の位置)から再生されます。

解決策 1

簡単な方法は audio 要素の preload 属性に "metadata" を設定するか、JavaScript で preload プロパティに "metadata" を設定します。

document.addEventListener('DOMContentLoaded', () => {
  const audio = document.querySelector('audio');
  // preload プロパティに metadata を設定
  audio.preload = 'metadata';
  audio.currentTime = 25;
});

iPhone でも他と同じように動作します。

解決策 2

もう1つの解決策は、loadeddata イベントで currentTime を設定します。

document.addEventListener('DOMContentLoaded', () => {
  const audio = document.querySelector('audio');
  // loadeddata イベントで設定
  audio.addEventListener('loadeddata', ()=> {
    audio.currentTime = 25;
  });
});

※ この場合、iPhone で見ると、シークバーの位置は 0 の位置に表示されますが、再生ボタンをタップすると指定した再生位置から再生されます。

関連項目:シークバー操作の iPhone 対策

再生位置変更のサンプル

以下は残り再生時間の取得の例に再生位置を5秒進めるボタンを追加したサンプルです。

残り再生時間:

HTML
<div class="audio-player">
  <audio src="sample.mp3"></audio>
  <div class="controls">
    <button class="toggle" type="button">Play</button>
    <button class="forward" type="button">+5 sec</button> <!-- 追加 -->
    <button class="stop" type="button">Stop</button>
  </div>
  <p class="time-remain">残り再生時間:<span></span> 秒</p>
</div>

iPhone 対策として、preload プロパティに 'metadata' を設定しています。

ボタンがクリックされたら、現在の再生位置を5秒先に進めるように currentTime を5増加させています。

JavaScript
document.addEventListener('DOMContentLoaded', () => {

 const audioPlayers = document.querySelectorAll('.audio-player');

 audioPlayers.forEach((audioPlayer) => {
   const audio = audioPlayer.querySelector('audio');
   audio.volume = 0.3;
   // iPhone 対策
   audio.preload  = 'metadata';

   // 5秒先に進むボタン
   const forwardBtn =  audioPlayer.querySelector('.forward');
   // 5秒先に進むボタンのクリックイベントにリスナーを登録
   forwardBtn.addEventListener('click', () => {
     // 現在の再生位置を5秒先に進める
     audio.currentTime += 5;
   }, false);

   // 以下の部分は「残り再生時間の取得の例」と同じ
   const toggleBtn = audioPlayer.querySelector('.toggle');
   const stopBtn =  audioPlayer.querySelector('.stop');
   const timeRemain = audioPlayer.querySelector('p.time-remain span');

   let duration;

   audio.addEventListener('loadedmetadata', () => {
     duration = audio.duration;
     timeRemain.textContent = Math.floor(duration);
   });

   audio.addEventListener('timeupdate', printTimeRemain, false);

   function printTimeRemain() {
     const cTime = audio.currentTime;
     timeRemain.textContent = Math.floor(duration - cTime) ;
   }

   toggleBtn.addEventListener('click', togglePlayPause, false);
   stopBtn.addEventListener('click', stopAudio, false);
   audio.addEventListener('ended', audioEnded, false);

   async function playAudio() {
     try {
       await audio.play();
       toggleBtn.textContent = 'Pause';
     } catch (err) {
       console.warn(err)
     }
   }
   function togglePlayPause() {
     if (audio.paused) {
       playAudio();
     } else {
       audio.pause();
       toggleBtn.textContent = 'Play';
     }
   }
   function stopAudio() {
     if (audio.paused) {
       audio.currentTime = 0;
     } else {
       audio.pause();
       audio.currentTime = 0;
       toggleBtn.textContent = 'Play';
     }
   }
   function audioEnded() {
     toggleBtn.textContent = 'Play';
   }
 });

});
再生位置をシークバーで操作

type="range" の input 要素(レンジ入力)を使って再生位置を操作する例です。

残り再生時間:

input 要素の max 属性は audio 要素の duration プロパティの値を取得して設定します。この例では min="0" max="100" としていますが、これはデフォルトなので省略可能です(※ 実際の max 属性の値は JavaScript で設定します)。

また、step 属性の値は 0.1 としていますが、音声データの長さや input 要素の幅により調整すると良いと思います。

HTML
<div class="audio-player">
  <audio src="sample.mp3"></audio>
  <div class="controls">
    <button class="toggle" type="button">Play</button>
    <input type="range" name="seek" min="0" max="100" value="0" step=".1">
  </div>
  <p class="time-remain">残り再生時間:<span></span> 秒</p>
</div>

シークバーの値が変更されたら、input 要素の input イベントでその値を currentTime に設定します。

input 要素の max 属性の値は loadedmetadata イベントで duration の値を取得して設定します。

currentTime が更新されたときに発生する timeupdate イベントで、その時点での currentTime を input 要素の value に設定しています。

JavaScript
document.addEventListener('DOMContentLoaded', () => {

 const audioPlayers = document.querySelectorAll('.audio-player');

 audioPlayers.forEach((audioPlayer) => {
   const audio =  audioPlayer.querySelector('audio');
   audio.volume = 0.3;
   // iPhone 対策
   audio.preload = 'metadata';

   // input 要素(シークバー)
   const seekBar = audioPlayer.querySelector('input[name="seek"]');

   // input 要素(シークバー)に input イベントリスナーを設定
   seekBar.addEventListener('input', (e) => {
     //  input 要素の値(value)を currentTime に設定
     audio.currentTime = e.currentTarget.value;
   });

   const toggleBtn = audioPlayer.querySelector('.toggle');
   const timeRemain = audioPlayer.querySelector('p.time-remain span');

   let duration;

   // duration を loadedmetadata イベントで取得
   audio.addEventListener('loadedmetadata', () => {
     duration = audio.duration;
     timeRemain.textContent = Math.floor(duration);
     // input 要素(シークバー)の max 属性に取得した duration の値を設定
     seekBar.setAttribute('max', Math.floor(duration));
   });

   // timeupdate イベントにリスナーを登録
   audio.addEventListener('timeupdate', updateTime, false);

   //timeupdate イベントのリスナー
   function updateTime() {
     const cTime = audio.currentTime;
     timeRemain.textContent = Math.floor(duration - cTime) ;
     // input 要素(シークバー)の value を更新
     seekBar.value = cTime;
   }

   toggleBtn.addEventListener('click', togglePlayPause, false);
   audio.addEventListener('ended', audioEnded, false);

   async function playAudio() {
     try {
       await audio.play();
       toggleBtn.textContent = 'Pause';
     } catch (err) {
       console.warn(err)
     }
   }
   function togglePlayPause() {
     if (audio.paused) {
       playAudio();
     } else {
       audio.pause();
       toggleBtn.textContent = 'Play';
     }
   }
   function audioEnded() {
     toggleBtn.textContent = 'Play';
   }
 });
});
シークバー操作の iPhone 対策

上記11行目の audio.preload = 'metadata'; は iPhone 対策のための記述です。

この記述がないと、iPhone で初回の再生前にシークバーで再生位置を変更してから再生を行うと、currentTime がただしく反映されず、そのため正しく動作しません。

再生位置を変更せずに再生後、その後シークバーを操作する場合は問題ありません。

そのため、audio.preload = 'metadata'; を設定したくない場合は、例えば以下のように再生前はシークバーでの再生位置を変更できないようにして、一度再生後は再生位置を変更できるようにすることもできます。

document.addEventListener('DOMContentLoaded', () => {
  const audioPlayers = document.querySelectorAll('.audio-player');
  audioPlayers.forEach((audioPlayer) => {
    const audio = audioPlayer.querySelector('audio');
    audio.volume = 0.3;
    //audio.preload = 'metadata'; // 削除

    // 一度再生されたかどうかのフラグを定義
    let isPlayed = false;

    const seekBar = audioPlayer.querySelector('input[name="seek"]');

    seekBar.addEventListener('input', (e) => {

      if(!isPlayed) {
        // 一度も再生されていなければ、シークバーの変更はできない
        seekBar.value = 0;
      }else{
        // 一度再生されればシークバーの変更を currentTime に反映
        audio.currentTime = e.currentTarget.value;
      }
    });

    const toggleBtn = audioPlayer.querySelector('.toggle');
    const timeRemain = audioPlayer.querySelector('p.time-remain span');
    let duration;

    audio.addEventListener('loadedmetadata', () => {
      duration = audio.duration;
      timeRemain.textContent = Math.floor(duration);
      seekBar.setAttribute('max', Math.floor(duration));
    });

    audio.addEventListener('timeupdate', updateTime, false);
    function updateTime() {
      const cTime = audio.currentTime;
      timeRemain.textContent = Math.floor(duration - cTime);
      seekBar.value = cTime;
    }

    toggleBtn.addEventListener('click', togglePlayPause, false);
    audio.addEventListener('ended', audioEnded, false);

    async function playAudio() {
      try {
        await audio.play();
        toggleBtn.textContent = 'Pause';
        // 一度再生されれば isPlayed を true に
        if(!isPlayed) isPlayed = true;
      } catch (err) {
        console.warn(err)
      }
    }

    function togglePlayPause() {
      if (audio.paused) {
        playAudio();
      } else {
        audio.pause();
        toggleBtn.textContent = 'Play';
      }
    }

    function audioEnded() {
      toggleBtn.textContent = 'Play';
    }
  });
});

上記の場合、以下のように初回再生前はシークバーの操作はできませんが、再生後はシークバーでの再生位置の変更ができ、preload に metadata を設定しなくても iPhone で正しく動作します。

残り再生時間:

ボリューム(volume)

ボリューム(音量)は volume プロパティを使って設定・参照することができます。値は無音 0.0 から最大 1.0 の間で指定します。初期値は 1.0 です。

※但し、iPhone や iPad など iOS では以下の方法ではボリュームの変更ができません(デバイス側で音量を調整する必要があります)。

以下はボリュームを調整するボタンを追加した例です(次項のスライダーを使うほうが簡単です)。

残り再生時間:

ボリューム:

HTML ではボリュームを調整するボタン( + と - )を追加しています。

HTML
<div class="audio-player">
  <audio src="sample.mp3"></audio>
  <div class="controls">
    <button class="toggle" type="button">Play</button>
    <input type="range" name="seek" value="0" step=".1">
    <button class="up" type="button"> + </button>
    <button class="down" type="button"> - </button>
  </div>
  <p class="time-remain">残り再生時間:<span></span> 秒</p>
  <p class="volume-value">ボリューム:<span></span></p>
</div>

この例ではボタンをクリックすると音量を 0.1(10%)変化さるので、その値を変数 changeAmount に代入しています。

音量のボタンの click イベントの設定では、増減させた値が 0.0〜1.0 の範囲を超えないようにしています。範囲を超えると、例えば caught DOMException: Failed to set the 'volume' property on 'HTMLMediaElement': The volume provided (1.1) is outside the range [0, 1]. のような例外がコンソールに出力されます。

また、値を増減する際、JavaScript の場合、小数計算で誤差が出ておかしなことになるので、この例では値を10倍して四捨五入して10分の1にして計算しています。

ボリュームをスライダーで操作

以下はボリュームをスライダー(input type="range" のレンジ入力)で実装する例です。

※ iPhone や iPad など iOS では以下の方法ではボリュームの変更はできません。

残り再生時間:

ボリュームの input 要素には volume プロパティに指定できる範囲の min="0.0" max="1.0" を指定します。また、この例では value 属性に初期値 0.5 を指定し、step 属性には 0.1 を指定しています。

<div class="audio-player">
  <audio src="sample.mp3"></audio>
  <div class="controls">
    <button class="toggle" type="button">Play</button>
    <input type="range" name="seek" value="0" step=".1">
    <label for="volume">vol</label>
    <input type="range" name="volume" id="volume" min="0.0" max="1.0" value="0.5" step=".1">
  </div>
  <p class="time-remain">残り再生時間:<span></span> 秒</p>
</div>

この例では volume プロパティの初期値を 0.5 に変更しているので(10行目)、17行目でボリュームバーの value に反映させるように volume プロパティの値を設定しています。

ボリュームバーの値が変更されたら、input 要素の input イベントでその値を volume プロパティに設定します(20〜22行目)。

ミュート(mute)

mute プロパティに true を設定すると無音にする(ミュートする)ことができます。mute プロパティに false を設定することで解除できます。mute プロパティの初期値は false です。

以下はミュートのトグルボタンを追加した例です。ミュートボタンをクリックするとミュート状態(無音)になり、ミュート状態でミュートボタンをクリックするとミュートは解除されます。

ミュートの状態がわかりやすいように状態によりボタンの背景色を変更し、ボタンのラベルも変更します。

また、ミュート状態にする際に、ボリュームスライダーの位置を音量 0 に移動するようにして、ミュート状態でボリュームのスライダーをクリックすると、ミュートを解除してクリックした位置の音量で再生します。

残り再生時間:

HTML
<div class="audio-player">
  <audio src="sample.mp3"></audio>
  <div class="controls">
    <button class="toggle" type="button">Play</button>
    <input type="range" name="seek" value="0" step=".1">
    <button class="mute" type="button">Mute</button>
    <input type="range" name="volume" min="0.0" max="1.0" value="1.0" step=".1">
  </div>
  <p class="time-remain">残り再生時間:<span></span> 秒</p>
</div>

ミュート状態でボリュームスライダーに変更が発生すれば input イベントで audio.muted = false によりミュートを解除します。

ミュートボタンがクリックされて、ミュート状態であれば audio.muted = false でミュートを解除し、ボリュームスライダーの value 属性にに現在の volume プロパティの値を設定して反映します。

ミュート状態でなければ、audio.muted = true でミュート(無音)状態にして、ボリュームスライダーの位置を 0 に移動します。

また、状態が変わる際に、テキストの変更とクラスの着脱によりボタンの見た目を変更しています。

active クラスのスタイル
/* .active はミュートが有効な場合に追加される */
.audio-player .controls .mute.active {
  background-color: #be4da2;
}
ミュートの発生を検出

再生を開始したときや再生を一時停止したときには play や pause イベントが発生しますが、mute になったかどうかを検出するイベントはありません。

mute の発生検出するには、音量を変更したときに発生する volumechange イベントを利用します。

音量を変更したときに発生する volumechange イベントで、その時の muted プロパティを調べることでミュートになったことを検出できます(ミュートは volume が 0 になるわけではありません)。

<div class="audio-player">
  <audio controls src="sample.mp3"></audio><!-- コントロールを表示(動作確認用) -->
  <div class="controls">
    <button class="toggle" type="button">Play</button>
    <button class="mute" type="button">Mute</button>
  </div>
</div>

以下はミュートボタンの click イベントではなく、volumechange イベントでミュートを検出してラベルやクラスを変更する例です。

document.addEventListener('DOMContentLoaded', () => {

  const audioPlayers = document.querySelectorAll('.audio-player');

  audioPlayers.forEach((audioPlayer) => {
    const audio = audioPlayer.querySelector('audio')
    audio.volume = .3;
    const toggleBtn = audioPlayer.querySelector('.toggle');

    async function playAudio() {
      try {
        await audio.play();
      } catch (err) {
        console.warn(err)
      }
    }

    toggleBtn.addEventListener('click', () => {
      if (audio.paused) {
        playAudio();
      } else {
        audio.pause();
      }
    });

    const muteBtn = audioPlayer.querySelector('.mute');

    // ミュートボタンの click イベントの設定
    muteBtn.addEventListener('click', () => {
      if (audio.muted) {
        audio.muted = false;
      } else {
        audio.muted = true;
      }
    });

    // volumechange イベントでミュート状態の変更を監視
    audio.addEventListener('volumechange', (e) => {
      // ボリュームが変化した際の muted プロパティを調べる
      if (e.currentTarget.muted) {
        // ミュートされた
        muteBtn.textContent = 'Unmute';
        muteBtn.classList.add('active');
      } else {
        // ミュートが解除された
        muteBtn.textContent = 'Mute';
        muteBtn.classList.remove('active');
      }
    }, false);

    audio.addEventListener('pause', () => {
      toggleBtn.textContent = 'Play';
    });

    audio.addEventListener('play', () => {
      toggleBtn.textContent = 'Pause';
    });

    audio.addEventListener('ended', () => {
      toggleBtn.textContent = 'Play';
    });

  });
});

この例では、独自のコントロールと audio 要素自体のコントロールの動きがわかるように audio 要素に controls 属性指定して意図的に audio 要素自体のコントロールも表示しています。

再生・停止ボタンのラベルの変更は play/pasuse イベントで行っています(play/pause イベント)。

Play ボタンをクリックすると音声データが再生されます。

ループ(loop)

loop プロパティに true を設定することで、連続再生(ループ)させることができます。ループを解除するには loop プロパティに false を設定します。初期値:false

以下はループのトグルボタンを追加した例です。

ループボタンをクリックすると連続再生が有効になり、連続再生が有効な状態でループボタンをクリックすると連続再生は解除されます。

また、現在連続再生(ループ)が有効かどうかがわかるように、状態によりボタンのラベル(テキスト)を変更し、クラスの追加・削除でボタンの背景色を変更します。

残り再生時間:

HTML
<div class="audio-player">
  <audio src="sample.mp3"></audio>
  <div class="controls">
    <button class="toggle" type="button">Play</button>
    <input type="range" name="seek" value="0" step=".1">
    <button class="mute" type="button">Mute</button>
    <input type="range" name="volume" min="0.0" max="1.0" value="1.0" step=".1">
    <button class="loop" type="button">Loop</button>
  </div>
  <p class="time-remain">残り再生時間:<span></span> 秒</p>
</div>
CSS
/* .active はループが有効な場合に追加される */
.audio-player .controls .loop.active {
  background-color: #f16b05;
}

ループボタンの click イベントでループの有効・無効を設定し、有効であれば active クラスをボタンに追加し、無効の場合はクラスを削除します。

プレイリストの作成

リストのアイコンをクリックすると音声データを再生するプレイリストの例です。

プレイリストの構造や作成方法は色々とありますが、以降では次のようなプレイリストを作成します。

  • アイコンをクリックすると再生・停止するだけ
  • アイコンをクリックすると再生・停止し、再生時間やシークバーを表示
  • リストに対して1つのコントロール(音声プレーヤー)を表示

また、以下のページでも異なるプレイリストのサンプルを掲載しています。

audio 要素と JavaScript でオーディオプレーヤーを作成

アイコンをクリックすると再生・停止

以下はアイコンをクリックすると再生・停止するだけのシンプルなプレイリストの例です。

再生中に他の項目のボタンをクリックすると、切り替わります。

音声リスト

  • 鳥のさえずり
  • 小川のせせらぎ
  • 波の音

全体を playlist クラスを指定した div 要素で囲み、その中の ul li 要素にリスト項目を記述します。

音声データの URL は li 要素の data-audio-src カスタム属性に指定します。

また、ボタンと audio 要素は HTML では記述せずに JavaScript で追加します。

HTML
<div class="playlist">
  <h3 class="playlist-title">音声リスト</h3>
  <div class="tracks">
    <ul>
      <li data-audio-src="birds.mp3">
        <div class="track-title">鳥のさえずり</div>
      </li>
      <li data-audio-src="stream.mp3">
        <div class="track-title">小川のせせらぎ</div>
      </li>
      <li data-audio-src="wave.mp3">
        <div class="track-title">波の音</div>
      </li>
    </ul>
  </div>
</div>

JavaScript を適用すると button 要素と audio 要素が挿入され、以下のような HTML が生成されます。

このサンプルの場合、button 要素にアイコンを使用しているのでテキストがないため、ボタンには aria-label 属性を指定しています。

JavaScript で生成される HTML
<div class="playlist">
  <h3 class="playlist-title">音声リスト</h3>
  <div class="tracks">
    <ul>
      <li data-audio-src="birds.mp3">
        <button class="play" type="button" aria-label="Play"></button>
        <audio></audio>
        <div class="track-title">鳥のさえずり</div>
      </li>
      <li data-audio-src="stream.mp3">
        <button class="play" type="button" aria-label="Play"></button>
        <audio></audio>
        <div class="track-title">小川のせせらぎ</div>
      </li>
      <li data-audio-src="wave.mp3">
        <button class="play" type="button" aria-label="Play"></button>
        <audio></audio>
        <div class="track-title">波の音</div>>
      </li>
    </ul>
  </div>
</div>

複数のプレイリストを同じページに配置できるように、JavaScript では playlist クラスを指定した div 要素を全て取得して、それぞれについて処理しています。

最初に各トラックでリストの項目に insertAdjacentHTML() を使ってボタンと audio 要素をタイトルのテキストの前に追加しています。

今までの例では、再生・停止ボタンはクリックすると文字を変更していましたが、この例ではクラスを付け替えて CSS でアイコンを切り替えています。

また、複数の項目を同時に再生できてしまうのは、音が重なってしまうため、play イベントを使って1つだけ再生するようにしています。

以下関連項目です。

JavaScript
document.addEventListener('DOMContentLoaded', () => {

  const playLists = document.querySelectorAll('.playlist');

  playLists.forEach((playList) => {

    // 全てのトラック(li 要素)を取得
    const tracks = playList.querySelectorAll('.tracks li');

    // 各トラックで
    tracks.forEach((track) => {
      // ボタンと audio 要素をタイトルのテキストの前に追加
      const playBtnElem =
      `<button class="play" type="button" aria-label="Play"></button>
      <audio></audio>`;
      track.insertAdjacentHTML('afterbegin', playBtnElem);

      // 再生ボタン
      const playBtn = track.querySelector('button.play');

      // audio 要素
      const audio = track.querySelector('audio');
      // src に data-audio-src 属性の値を設定
      audio.src = track.dataset.audioSrc;

      // 再生終了時
      audio.addEventListener('ended', () => {
        // ボタンから playing クラスを削除し、aria-label を変更
        playBtn.classList.remove('playing');
        playBtn.setAttribute('aria-label', 'Play');
      }, false);

      // 再生ボタンをクリックした際の処理
      playBtn.addEventListener('click', (e) => {
        // 再生・一時停止を実行する関数を呼び出す
        toggleBtn(track);
      }, false);

    });

    // 音声データを再生する非同期関数(Async Function)
    async function playAudio(track) {
      const audio = track.querySelector('audio');
      try {
        await audio.play();
        const btn = track.querySelector('button.play');
        btn.classList.add('playing');
        // aria-label 属性の値を更新
        btn.setAttribute('aria-label', 'Pause');
      } catch (err) {
        console.warn(err);
      }
    }

    // トグルボタンのリスナー
    function toggleBtn(track) {
      // 現在選択されているトラックのボタン
      const btn = track.querySelector('button.play');
      const audio = track.querySelector('audio');

      if (audio.paused) {
        // 停止中であれば setupAndPlay() を呼び出して再生
        playAudio(track);
      } else {
        // 再生中であれば停止
        audio.pause();
        // ボタンから playing クラスを削除し、aria-label を変更
        btn.setAttribute('aria-label', 'Play');
        btn.classList.remove('playing');
      }
    }

    // 現在クリックされた音声データのみを再生(他に再生中の音声データがあれば停止)
    playList.addEventListener('play', (e) => {
      // 全ての audio 要素を取得
      const audios = playList.querySelectorAll('audio');
      // それぞれの audio 要素で以下を実行
      audios.forEach((audio) => {
        // play イベントが発生した要素が自身でなければ
        if (audio !== e.target) {
          // 停止
          audio.pause();
          // 先頭に戻す
          audio.currentTime = 0;
          // 親要素(li 要素)
          const parent = audio.parentElement;
          // ボタンから playing クラスを削除し、aria-label を変更
          const btn = parent.querySelector('button.play');
          btn.classList.remove('playing');
          btn.setAttribute('aria-label', 'Play');
        }
      });
    }, true); // 第3引数(useCapture)を true に
  });
});

アイコンは CSS の ::before 疑似要素を使って background-image の値に url() 関数でデータ URL スキームを使って svg のコードを指定して表示しています。(CSS で svg 要素を表示)。

.playlist {
  position: relative;
  margin: 50px 0;
  width: 100%;
  max-width: 600px;
  border: 1px solid #eee;
  padding: 0;
}

.playlist h3 {
  margin: 0 0 1px;
  font-size: 16px;
  background-color: #eee;
  padding: 0.5rem;
}

.playlist .tracks ul {
  list-style-type: none;
  margin: 0;
  padding: 0;
}

.playlist .tracks ul li {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  margin: 2px 0;
  background-color: #eee;
  padding: 0.5rem 0.25rem;
}

.playlist .tracks ul li:last-of-type {
  margin-bottom: 0;
}

.playlist .tracks .track-title {
  font-size: 14px;
}

.playlist .tracks button {
  cursor: pointer;
  border: none;
  background-color: transparent;
  position: relative;
}

.playlist .tracks button.play::before {
  content: "";
  display: inline-block;
  height: 30px;
  width: 30px;
  vertical-align: -10px;
  margin-right: 8px;
  background-repeat: no-repeat;
}

.playlist .tracks .time {
  font-size: 12px;
  margin-left: 20px;
  color: #999;
}

.audio-playlist .track-list ul li {
  padding: .5rem 1rem;
  cursor: pointer;
  font-size: 13px;
  border: 1px solid #000;
}

.audio-playlist .track-list ul li.active {
  background-color: #eb213c;
}

/* Play ボタン*/
.playlist .tracks button.play::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%234d6fbe' viewBox='0 0 16 16'%3E  %3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z'/%3E  %3Cpath d='M6.271 5.055a.5.5 0 0 1 .52.038l3.5 2.5a.5.5 0 0 1 0 .814l-3.5 2.5A.5.5 0 0 1 6 10.5v-5a.5.5 0 0 1 .271-.445z'/%3E%3C/svg%3E");
  transition: background-image .1s;
}

/* Play ボタン :hover*/
.playlist .tracks button.play:hover::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%234d6fbe' viewBox='0 0 16 16'%3E  %3Cpath d='M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM6.79 5.093A.5.5 0 0 0 6 5.5v5a.5.5 0 0 0 .79.407l3.5-2.5a.5.5 0 0 0 0-.814l-3.5-2.5z'/%3E%3C/svg%3E");
}

/* Pause ボタン */
.playlist .tracks button.play.playing::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23eb213c' class='bi bi-pause-circle-fill' viewBox='0 0 16 16'%3E  %3Cpath d='M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM6.25 5C5.56 5 5 5.56 5 6.25v3.5a1.25 1.25 0 1 0 2.5 0v-3.5C7.5 5.56 6.94 5 6.25 5zm3.5 0c-.69 0-1.25.56-1.25 1.25v3.5a1.25 1.25 0 1 0 2.5 0v-3.5C11 5.56 10.44 5 9.75 5z'/%3E%3C/svg%3E");
}

アイコンの大きさと色を変更

アイコンの大きさは疑似要素 .playlist .tracks button.play::before の width と height で変更します。

また、アイコンの色はそれぞれのアイコンを背景画像(background-image)で設定している url() 内の fill 属性の値で変更できます。

url() 内の値はエンコード(エスケープ)されているので、fill 属性の値の「%23」は # を表しています。例えば、色を黒(#000000)にするには fill='%23000000' とします。赤にするには fill='red' とキーワードで指定することもできます。

duration や currentTime を表示

この例では、再生中の項目の右側に再生時間(duration)と再生位置を表す時間(currentTime)を表示するようにしています。

音声リスト

  • 鳥のさえずり
  • 小川のせせらぎ
  • 波の音

HTML は前述の例と同じですが、JavaScript で時間表示の部分を別途追加します。

<div class="playlist">
  <h3 class="playlist-title">音声リスト</h3>
  <div class="tracks">
    <ul>
      <li data-audio-src="birds.mp3">
        <div class="track-title">鳥のさえずり</div>
      </li>
      <li data-audio-src="stream.mp3">
        <div class="track-title">小川のせせらぎ</div>
      </li>
      <li data-audio-src="wave.mp3">
        <div class="track-title">波の音</div>
      </li>
    </ul>
  </div>
</div>

JavaScript を適用すると button 要素と audio 要素の他に時間を表示する部分の要素が挿入され、以下のような HTML が生成されます。

JavaScript で生成される HTML
<div class="playlist">
  <h3 class="playlist-title">音声リスト</h3>
  <div class="tracks">
    <ul>
      <li data-audio-src="birds.mp3">
        <button class="play" type="button" aria-label="Play"></button>
        <div class="track-title">鳥のさえずり</div>
        <div class="time">
          <span class="current-time">00:00</span>/
          <span class="duration">00:00</span>
        </div>
        <audio></audio>
      </li>
      <li data-audio-src="stream.mp3">
        <button class="play" type="button" aria-label="Play"></button>
        <div class="track-title">小川のせせらぎ</div>
        <div class="time">
          <span class="current-time">00:00</span>/
          <span class="duration">00:00</span>
        </div>
        <audio></audio>
      </li>
      <li data-audio-src="wave.mp3">
        <button class="play" type="button" aria-label="Play"></button>
        <div class="track-title">波の音</div>
        <div class="time">
          <span class="current-time">00:00</span>/
          <span class="duration">00:00</span>
        </div>
        <audio></audio>
      </li>
    </ul>
  </div>
</div>

JavaScript では duration や currentTime を mm:ss の形式に変換して表示するように、別途 secToHMS() という関数を定義しています(関連ページ:秒を時・分・秒や hh:mm:ss に変換)。

以下関連項目です。

JavaScript
document.addEventListener('DOMContentLoaded', () => {

  const playLists = document.querySelectorAll('.playlist');

  playLists.forEach((playList) => {

    // 全てのトラック(li 要素)を取得
    const tracks = playList.querySelectorAll('.tracks li');

    // 各トラックで
    tracks.forEach((track) => {

      // ボタンや時間を表示する要素、audio 要素をタイトルのテキストの前後に追加
      const playBtnElem =
        `<button class="play" type="button" aria-label="Play"></button>`;

      const timeElem = `<div class="time">
        <span class="current-time">00:00</span>/
        <span class="duration">00:00</span>
      </div>
      <audio></audio>`;

      track.insertAdjacentHTML('afterbegin', playBtnElem);
      track.insertAdjacentHTML('beforeend', timeElem);

      // 再生ボタン
      const playBtn = track.querySelector('button.play');

      // audio 要素
      const audio = track.querySelector('audio');
      audio.src = track.dataset.audioSrc;

      //再生時間(再生長)を取得して表示(loadedmetadata イベント)
      audio.addEventListener('loadedmetadata', () => {
        track.querySelector('.duration').textContent = secToHMS(Math.floor(audio.duration));
      }, false);

      //再生位置(時間)を表示(timeupdate イベント)
      audio.addEventListener('timeupdate', () => {
        track.querySelector('.current-time').textContent = secToHMS(Math.floor(audio.currentTime));
      }, false);

      audio.addEventListener('ended', () => {
        // ボタンから playing クラスを削除し、aria-label を変更
        playBtn.classList.remove('playing');
        playBtn.setAttribute('aria-label', 'Play');
      }, false);

      // 再生ボタンをクリックした際の処理
      playBtn.addEventListener('click', (e) => {
        // 再生・一時停止を実行する関数を呼び出す
        toggleBtn(track);
      }, false);

      // 現在の再生位置と再生時間(再生長)を表示する要素
      const timeDiv = track.querySelector('div.time');
      // 初期状態では非表示に
      timeDiv.style.display = 'none';
    });

    // 音声データを再生する非同期関数(Async Function)
    async function playAudio(track) {
      const audio = track.querySelector('audio');
      try {
        await audio.play();
        const btn = track.querySelector('button.play');
        btn.classList.add('playing');
        // aria-label 属性の値を更新
        btn.setAttribute('aria-label', 'Pause');
      } catch (err) {
        console.warn(err);
      }
    }

    // トグルボタンのリスナー
    function toggleBtn(track) {
      // 現在選択されているトラックのボタン
      const btn = track.querySelector('button.play');
      // 現在選択されているトラックの時間を表示する要素
      const time = track.querySelector('div.time');

      const audio = track.querySelector('audio');

      if (audio.paused) {
        // 停止中であれば setupAndPlay() を呼び出して再生
        playAudio(track);
        time.style.display = 'block';
      } else {
        // 再生中であれば停止
        audio.pause();
        // ボタンから playing クラスを削除し、aria-label を変更
        btn.setAttribute('aria-label', 'Play');
        btn.classList.remove('playing');
      }
    }

    // 現在クリックされた音声データのみを再生(他に再生中の音声データがあれば停止)
    playList.addEventListener('play', (e) => {
      // 全ての audio 要素を取得
      const audios = playList.querySelectorAll('audio');
      // それぞれの audio 要素で以下を実行
      audios.forEach((audio) => {
        // play イベントが発生した要素が自身でなければ
        if (audio !== e.target) {
          // 停止
          audio.pause();
          // 先頭に戻す
          audio.currentTime = 0;
          // 親要素(li 要素)
          const parent = audio.parentElement;
          // 時間を表示する要素を非表示に
          parent.querySelector('div.time').style.display = 'none';
          // ボタンから playing クラスを削除し、aria-label を変更
          const btn = parent.querySelector('button.play');
          btn.classList.remove('playing');
          btn.setAttribute('aria-label', 'Play');
        }
      });
    }, true); // 第3引数(useCapture)を true に
  });

  //秒数を hh:mm:ss に変換する関数
  function secToHMS(seconds) {
    const hour = Math.floor(seconds / 3600);
    const min = Math.floor(seconds % 3600 / 60);
    const sec = seconds % 60;
    let hh;
    if (hour < 100) {
      hh = (`00${hour}`).slice(-2);
    } else {
      hh = hour;
    }
    const mm = (`00${min}`).slice(-2);
    const ss = (`00${sec}`).slice(-2);
    let time = '';
    if (hour !== 0) {
      time = `${hh}:${mm}:${ss}`;
    } else {
      time = `${mm}:${ss}`;
    }
    return time;
  }
});

CSS は時間表示部分のスタイルを追加した以外は同じです。

.playlist {
  position: relative;
  margin: 50px 0;
  width: 100%;
  max-width: 600px;
  border: 1px solid #eee;
  padding: 0;
}

.playlist h3 {
  margin: 0 0 1px;
  font-size: 16px;
  background-color: #eee;
  padding: 0.5rem;
}

.playlist .tracks ul {
  list-style-type: none;
  margin: 0;
  padding: 0;
}

.playlist .tracks ul li {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  margin: 2px 0;
  background-color: #eee;
  padding: 0.5rem 0.25rem;
}

.playlist .tracks ul li:last-of-type {
  margin-bottom: 0;
}

.playlist .tracks .track-title {
  font-size: 14px;
}

.playlist .tracks button {
  cursor: pointer;
  border: none;
  background-color: transparent;
  position: relative;
}

.playlist .tracks button.play::before {
  content: "";
  display: inline-block;
  height: 30px;
  width: 30px;
  vertical-align: -10px;
  margin-right: 8px;
  background-repeat: no-repeat;
}

.playlist .tracks .time {
  font-size: 12px;
  margin-left: 20px;
  color: #999;
}

/* Play ボタン*/
.playlist .tracks button.play::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%234d6fbe' viewBox='0 0 16 16'%3E  %3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z'/%3E  %3Cpath d='M6.271 5.055a.5.5 0 0 1 .52.038l3.5 2.5a.5.5 0 0 1 0 .814l-3.5 2.5A.5.5 0 0 1 6 10.5v-5a.5.5 0 0 1 .271-.445z'/%3E%3C/svg%3E");
  transition: background-image .1s;
}

/* Play ボタン :hover*/
.playlist .tracks button.play:hover::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%234d6fbe' viewBox='0 0 16 16'%3E  %3Cpath d='M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM6.79 5.093A.5.5 0 0 0 6 5.5v5a.5.5 0 0 0 .79.407l3.5-2.5a.5.5 0 0 0 0-.814l-3.5-2.5z'/%3E%3C/svg%3E");
}

/* Pause ボタン */
.playlist .tracks button.play.playing::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23eb213c' class='bi bi-pause-circle-fill' viewBox='0 0 16 16'%3E  %3Cpath d='M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM6.25 5C5.56 5 5 5.56 5 6.25v3.5a1.25 1.25 0 1 0 2.5 0v-3.5C7.5 5.56 6.94 5 6.25 5zm3.5 0c-.69 0-1.25.56-1.25 1.25v3.5a1.25 1.25 0 1 0 2.5 0v-3.5C11 5.56 10.44 5 9.75 5z'/%3E%3C/svg%3E");
}
new Audio() で生成

今までのサンプルは、JavaScript で HTML 上に audio 要素を各トラックごとに追加していますが、以下は、new Audio() で audio 要素を1つ生成する例です。動作は前述の例と同じです。

但し、こちらのほうがコードがわかりにくいかも知れません(もっと良いコードがあると思います)。

HTML と CSS は前述の例と同じです。

JavaScript
document.addEventListener('DOMContentLoaded', () => {

  const playLists = document.querySelectorAll('.playlist');

  playLists.forEach((playList) => {
    // audio 要素を生成
    const audio = new Audio();
    // audio 要素を playList に追加
    playList.appendChild(audio);
    // 全てのトラック(li 要素)を取得
    const tracks = playList.querySelectorAll('.tracks li');
    // 選択されたトラックを表す変数の初期化
    let currentTrack = null;

    // timeupdate イベントに再生位置(時間)を表示する関数を登録
    audio.addEventListener('timeupdate', printTime, false);
    // 現在の再生位置(時間)を表示する関数
    function printTime() {
      currentTrack.querySelector('.current-time').textContent = secToHMS(Math.floor(audio.currentTime));
    }

    // ended イベントに再生終了時に実行する関数を登録
    audio.addEventListener('ended', audioEnded, false);
    function audioEnded() {
      const btn = currentTrack.querySelector('button.play');
      btn.classList.remove('playing');
      btn.setAttribute('aria-label', 'Play');
    }

    // 各トラックで
    tracks.forEach((track) => {
      // ボタンと時間を表示する要素をタイトルのテキストの前後に追加
      const playBtnElem = `<button class="play" type="button" aria-label="Play"></button>`;
      const timeElem = `<div class="time">
        <span class="current-time">00:00</span>/
        <span class="duration">00:00</span>
      </div>`;
      track.insertAdjacentHTML('afterbegin', playBtnElem);
      track.insertAdjacentHTML('beforeend', timeElem);

      // 再生ボタン
      const playBtn = track.querySelector('button.play');
      // 再生ボタンをクリックした際の処理
      playBtn.addEventListener('click', (e) => {
        // 変数 currentTrack にクリックした要素を代入
        currentTrack = playBtn.parentElement;
        // ここではまだ src を設定してはいけない(以下 は NG)
        // audio.src = track.dataset.audioSrc;
        // 再生・一時停止を実行する関数を呼び出す
        toggleBtn();
      }, false);

      // 現在の再生位置と再生時間(再生長)を表示する要素
      const timeDiv = track.querySelector('div.time');
      // 初期状態では非表示に
      timeDiv.style.display = 'none';
    });

    // 音声データを再生する非同期関数(Async Function)
    async function playAudio() {
      try {
        await audio.play();
        const btn = currentTrack.querySelector('button.play');
        btn.classList.add('playing');
        // aria-label 属性の値を更新
        btn.setAttribute('aria-label', 'Pause');
      } catch (err) {
        console.warn(err);
      }
    }

    // 全ての再生ボタン
    const playBtns = playList.querySelectorAll('button.play');

    // トグルボタンのリスナー
    function toggleBtn() {
      // 現在選択されているトラックのボタン
      const btn = currentTrack.querySelector('button.play');
      // 現在選択されているトラックの時間を表示する要素
      const time = currentTrack.querySelector('div.time');

      if (audio.paused) {
        // 停止中であれば setupAndPlay() を呼び出して再生
        setupAndPlay();
      } else {
        // 再生中であれば停止
        audio.pause();
        // aria-label 属性の値を更新
        btn.setAttribute('aria-label', 'Play');
        // 現在再生中のボタンがクリックされた場合(そのトラックを停止)
        if (btn.classList.contains('playing')) {
          btn.classList.remove('playing');
          btn.parentElement.classList.add('wasPlaying');
        } else {
          // 現在再生中以外のボタンがクリックされた場合(そのトラックを再生)
          playBtns.forEach((btn) => {
            // 全てのボタンから playing クラスを削除
            btn.classList.remove('playing');
          });
          // setupAndPlay() を呼び出して再生
          setupAndPlay();
        }
      }

      function setupAndPlay() {
        // 全てのトラックの時間表示を非表示に
        playList.querySelectorAll('div.time').forEach((timeDiv) => {
          timeDiv.style.display = 'none';
        });
        // 再生中のボタン以外がクリックされた場合
        if (!currentTrack.classList.contains('wasPlaying')) {
          // src を更新
          audio.src = currentTrack.dataset.audioSrc;
        }
        tracks.forEach((track) => {
          track.classList.remove('wasPlaying');
          track.querySelector('button.play').setAttribute('aria-label', 'Play');
        });
        // 現在選択されているトラックの時間を表示
        time.style.display = 'block';
        audio.addEventListener('loadedmetadata', () => {
          //再生時間を表示
          currentTrack.querySelector('.duration').textContent = secToHMS(Math.floor(audio.duration));
        }, false);
        playAudio();
      }
    }
  });

  //秒数を hh:mm:ss に変換する関数
  function secToHMS(seconds) {
    const hour = Math.floor(seconds / 3600);
    const min = Math.floor(seconds % 3600 / 60);
    const sec = seconds % 60;
    let hh;
    if (hour < 100) {
      hh = (`00${hour}`).slice(-2);
    } else {
      hh = hour;
    }
    const mm = (`00${min}`).slice(-2);
    const ss = (`00${sec}`).slice(-2);
    let time = '';
    if (hour !== 0) {
      time = `${hh}:${mm}:${ss}`;
    } else {
      time = `${mm}:${ss}`;
    }
    return time;
  }
});

シークバーを表示

以下は type="range" の input 要素(レンジ入力)を使って再生位置を表示・操作するシークバーも表示する例です。

音声リスト

  • 鳥のさえずり
  • 小川のせせらぎ
  • 波の音

関連項目:再生位置をシークバーで操作

document.addEventListener('DOMContentLoaded', () => {

  const playLists = document.querySelectorAll('.playlist');

  playLists.forEach((playList) => {

    // 全てのトラック(li 要素)を取得
    const tracks = playList.querySelectorAll('.tracks li');

    // 各トラックで
    tracks.forEach((track) => {

      // ボタンと時間を表示する要素、シークバーの input 要素をタイトルのテキストの前後に追加
      const playBtnElem =
        `<button class="play" type="button" aria-label="Play"></button>`;
      const controlsElem = `<div class="controls">
    <div class="current-time">00:00</div>
    <input type="range" name="seek" min="0" max="100" value="0" step=".1">
    <div class="duration">00:00</div>
  </div>
  <audio></audio>`;
      track.insertAdjacentHTML('afterbegin', playBtnElem);
      track.insertAdjacentHTML('beforeend', controlsElem);

      // 再生ボタン
      const playBtn = track.querySelector('button.play');

      // audio 要素
      const audio = track.querySelector('audio');
      audio.src = track.dataset.audioSrc;

      let duration;

      //再生時間(再生長)を取得して表示(loadedmetadata イベント)
      audio.addEventListener('loadedmetadata', () => {
        duration = audio.duration;
        track.querySelector('.duration').textContent = secToHMS(Math.floor(duration));
        // input 要素(シークバー)の max 属性に取得した duration の値を設定
        seekBar.setAttribute('max', Math.floor(duration));
      }, false);

      //再生位置(時間)を表示(timeupdate イベント)
      audio.addEventListener('timeupdate', () => {
        const cTime = audio.currentTime;
        // input 要素(シークバー)の value を更新
        seekBar.value = cTime;
        track.querySelector('.current-time').textContent = secToHMS(Math.floor(cTime));

      }, false);

      // input 要素(シークバー)
      const seekBar = track.querySelector('input[name="seek"]');

      // input 要素(シークバー)に input イベントリスナーを設定
      seekBar.addEventListener('input', (e) => {
        //  input 要素の値(value)を currentTime に設定
        audio.currentTime = e.currentTarget.value;
      });

      audio.addEventListener('ended', () => {
        // ボタンから playing クラスを削除し、aria-label を変更
        playBtn.classList.remove('playing');
        playBtn.setAttribute('aria-label', 'Play');
      }, false);

      // 再生ボタンをクリックした際の処理
      playBtn.addEventListener('click', (e) => {
        // 再生・一時停止を実行する関数を呼び出す
        toggleBtn(track);
      }, false);

      // コントロール(時間やシークバー)を表示する要素
      const controls = track.querySelector('.controls');
      // 初期状態では時間やシークバーを非表示に
      controls.style.display = 'none';
    });

    // 音声データを再生する非同期関数(Async Function)
    async function playAudio(track) {
      const audio = track.querySelector('audio');
      try {
        await audio.play();
        const btn = track.querySelector('button.play');
        btn.classList.add('playing');
        // aria-label 属性の値を更新
        btn.setAttribute('aria-label', 'Pause');
      } catch (err) {
        console.warn(err);
      }
    }

    // トグルボタンのリスナー
    function toggleBtn(track) {
      // 現在選択されているトラックのボタン
      const btn = track.querySelector('button.play');
      // 現在選択されているトラックのコントロールを表示する要素
      const controls = track.querySelector('.controls');

      const audio = track.querySelector('audio');

      if (audio.paused) {
        // 停止中であれば playAudio() を呼び出して再生
        playAudio(track);
        // 時間やシークバーを表示(display= 'none' から変更)
        controls.style.display = 'flex';
      } else {
        // 再生中であれば停止
        audio.pause();
        // ボタンから playing クラスを削除し、aria-label を変更
        btn.setAttribute('aria-label', 'Play');
        btn.classList.remove('playing');
      }
    }

    // 現在クリックされた音声データのみを再生(他に再生中の音声データがあれば停止)
    playList.addEventListener('play', (e) => {
      // 全ての audio 要素を取得
      const audios = playList.querySelectorAll('audio');
      // それぞれの audio 要素で以下を実行
      audios.forEach((audio) => {
        // play イベントが発生した要素が自身でなければ
        if (audio !== e.target) {
          // 停止
          audio.pause();
          // 先頭に戻す
          audio.currentTime = 0;
          // 親要素(li 要素)
          const parent = audio.parentElement;
          // 時間を表示する要素を非表示に
          parent.querySelector('.controls').style.display = 'none';
          // ボタンから playing クラスを削除し、aria-label を変更
          const btn = parent.querySelector('button.play');
          btn.classList.remove('playing');
          btn.setAttribute('aria-label', 'Play');
        }
      });
    }, true); // 第3引数(useCapture)を true に
  });

  //秒数を hh:mm:ss に変換する関数
  function secToHMS(seconds) {
    const hour = Math.floor(seconds / 3600);
    const min = Math.floor(seconds % 3600 / 60);
    const sec = seconds % 60;
    let hh;
    if (hour < 100) {
      hh = (`00${hour}`).slice(-2);
    } else {
      hh = hour;
    }
    const mm = (`00${min}`).slice(-2);
    const ss = (`00${sec}`).slice(-2);
    let time = '';
    if (hour !== 0) {
      time = `${hh}:${mm}:${ss}`;
    } else {
      time = `${mm}:${ss}`;
    }
    return time;
  }
});

HTML と CSS は今までの例と同じです。

<div class="playlist">
  <h3 class="playlist-title">音声リスト</h3>
  <div class="tracks">
    <ul>
      <li data-audio-src="birds.mp3">
        <div class="track-title">鳥のさえずり</div>
      </li>
      <li data-audio-src="stream.mp3">
        <div class="track-title">小川のせせらぎ</div>
      </li>
      <li data-audio-src="wave.mp3">
        <div class="track-title">波の音</div>
      </li>
    </ul>
  </div>
</div>
.playlist {
  position: relative;
  margin: 50px 0;
  width: 100%;
  max-width: 600px;
  border: 1px solid #eee;
  padding: 0;
}

.playlist h3 {
  margin: 0 0 1px;
  font-size: 16px;
  background-color: #eee;
  padding: 0.5rem;
}

.playlist .tracks ul {
  list-style-type: none;
  margin: 0;
  padding: 0;
}

.playlist .tracks ul li {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  margin: 2px 0;
  background-color: #eee;
  padding: 0.5rem 0.25rem;
}

.playlist .tracks ul li:last-of-type {
  margin-bottom: 0;
}

.playlist .tracks .track-title {
  font-size: 14px;
}

.playlist .tracks button {
  cursor: pointer;
  border: none;
  background-color: transparent;
  position: relative;
}

.playlist .tracks button.play::before {
  content: "";
  display: inline-block;
  height: 30px;
  width: 30px;
  vertical-align: -10px;
  margin-right: 8px;
  background-repeat: no-repeat;
}

.playlist .tracks ul li .controls {
  display: flex;
  flex-wrap: nowrap;
  gap: 10px;
  margin-left: 20px;
}

.playlist .tracks ul li .controls .current-time,
.playlist .tracks ul li .controls .duration {
  font-size: 13px;
  color: #999;
}

.audio-playlist .track-list ul li {
  padding: .5rem 1rem;
  cursor: pointer;
  font-size: 13px;
  border: 1px solid #000;
}

.audio-playlist .track-list ul li.active {
  background-color: #eb213c;
}

/* Play ボタン*/
.playlist .tracks button.play::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%234d6fbe' viewBox='0 0 16 16'%3E  %3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z'/%3E  %3Cpath d='M6.271 5.055a.5.5 0 0 1 .52.038l3.5 2.5a.5.5 0 0 1 0 .814l-3.5 2.5A.5.5 0 0 1 6 10.5v-5a.5.5 0 0 1 .271-.445z'/%3E%3C/svg%3E");
  transition: background-image .1s;
}

/* Play ボタン :hover*/
.playlist .tracks button.play:hover::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%234d6fbe' viewBox='0 0 16 16'%3E  %3Cpath d='M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM6.79 5.093A.5.5 0 0 0 6 5.5v5a.5.5 0 0 0 .79.407l3.5-2.5a.5.5 0 0 0 0-.814l-3.5-2.5z'/%3E%3C/svg%3E");
}

/* Pause ボタン */
.playlist .tracks button.play.playing::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23eb213c' class='bi bi-pause-circle-fill' viewBox='0 0 16 16'%3E  %3Cpath d='M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM6.25 5C5.56 5 5 5.56 5 6.25v3.5a1.25 1.25 0 1 0 2.5 0v-3.5C7.5 5.56 6.94 5 6.25 5zm3.5 0c-.69 0-1.25.56-1.25 1.25v3.5a1.25 1.25 0 1 0 2.5 0v-3.5C11 5.56 10.44 5 9.75 5z'/%3E%3C/svg%3E");
}

リストの下にコントロールを表示

以下はリストのアイコンで再生・停止でき、下に表示されているコントロールでも操作できる例です。

現在選択状態の項目は下のコントロールの右側に表示されます。

音声リスト

  • 鳥のさえずり
  • 小川のせせらぎ
  • 波の音

HTML は今までの例と同じです。

<div class="playlist">
  <h3 class="playlist-title">音声リスト</h3>
  <div class="tracks">
    <ul>
      <li data-audio-src="birds.mp3">
        <div class="track-title">鳥のさえずり</div>
      </li>
      <li data-audio-src="stream.mp3">
        <div class="track-title">小川のせせらぎ</div>
      </li>
      <li data-audio-src="wave.mp3">
        <div class="track-title">波の音</div>
      </li>
    </ul>
  </div>
</div>

但し、JavaScript が適用されると button 要素の他、コントロール(audio 要素)と現在選択されている項目を表示する div 要素(.now-playing)が挿入され、以下のような HTML が生成されます。

JavaScript で生成される HTML
<div class="playlist">
  <h3 class="playlist-title noindex">音声リスト</h3>
  <div class="tracks">
    <ul>
      <li data-audio-src="birds.mp3">
        <button class="play" type="button" aria-label="Play"></button>
        <div class="track-title">鳥のさえずり</div>
      </li>
      <li data-audio-src="stream.mp3">
        <button class="play" type="button" aria-label="Play"></button>
        <div class="track-title">小川のせせらぎ</div>
      </li>
      <li data-audio-src="wave.mp3">
        <button class="play" type="button" aria-label="Play"></button>
        <div class="track-title">波の音</div>
      </li>
    </ul>
  </div>
  <div class="now-playing">
    <audio controls></audio>
    <div class="current-title"></div>
  </div>
</div>

JavaScript では、これまでと違い、トラックのボタンのクラスや aria-label 属性の更新はコントロール(ブラウザの音声プレイヤー)の動作にも合わせるように play イベントと pause イベントで実施しています。

また、これまでの例では(new Audio() で生成の場合を除き)トラックごとに audio 要素を配置していましたが、この例の場合、audio 要素は1つのみです。

音声データを再生する非同期関数 playAudio() では、呼び出された際に、今まで再生されていたトラックと異なる場合は、src 属性を変更し currentTrack を更新します。今まで再生していたトラックと同じ場合は、src 属性は変更しません(src 属性に値を代入してしまうと、再読込され、停止位置がリセットされてしまい、一時停止後でも最初から再生されます)。

JavaScript
document.addEventListener('DOMContentLoaded', () => {

  const playLists = document.querySelectorAll('.playlist');

  playLists.forEach((playList) => {

    // audio 要素と現在選択されている項目のタイトルを追加
    const nowPlaying = `<div class="now-playing">
      <audio controls></audio>
      <div class="current-title"></div>
    </div>`;
    playList.insertAdjacentHTML('beforeend', nowPlaying);

    // 全てのトラック(li 要素)を取得
    const tracks = playList.querySelectorAll('.tracks li');

    // 選択されたトラックを表す変数の初期化
    let currentTrack = tracks[0];

    // audio 要素
    const audio = playList.querySelector('audio');
    audio.src = currentTrack.dataset.audioSrc;

    const currentTitle = playList.querySelector('.now-playing .current-title');
    currentTitle.textContent = currentTrack.textContent;

    // 各トラックで
    tracks.forEach((track) => {

      // ボタンを表示する要素をタイトルのテキストの前に追加
      const playBtnElem =
        `<button class="play" type="button" aria-label="Play"></button>`;
      track.insertAdjacentHTML('afterbegin', playBtnElem);

      // 再生ボタン
      const playBtn = track.querySelector('button.play');

      // 再生ボタンをクリックした際の処理
      playBtn.addEventListener('click', (e) => {
        // 再生・一時停止を実行する関数を呼び出す
        toggleBtn(track);
      }, false);
    });

    // 音声データを再生する非同期関数(Async Function)
    async function playAudio(track) {
      try {
        // 異なるトラック(項目)が選択された場合
        if (track !== currentTrack) {
          audio.src = track.dataset.audioSrc;
          const btn = track.querySelector('button.play');
          //currentTrack を更新(awaitの後ではplayイベントでcurrentTrackの検出が遅れる)
          currentTrack = btn.parentElement;
        }
        await audio.play();
        // currentTitle を更新
        currentTitle.textContent = currentTrack.textContent;
      } catch (err) {
        console.warn(err);
      }
    }

    // トグルボタンのリスナー
    function toggleBtn(track) {
      if (audio.paused) {
        // 停止中であれば playAudio() を呼び出して再生
        playAudio(track);
      } else {
        if (track === currentTrack) {
          // 再生中であれば停止
          audio.pause();
        } else {
          // 全てのトラックのボタンを初期状態にリセット
          reseAllBtns();
          // 再生
          playAudio(track);
        }
      }
    }

    // 全てのトラックのボタンを初期状態にリセットする関数
    function reseAllBtns() {
      tracks.forEach((track) => {
        // リストのボタン
        const btn = track.querySelector('button.play');
        btn.setAttribute('aria-label', 'Play');
        btn.classList.remove('playing');
      });
    }

    // play イベントでボタンのクラスと aria-label を変更
    audio.addEventListener('play', (e) => {
      const btn = currentTrack.querySelector('button.play');
      btn.classList.add('playing');
      btn.setAttribute('aria-label', 'Pause');
    }, false);

    // pause イベントで全てのトラックのボタンを初期状態にリセット
    audio.addEventListener('pause', (e) => {
      reseAllBtns();
    }, false);
  });
});

CSS は .now-playing の部分が追加されているだけで、その他は同じです。

.playlist {
  position: relative;
  margin: 50px 0;
  width: 100%;
  max-width: 600px;
  border: 1px solid #eee;
  padding: 0;
}

.playlist h3 {
  margin: 0 0 1px;
  font-size: 16px;
  background-color: #eee;
  padding: 0.5rem;
}

.playlist .tracks ul {
  list-style-type: none;
  margin: 0;
  padding: 0;
}

.playlist .tracks ul li {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  margin: 2px 0;
  background-color: #eee;
  padding: 0.5rem 0.25rem;
}

.playlist .tracks ul li:last-of-type {
  margin-bottom: 0;
}

.playlist .tracks .track-title {
  font-size: 14px;
}

.playlist .tracks button {
  cursor: pointer;
  border: none;
  background-color: transparent;
  position: relative;
}

.playlist .tracks button.play::before {
  content: "";
  display: inline-block;
  height: 30px;
  width: 30px;
  vertical-align: -10px;
  margin-right: 8px;
  background-repeat: no-repeat;
}

.playlist .tracks ul li .controls {
  display: flex;
  flex-wrap: nowrap;
  gap: 10px;
  margin-left: 20px;
}

.playlist .tracks ul li .controls .current-time,
.playlist .tracks ul li .controls .duration {
  font-size: 13px;
  color: #999;
}

.playlist .now-playing {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  padding: 20px;
}

.playlist .current-title {
  margin: 0 20px;
}

/* Play ボタン*/
.playlist .tracks button.play::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%234d6fbe' viewBox='0 0 16 16'%3E  %3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z'/%3E  %3Cpath d='M6.271 5.055a.5.5 0 0 1 .52.038l3.5 2.5a.5.5 0 0 1 0 .814l-3.5 2.5A.5.5 0 0 1 6 10.5v-5a.5.5 0 0 1 .271-.445z'/%3E%3C/svg%3E");
  /* transition: background-image .1s; */
}

/* Play ボタン :hover*/
.playlist .tracks button.play:hover::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%234d6fbe' viewBox='0 0 16 16'%3E  %3Cpath d='M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM6.79 5.093A.5.5 0 0 0 6 5.5v5a.5.5 0 0 0 .79.407l3.5-2.5a.5.5 0 0 0 0-.814l-3.5-2.5z'/%3E%3C/svg%3E");
}

/* Pause ボタン */
.playlist .tracks button.play.playing::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23eb213c' class='bi bi-pause-circle-fill' viewBox='0 0 16 16'%3E  %3Cpath d='M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM6.25 5C5.56 5 5 5.56 5 6.25v3.5a1.25 1.25 0 1 0 2.5 0v-3.5C7.5 5.56 6.94 5 6.25 5zm3.5 0c-.69 0-1.25.56-1.25 1.25v3.5a1.25 1.25 0 1 0 2.5 0v-3.5C11 5.56 10.44 5 9.75 5z'/%3E%3C/svg%3E");
}

以下は上記と同じプレイリストで audio 要素のコントロールを独自にカスタマイズして表示する例です。

この例では audio 要素のコントロールを独自にカスタマイズする関数を別途用意しています。

音声リスト

  • 鳥のさえずり
  • 小川のせせらぎ
  • 波の音

プレイリストの HTML と CSS は前述の例と同じです。プレイリストの JavaScript はコントロールを独自にカスタマイズする関数を呼び出す部分のみが異なっています(21行目)。

document.addEventListener('DOMContentLoaded', () => {

  const playLists = document.querySelectorAll('.playlist');

  playLists.forEach((playList) => {

    const nowPlaying = `<div class="now-playing">
      <audio controls></audio>
      <div class="current-title"></div>
    </div>`;
    playList.insertAdjacentHTML('beforeend', nowPlaying);

    const tracks = playList.querySelectorAll('.tracks li');

    let currentTrack = tracks[0];

    const audio = playList.querySelector('audio');
    audio.src = currentTrack.dataset.audioSrc;

    // 別途定義した関数で audio 要素を独自のスタイルで表示(この部分のみ追加)
    createAudioPlayer(audio);

    const currentTitle = playList.querySelector('.now-playing .current-title');
    currentTitle.textContent = currentTrack.textContent;

    tracks.forEach((track) => {

      const playBtnElem =
        `<button class="play" type="button" aria-label="Play"></button>`;
      track.insertAdjacentHTML('afterbegin', playBtnElem);

      const playBtn = track.querySelector('button.play');

      playBtn.addEventListener('click', (e) => {
        toggleBtn(track);
      }, false);
    });

    async function playAudio(track) {
      try {
        if (track !== currentTrack) {
          audio.src = track.dataset.audioSrc;
          const btn = track.querySelector('button.play');
          currentTrack = btn.parentElement;
        }
        await audio.play();
        currentTitle.textContent = currentTrack.textContent;
      } catch (err) {
        console.warn(err);
      }
    }

    function toggleBtn(track) {
      if (audio.paused) {
        playAudio(track);
      } else {
        if (track === currentTrack) {
          audio.pause();
        } else {
          reseAllBtns();
          playAudio(track);
        }
      }
    }

    function reseAllBtns() {
      tracks.forEach((track) => {
        const btn = track.querySelector('button.play');
        btn.setAttribute('aria-label', 'Play');
        btn.classList.remove('playing');
      });
    }

    audio.addEventListener('play', (e) => {
      const btn = currentTrack.querySelector('button.play');
      btn.classList.add('playing');
      btn.setAttribute('aria-label', 'Pause');
    }, false);

    audio.addEventListener('pause', (e) => {
      reseAllBtns();
    }, false);
  });
});
/**
  * audio 要素をカスタムプレーヤーで表示する関数
  * 別途定義した関数 secToHMS() と updateSlider() が必要
  * @param {HTMLElement}  audio (カスタムプレーヤーで表示する audio 要素 ※必須)
  * @param {String}  bgc スライダー部分のトラックの背景色(省略時は #ccc)
  * @param {String}  color スライダー部分の変化する領域の背景色(省略時は #8ea8f9)
  * @param {Float}  vol 初期ボリューム。0.0〜1.0 の範囲で指定(省略時は 1.0)
  */
function createAudioPlayer(audio, bgc, color, vol = 1.0) {

  // 第1引数の audio が存在しない場合やそれが audio 要素でなければ終了
  if (!audio || audio.tagName !== 'AUDIO') {
    return;
  }

  // オーディオプレーヤーをラップする div 要素(ラッパー)を作成
  const audioPlayer = document.createElement('div');
  // クラス属性を付与
  audioPlayer.className = 'audio-player';
  // audio 要素の前にラッパーを挿入
  audio.parentNode.insertBefore(audioPlayer, audio);
  // audio 要素をラッパーに追加
  audioPlayer.appendChild(audio);

  // コントロール部分の HTML
  const controls = `<div class="controls">
  <button class="toggle play-btn" type="button" aria-label="Play"></button>
  <div class="time" role="timer">
    <span class="current-time">0:00</span>
  </div>
  <input class="range-slider" type="range" name="seek" value="0" step=".1" aria-label="seek bar">
  <div class="time" role="timer">
    <span class="duration">0:00</span>
  </div>
  <button class="mute volume-btn" type="button" aria-label="Mute"></button>
  <input class="range-slider" type="range" name="vol" min="0.0" max="1.0" value="1.0" step=".1" aria-label="volume bar">
  <button class="loop loop-btn" type="button" aria-label="Loop"></button>
</div>`;

  // コントロール部分を insertAdjacentHTML でラッパーに追加
  audioPlayer.insertAdjacentHTML('afterbegin', controls);
  // audio 要素を非表示に
  audio.controls = false;

  // 引数の vol を volume に設定
  audio.volume = vol;
  // iPhone 対策
  audio.preload = 'metadata';

  // トグルボタン(再生・停止ボタン)
  const toggleBtn = audioPlayer.querySelector('.toggle');
  // 現在の再生位置(時間)を表示する要素
  const ctSpan = audioPlayer.querySelector('.time .current-time');
  // 現在の再生位置(時間)を hh:mm:ss に変換して表示
  ctSpan.textContent = secToHMS(audio.currentTime);
  // 再生時間を表示する要素
  const durSpan = audioPlayer.querySelector('.time .duration');

  // ループボタン
  const loopBtn = audioPlayer.querySelector('.loop');

  // ループボタンがクリックされた際の処理
  loopBtn.addEventListener('click', () => {
    if (audio.loop) {
      audio.loop = false;
      loopBtn.classList.remove('looped');
      loopBtn.setAttribute('aria-label', 'Loop');
    } else {
      audio.loop = true;
      loopBtn.classList.add('looped');
      loopBtn.setAttribute('aria-label', 'Unloop');
    }
  });

  // 記述されている audio 要素の loop 属性の値をループボタンに反映
  if (!audio.loop) {
    audio.loop = false;
    loopBtn.classList.remove('looped');
    loopBtn.setAttribute('aria-label', 'Loop');
  } else {
    audio.loop = true;
    loopBtn.classList.add('looped');
    loopBtn.setAttribute('aria-label', 'Unloop');
  }

  // ボリュームスライダー
  const volumeBar = audioPlayer.querySelector('input[name="vol"]');
  // スライダーの値に現在の volume の値(初期値)を設定
  volumeBar.value = audio.volume;
  // スライダーの値が変更されたら
  volumeBar.addEventListener('input', (e) => {
    // スライダーの値に現在の値を設定
    audio.volume = e.currentTarget.value;
    // ミュート中であればミュートを解除
    if (audio.muted) {
      audio.muted = false;
      muteBtn.classList.remove('muted');
      muteBtn.setAttribute('aria-label', 'Mute');
    }
  });

  // ミュートボタン
  const muteBtn = audioPlayer.querySelector('.mute');

  // ミュートボタンがクリックされた際の処理
  muteBtn.addEventListener('click', () => {
    // ミュート中であれば
    if (audio.muted) {
      // ミュートを解除
      audio.muted = false;
      // ボリュームバーの位置を更新
      volumeBar.value = audio.volume;
      // ボリュームバーの背景色を更新
      updateSlider(volumeBar, bgc, color);
      // ミュートボタンの背景色を変更
      muteBtn.classList.remove('muted');
      // ミュートボタンの aria-label 属性の値を変更
      muteBtn.setAttribute('aria-label', 'Mute');
    } else {
      // ミュートする
      audio.muted = true;
      // ボリュームバーの位置を0に
      volumeBar.value = 0;
      // ボリュームバーの背景色を更新
      updateSlider(volumeBar, bgc, color);
      muteBtn.classList.add('muted');
      muteBtn.setAttribute('aria-label', 'Unmute');
    }
  });

  // 記述されている audio 要素の muted 属性の値を反映
  if (!audio.muted) {
    audio.muted = false;
    volumeBar.value = audio.volume;
    updateSlider(volumeBar, bgc, color);
    muteBtn.classList.remove('muted');
    muteBtn.setAttribute('aria-label', 'Mute');
  } else {
    audio.muted = true;
    volumeBar.value = 0;
    updateSlider(volumeBar, bgc, color);
    muteBtn.classList.add('muted');
    muteBtn.setAttribute('aria-label', 'Unmute');
  }

  // ミュート状態の変更を検出
  // 独自コントロールと audio 要素のコントロールを同期するための処理
  audio.addEventListener('volumechange', (e) => {
    // ボリュームが変更されてミュート状態であれば
    if (e.currentTarget.muted) {
      // ボリュームバーの値を0に
      volumeBar.value = 0;
      // ボリュームバーの背景色を更新
      updateSlider(volumeBar, bgc, color);
      muteBtn.classList.add('muted');
      muteBtn.setAttribute('aria-label', 'Unmute');
    } else {
      // ボリュームバーの値を現在の volue の値に
      volumeBar.value = audio.volume;
      // ボリュームバーの背景色を更新
      updateSlider(volumeBar, bgc, color);
      muteBtn.classList.remove('muted');
      muteBtn.setAttribute('aria-label', 'Mute');
    }
  }, false);

  // シークバー(再生位置を表すレンジ入力欄のスライダー)
  const seekBar = audioPlayer.querySelector('input[name="seek"]');
  // シークバーの値が変更されたら
  seekBar.addEventListener('input', (e) => {
    //再生位置を変更された値に更新
    audio.currentTime = e.currentTarget.value;
  });

  // 再生時間(音声データの長さ)
  let duration;
  // メタデータの読み込みが完了した時点(loadedmetadata)で再生時間を取得
  audio.addEventListener('loadedmetadata', () => {
    duration = audio.duration;
    // 再生時間を hh:mm:ss に変換して表示
    durSpan.textContent = secToHMS(Math.floor(duration));
    // シークバー(レンジ入力欄)の max 属性に再生時間を設定
    seekBar.setAttribute('max', Math.floor(duration));
  });

  // currentTime プロパティの値が更新される際のリスナーの登録
  audio.addEventListener('timeupdate', updateTime, false);
  function updateTime() {
    const cTime = audio.currentTime;
    // 現在の再生位置(時間)の表示を更新
    ctSpan.textContent = secToHMS(Math.floor(cTime));
    // シークバーの現在の再生位置を更新
    seekBar.value = cTime;
    // シークバーの塗り色を更新
    updateSlider(seekBar, bgc, color);
  }

  // トグルボタンのクリックイベントのリスナーの登録
  toggleBtn.addEventListener('click', togglePlayPause, false);
  function togglePlayPause() {
    // 停止中であれば
    if (audio.paused) {
      // 再生用関数 playAudio() を呼び出す
      playAudio();
    } else {
      // 再生中であれば停止(ボタンのラベルと背景色は pause イベントで変更)
      audio.pause();
    }
  }

  // 音声データを再生する関数(Async Function)
  async function playAudio() {
    try {
      // await を指定(Promise が確定するまで待つ)
      await audio.play();
      // Promise が解決されたらボタンの aria-label の値を変更し、クラスを追加
      toggleBtn.classList.add('playing');
      toggleBtn.setAttribute('aria-label', 'Pause');
    } catch (err) {
      // 再生が失敗し、クラスが追加されていればクラスを削除
      toggleBtn.classList.remove('playing');
      // コンソールにエラーを出力
      console.warn(err)
    }
  }

  // pause イベントのリスナー(ボタンのラベルと背景色を変更)
  audio.addEventListener('pause', () => {
    toggleBtn.classList.remove('playing');
    toggleBtn.setAttribute('aria-label', 'Play');
  });

  // play イベントのリスナー(ボタンのラベルと背景色を変更)
  // 独自コントロールと audio 要素のコントロールを同期するための処理
  audio.addEventListener('play', (e) => {
    toggleBtn.classList.add('playing');
    toggleBtn.setAttribute('aria-label', 'Pause');
  });

  // 再生終了時に発火する ended イベントのリスナー登録
  audio.addEventListener('ended', audioEnded, false);
  function audioEnded() {
    toggleBtn.classList.remove('playing');
    toggleBtn.setAttribute('aria-label', 'Play');
    // 再生位置を先頭に戻す場合は以下のコメントを外す
    //audio.currentTime = 0;
  }

  // ボリュームと再生位置のレンジスライダー(レンジ入力欄の背景色を設定)を取得
  const rangeSliders = audioPlayer.querySelectorAll('.range-slider');
  // レンジスライダーの input イベントに別途定義した関数 updateSlider を設定
  rangeSliders.forEach((slider) => {
    slider.addEventListener('input', (e) => {
      // 背景色を更新
      updateSlider(e.target, bgc, color);
    });
    // 初期状態に現在の状態での背景色を反映
    updateSlider(slider, bgc, color);
  });
};

/* 秒数を引数に受け取り hh:mm:ss に変換する関数  */
function secToHMS(seconds) {
  const hour = Math.floor(seconds / 3600);
  const min = Math.floor(seconds % 3600 / 60);
  const sec = seconds % 60;
  let hh;
  if (hour < 100) {
    hh = (`00${hour}`).slice(-2);
  } else {
    hh = hour;
  }
  const mm = (`00${min}`).slice(-2);
  const ss = (`00${sec}`).slice(-2);
  let time = '';
  if (hour !== 0) {
    time = `${hh}:${mm}:${ss}`;
  } else {
    time = `${mm}:${ss}`;
  }
  return time;
}

/* レンジスライダーのトラックの塗りの範囲と色を更新する関数 */
function updateSlider(slider, bgc = '#ccc', color = '#8ea8f9') {
  if (!slider.max) {
    slider.max = 100;
  }
  const progress = (slider.value / slider.max) * 100;
  slider.style.background = `linear-gradient(to right, ${color} ${progress}%, ${bgc} ${progress}%)`;
}
.audio-player {
  position: relative;
  margin: 0;
  width: 100%;
  max-width: 420px;
  padding: 10px;
  background-color: #2c406e;
  color: #fff;
  border-radius: 29px;
}

.audio-player .controls {
  display: flex;
  gap: 5px;
  flex-wrap: nowrap;
  align-items: center;
}

.audio-player .controls button {
  cursor: pointer;
  border: none;
  background-color: transparent;
}

.audio-player .controls button.play-btn,
.audio-player .controls button.volume-btn {
  margin-right: -20px;
  padding-right: 10px;
}

.audio-player input[name="vol"] {
  display: none;
}

@media screen and (min-width: 480px) {
  .audio-player {
    padding: 10px 10px;
  }

  .audio-player .controls {
    gap: 10px;
  }

  .audio-player input[name="vol"] {
    display: block;
  }
}

/* レンジスライダー */
.audio-player input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  cursor: pointer;
  outline: none;
  border-radius: 15px;
  height: 6px;
  background: #ccc;
}

.audio-player input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  height: 15px;
  width: 15px;
  background-color: #3976d8;
  border-radius: 50%;
  border: none;
  transition: .2s ease-in-out;
}

.audio-player input[type="range"]::-moz-range-thumb {
  height: 15px;
  width: 15px;
  background-color: #3976d8;
  border-radius: 50%;
  border: none;
  transition: .2s ease-in-out;
}

.audio-player input[type="range"]::-webkit-slider-thumb:hover {
  box-shadow: 0 0 0 8px rgba(251, 255, 0, 0.3)
}

.audio-player input[type="range"]:active::-webkit-slider-thumb {
  box-shadow: 0 0 0 5px rgba(251, 255, 0, .4)
}

.audio-player input[type="range"]:focus::-webkit-slider-thumb {
  box-shadow: 0 0 0 5px rgba(251, 255, 0, .4)
}

.audio-player input[type="range"]::-moz-range-thumb:hover {
  box-shadow: 0 0 0 8px rgba(251, 255, 0, .3)
}

.audio-player input[type="range"]:active::-moz-range-thumb {
  box-shadow: 0 0 0 5px rgba(251, 255, 0, .4)
}

.audio-player input[type="range"]:focus::-moz-range-thumb {
  box-shadow: 0 0 0 5px rgba(251, 255, 0, .4)
}

/* ボタンに挿入する疑似要素の共通設定 */
.audio-player .controls button::before {
  content: "";
  display: inline-block;
  height: 24px;
  width: 24px;
  vertical-align: -10px;
  margin-right: 8px;
  background-repeat: no-repeat;
}

/* ボタンによって異なる大きさや位置の個別の設定 */
.audio-player .controls button.loop-btn::before {
  height: 20px;
  width: 20px;
  vertical-align: -4px;
}

.audio-player .controls .time {
  font-size: 12px;
  line-height: 36px;
}

/* ボタンに表示する SVG アイコン*/
/* Play ボタン*/
.audio-player button.play-btn::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' viewBox='0 0 16 16'%3E  %3Cpath d='m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z'/%3E%3C/svg%3E");
}

/* Pause ボタン */
.audio-player button.play-btn.playing::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' viewBox='0 0 16 16'%3E  %3Cpath d='M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z'/%3E%3C/svg%3E");
}

/* Mute ボタン */
.audio-player button.volume-btn.muted::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23f74848' viewBox='0 0 16 16'%3E  %3Cpath d='M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04 4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96V5.04zm7.854.606a.5.5 0 0 1 0 .708L12.207 8l1.647 1.646a.5.5 0 0 1-.708.708L11.5 8.707l-1.646 1.647a.5.5 0 0 1-.708-.708L10.793 8 9.146 6.354a.5.5 0 1 1 .708-.708L11.5 7.293l1.646-1.647a.5.5 0 0 1 .708 0z'/%3E%3C/svg%3E");
}

.audio-player button.volume-btn.muted {
  transform: scale(1.1);
}

/* Volume ボタン */
.audio-player button.volume-btn::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' viewBox='0 0 16 16'%3E  %3Cpath d='M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303l.708.707z'/%3E  %3Cpath d='M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.483 5.483 0 0 1 11.025 8a5.483 5.483 0 0 1-1.61 3.89l.706.706z'/%3E  %3Cpath d='M10.025 8a4.486 4.486 0 0 1-1.318 3.182L8 10.475A3.489 3.489 0 0 0 9.025 8c0-.966-.392-1.841-1.025-2.475l.707-.707A4.486 4.486 0 0 1 10.025 8zM7 4a.5.5 0 0 0-.812-.39L3.825 5.5H1.5A.5.5 0 0 0 1 6v4a.5.5 0 0 0 .5.5h2.325l2.363 1.89A.5.5 0 0 0 7 12V4zM4.312 6.39 6 5.04v5.92L4.312 9.61A.5.5 0 0 0 4 9.5H2v-3h2a.5.5 0 0 0 .312-.11z'/%3E%3C/svg%3E");
}

/* Loop ボタン */
.audio-player button.loop-btn::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' viewBox='0 0 16 16'%3E  %3Cpath d='M11 5.466V4H5a4 4 0 0 0-3.584 5.777.5.5 0 1 1-.896.446A5 5 0 0 1 5 3h6V1.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384l-2.36 1.966a.25.25 0 0 1-.41-.192Zm3.81.086a.5.5 0 0 1 .67.225A5 5 0 0 1 11 13H5v1.466a.25.25 0 0 1-.41.192l-2.36-1.966a.25.25 0 0 1 0-.384l2.36-1.966a.25.25 0 0 1 .41.192V12h6a4 4 0 0 0 3.585-5.777.5.5 0 0 1 .225-.67Z'/%3E%3C/svg%3E");
}

/* Loop ボタン (Active) */
.audio-player button.loop-btn.looped::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fbf704' viewBox='0 0 16 16'%3E  %3Cpath d='M11 5.466V4H5a4 4 0 0 0-3.584 5.777.5.5 0 1 1-.896.446A5 5 0 0 1 5 3h6V1.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384l-2.36 1.966a.25.25 0 0 1-.41-.192Zm3.81.086a.5.5 0 0 1 .67.225A5 5 0 0 1 11 13H5v1.466a.25.25 0 0 1-.41.192l-2.36-1.966a.25.25 0 0 1 0-.384l2.36-1.966a.25.25 0 0 1 .41.192V12h6a4 4 0 0 0 3.585-5.777.5.5 0 0 1 .225-.67Z'/%3E%3C/svg%3E");
}

.audio-player button.loop-btn.looped {
  transform: scale(1.1);
}

関連ページ:audio 要素と JavaScript でオーディオプレーヤーを作成

JavaScript オープンソースのメディアプレーヤー MediaElement.js を使えば同じことをもっと簡単に実装できます。

関連ページ:MediaElement.js の使い方