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

+39 -3
View File
@@ -28,14 +28,31 @@ internal static class AudioSpectrumGenerator
/// </summary>
private const double MonoGain = 0.70710678118654752;
/// <summary>Store created by this class when the mixer store cannot be shared (disposed by <see cref="Clear"/>); otherwise null.</summary>
private static AudioClipStore? _ownStore;
/// <summary>Store the song source belongs to: the mixer store or <see cref="_ownStore"/>.</summary>
private static AudioClipStore? _store;
/// <summary>Song source at the output sample rate, or null when not initialized or the song could not be loaded.</summary>
private static AudioClipSource? _source;
/// <summary>Cursor on <see cref="_source"/> moved to each analysis window, so the decoder thread loads the chunks there.</summary>
private static AudioCursor? _cursor;
/// <summary>Chunk currently held for <see cref="ReadSample"/> (invalid when none).</summary>
private static ChunkView _view;
/// <summary>True after a chunk loading error was logged (logged once per render).</summary>
private static bool _loadErrorLogged;
/// <summary>RenderManager.frameCount of the spectra in <see cref="CachedSpectrums"/>; -1 when empty.</summary>
private static long _cachedFrame = -1;
/// <summary>
/// Spectra computed in the current render frame, by spectrum length and source channel. Several game objects read the
/// spectrum every frame; within one frame the render time does not change, so they receive the same values.
/// </summary>
private static readonly Dictionary<(int Size, int Channel), float[]> CachedSpectrums = new();
/// <summary>Registers the song clip. Call after the <see cref="OfflineAudioMixer"/> is created (null when audio is not rendered).</summary>
@@ -93,6 +110,17 @@ internal static class AudioSpectrumGenerator
CachedSpectrums.Clear();
}
/// <summary>
/// Fills <paramref name="samples"/> with the magnitude spectrum of the song at the current render time
/// (called by the AudioSource.GetSpectrumData patch while capturing with ProcessAudio on, for any AudioSource).
/// Bin k covers frequency k * outputRate / (2 * samples.Length) Hz in output time; values are 0 ~ 1.
/// Zeros are written when the song is not available, the conductor has not started the song, or the length is not a power of two.
/// <paramref name="window"/> is not read: the Blackman-Harris window is always used (every GetSpectrumData call in the game
/// passes FFTWindow.BlackmanHarris). Main thread only.
/// </summary>
/// <param name="samples">Destination; its length is the spectrum length (power of two).</param>
/// <param name="channel">Requested channel (0 = left, 1 = right), clamped to the song's channels; ignored for mono songs.</param>
/// <param name="window">Requested window (not used).</param>
public static void GetSpectrumData(float[] samples, int channel, FFTWindow window)
{
var n = samples.Length;
@@ -127,6 +155,8 @@ internal static class AudioSpectrumGenerator
var now = render.dspTimeBase + render.AudioTime;
var songSeconds = (now - conductor.dspTimeSong) * pitch - countdown;
// Window of 2n output samples centered on the current time: output sample i reads source frame first + i * pitch,
// so the window starts n output samples (n * pitch source frames) before the song position.
var fftSize = 2 * n;
var gain = volume * (_source.Channels == 1 ? MonoGain : 1.0);
var first = songSeconds * _store.OutputRate - n * pitch;
@@ -134,6 +164,7 @@ internal static class AudioSpectrumGenerator
var windowSamples = new float[fftSize];
try
{
// Keep the cursor at the window start so the decoder thread loads (and keeps) the chunks the window reads.
_cursor!.Position = Math.Min(Math.Max((long)first, 0), _source.TotalFrames - 1);
for (var i = 0; i < fftSize; i++)
{
@@ -178,6 +209,10 @@ internal static class AudioSpectrumGenerator
return _view.Data[(int)(frame - _view.StartFrame) * _view.Channels + channel];
}
/// <summary>
/// Magnitude spectrum of <paramref name="samples"/> (length n, a power of two) after the Blackman-Harris window:
/// writes min(n / 2, spectrum.Length) bins, bin k = min(|X[k]| * 2.5 / n, 1).
/// </summary>
private static void CalculateFFT(float[] samples, float[] spectrum)
{
var n = samples.Length;
@@ -199,10 +234,10 @@ internal static class AudioSpectrumGenerator
im[i] = 0f;
}
// Cooley-Tukey Radix-2 FFT
// Cooley-Tukey Radix-2 FFT (iterative, in place, decimation in time); m = log2(n) stages.
var m = (int)Math.Round(Math.Log(n, 2));
// Bit-reversal
// Bit-reversal: reorder the input so element i moves to the index with the bits of i reversed (j tracks that index).
var j = 0;
for (var i = 0; i < n - 1; i++)
{
@@ -225,7 +260,8 @@ internal static class AudioSpectrumGenerator
j += k;
}
// FFT stages
// FFT stages: stage s combines pairs of transforms of length le/2 into length le = 2^s with butterflies.
// (ur, ui) is the twiddle factor exp(-i * pi * sub / le2), advanced by multiplying with (wr, wi) = exp(-i * pi / le2).
for (var stage = 1; stage <= m; stage++)
{
var le = 1 << stage;