[Unity] 正確な時間間隔で効果音を出す

メトロノームがほしい、そんな夜もあります。

Unityで、たとえばメトロノームのように、正確な時間間隔で効果音を鳴らしたい場合あなたならどうしますか?

```c#:BadMetronome.cs

public class BadMetronome : MonoBehaviour { [SerializeField] AudioSource _ring; float _bpm = 140f; float _wait;

//Updateは60FPSで回っているとします
void Update() {
    _wait -= Time.deltaTime;

    if (_wait <= 0f) {
        _wait = _bpm / 60f;
        _ring.Play();
    }
}

}


- 再生してもこのコードでは心が落ち着きません。処理落ちが一切ないとしても、間隔がガッタガタです。
- なぜ等間隔にならないのかというと、上記例でいうとBPMは140なので 60/140秒(約0.4285714)に一回Play()したいわけですが、60FPSだとUpdate1/60秒 (約0.0166667秒)に1回しか回ってきません。Updateとメトロノームのタイミングはめったに一致しないわけです

## スムースなメトロノームはどこ?
- Updateを140FPSで回してみる?
    - 尻から御飯食べるような解決法。これでもPC向けなら実装可能ですがモバイル向けだと60FPS超えられませんし、BPMを変更するたびにFPS変わる奇怪なアプリが存在していいのか私には疑問です。
- そのうえNotGoodMetronome。何が良くないかというとUpdate()が理想的に毎回同じ時間に回ってくることが期待できないのです。

## Updateでゆらぐことのない時計を探してみる
- Updateで処理落ちしても遅れることなく動いているものがあります。そう、音楽です。
- 音楽はUpdateが回ってこなくても動く時計、[AudioSetting.dspTime](https://docs.unity3d.com/ja/current/ScriptReference/AudioSettings-dspTime.html)を基準にしています
- そして、同期発音のための命令も実は準備されています[AudioSource.PlayScheduled(double)](https://docs.unity3d.com/ja/current/ScriptReference/AudioSource.PlayScheduled.html)です。Updateと独立に指定のdspTimeが来ると音を再生してくれるスグレモノです。
- ほとんど同じにみえる[AudioSource.PlayDelayed(float)](https://docs.unity3d.com/ja/current/ScriptReference/AudioSource.PlayDelayed.html)はfloat精度なので長い時間繰り返し続けるならdouble精度のPlayScheduledがよいようです

## メトロノームはこんなふうに書けた

```c#:GoodMetronome.cs
public class GoodMetronome : MonoBehaviour {

    [SerializeField] AudioSource _ring;

    double _bpm = 140d;
    double _metronomeStartDspTime;
    double _buffer = 2 / 60d;

    void Start() {
        _metronomeStartDspTime = AudioSettings.dspTime;
    }

    void FixedUpdate() {
        var nxtRng = NextRingTime();

        if (nxtRng < AudioSettings.dspTime + _buffer) {
            _ring.PlayScheduled(nxtRng);
        }
    }

    double NextRingTime() {
        var beatInterval = 60d / _bpm;
        var elapsedDspTime = AudioSettings.dspTime - _metronomeStartDspTime;
        var beats = System.Math.Floor(elapsedDspTime / beatInterval);

        return _metronomeStartDspTime + (beats + 1d) * beatInterval;
    }
}