Document Audio Pipeline

This commit is contained in:
copecone committed 2026-10-15 16:17:12 +09:00
1 parent 7bb47e4974
commit 19d0e258ac
22 files changed
+634 -12

No files matched your search

+37 -1
View File
@@ -10,7 +10,7 @@ namespace ADOFAIRenderer.Util.Audio.Mixer;
///
/// Main thread: the Set methods queue a change that takes effect at a given time (seconds relative to the render start,
/// the time at which the game made the change). Changes of one voice must be queued in non-decreasing time order.
/// Render thread: <see cref="VoiceMixer.Mix"/> applies each change at sample round(time * OutputRate).
/// Render thread: <see cref="VoiceMixer.Mix"/> applies each change at sample floor(time * OutputRate) (<see cref="VoiceMixer.ToSample"/>).
///
/// Playback model (output samples n, clip positions in frames of the source at the output sample rate):
/// - The voice is silent before its start sample and from its end sample on.
@@ -21,20 +21,37 @@ namespace ADOFAIRenderer.Util.Audio.Mixer;
/// </summary>
internal sealed class Voice
{
/// <summary>Kind of a queued <see cref="Change"/>; the meaning of <see cref="Change.Value"/> depends on it.</summary>
internal enum ChangeKind
{
/// <summary>Value: playback rate, at least 0.</summary>
Pitch,
/// <summary>Value: linear gain.</summary>
Volume,
/// <summary>Value: 1 = loop on, 0 = loop off.</summary>
Loop,
/// <summary>Value: clip position in seconds, at least 0.</summary>
Position,
/// <summary>Value: end time in seconds relative to the render start.</summary>
EndTime,
/// <summary>Value: unused (0). Ends playback at the sample the change is applied at.</summary>
Stop
}
/// <summary>One timed change of a voice, queued by the main thread and applied by the render thread.</summary>
internal readonly struct Change
{
public readonly ChangeKind Kind;
/// <summary>Output sample at which the change takes effect (<see cref="VoiceMixer.ToSample"/> of the change time).</summary>
public readonly long Sample;
/// <summary>Argument of the change (see <see cref="ChangeKind"/>).</summary>
public readonly double Value;
public Change(ChangeKind kind, long sample, double value)
@@ -45,6 +62,7 @@ internal sealed class Voice
}
}
/// <summary>Mixer that owns this voice; the Set methods queue their changes on it.</summary>
private readonly VoiceMixer _mixer;
// ── Render thread state ──
@@ -58,11 +76,22 @@ internal sealed class Voice
/// <summary>Output sample at which playback stops (exclusive); long.MaxValue when not set.</summary>
internal long EndSample = long.MaxValue;
/// <summary>Output sample of the current anchor (see the class summary).</summary>
internal long AnchorSample;
/// <summary>Clip position at <see cref="AnchorSample"/>, in frames at the output sample rate (may be fractional).</summary>
internal double AnchorPosition;
/// <summary>Clip frames advanced per output sample (1 = original speed), at least 0.</summary>
internal double Pitch;
/// <summary>Linear gain applied to the clip samples.</summary>
internal double Volume;
/// <summary>True while the clip repeats at <see cref="Period"/>.</summary>
internal bool Loop;
/// <summary>Set by the render thread when playback has ended; the voice is then released and removed from the mixer.</summary>
internal bool Finished;
/// <summary>
@@ -74,7 +103,10 @@ internal sealed class Voice
/// <summary>Loop length in frames at the output sample rate (source duration * output rate).</summary>
internal double Period;
/// <summary>Read position registered on <see cref="Source"/> so the decoder thread keeps the chunks ahead loaded; null without a usable source.</summary>
internal AudioCursor? Cursor;
/// <summary>Chunk of <see cref="Source"/> currently held for reading (invalid when none). Render thread only.</summary>
internal ChunkView View;
internal Voice(VoiceMixer mixer, AudioClipSource? source, string name)
@@ -128,6 +160,9 @@ internal sealed class Voice
/// </summary>
internal void Apply(in Change change, long sample, int outputRate)
{
// Anchor changes made before the start take effect at the start sample (playback starts from the new state).
// EndTime uses the change's own value; Stop ends at the sample it is applied at, so a Stop before the start
// ends the voice before it plays.
var at = Math.Max(sample, StartSample);
switch (change.Kind)
{
@@ -155,6 +190,7 @@ internal sealed class Voice
}
}
/// <summary>Moves the anchor to <paramref name="sample"/>, keeping the (wrapped) position there, before a pitch or loop change.</summary>
private void Reanchor(long sample)
{
AnchorPosition = WrappedPositionAt(sample);