SHA256
68 lines
3.3 KiB
C#
68 lines
3.3 KiB
C#
using System;
|
|
using System.IO;
|
|
using MelonLoader;
|
|
using UnityEngine;
|
|
|
|
namespace ADOFAIRenderer.Util.Audio.Clip;
|
|
|
|
/// <summary>
|
|
/// Opens the <see cref="AudioClipSource"/> for a clip the game plays, keyed by the game's audioLib key (also AudioClip.name):
|
|
/// - Key recorded in <see cref="AudioClipPaths"/> and the file exists: decoded from the file with <see cref="FFmpegPCMReader"/>
|
|
/// (mono MP3 through <see cref="MP3MonoLayoutReader"/>)
|
|
/// (song files and PlaySound files of custom levels).
|
|
/// - Otherwise, or when FFmpeg cannot open the file: the Unity clip is copied with <see cref="UnityClipLoader"/>
|
|
/// (built-in hit sounds from Resources, official/DLC level songs from Addressables).
|
|
/// Main thread only (the Unity copy calls AudioClip.GetData).
|
|
/// </summary>
|
|
internal static class AudioClipResolver
|
|
{
|
|
/// <summary>
|
|
/// Returns the source registered under <paramref name="key"/> in <paramref name="store"/>, opening it on first use
|
|
/// (file first, then the Unity clip; see the class summary). A key that could not be opened is remembered as null.
|
|
/// </summary>
|
|
/// <param name="store">Store of the current render.</param>
|
|
/// <param name="key">audioLib key of the clip (for example "song.ogg*external" or "sndKick").</param>
|
|
/// <param name="clip">Loaded Unity clip for the key, used when the clip has no file; may be null when the key has a file.</param>
|
|
/// <returns>The source, or null when neither the file nor the clip can be read.</returns>
|
|
public static AudioClipSource? GetOrAdd(AudioClipStore store, string key, AudioClip? clip)
|
|
{
|
|
if (store.TryGet(key, out var existing)) return existing;
|
|
|
|
if (AudioClipPaths.TryGetPath(key, out var path) && File.Exists(path))
|
|
{
|
|
IPCMReader? fileReader = null;
|
|
try
|
|
{
|
|
fileReader = OpenFile(path);
|
|
if (fileReader is MP3MonoLayoutReader)
|
|
Melon<RenderMod>.Logger.Warning($"[AudioClipResolver] Mono MP3 '{path}' is rendered like the game: half speed, one octave lower.");
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Melon<RenderMod>.Logger.Warning($"[AudioClipResolver] FFmpeg could not open '{path}' for '{key}', using the Unity clip: {e.Message}");
|
|
}
|
|
|
|
// The reader opened here becomes the first decode stream; additional streams reopen the file with OpenFile.
|
|
// When no source results (store disposed, or the source could not be created), the reader is disposed here
|
|
// (readers allow Dispose more than once).
|
|
if (fileReader != null)
|
|
{
|
|
var source = store.GetOrAdd(key, () => fileReader, () => OpenFile(path));
|
|
if (source == null) fileReader.Dispose();
|
|
return source;
|
|
}
|
|
}
|
|
|
|
if (!clip) return store.GetOrAdd(key, () => null);
|
|
return store.GetOrAdd(key, () => UnityClipLoader.Load(clip!, store));
|
|
}
|
|
|
|
/// <summary>Opens <paramref name="path"/> with FFmpeg; mono MP3 files are wrapped in <see cref="MP3MonoLayoutReader"/> (game layout).</summary>
|
|
private static IPCMReader OpenFile(string path)
|
|
{
|
|
var reader = new FFmpegPCMReader(path);
|
|
if (!reader.IsMP3 || reader.Channels != 1) return reader;
|
|
return new MP3MonoLayoutReader(reader);
|
|
}
|
|
}
|