补充 AudioService 异步播放入口
This commit is contained in:
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using FlowScope.Resources;
|
using FlowScope.Resources;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
@@ -35,48 +36,49 @@ namespace FlowScope.Audio
|
|||||||
public IAudioHandle PlayBgm(string key, bool loop = true, float fadeIn = 0f)
|
public IAudioHandle PlayBgm(string key, bool loop = true, float fadeIn = 0f)
|
||||||
{
|
{
|
||||||
EnsureInitialized();
|
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;
|
public async Task<IAudioHandle> PlayBgmAsync(
|
||||||
return new AudioHandle(
|
string key,
|
||||||
() => source != null && source.isPlaying,
|
bool loop = true,
|
||||||
fadeOut => StopBgmSource(source, fadeOut));
|
float fadeIn = 0f,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
EnsureInitialized();
|
||||||
|
var clipHandle = await LoadClipAsync(key, cancellationToken);
|
||||||
|
return PlayBgm(clipHandle, loop, fadeIn);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IAudioHandle PlaySfx(string key)
|
public IAudioHandle PlaySfx(string key)
|
||||||
{
|
{
|
||||||
EnsureInitialized();
|
EnsureInitialized();
|
||||||
|
var clipHandle = LoadClipIfCompleted(key);
|
||||||
var slot = GetSfxSlot(key);
|
var slot = GetSfxSlot(key);
|
||||||
if (slot == null)
|
if (slot == null)
|
||||||
{
|
{
|
||||||
|
clipHandle.Dispose();
|
||||||
return AudioHandle.Stopped;
|
return AudioHandle.Stopped;
|
||||||
}
|
}
|
||||||
|
|
||||||
var clipHandle = LoadClip(key);
|
return PlaySfx(slot, 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(
|
public async Task<IAudioHandle> PlaySfxAsync(
|
||||||
() => slot.Source != null && slot.Source.isPlaying,
|
string key,
|
||||||
fadeOut => StopSfxSlot(slot, fadeOut));
|
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)
|
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 EffectiveBgmVolume => _mute ? 0f : _bgmVolume;
|
||||||
private float EffectiveSfxVolume => _mute ? 0f : _sfxVolume;
|
private float EffectiveSfxVolume => _mute ? 0f : _sfxVolume;
|
||||||
|
|
||||||
@@ -145,13 +183,44 @@ namespace FlowScope.Audio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IResourceHandle<AudioClip> LoadClip(string key)
|
private IResourceHandle<AudioClip> LoadClipIfCompleted(string key)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var handle = _resources.LoadAsync<AudioClip>(key, CancellationToken.None)
|
var task = _resources.LoadAsync<AudioClip>(key, CancellationToken.None);
|
||||||
.GetAwaiter()
|
if (!task.IsCompleted)
|
||||||
.GetResult();
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Audio clip load is asynchronous: {key}. Use PlayBgmAsync or PlaySfxAsync.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValidateLoadedClip(key, task.Result);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Failed to load audio clip '{key}'.", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
if (handle == null || handle.Asset == null)
|
||||||
{
|
{
|
||||||
handle?.Dispose();
|
handle?.Dispose();
|
||||||
@@ -160,11 +229,6 @@ namespace FlowScope.Audio
|
|||||||
|
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Failed to load audio clip '{key}'.", exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private SfxSlot GetSfxSlot(string key)
|
private SfxSlot GetSfxSlot(string key)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace FlowScope.Audio
|
namespace FlowScope.Audio
|
||||||
{
|
{
|
||||||
public interface IAudioService
|
public interface IAudioService
|
||||||
{
|
{
|
||||||
IAudioHandle PlayBgm(string key, bool loop = true, float fadeIn = 0f);
|
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);
|
IAudioHandle PlaySfx(string key);
|
||||||
|
Task<IAudioHandle> PlaySfxAsync(
|
||||||
|
string key,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
void StopBgm(float fadeOut = 0f);
|
void StopBgm(float fadeOut = 0f);
|
||||||
void SetBgmVolume(float volume);
|
void SetBgmVolume(float volume);
|
||||||
void SetSfxVolume(float volume);
|
void SetSfxVolume(float volume);
|
||||||
|
|||||||
@@ -162,6 +162,51 @@ namespace FlowScope.Tests.PlayMode.Audio
|
|||||||
Assert.That(exception.Message, Does.Contain("missing-bgm"));
|
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)
|
private AudioService CreateService(AudioServiceConfig config = null)
|
||||||
{
|
{
|
||||||
var gameObject = new GameObject("AudioServiceTest");
|
var gameObject = new GameObject("AudioServiceTest");
|
||||||
@@ -182,6 +227,26 @@ namespace FlowScope.Tests.PlayMode.Audio
|
|||||||
Assert.That(condition(), Is.True, message);
|
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)
|
private static AudioClip CreateClip(string name, float lengthSeconds)
|
||||||
{
|
{
|
||||||
var sampleCount = Mathf.CeilToInt(44100 * lengthSeconds);
|
var sampleCount = Mathf.CeilToInt(44100 * lengthSeconds);
|
||||||
@@ -199,21 +264,39 @@ namespace FlowScope.Tests.PlayMode.Audio
|
|||||||
private sealed class FakeResourceService : IResourceService
|
private sealed class FakeResourceService : IResourceService
|
||||||
{
|
{
|
||||||
private readonly Dictionary<string, AudioClip> clips = new();
|
private readonly Dictionary<string, AudioClip> clips = new();
|
||||||
|
private readonly Dictionary<string, TaskCompletionSource<AudioClip>> pendingClips = new();
|
||||||
|
|
||||||
public void Add(string key, AudioClip clip)
|
public void Add(string key, AudioClip clip)
|
||||||
{
|
{
|
||||||
clips[key] = 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
|
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}");
|
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()
|
public IResourceGroup CreateGroup()
|
||||||
|
|||||||
Reference in New Issue
Block a user