[HTML5] 音声をフェードイン・フェードアウトする – audioタグ編

  • このエントリーをはてなブックマークに追加
  • LINEで送る

今回は音声ファイルを音量が徐々に上がっていく「フェードイン」する形で再生を開始、逆に音量が徐々に下がっていく「フェードアウト」し再生を終了することに挑戦してみます。

通常は以下のようにplay()で開始するわけですが、これだと音量MAXで始まってしまうため唐突感が生まれてしまう場合があります。停止する際にpause()を実行する場合も同様です。それぞれ「カットイン」「カットアウト」と呼ばれますが、意図したものであればもちろんこれで問題はありません。

<audio id="bgm" src="xxx.mp3" preload></audio>

<script>
  document.querySelector("#bgm").play():   // 唐突に再生される(カットイン)
  document.querySelector("#bgm").pause():  // 唐突に止まる(カットアウト)
</script>

SE(効果音)やジングルなんかはカットインがほとんどですが、しんみりした雰囲気をBGMで出したい場合はフェードイン/フェードアウトを多用することになりますね。

※ご注意※
この方法はiPhoneやiPadなどiOSのWebブラウザでは利用できません。もし対応する場合にはWebAudioAPIを採用する必要があります。
- Sponsored Link -

基本的な原理

フェードイン/フェードアウトの原理は非常にシンプルでsetInterval()で一定時間毎に音量を上げ下げし、一定のボリュームになったらタイマーを終了するだけです。

注意点としてはvolumeに0〜1以外の範囲の値を入れると実行時エラーとなるため代入前にチェックする必要があります。1.0001など小数点以下もエラーとなります。

//--------------------------------
// フェードイン
//--------------------------------
const bgm = document.querySelector("#bgm");
bgm.volume = 0;  //ボリュームを最低値にする
bgm.play();

let timerid = setInterval( ()=>{
  // ボリュームが1になったら終了
  if( (bgm.volume + 0.1) >= 1 ){
    bgm.volume = 1;
    clearInterval(timerid);  //タイマー解除
  }
  // 0.1ずつボリュームを足していく
  else{
    bgm.volume += 0.1;
  }
}
, 200); //0.2秒ごとに繰り返す
//--------------------------------
// フェードアウト
//--------------------------------
const bgm = document.querySelector("#bgm");
bgm.volume = 1;
bgm.play();

let timerid = setInterval( ()=>{
  // ボリュームが0になったら終了
  if( (bgm.volume - 0.1) <= 0 ){
    bgm.volume = 0;
    bgm.pause();
    clearInterval(timerid);  //タイマー解除
  }
  // 0.1ずつボリュームを減らしていく
  else{
    bgm.volume -= 0.1;
  }
}
, 200); //0.2秒ごとに繰り返す

ではもう少し汎用的なJavaScriptのクラスも用意しつつ、簡単なサンプルを作成してみます。

フェードイン・フェードアウトを実装する

実行例

以下のページから実際にサンプルをお試しいただけます。

  • ボタンを押すと再生をフェードインしながら開始します。
  • 再生中にボタンを押すとフェードアウトしながら停止します。
  • フェードイン/フェードアウト中はボタンが「ぐるぐる」回るアイコンになり、この間は操作ができなくなります。

ボタンの上のアイコンは「Font Awesome」、音声ファイルは「魔王魂」さんからお借りしています。

ソース

HTML

実際のフェードイン/フェードアウトの処理は外部のJSで行っているため、HTMLは最低限の物になります。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>HTML5 Audio Sample3</title>
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
  <style>
    #btn-play{ width:100px; height:40px; padding:10px; font-size:18px; }
  </style>
</head>
<body>

<h1>HTML5 Audio フェードイン/フェードアウト</h1>

<!-- 音声ファイル -->
<audio id="bgm1" preload>
  <source src="op.ogg" type="audio/ogg">
  <source src="op.mp3" type="audio/mp3">
