#nullable enable using System; using System.Collections.Generic; namespace T3.Core.Audio; /// /// Holds all buffers and state for audio analysis (FFT, waveform, frequency bands). /// /// Thread Safety: /// This class is NOT thread-safe. All access to a single instance must be synchronized /// externally if used from multiple threads. The default instance /// is designed for single-threaded use on the main update loop. /// /// MultiThreading Migration Path: /// To enable multithreaded audio analysis: /// /// Create separate instances per thread/consumer /// Pass the context explicitly to analysis methods instead of using /// Ensure BASS channel reads are synchronized (BASS itself may have thread constraints) /// Use locks or concurrent collections if sharing results between threads /// /// /// Example - Per-Thread Analysis: /// /// // 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. /// /// public sealed class AudioAnalysisContext { /// /// The default context used by the main thread audio update loop. /// This is the instance used when no explicit context is provided. /// /// Warning: Only access this from the main thread. For multithreaded /// analysis, create separate instances. /// internal static AudioAnalysisContext Default { get; } = new(); #region FFT Buffers /// /// Raw FFT gain values from BASS. Written by . /// internal readonly float[] FftGainBuffer = new float[AudioConfig.FftBufferSize]; /// /// FFT values converted to dB and normalized to 0-1 range. /// internal readonly float[] FftNormalizedBuffer = new float[AudioConfig.FftBufferSize]; #endregion #region Frequency Band Analysis /// /// Current frequency band levels (0-1 normalized). /// internal readonly float[] FrequencyBands = new float[AudioConfig.FrequencyBandCount]; /// /// Peak-hold values for frequency bands with decay. /// internal readonly float[] FrequencyBandPeaks = new float[AudioConfig.FrequencyBandCount]; /// /// Attack values for frequency bands (rate of increase). /// internal readonly float[] FrequencyBandAttacks = new float[AudioConfig.FrequencyBandCount]; /// /// Peak attack values with slower decay. /// internal readonly float[] FrequencyBandAttackPeaks = new float[AudioConfig.FrequencyBandCount]; /// /// Onset detection values for beat synchronization. /// 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[] _frequencyBandHistories; #endregion #region Waveform Buffers /// /// Interleaved stereo sample buffer from BASS. Written by . /// internal readonly float[] InterleavedSampleBuffer = new float[AudioConfig.WaveformSampleCount * 2]; /// /// Result code from last BASS waveform data fetch. /// internal int LastWaveformFetchResult; /// /// Left channel waveform samples. /// internal readonly float[] WaveformLeftBuffer = new float[AudioConfig.WaveformSampleCount]; /// /// Right channel waveform samples. /// internal readonly float[] WaveformRightBuffer = new float[AudioConfig.WaveformSampleCount]; /// /// Low-frequency waveform (filtered). /// internal readonly float[] WaveformLowBuffer = new float[AudioConfig.WaveformSampleCount]; /// /// Mid-frequency waveform (filtered). /// internal readonly float[] WaveformMidBuffer = new float[AudioConfig.WaveformSampleCount]; /// /// High-frequency waveform (filtered). /// internal readonly float[] WaveformHighBuffer = new float[AudioConfig.WaveformSampleCount]; /// /// Whether waveform data has been requested by an operator this session. /// 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 /// /// Frame number when waveform was last updated (prevents duplicate updates per frame). /// internal int LastWaveformUpdateFrame = -1; #endregion /// /// Creates a new audio analysis context with freshly allocated buffers. /// private AudioAnalysisContext() { _frequencyBandHistories = new Queue[AudioConfig.FrequencyBandCount]; for (var i = 0; i < AudioConfig.FrequencyBandCount; i++) { _frequencyBandHistories[i] = new Queue(FrequencyBandHistoryLength); } } /// /// Resets all buffers and state to initial values. /// Useful when starting a new analysis session or switching audio sources. /// 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); /// /// Processes the FFT gain buffer to compute frequency bands, peaks, attacks, and onsets. /// Call this after has been populated with FFT data. /// /// Multiplier for FFT gain values. /// Decay factor for peak values (0-1, higher = slower decay). 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; /// /// Lookup table mapping FFT bin indices to frequency band indices. /// Shared across all contexts since it's read-only configuration data. /// 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 }