From afcb02591d21781356624daae0cf6ec7e1face49 Mon Sep 17 00:00:00 2001 From: "JSD\\13999" <1399945104@qq.com> Date: Tue, 19 May 2026 20:42:36 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=20AudioService=20=E5=BC=82?= =?UTF-8?q?=E6=AD=A5=E6=92=AD=E6=94=BE=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FlowScope/Runtime/Audio/AudioService.cs | 138 +++++++++++++----- .../FlowScope/Runtime/Audio/IAudioService.cs | 13 ++ .../Tests/PlayMode/Audio/AudioServiceTests.cs | 89 ++++++++++- 3 files changed, 200 insertions(+), 40 deletions(-) diff --git a/My project/Assets/FlowScope/Runtime/Audio/AudioService.cs b/My project/Assets/FlowScope/Runtime/Audio/AudioService.cs index 85d5f62..06e9076 100644 --- a/My project/Assets/FlowScope/Runtime/Audio/AudioService.cs +++ b/My project/Assets/FlowScope/Runtime/Audio/AudioService.cs @@ -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(); - _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 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 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 clipHandle, bool loop, float fadeIn) + { + StopBgm(); + _bgmSource = gameObject.AddComponent(); + _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 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 LoadClip(string key) + private IResourceHandle LoadClipIfCompleted(string key) { try { - var handle = _resources.LoadAsync(key, CancellationToken.None) - .GetAwaiter() - .GetResult(); - if (handle == null || handle.Asset == null) + var task = _resources.LoadAsync(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> LoadClipAsync( + string key, + CancellationToken cancellationToken) + { + try + { + var handle = await _resources.LoadAsync(key, cancellationToken); + return ValidateLoadedClip(key, handle); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to load audio clip '{key}'.", exception); + } + } + + private static IResourceHandle ValidateLoadedClip( + string key, + IResourceHandle 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) diff --git a/My project/Assets/FlowScope/Runtime/Audio/IAudioService.cs b/My project/Assets/FlowScope/Runtime/Audio/IAudioService.cs index 54efa74..7bf8038 100644 --- a/My project/Assets/FlowScope/Runtime/Audio/IAudioService.cs +++ b/My project/Assets/FlowScope/Runtime/Audio/IAudioService.cs @@ -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 PlayBgmAsync( + string key, + bool loop = true, + float fadeIn = 0f, + CancellationToken cancellationToken = default); + IAudioHandle PlaySfx(string key); + Task PlaySfxAsync( + string key, + CancellationToken cancellationToken = default); + void StopBgm(float fadeOut = 0f); void SetBgmVolume(float volume); void SetSfxVolume(float volume); diff --git a/My project/Assets/FlowScope/Tests/PlayMode/Audio/AudioServiceTests.cs b/My project/Assets/FlowScope/Tests/PlayMode/Audio/AudioServiceTests.cs index 95ed4df..ce72269 100644 --- a/My project/Assets/FlowScope/Tests/PlayMode/Audio/AudioServiceTests.cs +++ b/My project/Assets/FlowScope/Tests/PlayMode/Audio/AudioServiceTests.cs @@ -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().Length; + + var exception = Assert.Throws(() => service.PlayBgm("slow-bgm")); + + Assert.That(exception.Message, Does.Contain("slow-bgm")); + Assert.That(Object.FindObjectsOfType().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().Length; + + var exception = Assert.Throws(() => service.PlaySfx("slow-sfx")); + + Assert.That(exception.Message, Does.Contain("slow-sfx")); + Assert.That(Object.FindObjectsOfType().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(Task task, Action 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 clips = new(); + private readonly Dictionary> pendingClips = new(); public void Add(string key, AudioClip clip) { clips[key] = clip; } - public Task> LoadAsync(string key, CancellationToken cancellationToken) + public TaskCompletionSource AddPending(string key) + { + var pending = new TaskCompletionSource(); + pendingClips[key] = pending; + return pending; + } + + public async Task> LoadAsync(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>(new FakeResourceHandle(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(key, clip as T); } public IResourceGroup CreateGroup()