</audio>

<!-- 再生ボタン -->
<button id="btn-play" type="button"><i class="fas fa-play"></i></button>

<!-- 外部のJSファイルで処理する -->
<script src="audiofade.js"></script>
<script src="app.js"></script>

</body>
</html>

JavaScript – app.js

メインの処理を書いています。
以前の記事で紹介したコードを流用しています。今回は後述するAudioFadeクラスを作成し実際の泥臭い処理はそちらで行っています。

ボタンがクリックされると現在の再生状態に合わせてフェードイン/フェードアウトを行っています。ポイントとしては同時に複数の処理が走ると無限ループに陥る可能性があるため簡単なロック機構を用意しています。

//------------------------------------------------
// 定数
//------------------------------------------------
const BUTTON_PLAY  = '<i class="fas fa-play"></i>';   // 再生ボタン
const BUTTON_PAUSE = '<i class="fas fa-pause"></i>';  // 停止ボタン
const BUTTON_RUNNING = '<i class="fas fa-circle-notch fa-spin"></i>';  // 処理中ボタン

/**
 * [イベント] ページの読み込み完了
 */
window.onload = ()=>{
  const bgm1 = document.querySelector("#bgm1");       // <audio>
  const btn  = document.querySelector("#btn-play");   // <button>

  // フェードイン・アウト操作クラス
  const bgm1Ctrl = new AudioFade(bgm1);

  /**
   * [イベント] 再生ボタンをクリック
   */
  btn.addEventListener("click", ()=>{
    // 処理中であれば無視する
    if( bgm1Ctrl.isRun ){
      return(false);
    }

    // フェード◯◯中はぐるぐるボタンにする
    btn.innerHTML = BUTTON_RUNNING;
    btn.setAttribute("disabled", true);

    //---------------------------
    // フェードイン
    //---------------------------
    if( bgm1Ctrl.paused ){    // pausedがtrue=>停止, false=>再生中
      bgm1Ctrl.fadeIn(()=>{
        // フェードイン完了時の処理
        btn.innerHTML = BUTTON_PAUSE;
        btn.removeAttribute("disabled");
      });
    }
    //---------------------------
    // フェードアウト
    //---------------------------
    else{
      bgm1Ctrl.fadeOut(()=>{
        // フェードアウト完了時の処理
        btn.innerHTML = BUTTON_PLAY;
        btn.removeAttribute("disabled");
      });
    }
  });

  /**
   * [イベント] 再生終了時に実行
   */
  bgm1.addEventListener("ended", ()=>{
    bgm1.currentTime = 0;  // 再生位置を先頭に移動
    bgm1.volume = 1;       // ボリュームを既定値
    btn.innerHTML = BUTTON_PLAY;  // 「再生ボタン」に変更
  });
}

JavaScript – audiofade.js

フェードイン/フェードアウトを行うJavaScriptのクラスです。基本的な利用方法はコンストラクタにaudioタグのオブジェクトを渡し、任意のタイミングでfadeIn()またはfadeOut()メソッドを実行するだけです。

/**
 * [HTML5 Audio] フェードアウト・フェードイン 簡易操作クラス
 *
 * @author M.katsube <katsubemakito@gmail.com>
 */
class AudioFade{
  /**
   * コンストラクタ
   *
   * @param  {Object} audio      - <audio>タグのオブジェクト
   * @param  {number} [maxvol=1] - 最大ボリューム
   * @param  {number} [minvol=0] - 最低ボリューム
   * @param  {number} [wait=200] - setInterval()の間隔
   * @return {void}
   */
  constructor(audio, maxvol=1, minvol=0, wait=200){
    // iOSであれば終了する
    if( navigator.userAgent.match(/iPhone|iPad|iPod/) ){
      throw {cd:"E001", message:"Does not work iOS(iPhone, iPad, iPod)", ua:navigator.userAgent};
    }

    // 引数をプロパティに詰める
    this._audio  = audio;
    this._maxvol = maxvol;
    this._minvol = minvol;
    this._wait   = wait;

    // 何らかの処理がすでに実行中か
    this._isrun = false;
  }

