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

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

通常は以下のように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を採用する必要があります。

基本的な原理

フェードイン/フェードアウトの原理は非常にシンプルで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のクラスも用意しつつ、簡単なサンプルを作成してみます。

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

実行例

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

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

ボタンの上のアイコンは「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)
  }
}

参考ページ