イントロ付きの BGM をループ再生する

ページ更新日 :
ページ作成日 :

検証環境

Windows
  • Windows 11
Unity エディター
  • 2021.3.3f1
入力システムパッケージ
  • 1.3.0

この Tips の前提設定

この Tips の説明の前提として以下の設定を事前に行っています。

サンプルに付属している素材について

以下のサイト様より BGM を借用しています。

イントロ付きループの音声ファイルについて

今回 Unity の標準機能のみでイントロ付きループ再生を行いますが、標準機能としてこれをサポートはしておりません。 そもそも音声ファイルとしてイントロ付きループの仕様は決まっていないのでゲームフレームワークによって作り方が異なっています。

今回は「イントロ」部分と「ループ」部分の音声ファイルを2つ用意してイントロ部分を1回再生し終わったらループ部分を繰り返し再生する方法を行います。 ですので、音声ファイルとしては上記の2ファイルを用意してください。

配布サイトによってはこれを考慮してあらかじめ音声ファイルを分割して配布しているサイトもあります。 ない場合は音声編集ツールなどで自分で作る必要があります。

ファイルを2つに分割する場合は以下の形式の音声ファイルをお勧めします。

  • OggVorbis (.ogg)
  • WAV (.wav)

詳しくは Unity 公式のマニュアルを参照してください。

イントロ付きループ BGM の再生について

サンプルとして BGM を再生・一時停止・停止ができるように UI を作成します。 通常の BGM 再生のサンプルと同じレイアウトです。

イントロとループの2つに分割した音声ファイル2つをプロジェクトに追加します。

今回イントロ付きループ BGM を再生するために独自にプログラムを作成します。 スクリプトを作成し名前は IntroLoopAudio としておきます。

スクリプトは以下のようにします。 以下のサイトのコードを参考にしていますが、WebGL でうまく動かなかったのともう少し細かく制御したかったのでいろいろコードを追加しています。

【参考】 【Unity】イントロ+ループ再生を実装する - 7080 + 1

using UnityEngine;

/// <summary>
/// イントロ付きループ BGM を制御するクラスです。
/// </summary>
/// <remarks>
/// WebGL では PlayScheduled で再生するとループしないのでその対応を入れている。
/// WebGL では2つの AudioSource を交互に再生。
/// </remarks>
public class IntroLoopAudio : MonoBehaviour
{
  /// <summary>BGM のイントロ部分の音声データ。</summary>
  [SerializeField] private AudioClip AudioClipIntro;

  /// <summary>BGM のループ部分の音声データ。</summary>
  [SerializeField] private AudioClip AudioClipLoop;

  /// <summary>BGM のイントロ部分の AudioSource。</summary>
  private AudioSource _introAudioSource;

  /// <summary>BGM のループ部分の AudioSource。</summary>
  private AudioSource[] _loopAudioSources = new AudioSource[2];

  /// <summary>一時停止中かどうか。</summary>
  private bool _isPause;

  /// <summary>現在の再生するループ部分のインデックス。</summary>
  private int _nowPlayIndex = 0;

  /// <summary>ループ部分に使用する AudioSource の数。</summary>
  private int _loopSourceCount = 0;

  /// <summary>再生中であるかどうか。一時停止、非アクティブの場合は false を返す。</summary>
  private bool IsPlaying
    => (_introAudioSource.isPlaying || _introAudioSource.time > 0)
      || (_loopAudioSources[0].isPlaying || _loopAudioSources[0].time > 0)
      || (_loopAudioSources[1] != null && (_loopAudioSources[1].isPlaying || _loopAudioSources[1].time > 0));

  /// <summary>現在アクティブで再生しているループ側の AudioSource。</summary>
  private AudioSource LoopAudioSourceActive
    => _loopAudioSources[1] != null && _loopAudioSources[1].time > 0 ? _loopAudioSources[1] : _loopAudioSources[0];

