WIP: Live mixing support.

checkpoint: StreamingAsset Loader.
This commit is contained in:
2026-05-18 19:25:09 +08:00
parent 3190802bd2
commit 3c2558f5e7
7 changed files with 237 additions and 22 deletions
@@ -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
}