SHA256
Document Audio Pipeline
This commit is contained in:
22 files changed
+634
-12
No files matched your search
@@ -13,9 +13,13 @@ namespace ADOFAIRenderer.Util.Audio.Clip;
|
||||
internal sealed class AudioCursor
|
||||
{
|
||||
private readonly AudioClipSource _source;
|
||||
|
||||
/// <summary>Backing value of <see cref="Position"/>, accessed with Interlocked (64-bit reads are not atomic on every platform).</summary>
|
||||
private long _position;
|
||||
|
||||
private volatile bool _loop;
|
||||
|
||||
/// <summary>Created by <see cref="AudioClipSource.AddCursor"/>.</summary>
|
||||
internal AudioCursor(AudioClipSource source, long position, bool loop)
|
||||
{
|
||||
_source = source;
|
||||
@@ -39,7 +43,10 @@ internal sealed class AudioCursor
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>When true, the chunk after the last chunk is chunk 0.</summary>
|
||||
/// <summary>
|
||||
/// When true, the chunk after the last chunk is chunk 0 (the window wraps to the clip start), and a position past the end
|
||||
/// wraps into the clip. Setting it wakes the decoder thread.
|
||||
/// </summary>
|
||||
public bool Loop
|
||||
{
|
||||
get => _loop;
|
||||
@@ -70,8 +77,10 @@ internal readonly struct ChunkView
|
||||
/// <summary>Channel count of the stored samples (same as the source clip).</summary>
|
||||
public readonly int Channels;
|
||||
|
||||
/// <summary>Index of the chunk in its source (used by <see cref="AudioClipSource.Release"/>).</summary>
|
||||
public readonly int ChunkIndex;
|
||||
|
||||
/// <summary>Created by <see cref="AudioClipSource.TryAcquire"/>; default(ChunkView) is the invalid view.</summary>
|
||||
internal ChunkView(float[] data, long startFrame, int frameCount, int channels, int chunkIndex)
|
||||
{
|
||||
Data = data;
|
||||
@@ -81,10 +90,13 @@ internal readonly struct ChunkView
|
||||
ChunkIndex = chunkIndex;
|
||||
}
|
||||
|
||||
/// <summary>True for a view that holds a chunk; false for default(ChunkView).</summary>
|
||||
public bool IsValid => Data != null;
|
||||
|
||||
/// <summary>Source frame after the last valid frame (exclusive end).</summary>
|
||||
public long EndFrame => StartFrame + FrameCount;
|
||||
|
||||
/// <summary>True when source frame <paramref name="frame"/> is one of the valid frames of this chunk.</summary>
|
||||
public bool Contains(long frame) => frame >= StartFrame && frame < StartFrame + FrameCount;
|
||||
|
||||
/// <summary>
|
||||
@@ -171,26 +183,45 @@ internal sealed class AudioClipSource
|
||||
internal int DecodeStreamLimit { get; set; } = MaxDecodeStreams;
|
||||
|
||||
private readonly AudioClipStore _store;
|
||||
|
||||
/// <summary>Reader passed to the constructor (the first decode stream); disposed again in <see cref="DisposeInternal"/>.</summary>
|
||||
private readonly IPCMReader _firstReader;
|
||||
|
||||
/// <summary>Opens another reader for an additional decode stream; null when only the first reader can be used.</summary>
|
||||
private readonly Func<IPCMReader?>? _openReader;
|
||||
|
||||
// Guarded by _sync: chunk table, hold counts, cursors, demand set, loaded set.
|
||||
private readonly object _sync = new();
|
||||
|
||||
/// <summary>Sample array of each chunk, null while not loaded.</summary>
|
||||
private readonly float[]?[] _chunks;
|
||||
|
||||
/// <summary>Number of consumers holding each chunk (TryAcquire minus Release); a held chunk is not freed.</summary>
|
||||
private readonly int[] _holdCounts;
|
||||
|
||||
private readonly List<AudioCursor> _cursors = new();
|
||||
|
||||
/// <summary>Chunks a consumer asked for while they were not loaded; an index is removed when the chunk is acquired.</summary>
|
||||
private readonly HashSet<int> _demanded = new();
|
||||
|
||||
/// <summary>Indices of the loaded chunks (non-null entries of <see cref="_chunks"/>).</summary>
|
||||
private readonly HashSet<int> _loaded = new();
|
||||
|
||||
private int _residentLoadedCount; // Resident: chunks [0, _residentLoadedCount) are loaded
|
||||
|
||||
// Guarded by _loadLock: decode streams.
|
||||
private readonly object _loadLock = new();
|
||||
private readonly List<DecodeStream> _streams = new();
|
||||
|
||||
/// <summary>Incremented on every load; stored in <see cref="DecodeStream.LastUse"/> for least-recently-used selection.</summary>
|
||||
private long _streamUseCounter;
|
||||
|
||||
/// <summary>True after opening an additional reader failed; no further readers are opened for this source.</summary>
|
||||
private bool _openFailed;
|
||||
|
||||
private volatile bool _isFailed;
|
||||
|
||||
/// <summary>Key the source is registered under in the store (the game's audioLib key / AudioClip.name).</summary>
|
||||
public string Key { get; }
|
||||
|
||||
/// <summary>Channel count of the stored samples (same as the clip).</summary>
|
||||
@@ -205,12 +236,19 @@ internal sealed class AudioClipSource
|
||||
/// <summary>Clip length in seconds: clip frames / clip rate.</summary>
|
||||
public double Duration { get; }
|
||||
|
||||
/// <summary>Frames per chunk: <see cref="ChunkFloats"/> / <see cref="Channels"/> (rounded down).</summary>
|
||||
public int FramesPerChunk { get; }
|
||||
|
||||
/// <summary>Number of chunks: ceil(<see cref="TotalFrames"/> / <see cref="FramesPerChunk"/>); 0 for an empty clip.</summary>
|
||||
public int ChunkCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="ChunkPolicy.Resident"/> for forward-only readers and clips of at most one chunk, otherwise
|
||||
/// <see cref="ChunkPolicy.Windowed"/>. Fixed at construction.
|
||||
/// </summary>
|
||||
public ChunkPolicy Policy { get; }
|
||||
|
||||
/// <summary>True when <see cref="Policy"/> is <see cref="ChunkPolicy.Resident"/>.</summary>
|
||||
public bool IsResident => Policy == ChunkPolicy.Resident;
|
||||
|
||||
/// <summary>True after loading failed. A failed source returns no chunks.</summary>
|
||||
@@ -279,6 +317,7 @@ internal sealed class AudioClipSource
|
||||
return cursor;
|
||||
}
|
||||
|
||||
/// <summary>Unregisters <paramref name="cursor"/>; its window chunks become freeable on the decoder thread's next pass.</summary>
|
||||
public void RemoveCursor(AudioCursor cursor)
|
||||
{
|
||||
lock (_sync)
|
||||
@@ -288,6 +327,7 @@ internal sealed class AudioClipSource
|
||||
_store.RequestService();
|
||||
}
|
||||
|
||||
/// <summary>Wakes the decoder thread after a cursor changed chunk or loop flag (resident sources ignore cursors).</summary>
|
||||
internal void OnCursorMoved()
|
||||
{
|
||||
if (!IsResident) _store.RequestService();
|
||||
@@ -325,6 +365,7 @@ internal sealed class AudioClipSource
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Ends one hold of the chunk of <paramref name="view"/> taken by <see cref="TryAcquire"/>. An invalid view is ignored.</summary>
|
||||
public void Release(in ChunkView view)
|
||||
{
|
||||
if (!view.IsValid) return;
|
||||
@@ -335,6 +376,7 @@ internal sealed class AudioClipSource
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>View of chunk <paramref name="index"/>; the last chunk's frame count stops at <see cref="TotalFrames"/>.</summary>
|
||||
private ChunkView CreateView(int index, float[] data)
|
||||
{
|
||||
var start = (long)index * FramesPerChunk;
|
||||
@@ -406,6 +448,8 @@ internal sealed class AudioClipSource
|
||||
return true;
|
||||
}
|
||||
|
||||
// For each cursor, the first missing chunk of its window; the lowest slot over all cursors wins
|
||||
// (ties go to the earlier cursor).
|
||||
foreach (var cursor in _cursors)
|
||||
{
|
||||
if (!TryGetWindowStart(cursor, out var first, out var loop)) continue;
|
||||
@@ -441,6 +485,10 @@ internal sealed class AudioClipSource
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// First chunk of the cursor's window: the chunk of its position (negative positions count as 0; positions past the end
|
||||
/// wrap when looping). False when a non-looping cursor is past the end (no window). Caller holds _sync.
|
||||
/// </summary>
|
||||
private bool TryGetWindowStart(AudioCursor cursor, out int first, out bool loop)
|
||||
{
|
||||
var position = cursor.Position;
|
||||
@@ -456,6 +504,7 @@ internal sealed class AudioClipSource
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Chunk index of window slot <paramref name="slot"/> after <paramref name="first"/>; wraps when looping, false past the end otherwise.</summary>
|
||||
private bool TryWindowIndex(int first, int slot, bool loop, out int index)
|
||||
{
|
||||
index = first + slot;
|
||||
@@ -493,6 +542,7 @@ internal sealed class AudioClipSource
|
||||
stream.LastUse = ++_streamUseCounter;
|
||||
stream.Fill(start, frames, data);
|
||||
}
|
||||
// Out of memory fails the whole render (store fatal error); any other error makes only this source silent.
|
||||
catch (OutOfMemoryException e)
|
||||
{
|
||||
ReturnArray(data);
|
||||
@@ -568,12 +618,14 @@ internal sealed class AudioClipSource
|
||||
return oldest!;
|
||||
}
|
||||
|
||||
/// <summary>Marks the source failed (no more chunks are returned or loaded); a resident source also closes its readers now. Caller holds _loadLock.</summary>
|
||||
private void Fail()
|
||||
{
|
||||
_isFailed = true;
|
||||
if (IsResident) DisposeStreams();
|
||||
}
|
||||
|
||||
/// <summary>Disposes every decode stream (reader and resampler) and clears the list. Caller holds _loadLock.</summary>
|
||||
private void DisposeStreams()
|
||||
{
|
||||
foreach (var stream in _streams)
|
||||
@@ -581,6 +633,7 @@ internal sealed class AudioClipSource
|
||||
_streams.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Gives a chunk array back to the store: exact arrays (resident) are uncounted, pooled arrays (windowed) are returned to the pool.</summary>
|
||||
private void ReturnArray(float[]? data)
|
||||
{
|
||||
if (data == null) return;
|
||||
@@ -609,5 +662,6 @@ internal sealed class AudioClipSource
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>ceil(a / b) for a ≥ 0, b > 0.</summary>
|
||||
private static long CeilDiv(long a, long b) => (a + b - 1) / b;
|
||||
}
|
||||
Reference in new issue
Block a user