Unityで「AudioSource.Playの鳴り終わり」を安全に検知する最小コルーチン

「効果音が鳴き終わったらUIを有効化したい」「次のSEをつなげたい」――AudioSource には OnComplete のようなイベントはありません。そこで、1フレームの反映待ち→isPlaying が false になるまで待つというシンプルなコルーチンで“鳴り終わり”を拾う方法をまとめます。


完成コード(最小)

using System;
using System.Collections;
using UnityEngine;

public static class AudioWaiter
{
    public static IEnumerator WaitUntilFinished(AudioSource src, Action onEnd)
    {
        // 反映待ち(Play直後は内部状態が1フレーム遅れることがある)
        yield return null;

        while (src && src.isPlaying)
            yield return null;

        onEnd?.Invoke();
    }
}

使い方

// どこかのMonoBehaviour内
audioSource.Play();
StartCoroutine(AudioWaiter.WaitUntilFinished(audioSource, () =>
{
    Debug.Log("終わった");
    // 次の処理…
}));

ポイント

  • yield return null を最初に1回入れているのは、Play() 呼び出し直後に isPlaying が反映されないフレームがあるため。
  • while (src && src.isPlaying) としておくと、AudioSource や GameObject が破棄された場合にも自然に抜けます(src が null になってループ終了)。

これで拾える「終わり」と拾えないケース

  • 自然終了(loop = false のクリップ再生が最後まで到達)
  • 手動停止(src.Stop() / src.enabled = false / GameObject破棄)でも onEnd は呼ばれる(=“とにかく終わった”扱い)

自然終了のときだけコールバックしたい」など要件がある場合は、後述の“厳密版”を使います。


よくある代替案と注意点

1) WaitForSeconds(clip.length) はダメ?

pitch を変更すると実時間の再生長が伸縮しますし、途中停止も拾えません。clip.length ベタ待ちはずれたり無駄に待ったりしがちです。

2) OnAudioFilterRead で検知?

DSPスレッド側のフックで、終端検知用途にはオーバーキル。Audio処理を自前でやるのでなければ不要です。

3) DOTween の OnComplete ?

DOTween はトゥイーンの完了を教えてくれますが、AudioSourceの再生完了ではありません。ボリュームフェードの完了=音の終了とは限らない点に注意。


もう一歩:中断/自然終了を判別したいとき

最後まで鳴き切ったときだけ true を返す」改良版です。timeSamples を毎フレーム記録し、終了時に末端近くまで到達していたかで判定します(サンプル誤差許容あり)。

public static IEnumerator WaitUntilFinishedStrict(AudioSource src, Action<bool> onEnd)
{
    if (!src) yield break;

    yield return null; // 反映待ち

    int lastSamples = 0;
    int tolerance = 1024; // 許容サンプル(約23ms@44.1kHz)

    while (src && src.isPlaying)
    {
        lastSamples = src.timeSamples;
        yield return null;
    }

    bool completed = false;

    if (src && src.clip)
    {
        int endThreshold = Mathf.Max(0, src.clip.samples - tolerance);
        completed = lastSamples >= endThreshold;
    }

    onEnd?.Invoke(completed); // completed==true なら自然終了
}

使い方:

audioSource.Play();
StartCoroutine(AudioWaiter.WaitUntilFinishedStrict(audioSource, completed =>
{
    if (completed) Debug.Log("最後まで鳴き切った");
    else           Debug.Log("途中で止まった/破棄された");
}));

注意

  • ループ(loop = true)の場合は永遠に終わりません。必要なら loop を一時的に false にする、フェードアウトして Stop() する等の終了条件を明示してから待機しましょう。

PlayOneShotのときは?

PlayOneShot でも多くのケースで isPlaying は反映されますが、重ね鳴らし別ソースとの併用があると挙動が読みにくくなることがあります。確定的に待ちたいなら、専用の一時AudioSourceを AddComponent<AudioSource>() で生やして clip を直接 Play() し、終わり次第 Destroy するのが簡単です。

public static IEnumerator PlayOneShotAndWait(GameObject host, AudioClip clip, float volume, Action onEnd)
{
    var s = host.AddComponent<AudioSource>();
    s.playOnAwake = false;
    s.clip = clip;
    s.volume = volume;
    s.Play();

    yield return WaitUntilFinished(s, () =>
    {
        onEnd?.Invoke();
        UnityEngine.Object.Destroy(s);
    });
}

正確さ最優先:DSP時刻で終了時刻を見積もる

Play() の直後に DSP時刻ベースで終了時刻を算出し、そこまで待つ案です。ピッチ変更も反映できます。

public static IEnumerator WaitByDSPTime(AudioSource src, Action onEnd)
{
    if (!src || !src.clip) yield break;

    // pitchの絶対値で再生時間を補正
    double end = AudioSettings.dspTime + (src.clip.length / Mathf.Max(0.0001f, Mathf.Abs(src.pitch)));
    yield return null; // 反映待ち

    while (AudioSettings.dspTime < end && src && src.isPlaying)
        yield return null;

    if (src && src.isPlaying == false)
        onEnd?.Invoke();
    else if (AudioSettings.dspTime >= end)
        onEnd?.Invoke(); // 自然終了(見込み)
}

早期停止を「自然終了」と区別したい場合は、前節の Strict版 と組み合わせるのが確実です。


実運用 Tips

  • フェードアウトと組み合わせる:ボリュームを 0.2〜0.5秒 で下げてから Stop()。UI遷移の違和感が激減します。(フェード完了→Stop()→WaitUntilFinished の順で)
  • タイムスケール0(ポーズ)でも動かしたい:本稿の待機は yield return null のフレーム依存なのでポーズ中でも進む(Updateは止まらない限りフレームは進む)。WaitForSecondsRealtime が必要なのは時間で待つときだけです。
  • シーン破棄でのハング防止:while (src && src.isPlaying) にしてあるので、破棄時も抜ける設計です。onEnd を呼びたくない(厳密に自然終了のみ)場合は Strict版 を使うか、破棄検知時は return しましょう。

まとめ

  • 最小実装は yield return null → while (src && src.isPlaying) yield return null;。
  • 自然終了のみ拾いたければ Strict版(timeSamples 最終値)を。
  • 正確な到達時刻の見積もりが必要なら DSP時刻ベースも有効。
  • PlayOneShot の多重再生など複雑化する場合は専用AudioSourceを都度生成して待機するのが堅実。

ni

訪問数 5 回, 今日の訪問数 1回