补充 AudioService 异步播放入口
This commit is contained in:
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FlowScope.Resources;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -35,48 +36,49 @@ namespace FlowScope.Audio
|
||||
public IAudioHandle PlayBgm(string key, bool loop = true, float fadeIn = 0f)
|
||||
{
|
||||
EnsureInitialized();
|
||||
var clipHandle = LoadClip(key);
|
||||
var clipHandle = LoadClipIfCompleted(key);
|
||||
return PlayBgm(clipHandle, loop, fadeIn);
|
||||
}
|
||||
|
||||
StopBgm();
|
||||
_bgmSource = gameObject.AddComponent<AudioSource>();
|
||||
_bgmResource = clipHandle;
|
||||
_bgmSource.clip = clipHandle.Asset;
|
||||
_bgmSource.loop = loop;
|
||||
_bgmSource.volume = fadeIn > 0f ? 0f : EffectiveBgmVolume;
|
||||
_bgmSource.Play();
|
||||
|
||||
if (fadeIn > 0f)
|
||||
{
|
||||
_bgmFade = StartCoroutine(FadeVolume(_bgmSource, 0f, EffectiveBgmVolume, fadeIn, null));
|
||||
}
|
||||
|
||||
var source = _bgmSource;
|
||||
return new AudioHandle(
|
||||
() => source != null && source.isPlaying,
|
||||
fadeOut => StopBgmSource(source, fadeOut));
|
||||
public async Task<IAudioHandle> PlayBgmAsync(
|
||||
string key,
|
||||
bool loop = true,
|
||||
float fadeIn = 0f,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureInitialized();
|
||||
var clipHandle = await LoadClipAsync(key, cancellationToken);
|
||||
return PlayBgm(clipHandle, loop, fadeIn);
|
||||
}
|
||||
|
||||
public IAudioHandle PlaySfx(string key)
|
||||
{
|
||||
EnsureInitialized();
|
||||
var clipHandle = LoadClipIfCompleted(key);
|
||||
var slot = GetSfxSlot(key);
|
||||
if (slot == null)
|
||||
{
|
||||
clipHandle.Dispose();
|
||||
return AudioHandle.Stopped;
|
||||
}
|
||||
|
||||
var clipHandle = LoadClip(key);
|
||||
slot.Release();
|
||||
slot.Resource = clipHandle;
|
||||
slot.Source.clip = clipHandle.Asset;
|
||||
slot.Source.loop = false;
|
||||
slot.Source.volume = EffectiveSfxVolume;
|
||||
slot.Sequence = ++_playSequence;
|
||||
slot.Source.Play();
|
||||
return PlaySfx(slot, clipHandle);
|
||||
}
|
||||
|
||||
return new AudioHandle(
|
||||
() => slot.Source != null && slot.Source.isPlaying,
|
||||
fadeOut => StopSfxSlot(slot, fadeOut));
|
||||
public async Task<IAudioHandle> PlaySfxAsync(
|
||||
string key,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureInitialized();
|
||||
var clipHandle = await LoadClipAsync(key, cancellationToken);
|
||||
var slot = GetSfxSlot(key);
|
||||
if (slot == null)
|
||||
{
|
||||
clipHandle.Dispose();
|
||||
return AudioHandle.Stopped;
|
||||
}
|
||||
|
||||
return PlaySfx(slot, clipHandle);
|
||||
}
|
||||
|
||||
public void StopBgm(float fadeOut = 0f)
|
||||
@@ -122,6 +124,42 @@ namespace FlowScope.Audio
|
||||
}
|
||||
}
|
||||
|
||||
private IAudioHandle PlayBgm(IResourceHandle<AudioClip> clipHandle, bool loop, float fadeIn)
|
||||
{
|
||||
StopBgm();
|
||||
_bgmSource = gameObject.AddComponent<AudioSource>();
|
||||
_bgmResource = clipHandle;
|
||||
_bgmSource.clip = clipHandle.Asset;
|
||||
_bgmSource.loop = loop;
|
||||
_bgmSource.volume = fadeIn > 0f ? 0f : EffectiveBgmVolume;
|
||||
_bgmSource.Play();
|
||||
|
||||
if (fadeIn > 0f)
|
||||
{
|
||||
_bgmFade = StartCoroutine(FadeVolume(_bgmSource, 0f, EffectiveBgmVolume, fadeIn, null));
|
||||
}
|
||||
|
||||
var source = _bgmSource;
|
||||
return new AudioHandle(
|
||||
() => source != null && source.isPlaying,
|
||||
fadeOut => StopBgmSource(source, fadeOut));
|
||||
}
|
||||
|
||||
private IAudioHandle PlaySfx(SfxSlot slot, IResourceHandle<AudioClip> clipHandle)
|
||||
{
|
||||
slot.Release();
|
||||
slot.Resource = clipHandle;
|
||||
slot.Source.clip = clipHandle.Asset;
|
||||
slot.Source.loop = false;
|
||||
slot.Source.volume = EffectiveSfxVolume;
|
||||
slot.Sequence = ++_playSequence;
|
||||
slot.Source.Play();
|
||||
|
||||
return new AudioHandle(
|
||||
() => slot.Source != null && slot.Source.isPlaying,
|
||||
fadeOut => StopSfxSlot(slot, fadeOut));
|
||||
}
|
||||
|
||||
private float EffectiveBgmVolume => _mute ? 0f : _bgmVolume;
|
||||
private float EffectiveSfxVolume => _mute ? 0f : _sfxVolume;
|
||||
|
||||
@@ -145,20 +183,18 @@ namespace FlowScope.Audio
|
||||
}
|
||||
}
|
||||
|
||||
private IResourceHandle<AudioClip> LoadClip(string key)
|
||||
private IResourceHandle<AudioClip> LoadClipIfCompleted(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
var handle = _resources.LoadAsync<AudioClip>(key, CancellationToken.None)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
if (handle == null || handle.Asset == null)
|
||||
var task = _resources.LoadAsync<AudioClip>(key, CancellationToken.None);
|
||||
if (!task.IsCompleted)
|
||||
{
|
||||
handle?.Dispose();
|
||||
throw new InvalidOperationException($"Audio clip is missing: {key}");
|
||||
throw new InvalidOperationException(
|
||||
$"Audio clip load is asynchronous: {key}. Use PlayBgmAsync or PlaySfxAsync.");
|
||||
}
|
||||
|
||||
return handle;
|
||||
return ValidateLoadedClip(key, task.Result);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
@@ -166,6 +202,34 @@ namespace FlowScope.Audio
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IResourceHandle<AudioClip>> LoadClipAsync(
|
||||
string key,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var handle = await _resources.LoadAsync<AudioClip>(key, cancellationToken);
|
||||
return ValidateLoadedClip(key, handle);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to load audio clip '{key}'.", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static IResourceHandle<AudioClip> ValidateLoadedClip(
|
||||
string key,
|
||||
IResourceHandle<AudioClip> handle)
|
||||
{
|
||||
if (handle == null || handle.Asset == null)
|
||||
{
|
||||
handle?.Dispose();
|
||||
throw new InvalidOperationException($"Audio clip is missing: {key}");
|
||||
}
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
private SfxSlot GetSfxSlot(string key)
|
||||
{
|
||||
foreach (var slot in _sfxSlots)
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FlowScope.Audio
|
||||
{
|
||||
public interface IAudioService
|
||||
{
|
||||
IAudioHandle PlayBgm(string key, bool loop = true, float fadeIn = 0f);
|
||||
Task<IAudioHandle> PlayBgmAsync(
|
||||
string key,
|
||||
bool loop = true,
|
||||
float fadeIn = 0f,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
IAudioHandle PlaySfx(string key);
|
||||
Task<IAudioHandle> PlaySfxAsync(
|
||||
string key,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
void StopBgm(float fadeOut = 0f);
|
||||
void SetBgmVolume(float volume);
|
||||
void SetSfxVolume(float volume);
|
||||
|
||||
@@ -162,6 +162,51 @@ namespace FlowScope.Tests.PlayMode.Audio
|
||||
Assert.That(exception.Message, Does.Contain("missing-bgm"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PlayBgm_WhenResourceLoadIsPending_ThrowsInsteadOfBlocking()
|
||||
{
|
||||
var service = CreateService();
|
||||
var pending = resources.AddPending("slow-bgm");
|
||||
var sourceCountBeforePlay = Object.FindObjectsOfType<AudioSource>().Length;
|
||||
|
||||
var exception = Assert.Throws<InvalidOperationException>(() => service.PlayBgm("slow-bgm"));
|
||||
|
||||
Assert.That(exception.Message, Does.Contain("slow-bgm"));
|
||||
Assert.That(Object.FindObjectsOfType<AudioSource>().Length, Is.EqualTo(sourceCountBeforePlay));
|
||||
pending.SetResult(bgmA);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PlaySfx_WhenResourceLoadIsPending_ThrowsWithoutCreatingSlot()
|
||||
{
|
||||
var service = CreateService();
|
||||
var pending = resources.AddPending("slow-sfx");
|
||||
var sourceCountBeforePlay = Object.FindObjectsOfType<AudioSource>().Length;
|
||||
|
||||
var exception = Assert.Throws<InvalidOperationException>(() => service.PlaySfx("slow-sfx"));
|
||||
|
||||
Assert.That(exception.Message, Does.Contain("slow-sfx"));
|
||||
Assert.That(Object.FindObjectsOfType<AudioSource>().Length, Is.EqualTo(sourceCountBeforePlay));
|
||||
pending.SetResult(sfx);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public System.Collections.IEnumerator PlayBgmAsync_WhenResourceCompletes_PlaysClip()
|
||||
{
|
||||
var service = CreateService();
|
||||
var pending = resources.AddPending("slow-bgm");
|
||||
IAudioHandle handle = null;
|
||||
|
||||
var task = service.PlayBgmAsync("slow-bgm");
|
||||
Assert.That(task.IsCompleted, Is.False);
|
||||
|
||||
pending.SetResult(bgmA);
|
||||
yield return AwaitTask(task, result => handle = result);
|
||||
|
||||
yield return null;
|
||||
Assert.That(handle.IsPlaying, Is.True);
|
||||
}
|
||||
|
||||
private AudioService CreateService(AudioServiceConfig config = null)
|
||||
{
|
||||
var gameObject = new GameObject("AudioServiceTest");
|
||||
@@ -182,6 +227,26 @@ namespace FlowScope.Tests.PlayMode.Audio
|
||||
Assert.That(condition(), Is.True, message);
|
||||
}
|
||||
|
||||
private static System.Collections.IEnumerator AwaitTask<T>(Task<T> task, Action<T> onCompleted)
|
||||
{
|
||||
while (!task.IsCompleted)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (task.IsFaulted)
|
||||
{
|
||||
throw task.Exception.GetBaseException();
|
||||
}
|
||||
|
||||
if (task.IsCanceled)
|
||||
{
|
||||
throw new OperationCanceledException();
|
||||
}
|
||||
|
||||
onCompleted(task.Result);
|
||||
}
|
||||
|
||||
private static AudioClip CreateClip(string name, float lengthSeconds)
|
||||
{
|
||||
var sampleCount = Mathf.CeilToInt(44100 * lengthSeconds);
|
||||
@@ -199,21 +264,39 @@ namespace FlowScope.Tests.PlayMode.Audio
|
||||
private sealed class FakeResourceService : IResourceService
|
||||
{
|
||||
private readonly Dictionary<string, AudioClip> clips = new();
|
||||
private readonly Dictionary<string, TaskCompletionSource<AudioClip>> pendingClips = new();
|
||||
|
||||
public void Add(string key, AudioClip clip)
|
||||
{
|
||||
clips[key] = clip;
|
||||
}
|
||||
|
||||
public Task<IResourceHandle<T>> LoadAsync<T>(string key, CancellationToken cancellationToken)
|
||||
public TaskCompletionSource<AudioClip> AddPending(string key)
|
||||
{
|
||||
var pending = new TaskCompletionSource<AudioClip>();
|
||||
pendingClips[key] = pending;
|
||||
return pending;
|
||||
}
|
||||
|
||||
public async Task<IResourceHandle<T>> LoadAsync<T>(string key, CancellationToken cancellationToken)
|
||||
where T : class
|
||||
{
|
||||
if (typeof(T) != typeof(AudioClip) || !clips.TryGetValue(key, out var clip))
|
||||
if (typeof(T) != typeof(AudioClip))
|
||||
{
|
||||
throw new InvalidOperationException($"missing resource: {key}");
|
||||
}
|
||||
|
||||
return Task.FromResult<IResourceHandle<T>>(new FakeResourceHandle<T>(key, clip as T));
|
||||
if (!clips.TryGetValue(key, out var clip))
|
||||
{
|
||||
if (!pendingClips.TryGetValue(key, out var pending))
|
||||
{
|
||||
throw new InvalidOperationException($"missing resource: {key}");
|
||||
}
|
||||
|
||||
clip = await pending.Task;
|
||||
}
|
||||
|
||||
return new FakeResourceHandle<T>(key, clip as T);
|
||||
}
|
||||
|
||||
public IResourceGroup CreateGroup()
|
||||
|
||||
Reference in New Issue
Block a user