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




ディスカッション
コメント一覧
まだ、コメントがありません