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