  /**
   * Getter: 実行中フラグ
   *
   * @readonly
   * @memberof AudioFade
   */
  get isRun(){
    return( this._isrun );
  }

  /**
   * Getter: 再生状態
   *
   * @readonly
   * @memberof AudioFade
   */
  get paused(){
    return( this._audio.paused );
  }

  /**
   * フェードイン
   *
   * @param  {function} [callback=null] - フェードイン完了時に実行する関数
   * @param  {number}   [sec=3] - 最大音量になるまでの秒数
   * @return {void}
   * @public
   */
  fadeIn(callback=null, sec=3){
    const audio = this._audio;

    //-----------------------------------
    // 再生中 or すでに実行中ならやめる
    //-----------------------------------
    if( (! audio.paused) || (this._isrun === true) ) return(false);
    this._isrun = true;   // 実行中フラグを立てる

    //-----------------------------------
    // 1回あたりに増やす音量を計算
    //-----------------------------------
    const step = parseFloat( (this._maxvol / ((sec * 1000) / this._wait)).toFixed(3) );

    //-----------------------------------
    // 最大音量までタイマーで繰り返す
    //-----------------------------------
    let timerid = setInterval(()=>{
      // 停止状態であれば再生する
      if( audio.paused ){
        audio.volume = 0;
        audio.play();
      }
      else{
        // 最大音量に到達したらタイマー停止
        if( (audio.volume + step) >= this._maxvol ){
          audio.volume = this._maxvol;    // 最後の音量は決め打ち
          this._isrun = false;            // 実行フラグを下ろす
          clearInterval(timerid);         // タイマー解除

          // 完了時のCallback関数を実行
          if( callback !== null ){
            callback();
          }
        }
        else{
          audio.volume += step;
        }
      }
    },
    this._wait);
  }

  /**
   * フェードアウト
   *
   * @param  {function} [callback=null] - フェードアウト完了時に実行する関数
   * @param  {number}   [sec=3] - 最低音量になるまでの秒数
   * @return {void}
   * @public
   */
  fadeOut(callback=null, sec=3){
    const audio = this._audio;

    //-----------------------------------
    // 停止中 or すでに実行中ならやめる
    //-----------------------------------
    if( audio.paused || (this._isrun === true) ) return(false);
    this._isrun = true;   // 実行中フラグを立てる

    //-----------------------------------
    // 1回あたりに減らす音量を計算
    //-----------------------------------
    const step = parseFloat( ( (audio.volume - this._minvol) / ((sec * 1000) / this._wait)).toFixed(3) );

    //-----------------------------------
    // 最低音量までタイマーで繰り返す
    //-----------------------------------
    let timerid = setInterval(()=>{
      // 最低音量に到達したらタイマー停止
      if( (audio.volume - step) <= this._minvol ){
        audio.volume = this._minvol;    // 最後の音量は決め打ち
        audio.pause();                  // 再生を停止
        this._isrun = false;            // 実行フラグを下ろす
        clearInterval(timerid);         // タイマー解除

        // 完了時のCallback関数を実行
        if( callback !== null ){
          callback();
        }
      }
      else{
        audio.volume -= step;
      }
    },
    this._wait)
  }
}

参考ページ

このブログを応援する

お寄せいただいたお気持ちは全額サーバ代や次の記事を執筆するための原資として活用させていただいております。この記事が参考になった場合などぜひご検討ください。

PayPal(ペイパル)
PayPalで500円支払う
※金額は任意で変更できます。
※100円でも泣いて喜びますw
- Sponsored Link -

ご質問やリクエストなどお気軽に。メールアドレスの入力は任意です。書き込みが反映されるまで時間がかかります。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください