Unityでイントロ付きMP3形式BGMを切れ目なく再生する方法

# TL; DR
`AudioSource.time`は不正確なので`AudioSource.timeSamples`を使う
`Update`のタイミングは掴めないので`timeSamples`の操作はループ時間単位で行う

# やろうとしていること
図のようにBGMの先頭にイントロがあるループBGMを再生したい
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520610/8e832c83-20f0-239d-a880-bf0f4eab831c.png)

# 問題点
- Unityにはネイティブでそのような機能がない
    - wavファイルならできなくもないが元ファイルの容量がデカいのでプロジェクトの共有が大変
- 以下のようなソースコードで実装するとループ時にブツッと鳴ってしまう

```csharp:BGMLoopController.cs
using UnityEngine;

public class BGMLoopController : MonoBehaviour
{
    public AudioSource AudioSource;
    public float LoopEndTime;
    public float LoopLengthTime;
    private void Update()
    {
        if(AudioSource.time >= LoopEndTime)
        {
            AudioSource.time -= LoopLengthTime;
        }
    }
}
```

# 解決法
AudioSource.timeの低精度floatをやめてAudioSource.timeSamplesを使おう
## 前準備：サンプル数を調べる
そもそもサンプル数とは何ぞや？という人に簡単に説明すると、音声波形を記録する際のデータ1つ1つ(サンプル)の数です。音声波形に使えるデータ量は有限なので、**通常は**1秒あたりに記録するサンプルの量(サンプルレート/ビットレート)を決めています。音声のある時刻`t`において、ファイルの先頭から何サンプル分経過した地点にあるかをここでは「時刻`t`のサンプル数」と表記します。
「通常は」としたのは、「可変ビットレート」と呼ばれる、音声の状態に応じてサンプルレート/ビットレートを変えるファイル形式もあるためです。ただし、この形式のファイルは一般に容量が小さい代わりに扱いづらいため、本稿では対象外とします。

本題に戻ります。今回必要なのは__ループ終端時刻のサンプル数(A)__と__ループ時間に含まれるサンプル数(B)__です。これを調べるには音声波形編集ソフトを使うのが手っ取り早いです。今回はフリーウェアの[Audacity](https://www.audacityteam.org/ "Audacityのホームページに飛びます")を使います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520610/8e832c83-20f0-239d-a880-bf0f4eab831c.png)
使用する音声をインポートしたら、この図のように、ループ部分を選択してください。このとき、Audacityの機能でループさせ、ループ直前と直後の繋がりが不自然でないかを確認します。具体的には、

- 大まかにループ部分を選択する
    - ループ部分の前後はたいてい同じような波形になっています。
- ループ開始地点を拡大しながら、キーとなる場所を見つけて開始地点を移動する
    - キーとなる場所の例：波形の一番高い(低い)点
- ループ終了地点を拡大しながら、開始地点で定めたキーと同じ点を見つけて終了地点を移動する
- 選択範囲でループするよう設定する
    - 先に示した画像上部の時間軸部分を右クリック→選択範囲をループ範囲に設定
    - うまくいくと画像のように時間軸部分の選択範囲の左右に小さな三角が表示される
- 再生パネルのループボタンでループ機能を有効化する
- ループ終了地点の数秒前から再生を開始しループ前後で音が切れないかを確認する
    - 時間軸部分をクリックするとそのタイミングから再生を開始できる

このようにして自然にループできる範囲を探します。
最後に、ループが自然に聞こえる範囲を選択し、選択範囲のサンプル値を確認します。サンプル値は画面下部の
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520610/d53fd411-0254-7c0e-3e2b-72388f33dd30.png)
ここで確認できます。
ただし、初期設定ではサンプル数ではなく時間で表示されているので、枠の右端にある▼から「サンプル」を選んでサンプル数表示に切り替えます。また、初期設定ではこれら2つは調べたい数字にはなっていないため、その真上にあるプルダウンメニューから「選択範囲の長さと終了点」を選びます。
このようにして出てきた2つの数字が、それぞれ__ループ終端時刻のサンプル数(A)__と__ループ時間に含まれるサンプル数(B)__です。B<Aである点に注意してメモしましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520610/c3a53bf5-a270-2cae-0210-c7d36c061bd1.png)

## Unityでの作業
さて、先ほどのソースコードでは時間をもとにループさせていました。しかし、各サンプル数を調べた今、その手法からはおさらばすることになります。
ソースコードを以下のように書き換えます。

```csharp:BGMLoopController.cs
using UnityEngine;

public class BGMLoopController : MonoBehaviour
{
    public AudioSource AudioSource;
    public int LoopEndSamples; // A
    public int LoopLengthSamples; // B
    private void Update()
    {
        if(AudioSource.timeSamples >= LoopEndSamples)
        {
            AudioSource.timeSamples -= LoopLengthSamples;
        }
    }
}
```

Unityのインスペクタでそれぞれ以下のように値を設定します。

- Loop End Samples：__ループ終端時刻のサンプル数(A)__
- Loop Length Samples：__ループ時間に含まれるサンプル数(B)__

AudioSourceの設定は以下の通りとします。

- AudioClip：音源ファイル
- Play On Awake：オン

このようにして再生してみましょう。ループ時にブツブツと音がしなければ成功です！

# 余談
今回はループ開始地点ではなくループ時間を用いて現在の位置から引くことで実装しました。なぜループ開始地点を用いて`AudioSource.timeSamples = LoopStartSamples;`のようにしなかったのかというと、UnityのUpdateが必ずしも`AudioSource.timeSamples==LoopEndSamples`となるタイミングで発動するとは限らず、Updateが呼ばれるまでに再生された分がループ開始地点に戻った際に二重で再生されてしまい、プツッという音が鳴ってしまう可能性が高いためです。
ループ長で引く処理にすることで、Updateが呼ばれるまでに再生された分がループ開始地点で再度再生されることがなく、安定してループを行うことができます。