Files
tooll3--t3/Core/Audio/AudioAnalysisContext.cs
2026-07-13 13:13:17 +08:00

385 lines
14 KiB
C#

#nullable enable
using System;
using System.Collections.Generic;
namespace T3.Core.Audio;
/// <summary>
/// Holds all buffers and state for audio analysis (FFT, waveform, frequency bands).
///
/// <para><b>Thread Safety:</b></para>
/// This class is NOT thread-safe. All access to a single instance must be synchronized
/// externally if used from multiple threads. The default <see cref="Default"/> instance
/// is designed for single-threaded use on the main update loop.
///
/// <para><b>MultiThreading Migration Path:</b></para>
/// To enable multithreaded audio analysis:
/// <list type="number">
/// <item>Create separate <see cref="AudioAnalysisContext"/> instances per thread/consumer</item>
/// <item>Pass the context explicitly to analysis methods instead of using <see cref="Default"/></item>
/// <item>Ensure BASS channel reads are synchronized (BASS itself may have thread constraints)</item>
/// <item>Use locks or concurrent collections if sharing results between threads</item>
/// </list>
///
/// <para><b>Example - Per-Thread Analysis:</b></para>
/// <code>
/// // Create a dedicated context for background analysis
/// var backgroundContext = new AudioAnalysisContext();
///
/// // On background thread:
/// lock (bassLock)
/// {
/// AudioEngine.UpdateFftBuffer(streamHandle, backgroundContext);
/// }
/// backgroundContext.ProcessFftUpdate();
///
/// // Access results from backgroundContext.FrequencyBands, etc.
/// </code>
/// </summary>
public sealed class AudioAnalysisContext
{
/// <summary>
/// The default context used by the main thread audio update loop.
/// This is the instance used when no explicit context is provided.
///
/// <para><b>Warning:</b> Only access this from the main thread. For multithreaded
/// analysis, create separate instances.</para>
/// </summary>
internal static AudioAnalysisContext Default { get; } = new();
#region FFT Buffers
/// <summary>
/// Raw FFT gain values from BASS. Written by <see cref="AudioEngine.UpdateFftBufferFromSoundtrack"/>.
/// </summary>
internal readonly float[] FftGainBuffer = new float[AudioConfig.FftBufferSize];
/// <summary>
/// FFT values converted to dB and normalized to 0-1 range.
/// </summary>
internal readonly float[] FftNormalizedBuffer = new float[AudioConfig.FftBufferSize];
#endregion
#region Frequency Band Analysis
/// <summary>
/// Current frequency band levels (0-1 normalized).
/// </summary>
internal readonly float[] FrequencyBands = new float[AudioConfig.FrequencyBandCount];
/// <summary>
/// Peak-hold values for frequency bands with decay.
/// </summary>
internal readonly float[] FrequencyBandPeaks = new float[AudioConfig.FrequencyBandCount];
/// <summary>
/// Attack values for frequency bands (rate of increase).
/// </summary>
internal readonly float[] FrequencyBandAttacks = new float[AudioConfig.FrequencyBandCount];
/// <summary>
/// Peak attack values with slower decay.
/// </summary>
internal readonly float[] FrequencyBandAttackPeaks = new float[AudioConfig.FrequencyBandCount];
/// <summary>
/// Onset detection values for beat synchronization.
/// </summary>
internal readonly float[] FrequencyBandOnSets = new float[AudioConfig.FrequencyBandCount];
// Internal state for frequency band processing
private readonly float[] _frequencyBandsPrevious = new float[AudioConfig.FrequencyBandCount];
private readonly float[] _frequencyBandAverages = new float[AudioConfig.FrequencyBandCount];
private readonly float[] _bandStrengthSums = new float[AudioConfig.FrequencyBandCount];
private readonly Queue<float>[] _frequencyBandHistories;
#endregion
#region Waveform Buffers
/// <summary>
/// Interleaved stereo sample buffer from BASS. Written by <see cref="AudioEngine.UpdateFftBufferFromSoundtrack"/>.
/// </summary>
internal readonly float[] InterleavedSampleBuffer = new float[AudioConfig.WaveformSampleCount * 2];
/// <summary>
/// Result code from last BASS waveform data fetch.
/// </summary>
internal int LastWaveformFetchResult;
/// <summary>
/// Left channel waveform samples.
/// </summary>
internal readonly float[] WaveformLeftBuffer = new float[AudioConfig.WaveformSampleCount];
/// <summary>
/// Right channel waveform samples.
/// </summary>
internal readonly float[] WaveformRightBuffer = new float[AudioConfig.WaveformSampleCount];
/// <summary>
/// Low-frequency waveform (filtered).
/// </summary>
internal readonly float[] WaveformLowBuffer = new float[AudioConfig.WaveformSampleCount];
/// <summary>
/// Mid-frequency waveform (filtered).
/// </summary>
internal readonly float[] WaveformMidBuffer = new float[AudioConfig.WaveformSampleCount];
/// <summary>
/// High-frequency waveform (filtered).
/// </summary>
internal readonly float[] WaveformHighBuffer = new float[AudioConfig.WaveformSampleCount];
/// <summary>
/// Whether waveform data has been requested by an operator this session.
/// </summary>
internal bool WaveformRequested;
// Filter state for waveform processing (maintains continuity between frames)
internal float LowFilterY1;
internal float MidHighPassY1;
internal float MidHighPassX1;
internal float MidLowPassY1;
internal float HighFilterY1;
internal float HighFilterX1;
// Temporary buffers for waveform filtering
internal readonly float[] MidFilterBuffer = new float[AudioConfig.WaveformSampleCount];
internal readonly float[] TempBuffer = new float[AudioConfig.WaveformSampleCount];
// Export accumulation buffer
internal readonly float[] ExportAccumulationBuffer = new float[AudioConfig.WaveformSampleCount * 2];
#endregion
#region Frame Tracking
/// <summary>
/// Frame number when waveform was last updated (prevents duplicate updates per frame).
/// </summary>
internal int LastWaveformUpdateFrame = -1;
#endregion
/// <summary>
/// Creates a new audio analysis context with freshly allocated buffers.
/// </summary>
private AudioAnalysisContext()
{
_frequencyBandHistories = new Queue<float>[AudioConfig.FrequencyBandCount];
for (var i = 0; i < AudioConfig.FrequencyBandCount; i++)
{
_frequencyBandHistories[i] = new Queue<float>(FrequencyBandHistoryLength);
}
}
/// <summary>
/// Resets all buffers and state to initial values.
/// Useful when starting a new analysis session or switching audio sources.
/// </summary>
internal void Reset()
{
Array.Clear(FftGainBuffer, 0, FftGainBuffer.Length);
Array.Clear(FftNormalizedBuffer, 0, FftNormalizedBuffer.Length);
Array.Clear(FrequencyBands, 0, FrequencyBands.Length);
Array.Clear(FrequencyBandPeaks, 0, FrequencyBandPeaks.Length);
Array.Clear(FrequencyBandAttacks, 0, FrequencyBandAttacks.Length);
Array.Clear(FrequencyBandAttackPeaks, 0, FrequencyBandAttackPeaks.Length);
Array.Clear(FrequencyBandOnSets, 0, FrequencyBandOnSets.Length);
Array.Clear(_frequencyBandsPrevious, 0, _frequencyBandsPrevious.Length);
Array.Clear(_frequencyBandAverages, 0, _frequencyBandAverages.Length);
Array.Clear(_bandStrengthSums, 0, _bandStrengthSums.Length);
foreach (var queue in _frequencyBandHistories)
queue.Clear();
Array.Clear(InterleavedSampleBuffer, 0, InterleavedSampleBuffer.Length);
Array.Clear(WaveformLeftBuffer, 0, WaveformLeftBuffer.Length);
Array.Clear(WaveformRightBuffer, 0, WaveformRightBuffer.Length);
Array.Clear(WaveformLowBuffer, 0, WaveformLowBuffer.Length);
Array.Clear(WaveformMidBuffer, 0, WaveformMidBuffer.Length);
Array.Clear(WaveformHighBuffer, 0, WaveformHighBuffer.Length);
Array.Clear(MidFilterBuffer, 0, MidFilterBuffer.Length);
Array.Clear(TempBuffer, 0, TempBuffer.Length);
Array.Clear(ExportAccumulationBuffer, 0, ExportAccumulationBuffer.Length);
LowFilterY1 = 0;
MidHighPassY1 = 0;
MidHighPassX1 = 0;
MidLowPassY1 = 0;
HighFilterY1 = 0;
HighFilterX1 = 0;
LastWaveformFetchResult = 0;
LastWaveformUpdateFrame = -1;
WaveformRequested = false;
}
#region FFT Processing
private const float EstimatedAudioUpdatePeriod = 0.003f;
private const int FrequencyBandHistoryLength = (int)(1 / EstimatedAudioUpdatePeriod);
/// <summary>
/// Processes the FFT gain buffer to compute frequency bands, peaks, attacks, and onsets.
/// Call this after <see cref="FftGainBuffer"/> has been populated with FFT data.
/// </summary>
/// <param name="gainFactor">Multiplier for FFT gain values.</param>
/// <param name="decayFactor">Decay factor for peak values (0-1, higher = slower decay).</param>
internal void ProcessFftUpdate(float gainFactor = 1f, float decayFactor = 0.9f)
{
var lastTargetIndex = -1;
lock (FrequencyBands)
{
for (var binIndex = 0; binIndex < AudioConfig.FftBufferSize; binIndex++)
{
var gain = FftGainBuffer[binIndex] * gainFactor;
var gainDb = gain <= 0.000001f ? float.NegativeInfinity : 20 * MathF.Log10(gain);
var normalizedValue = RemapAndClamp(gainDb, -80, 0, 0, 1);
FftNormalizedBuffer[binIndex] = normalizedValue;
var bandIndex = _bandIndexForFftBin[binIndex];
if (bandIndex == NoBandIndex)
continue;
if (bandIndex != lastTargetIndex)
{
FrequencyBands[bandIndex] = 0;
lastTargetIndex = bandIndex;
}
FrequencyBands[bandIndex] = MathF.Max(FrequencyBands[bandIndex], normalizedValue);
}
}
UpdateSlidingWindowAverages();
lock (FrequencyBandPeaks)
{
for (var bandIndex = 0; bandIndex < AudioConfig.FrequencyBandCount; bandIndex++)
{
// Compute attacks
{
var lastPeak = FrequencyBandPeaks[bandIndex];
var decayed = lastPeak * decayFactor;
var currentValue = FrequencyBands[bandIndex];
var newPeak = MathF.Max(decayed, currentValue);
FrequencyBandPeaks[bandIndex] = newPeak;
const float attackAmplification = 4;
var newAttack = Clamp((newPeak - lastPeak) * attackAmplification, 0, 10000);
var lastAttackDecayed = FrequencyBandAttacks[bandIndex] * decayFactor;
FrequencyBandAttacks[bandIndex] = MathF.Max(newAttack, lastAttackDecayed);
}
FrequencyBandAttackPeaks[bandIndex] = MathF.Max(FrequencyBandAttackPeaks[bandIndex] * 0.995f, FrequencyBandAttacks[bandIndex]);
// Compute onsets for beat synchronization
{
var lastValue = _frequencyBandsPrevious[bandIndex];
var smoothed = _frequencyBandAverages[bandIndex];
var newValueAboveAverage = FrequencyBands[bandIndex] - smoothed;
_frequencyBandsPrevious[bandIndex] = newValueAboveAverage;
var delta = Clamp((newValueAboveAverage - lastValue) * 2, 0, 1000);
FrequencyBandOnSets[bandIndex] = delta;
}
}
}
}
private void UpdateSlidingWindowAverages()
{
for (var i = 0; i < AudioConfig.FrequencyBandCount; i++)
{
var currentStrength = FrequencyBands[i];
_frequencyBandHistories[i].Enqueue(currentStrength);
_bandStrengthSums[i] += currentStrength;
if (_frequencyBandHistories[i].Count > FrequencyBandHistoryLength)
{
_bandStrengthSums[i] -= _frequencyBandHistories[i].Dequeue();
}
var averageStrength = 0f;
if (_frequencyBandHistories[i].Count > 0)
{
averageStrength = _bandStrengthSums[i] / _frequencyBandHistories[i].Count;
}
_frequencyBandAverages[i] = averageStrength;
}
}
#endregion
#region Static Lookup Table
private const int NoBandIndex = -1;
/// <summary>
/// Lookup table mapping FFT bin indices to frequency band indices.
/// Shared across all contexts since it's read-only configuration data.
/// </summary>
private static readonly int[] _bandIndexForFftBin = InitializeBandLookupTable();
private static int[] InitializeBandLookupTable()
{
var r = new int[AudioConfig.FftBufferSize];
const float lowestBandFrequency = 55;
const float highestBandFrequency = 15000;
var maxOctave = MathF.Log2(highestBandFrequency / lowestBandFrequency);
for (var i = 0; i < AudioConfig.FftBufferSize; i++)
{
var bandIndex = NoBandIndex;
var freq = (float)i / AudioConfig.FftBufferSize * (AudioConfig.MixerFrequency / 2f);
switch (i)
{
case 0:
break;
case < 6:
bandIndex = i - 1;
break;
default:
{
var octave = MathF.Log2(freq / lowestBandFrequency);
var octaveNormalized = octave / maxOctave;
bandIndex = (int)(octaveNormalized * AudioConfig.FrequencyBandCount);
if (bandIndex >= AudioConfig.FrequencyBandCount)
bandIndex = NoBandIndex;
break;
}
}
r[i] = bandIndex;
}
return r;
}
#endregion
#region Helper Methods
private static float RemapAndClamp(float value, float inMin, float inMax, float outMin, float outMax)
{
var t = (value - inMin) / (inMax - inMin);
t = MathF.Max(0, MathF.Min(1, t));
return outMin + t * (outMax - outMin);
}
private static float Clamp(float value, float min, float max)
{
return MathF.Max(min, MathF.Min(max, value));
}
#endregion
}