  /// <summary>現在の再生時間 (s)。</summary>
  public float time
    => _introAudioSource == null ? 0
      : _introAudioSource.time > 0 ? _introAudioSource.time
      : LoopAudioSourceActive.time > 0 ? AudioClipIntro.length + LoopAudioSourceActive.time
      : 0;


  void Start()
  {
    _loopSourceCount = 2;   // WebGL でなければ 1 でもよい

    // AudioSource を自身に追加
    _introAudioSource = gameObject.AddComponent<AudioSource>();
    _loopAudioSources[0] = gameObject.AddComponent<AudioSource>();
    if (_loopSourceCount >= 2)
    {
      _loopAudioSources[1] = gameObject.AddComponent<AudioSource>();
    }

    _introAudioSource.clip = AudioClipIntro;
    _introAudioSource.loop = false;
    _introAudioSource.playOnAwake = false;

    _loopAudioSources[0].clip = AudioClipLoop;
    _loopAudioSources[0].loop = _loopSourceCount == 1;
    _loopAudioSources[0].playOnAwake = false;
    if (_loopAudioSources[1] != null)
    {
      _loopAudioSources[1].clip = AudioClipLoop;
      _loopAudioSources[1].loop = false;
      _loopAudioSources[1].playOnAwake = false;
    }
  }

  void Update()
  {
    // WebGL のためのループ切り替え処理
    if (_loopSourceCount >= 2)
    {
      // 終了する1秒前から次の再生のスケジュールを登録する
      if (_nowPlayIndex == 0 && _loopAudioSources[0].time >= AudioClipLoop.length - 1)
      {
        _loopAudioSources[1].PlayScheduled(AudioSettings.dspTime + (AudioClipLoop.length - _loopAudioSources[0].time));
        _nowPlayIndex = 1;
      }
      else if (_nowPlayIndex == 1 && _loopAudioSources[1].time >= AudioClipLoop.length - 1)
      {
        _loopAudioSources[0].PlayScheduled(AudioSettings.dspTime + (AudioClipLoop.length - _loopAudioSources[1].time));
        _nowPlayIndex = 0;
      }
    }
  }

  public void Play()
  {
    // クリップが設定されていない場合は何もしない
    if (_introAudioSource == null || _loopAudioSources == null) return;

    // Pause 中は isPlaying は false
    // 標準機能だけでは一時停止中か判別不可能
    if (_isPause)
    {
      _introAudioSource.UnPause();
      if (_introAudioSource.isPlaying)
      {
        // イントロ中ならループ開始時間を残り時間で再設定
        _loopAudioSources[0].Stop();
        _loopAudioSources[0].PlayScheduled(AudioSettings.dspTime + AudioClipIntro.length - _introAudioSource.time);
      }
      else
      {
        if (_loopSourceCount >= 2)
        {
          // WebGL の場合は切り替え処理を実行
          if (_loopAudioSources[0].time > 0)
          {
            _loopAudioSources[0].UnPause();
            if (_loopAudioSources[0].time >= AudioClipLoop.length - 1)
            {
              _loopAudioSources[1].Stop();
              _loopAudioSources[1].PlayScheduled(AudioSettings.dspTime + (AudioClipLoop.length - _loopAudioSources[0].time));
              _nowPlayIndex = 1;
            }
          }
          else
          {
            _loopAudioSources[1].UnPause();
            if (_loopAudioSources[1].time >= AudioClipLoop.length - 1)
            {
              _loopAudioSources[0].Stop();
              _loopAudioSources[0].PlayScheduled(AudioSettings.dspTime + (AudioClipLoop.length - _loopAudioSources[0].time));
              _nowPlayIndex = 0;
            }
          }
        }
        else
        {
          // WebGL 以外は UnPause するだけ
          _loopAudioSources[0].UnPause();
        }
      }
    }
    else if (IsPlaying == false)
    {
      // 最初から再生
      Stop();
      _introAudioSource.Play();

      // イントロの時間が経過した後に再生できるようにする
      // 設定する時間はゲーム刑か時間での設定となる
      _loopAudioSources[0].PlayScheduled(AudioSettings.dspTime + AudioClipIntro.length);
    }

    _isPause = false;
  }

