SHA256
Document Audio Pipeline
This commit is contained in:
22 files changed
+634
-12
No files matched your search
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user