#nullable enable using System; using T3.Core.Animation; using T3.Core.Operator; namespace T3.Core.Audio; /// /// Converts sample data from BASS into a list of buffers that can then be used by Operators like [AudioWaveform]. /// /// Note: This static class delegates to . /// For multi-threaded analysis, create separate instances. /// public static class WaveFormProcessing { private static AudioAnalysisContext Context => AudioAnalysisContext.Default; #region Buffer Accessors (delegate to default context) /// /// Interleaved stereo sample buffer from BASS. /// Delegates to . /// internal static float[] InterleavenSampleBuffer => Context.InterleavedSampleBuffer; /// /// Result code from last BASS waveform data fetch. /// internal static int LastFetchResultCode { get => Context.LastWaveformFetchResult; set => Context.LastWaveformFetchResult = value; } /// /// To avoid unnecessary processing we only fetch wave data from BASS when requested once from an Operator. /// internal static bool RequestedOnce { get => Context.WaveformRequested; set => Context.WaveformRequested = value; } /// /// Left channel waveform samples. /// Delegates to . /// public static float[] WaveformLeftBuffer => Context.WaveformLeftBuffer; /// /// Right channel waveform samples. /// Delegates to . /// public static float[] WaveformRightBuffer => Context.WaveformRightBuffer; /// /// Low frequency waveform (filtered). /// Delegates to . /// public static float[] WaveformLowBuffer => Context.WaveformLowBuffer; /// /// Mid frequency waveform (filtered). /// Delegates to . /// public static float[] WaveformMidBuffer => Context.WaveformMidBuffer; /// /// High frequency waveform (filtered). /// Delegates to . /// public static float[] WaveformHighBuffer => Context.WaveformHighBuffer; #endregion /// /// Needs to be called from Operators that want to access Waveform Data. /// It will prevent multiple updates per frame. /// Uses the default context. /// public static void UpdateWaveformData() { UpdateWaveformData(Context); } /// /// Updates waveform data for a specific context. /// /// The analysis context to update public static void UpdateWaveformData(AudioAnalysisContext context) { context.WaveformRequested = true; // Prevent multiple updates if (Playback.FrameCount == context.LastWaveformUpdateFrame) return; context.LastWaveformUpdateFrame = Playback.FrameCount; if (context.LastWaveformFetchResult <= 0) return; // Note: During export, waveform data is populated via PopulateFromExportBuffer() // which is called in AudioRendering.GetFullMixDownBuffer(). That buffer contains // operator audio regardless of audio source mode. So we don't need to check // for external audio mode here - the data is already valid. var idx = 0; for (var it = 0; it < context.InterleavedSampleBuffer.Length;) { context.WaveformLeftBuffer[idx] = context.InterleavedSampleBuffer[it++]; context.WaveformRightBuffer[idx] = context.InterleavedSampleBuffer[it++]; idx += 1; } // Apply improved filters to create frequency-separated waveforms ProcessFilteredWaveformsImproved(context); } private struct FilterCoefficients(float a, float b) { public readonly float A = a; public readonly float B = b; } private static FilterCoefficients CalculateLowPassCoeffs(float cutoffFreq) { float sampleRate = AudioConfig.MixerFrequency; var rc = 1.0f / (2.0f * MathF.PI * cutoffFreq); var dt = 1.0f / sampleRate; var alpha = dt / (rc + dt); return new FilterCoefficients(alpha, 1.0f - alpha); } private static FilterCoefficients CalculateHighPassCoeffs(float cutoffFreq) { float sampleRate = AudioConfig.MixerFrequency; float rc = 1.0f / (2.0f * MathF.PI * cutoffFreq); float dt = 1.0f / sampleRate; float alpha = rc / (rc + dt); return new FilterCoefficients(alpha, alpha); } private static void ApplyLowPassFilterImproved(float[] input, float[] output, FilterCoefficients coeffs, ref float y1) { for (int i = 0; i < input.Length; i++) { output[i] = coeffs.A * input[i] + coeffs.B * y1; y1 = output[i]; } } private static void ApplyHighPassFilterImproved(float[] input, float[] output, FilterCoefficients coeffs, ref float y1, ref float x1) { for (int i = 0; i < input.Length; i++) { output[i] = coeffs.A * (y1 + input[i] - x1); y1 = output[i]; x1 = input[i]; } } private static void ProcessFilteredWaveformsImproved(AudioAnalysisContext context) { // Create mono mix for filtering (reuse temp buffer) for (int i = 0; i < AudioConfig.WaveformSampleCount; i++) { context.TempBuffer[i] = (context.WaveformLeftBuffer[i] + context.WaveformRightBuffer[i]) * 0.5f; } // Apply filters with state preservation for better continuity // Low frequencies: Pure low-pass at 250Hz ApplyLowPassFilterImproved(context.TempBuffer, context.WaveformLowBuffer, _lowPassCoeffs, ref context.LowFilterY1); // High frequencies: Pure high-pass at 2000Hz ApplyHighPassFilterImproved(context.TempBuffer, context.WaveformHighBuffer, _highPassCoeffs, ref context.HighFilterY1, ref context.HighFilterX1); // Mid-frequencies: High-pass at 250Hz, then low-pass at 2000Hz (band-pass) ApplyHighPassFilterImproved(context.TempBuffer, context.MidFilterBuffer, _midHighPassCoeffs, ref context.MidHighPassY1, ref context.MidHighPassX1); ApplyLowPassFilterImproved(context.MidFilterBuffer, context.WaveformMidBuffer, _midLowPassCoeffs, ref context.MidLowPassY1); } // Improved filter coefficients (calculated once, shared across all contexts) private static readonly FilterCoefficients _lowPassCoeffs = CalculateLowPassCoeffs(AudioConfig.LowPassCutoffFrequency); private static readonly FilterCoefficients _midHighPassCoeffs = CalculateHighPassCoeffs(AudioConfig.LowPassCutoffFrequency); private static readonly FilterCoefficients _midLowPassCoeffs = CalculateLowPassCoeffs(AudioConfig.HighPassCutoffFrequency); private static readonly FilterCoefficients _highPassCoeffs = CalculateHighPassCoeffs(AudioConfig.HighPassCutoffFrequency); /// /// Populates the waveform buffers from an export mixdown buffer. /// Accumulates samples across frames to provide a rolling window of audio data. /// Called during export to provide waveform data to AudioWaveform operator. /// Uses the default context. /// /// Interleaved stereo float buffer from export mixdown public static void PopulateFromExportBuffer(float[] mixBuffer) { PopulateFromExportBuffer(mixBuffer, Context); } /// /// Populates the waveform buffers from an export mixdown buffer for a specific context. /// /// Interleaved stereo float buffer from export mixdown /// The analysis context to populate public static void PopulateFromExportBuffer(float[] mixBuffer, AudioAnalysisContext context) { if (mixBuffer == null || mixBuffer.Length < 2) return; int interleavedSampleCount = AudioConfig.WaveformSampleCount * 2; // Shift existing data to make room for new samples int samplesToAdd = Math.Min(mixBuffer.Length, interleavedSampleCount); int samplesToShift = interleavedSampleCount - samplesToAdd; if (samplesToShift > 0) { // Shift old data left Array.Copy(context.ExportAccumulationBuffer, samplesToAdd, context.ExportAccumulationBuffer, 0, samplesToShift); } // Add new samples at the end int sourceStartIndex = Math.Max(0, mixBuffer.Length - samplesToAdd); Array.Copy(mixBuffer, sourceStartIndex, context.ExportAccumulationBuffer, samplesToShift, samplesToAdd); // Copy to the interleaved sample buffer Array.Copy(context.ExportAccumulationBuffer, 0, context.InterleavedSampleBuffer, 0, interleavedSampleCount); context.LastWaveformFetchResult = interleavedSampleCount; } /// /// Resets the export accumulation buffer. Should be called when starting a new export. /// Uses the default context. /// public static void ResetExportBuffer() { ResetExportBuffer(Context); } /// /// Resets the export accumulation buffer for a specific context. /// /// The analysis context to reset public static void ResetExportBuffer(AudioAnalysisContext context) { Array.Clear(context.ExportAccumulationBuffer, 0, context.ExportAccumulationBuffer.Length); Array.Clear(context.InterleavedSampleBuffer, 0, context.InterleavedSampleBuffer.Length); } }