WIP: Live mixing support.
checkpoint: StreamingAsset Loader.
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace OCES
|
namespace OCES
|
||||||
@@ -20,9 +21,17 @@ namespace OCES
|
|||||||
{
|
{
|
||||||
return this.m_assetBundle.LoadAsset<T>(path);
|
return this.m_assetBundle.LoadAsset<T>(path);
|
||||||
}
|
}
|
||||||
public ResourceRequest LoadAsync<T>(string path) where T : UnityEngine.Object
|
|
||||||
|
public void LoadAsync<T>(string path, MonoBehaviour coroutineHost, Action<T> onComplete) where T : UnityEngine.Object
|
||||||
{
|
{
|
||||||
return this.m_assetBundle.LoadAssetAsync<T>(path);
|
coroutineHost.StartCoroutine(LoadAsyncCoroutine(path, onComplete));
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator LoadAsyncCoroutine<T>(string path, Action<T> onComplete) where T : UnityEngine.Object
|
||||||
|
{
|
||||||
|
AssetBundleRequest request = this.m_assetBundle.LoadAssetAsync<T>(path);
|
||||||
|
yield return request;
|
||||||
|
onComplete?.Invoke(request.asset as T);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ namespace OCES
|
|||||||
public interface IAssetProvider
|
public interface IAssetProvider
|
||||||
{
|
{
|
||||||
T Load<T>(string path) where T : UnityEngine.Object;
|
T Load<T>(string path) where T : UnityEngine.Object;
|
||||||
ResourceRequest LoadAsync<T>(string path) where T : UnityEngine.Object;
|
void LoadAsync<T>(string path, MonoBehaviour coroutineHost, Action<T> onComplete) where T : UnityEngine.Object;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ using System;
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using OCES.Audio;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Object = UnityEngine.Object;
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
@@ -57,34 +56,40 @@ namespace OCES
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.m_pendingCallbacks[path] = new List<Action<Object>> { obj => onComplete?.Invoke(obj as T) };
|
this.m_pendingCallbacks[path] = new List<Action<Object>> { obj => onComplete?.Invoke(obj as T) };
|
||||||
ResourceRequest newRequest;
|
|
||||||
if (this.m_assetProvider is not null)
|
if (this.m_assetProvider is not null)
|
||||||
{
|
{
|
||||||
newRequest = this.m_assetProvider.LoadAsync<T>(path);
|
this.m_assetProvider.LoadAsync<T>(path, this, asset =>
|
||||||
|
{
|
||||||
|
OnAsyncAssetLoaded(path, asset as Object);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
newRequest = Resources.LoadAsync<T>(path);
|
|
||||||
Debug.LogWarning($"[ResourceLoader] No IAssetProvider set, falling back to Resources.Load for '{path}'");
|
Debug.LogWarning($"[ResourceLoader] No IAssetProvider set, falling back to Resources.Load for '{path}'");
|
||||||
|
StartCoroutine(FallbackLoadAsyncCoroutine<T>(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
StartCoroutine(HandleAsyncLoadCompletion(path, newRequest));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IEnumerator HandleAsyncLoadCompletion(string path, ResourceRequest request)
|
IEnumerator FallbackLoadAsyncCoroutine<T>(string path) where T : Object
|
||||||
{
|
{
|
||||||
|
ResourceRequest request = Resources.LoadAsync<T>(path);
|
||||||
yield return request;
|
yield return request;
|
||||||
|
OnAsyncAssetLoaded(path, request.asset);
|
||||||
|
}
|
||||||
|
|
||||||
if (request.asset)
|
void OnAsyncAssetLoaded(string path, Object asset)
|
||||||
|
{
|
||||||
|
if (asset)
|
||||||
{
|
{
|
||||||
this.m_cachedObjects[path] = request.asset;
|
this.m_cachedObjects[path] = asset;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.m_pendingCallbacks.Remove(path, out List<Action<Object>> callbacks))
|
if (this.m_pendingCallbacks.Remove(path, out List<Action<Object>> callbacks))
|
||||||
{
|
{
|
||||||
foreach (Action<Object> callback in callbacks)
|
foreach (Action<Object> callback in callbacks)
|
||||||
{
|
{
|
||||||
callback?.Invoke(request.asset);
|
callback?.Invoke(asset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace OCES
|
namespace OCES
|
||||||
@@ -9,9 +10,17 @@ namespace OCES
|
|||||||
{
|
{
|
||||||
return Resources.Load<T>(path);
|
return Resources.Load<T>(path);
|
||||||
}
|
}
|
||||||
public ResourceRequest LoadAsync<T>(string path) where T : UnityEngine.Object
|
|
||||||
|
public void LoadAsync<T>(string path, MonoBehaviour coroutineHost, Action<T> onComplete) where T : UnityEngine.Object
|
||||||
{
|
{
|
||||||
return Resources.LoadAsync<T>(path);
|
coroutineHost.StartCoroutine(LoadAsyncCoroutine(path, onComplete));
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator LoadAsyncCoroutine<T>(string path, Action<T> onComplete) where T : UnityEngine.Object
|
||||||
|
{
|
||||||
|
ResourceRequest request = Resources.LoadAsync<T>(path);
|
||||||
|
yield return request;
|
||||||
|
onComplete?.Invoke(request.asset as T);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
|
#if UNITY_STANDALONE || UNITY_EDITOR
|
||||||
|
using System.Collections;
|
||||||
|
using System.IO;
|
||||||
|
using UnityEngine.Networking;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace OCES
|
||||||
|
{
|
||||||
|
#if UNITY_STANDALONE || UNITY_EDITOR
|
||||||
|
/// <summary>
|
||||||
|
/// 从 StreamingAssets 加载资源的 IAssetProvider 实现。
|
||||||
|
/// 仅支持桌面平台(Windows / macOS / Linux)及 Editor。
|
||||||
|
/// 无内置缓存——每次调用都从磁盘读取,缓存由上层 ResourceLoader 负责。
|
||||||
|
/// 目前仅支持 AudioClip(.wav 格式)。
|
||||||
|
/// </summary>
|
||||||
|
public class StreamingAssetProvider : IAssetProvider
|
||||||
|
{
|
||||||
|
public T Load<T>(string path) where T : Object
|
||||||
|
{
|
||||||
|
if (typeof(T) != typeof(AudioClip))
|
||||||
|
{
|
||||||
|
Debug.LogError($"[StreamingAssetProvider] Load<{typeof(T).Name}> 不支持,仅支持 AudioClip");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
string fullPath = Path.Combine(Application.streamingAssetsPath, path);
|
||||||
|
if (!File.Exists(fullPath))
|
||||||
|
{
|
||||||
|
Debug.LogError($"[StreamingAssetProvider] 文件 {fullPath} 不存在。");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] wavBytes = File.ReadAllBytes(fullPath);
|
||||||
|
AudioClip clip = ParseWav(wavBytes, Path.GetFileNameWithoutExtension(path));
|
||||||
|
return clip as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadAsync<T>(string path, MonoBehaviour coroutineHost, Action<T> onComplete) where T : Object
|
||||||
|
{
|
||||||
|
coroutineHost.StartCoroutine(LoadAsyncCoroutine(path, onComplete));
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator LoadAsyncCoroutine<T>(string path, Action<T> onComplete) where T : Object
|
||||||
|
{
|
||||||
|
if (typeof(T) != typeof(AudioClip))
|
||||||
|
{
|
||||||
|
Debug.LogError($"[StreamingAssetProvider] LoadAsync<{typeof(T).Name}> 不支持,仅支持 AudioClip");
|
||||||
|
onComplete?.Invoke(null);
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
string fullPath = Path.Combine(Application.streamingAssetsPath, path);
|
||||||
|
string url = "file://" + fullPath;
|
||||||
|
|
||||||
|
using UnityWebRequest request = UnityWebRequestMultimedia.GetAudioClip(url, AudioType.WAV);
|
||||||
|
yield return request.SendWebRequest();
|
||||||
|
|
||||||
|
if (request.result == UnityWebRequest.Result.Success)
|
||||||
|
{
|
||||||
|
AudioClip clip = DownloadHandlerAudioClip.GetContent(request);
|
||||||
|
onComplete?.Invoke(clip as T);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogError($"[StreamingAssetProvider] 加载失败: {url}, 错误: {request.error}");
|
||||||
|
onComplete?.Invoke(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// WAV 解析(PCM 16-bit / 8-bit,单声道 / 立体声)
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
static AudioClip ParseWav(byte[] wavBytes, string clipName)
|
||||||
|
{
|
||||||
|
int position = 0;
|
||||||
|
|
||||||
|
// RIFF header
|
||||||
|
if (wavBytes.Length < 44 ||
|
||||||
|
wavBytes[0] != 'R' || wavBytes[1] != 'I' || wavBytes[2] != 'F' || wavBytes[3] != 'F')
|
||||||
|
{
|
||||||
|
Debug.LogError("[StreamingAssetProvider] 无效的 WAV 文件:缺少 RIFF 头");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
position += 4; // "RIFF"
|
||||||
|
position += 4; // file size (unused)
|
||||||
|
if (wavBytes[8] != 'W' || wavBytes[9] != 'A' || wavBytes[10] != 'V' || wavBytes[11] != 'E')
|
||||||
|
{
|
||||||
|
Debug.LogError("[StreamingAssetProvider] 无效的 WAV 文件:缺少 WAVE 标识");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
position += 4; // "WAVE"
|
||||||
|
|
||||||
|
int channels = 1;
|
||||||
|
int sampleRate = 44100;
|
||||||
|
int bitsPerSample = 16;
|
||||||
|
byte[] pcmData = null;
|
||||||
|
|
||||||
|
// 遍历 chunk
|
||||||
|
while (position < wavBytes.Length - 8)
|
||||||
|
{
|
||||||
|
string chunkId = System.Text.Encoding.ASCII.GetString(wavBytes, position, 4);
|
||||||
|
position += 4;
|
||||||
|
int chunkSize = BitConverter.ToInt32(wavBytes, position);
|
||||||
|
position += 4;
|
||||||
|
|
||||||
|
if (chunkId == "fmt ")
|
||||||
|
{
|
||||||
|
int audioFormat = BitConverter.ToInt16(wavBytes, position);
|
||||||
|
if (audioFormat != 1) // PCM = 1
|
||||||
|
{
|
||||||
|
Debug.LogError($"[StreamingAssetProvider] 不支持的音频格式: {audioFormat},仅支持 PCM");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
channels = BitConverter.ToInt16(wavBytes, position + 2);
|
||||||
|
sampleRate = BitConverter.ToInt32(wavBytes, position + 4);
|
||||||
|
bitsPerSample = BitConverter.ToInt16(wavBytes, position + 14);
|
||||||
|
|
||||||
|
if (channels < 1 || channels > 2)
|
||||||
|
{
|
||||||
|
Debug.LogError($"[StreamingAssetProvider] 不支持的声道数: {channels}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (chunkId == "data")
|
||||||
|
{
|
||||||
|
pcmData = new byte[chunkSize];
|
||||||
|
Array.Copy(wavBytes, position, pcmData, 0, chunkSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
position += chunkSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pcmData == null || pcmData.Length == 0)
|
||||||
|
{
|
||||||
|
Debug.LogError("[StreamingAssetProvider] WAV 文件中未找到 data chunk");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PCM → float 样本
|
||||||
|
int sampleCount = pcmData.Length / (bitsPerSample / 8);
|
||||||
|
float[] samples = new float[sampleCount];
|
||||||
|
|
||||||
|
if (bitsPerSample == 16)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < sampleCount; i++)
|
||||||
|
{
|
||||||
|
short sample = BitConverter.ToInt16(pcmData, i * 2);
|
||||||
|
samples[i] = sample / 32768f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (bitsPerSample == 8)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < sampleCount; i++)
|
||||||
|
{
|
||||||
|
samples[i] = (pcmData[i] - 128f) / 128f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogError($"[StreamingAssetProvider] 不支持的位深度: {bitsPerSample}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioClip clip = AudioClip.Create(clipName, sampleCount / channels, channels, sampleRate, false);
|
||||||
|
clip.SetData(samples, 0);
|
||||||
|
return clip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
/// <summary>
|
||||||
|
/// 非桌面平台的桩实现——StreamingAssetProvider 仅支持桌面平台。
|
||||||
|
/// </summary>
|
||||||
|
public class StreamingAssetProvider : IAssetProvider
|
||||||
|
{
|
||||||
|
public T Load<T>(string path) where T : Object
|
||||||
|
{
|
||||||
|
Debug.LogError("[StreamingAssetProvider] 仅支持桌面平台 (Windows/macOS/Linux)");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadAsync<T>(string path, MonoBehaviour coroutineHost, Action<T> onComplete) where T : Object
|
||||||
|
{
|
||||||
|
Debug.LogError("[StreamingAssetProvider] 仅支持桌面平台 (Windows/macOS/Linux)");
|
||||||
|
onComplete?.Invoke(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7b8b3ffa721341f8ac66707c91bccb28
|
||||||
|
timeCreated: 1779096845
|
||||||
@@ -340,12 +340,8 @@ namespace OCES.Audio
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// AudioClip clip = Resources.Load<AudioClip>($"Audios/{segment.Name}");
|
AudioClip clip = AudioSystem.Instance.ResourceLoader.LoadSync<AudioClip>(
|
||||||
AudioClip clip = null;
|
$"{AudioExtendSettings.Instance.audioResourcePath}/{segment.Name}");
|
||||||
AudioSystem.Instance.ResourceLoader.LoadAsync<AudioClip>($"{AudioExtendSettings.Instance.audioResourcePath}/{segment.Name}", loadedClip =>
|
|
||||||
{
|
|
||||||
clip = loadedClip;
|
|
||||||
});
|
|
||||||
if (!clip)
|
if (!clip)
|
||||||
{
|
{
|
||||||
Debug.LogError($"[LongAudioContainerPlayer] 音频文件未找到: {segment.Name}");
|
Debug.LogError($"[LongAudioContainerPlayer] 音频文件未找到: {segment.Name}");
|
||||||
|
|||||||
Reference in New Issue
Block a user