  /// <summary>BGM を一時停止します。</summary>
  public void Pause()
  {
    if (_introAudioSource == null || _loopAudioSources == null) return;

    _introAudioSource.Pause();
    _loopAudioSources[0].Pause();
    if (_loopAudioSources[1] != null) _loopAudioSources[1].Pause();

    _isPause = true;
  }

  /// <summary>BGM を停止します。</summary>
  public void Stop()
  {
    if (_introAudioSource == null || _loopAudioSources == null) return;

    _introAudioSource.Stop();
    _loopAudioSources[0].Stop();
    if (_loopAudioSources[1] != null) _loopAudioSources[1].Stop();

    _isPause = false;
  }
}

コードが長いので詳細は省きますが、イントロ用とループ用の AudioClip をあらかじめセットしておき、 再生を開始したら最初にイントロ用を再生します。ループ用はイントロが終わったタイミングで再生できるようにスケジュールしておきます。 ループ用の再生が始まったらあとは繰り返し再生するようにしておきます。

本来ループ用は loop プロパティを true にするだけで良かったのですが、WebGL では正常に動作しなかったので ループ用の AudioSource を2つ用意しておき交互に切り替えて再生するようにしています。 WebGL を考慮しなくてもよいのであれば、コードは半分ぐらいに減らせると思います。

作成したスクリプトはオブジェクトにアタッチします。 通常は空オブジェクトなどを作成してアタッチしたほうが良いかもしれませんが面倒なので EventSystem にアタッチしておきます。

イントロとループの項目があるのでそれぞれ音声ファイルをドロップしてセットします。

ここまでくれば後は特別な処理は必要ありません。 ボタンをクリックしたときに処理をさせたいのでボタン用のスクリプト(ButtonEvent)を作成します。

スクリプトは以下のようにします。

using UnityEngine;

public class ButtonEvent : MonoBehaviour
{
  [SerializeField] private IntroLoopAudio IntroLoopAudio;

  public void OnClickPlay()
  {
    IntroLoopAudio.Play();
  }
  public void OnClickPause()
  {
    IntroLoopAudio.Pause();
  }
  public void OnClickStop()
  {
    IntroLoopAudio.Stop();
  }
}

IntroLoopAudio をインスペクターからセットできるようにしておき、ボタンごとの処理を追加します。

スクリプトは EventSystem にアタッチしておきます。 Intro Loop Audio をセットする必要があるので、Intro Loop Audio を持っている EventSystem をセットします。

後は3つのボタンのクリックイベントにメソッドを割り当ててください。

全ての設定が終わったらゲームを実行して再生してみてください。 一番最後まで再生が終わると曲の途中からループして再生することを確認できると思います。 もちろんこれは音声データがきれいに分割され、ループの繋ぎもきれいに作成されている前提となります。

現在の再生時間の表示

ここからはおまけですが、IntroLoopAudio に現在の再生時間を取得できるプロパティを追加しているので表示させてみます。

まずは時間を表示するテキストを配置します。

スクリプトを作成します。

using UnityEngine;
using UnityEngine;
using UnityEngine.UI;

public class TextEvent : MonoBehaviour
{
  [SerializeField] private IntroLoopAudio IntroLoopAudio;

  private Text _text;

  // Start is called before the first frame update
  void Start()
  {
    _text = GetComponent<Text>();
  }

  // Update is called once per frame
  void Update()
  {
    _text.text = $"AudioPlayTime : {IntroLoopAudio.time}";
  }
}

テキストにスクリプトをアタッチして IntroLoopAudio をもつ EventSystem をセットします。

実行して現在の再生時間が表示されるか確認してください。また、ループした際はループポイントに時間が戻るか確認してみてください。