SHA256
Document Audio Pipeline
This commit is contained in:
1 parent
7bb47e4974
commit
19d0e258ac
22 files changed
+634
-12
No files matched your search
@@ -29,18 +29,34 @@ internal sealed class AudioClipStore : IDisposable
|
||||
/// <summary>Longest wait for the decoder thread to stop in <see cref="Dispose"/>, in milliseconds.</summary>
|
||||
private const int DecoderStopTimeoutMs = 3000;
|
||||
|
||||
/// <summary>Sources by key; a null value marks a key whose reader could not be created. Also the lock of the source tables.</summary>
|
||||
private readonly Dictionary<string, AudioClipSource?> _sources = new();
|
||||
|
||||
/// <summary>Non-null sources in creation order, scanned by the decoder thread. Guarded by the <see cref="_sources"/> lock.</summary>
|
||||
private readonly List<AudioClipSource> _sourceList = new();
|
||||
|
||||
/// <summary>Free windowed chunk arrays (<see cref="AudioClipSource.ChunkFloats"/> floats each), guarded by <see cref="_poolLock"/>.</summary>
|
||||
private readonly Stack<float[]> _chunkPool = new();
|
||||
|
||||
private readonly object _poolLock = new();
|
||||
|
||||
/// <summary>Monitor the decoder thread pulses after each load (or failure); consumers in <see cref="Acquire"/> wait on it.</summary>
|
||||
private readonly object _signal = new();
|
||||
|
||||
/// <summary>Wakes the decoder thread when a cursor, demand or source changes (<see cref="RequestService"/>).</summary>
|
||||
private readonly AutoResetEvent _serviceRequested = new(false);
|
||||
|
||||
private Thread? _decoderThread;
|
||||
private volatile bool _stopRequested;
|
||||
|
||||
/// <summary>Bytes of allocated sample arrays (see <see cref="AllocatedBytes"/>), updated with Interlocked.</summary>
|
||||
private long _allocatedBytes;
|
||||
|
||||
private long _peakBytes;
|
||||
|
||||
/// <summary>First fatal error (out of memory, or an exception that stopped the decoder thread); null while none.</summary>
|
||||
private volatile Exception? _fatalError;
|
||||
|
||||
private volatile bool _disposed;
|
||||
|
||||
/// <summary>Sample rate every source is resampled to, in Hz.</summary>
|
||||
@@ -52,9 +68,12 @@ internal sealed class AudioClipStore : IDisposable
|
||||
/// <summary>Highest value of <see cref="AllocatedBytes"/> since creation.</summary>
|
||||
public long PeakBytes => Interlocked.Read(ref _peakBytes);
|
||||
|
||||
/// <summary>Out-of-memory error raised while loading, or null.</summary>
|
||||
/// <summary>
|
||||
/// Out-of-memory error raised while opening or loading a clip, or the exception that stopped the decoder thread; null while none.
|
||||
/// </summary>
|
||||
public Exception? FatalError => _fatalError;
|
||||
|
||||
/// <param name="outputRate">Sample rate in Hz every source is resampled to (positive). Call <see cref="Start"/> to begin decoding.</param>
|
||||
public AudioClipStore(int outputRate)
|
||||
{
|
||||
if (outputRate <= 0) throw new ArgumentOutOfRangeException(nameof(outputRate));
|
||||
@@ -75,6 +94,11 @@ internal sealed class AudioClipStore : IDisposable
|
||||
_decoderThread.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decoder thread body: loads one chunk per iteration, or sleeps until <see cref="RequestService"/> (at most
|
||||
/// <see cref="DecoderIdleWaitMs"/> ms) when nothing is missing. An unexpected exception ends the thread and is kept as the
|
||||
/// fatal error, so waiting consumers fail instead of waiting forever.
|
||||
/// </summary>
|
||||
private void DecoderLoop()
|
||||
{
|
||||
try
|
||||
@@ -124,6 +148,7 @@ internal sealed class AudioClipStore : IDisposable
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Wakes the decoder thread to re-evaluate what to free and load. Any thread.</summary>
|
||||
internal void RequestService() => _serviceRequested.Set();
|
||||
|
||||
// ── Sources ──
|
||||
@@ -169,6 +194,8 @@ internal sealed class AudioClipStore : IDisposable
|
||||
Melon<RenderMod>.Logger.Error($"[AudioClipStore] Failed to open clip '{key}': {e}");
|
||||
}
|
||||
|
||||
// The reader is created outside the lock (it may decode or copy a whole clip). If another caller registered the
|
||||
// same key meanwhile, its entry wins and this source is discarded.
|
||||
lock (_sources)
|
||||
{
|
||||
if (_disposed || _sources.TryGetValue(key, out var raced))
|
||||
@@ -188,6 +215,10 @@ internal sealed class AudioClipStore : IDisposable
|
||||
return source;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when <paramref name="key"/> has been registered; <paramref name="source"/> is then the source, or null when its reader
|
||||
/// could not be created. Any thread.
|
||||
/// </summary>
|
||||
public bool TryGet(string key, out AudioClipSource? source)
|
||||
{
|
||||
lock (_sources)
|
||||
@@ -214,6 +245,8 @@ internal sealed class AudioClipStore : IDisposable
|
||||
if (source.IsFailed || _disposed) return default;
|
||||
if (source.TryAcquire(frame, out var view)) return view;
|
||||
|
||||
// Retry under the signal lock so a load completed between the first try and the wait is not missed;
|
||||
// the timed wait also rechecks cancellation and failure states periodically.
|
||||
lock (_signal)
|
||||
{
|
||||
if (source.TryAcquire(frame, out view)) return view;
|
||||
@@ -224,6 +257,7 @@ internal sealed class AudioClipStore : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Throws <see cref="InvalidOperationException"/> wrapping <see cref="FatalError"/> when one is set.</summary>
|
||||
private void ThrowIfFatal()
|
||||
{
|
||||
var error = _fatalError;
|
||||
@@ -231,6 +265,7 @@ internal sealed class AudioClipStore : IDisposable
|
||||
throw new InvalidOperationException("Audio clip loading failed: " + error.Message, error);
|
||||
}
|
||||
|
||||
/// <summary>Wakes every consumer waiting in <see cref="Acquire"/> (after a load, a failure, or on dispose).</summary>
|
||||
internal void NotifyChunkLoaded()
|
||||
{
|
||||
lock (_signal)
|
||||
@@ -239,12 +274,14 @@ internal sealed class AudioClipStore : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Logs a load error of one source (the source is already marked failed and plays silence) and wakes waiting consumers.</summary>
|
||||
internal void ReportSourceFailure(AudioClipSource source, Exception error)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Error($"[AudioClipStore] Failed to load clip '{source.Key}', it will be silent: {error}");
|
||||
NotifyChunkLoaded();
|
||||
}
|
||||
|
||||
/// <summary>Logs an out-of-memory error, keeps it as <see cref="FatalError"/> (first one wins) and wakes waiting consumers.</summary>
|
||||
internal void ReportFatal(AudioClipSource source, OutOfMemoryException error)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Error($"[AudioClipStore] Out of memory while loading clip '{source.Key}' ({AllocatedBytes / 1048576.0:F1} MiB allocated): {error}");
|
||||
@@ -254,6 +291,7 @@ internal sealed class AudioClipStore : IDisposable
|
||||
|
||||
// ── Arrays ──
|
||||
|
||||
/// <summary>Takes a pooled array of <see cref="AudioClipSource.ChunkFloats"/> floats, or allocates (and counts) a new one. Contents are undefined.</summary>
|
||||
internal float[] RentChunkArray()
|
||||
{
|
||||
lock (_poolLock)
|
||||
@@ -265,6 +303,7 @@ internal sealed class AudioClipStore : IDisposable
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>Puts a rented array back into the pool (it stays counted); after dispose it is dropped and uncounted.</summary>
|
||||
internal void ReturnChunkArray(float[] data)
|
||||
{
|
||||
if (_disposed)
|
||||
@@ -278,6 +317,7 @@ internal sealed class AudioClipStore : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Allocates a counted array of exactly <paramref name="floats"/> floats (resident chunks, copied Unity clip blocks).</summary>
|
||||
internal float[] AllocateExactArray(int floats)
|
||||
{
|
||||
var data = new float[floats];
|
||||
@@ -285,8 +325,10 @@ internal sealed class AudioClipStore : IDisposable
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>Uncounts an array from <see cref="AllocateExactArray"/>; the array itself is left to the garbage collector.</summary>
|
||||
internal void FreeExactArray(float[] data) => AddAllocated(-data.LongLength * sizeof(float));
|
||||
|
||||
/// <summary>Adds <paramref name="bytes"/> (negative to subtract) to the allocation counter and raises the peak lock-free.</summary>
|
||||
private void AddAllocated(long bytes)
|
||||
{
|
||||
var current = Interlocked.Add(ref _allocatedBytes, bytes);
|
||||
@@ -297,6 +339,7 @@ internal sealed class AudioClipStore : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Logs the source count and the current and peak sample memory in MiB.</summary>
|
||||
public void LogMemoryUsage()
|
||||
{
|
||||
int sourceCount;
|
||||
|
||||
Reference in new issue
Block a user