Files
2026-07-13 13:13:17 +08:00

388 lines
12 KiB
C#

using System;
namespace T3.Core.Audio;
/// <summary>
/// Shared ADSR envelope calculator that can be used by any operator.
/// This is a pure calculation class with no dependencies on the operator system.
///
/// Supports two modes of operation:
/// 1. Frame-based: Call Update() once per frame with delta time (for UI/visual use)
/// 2. Sample-based: Call UpdateSample() per audio sample (for audio thread use)
/// </summary>
public sealed class AdsrCalculator
{
public enum Stage
{
Idle = 0,
Attack = 1,
Decay = 2,
Sustain = 3,
Release = 4
}
public enum TriggerMode
{
Gate = 0,
Trigger = 1
}
// Current state
public float Value { get; private set; }
public Stage CurrentStage { get; private set; } = Stage.Idle;
public bool IsActive => CurrentStage != Stage.Idle;
// Parameters (can be updated at any time)
private float _attackTime = 0.01f;
private float _decayTime = 0.1f;
private float _sustainLevel = 0.7f;
private float _releaseTime = 0.3f;
private TriggerMode _mode = TriggerMode.Gate;
private float _duration = float.MaxValue;
private int _sampleRate = 48000;
// Internal state for frame-based updates
private bool _previousGate;
private double _lastTime;
private float _stageTime;
private float _totalTime;
// Internal state for sample-based updates
private long _stageSampleCount;
private long _totalSamplesPlayed;
private long _durationSamples;
// Shared state
private double _releaseStartValue;
// Trigger signals for sample-based mode
private volatile int _triggerAttackSignal;
private volatile int _triggerReleaseSignal;
/// <summary>
/// Sets the ADSR parameters. Thread-safe for sample-based mode.
/// </summary>
public void SetParameters(float attack, float decay, float sustain, float release)
{
_attackTime = Math.Max(0.001f, attack);
_decayTime = Math.Max(0.001f, decay);
_sustainLevel = Math.Clamp(sustain, 0f, 1f);
_releaseTime = Math.Max(0.001f, release);
}
/// <summary>
/// Sets the trigger mode. Thread-safe for sample-based mode.
/// </summary>
public void SetMode(TriggerMode mode)
{
_mode = mode;
}
/// <summary>
/// Sets the duration for Trigger mode. Thread-safe for sample-based mode.
/// </summary>
public void SetDuration(float duration)
{
_duration = duration > 0 ? duration : float.MaxValue;
}
/// <summary>
/// Sets the sample rate for sample-based updates.
/// </summary>
public void SetSampleRate(int sampleRate)
{
_sampleRate = Math.Max(1, sampleRate);
}
/// <summary>
/// Triggers the attack phase. Thread-safe for use from UI thread.
/// </summary>
public void TriggerAttack()
{
System.Threading.Interlocked.Exchange(ref _triggerAttackSignal, 1);
}
/// <summary>
/// Triggers the release phase. Thread-safe for use from UI thread.
/// </summary>
public void TriggerRelease()
{
System.Threading.Interlocked.Exchange(ref _triggerReleaseSignal, 1);
}
/// <summary>
/// Updates the envelope for one audio sample. Call this from the audio thread.
/// Returns the current envelope value (0-1).
/// </summary>
public float UpdateSample()
{
// Check for trigger signals (thread-safe)
if (System.Threading.Interlocked.Exchange(ref _triggerAttackSignal, 0) == 1)
{
CurrentStage = Stage.Attack;
_stageSampleCount = 0;
_totalSamplesPlayed = 0;
_durationSamples = _duration >= float.MaxValue / 2f
? long.MaxValue
: (long)(_duration * _sampleRate);
}
if (System.Threading.Interlocked.Exchange(ref _triggerReleaseSignal, 0) == 1)
{
if (CurrentStage != Stage.Idle && CurrentStage != Stage.Release)
{
_releaseStartValue = Value;
_stageSampleCount = 0;
CurrentStage = Stage.Release;
}
}
// Convert times to samples
long attackSamples = (long)(_attackTime * _sampleRate);
long decaySamples = (long)(_decayTime * _sampleRate);
long releaseSamples = (long)(_releaseTime * _sampleRate);
// Check duration limit in Trigger mode
if (_mode == TriggerMode.Trigger &&
CurrentStage != Stage.Idle && CurrentStage != Stage.Release &&
_durationSamples < long.MaxValue &&
_totalSamplesPlayed >= _durationSamples)
{
_releaseStartValue = Value;
_stageSampleCount = 0;
CurrentStage = Stage.Release;
}
// Calculate envelope value
switch (CurrentStage)
{
case Stage.Idle:
Value = 0;
break;
case Stage.Attack:
if (_stageSampleCount < attackSamples)
{
Value = (float)_stageSampleCount / attackSamples;
}
else
{
Value = 1.0f;
_stageSampleCount = 0;
CurrentStage = Stage.Decay;
}
break;
case Stage.Decay:
if (_stageSampleCount < decaySamples)
{
float progress = (float)_stageSampleCount / decaySamples;
Value = 1.0f - progress * (1.0f - _sustainLevel);
}
else
{
Value = _sustainLevel;
_stageSampleCount = 0;
CurrentStage = Stage.Sustain;
}
break;
case Stage.Sustain:
Value = _sustainLevel;
break;
case Stage.Release:
if (_stageSampleCount < releaseSamples)
{
float progress = (float)_stageSampleCount / releaseSamples;
Value = (float)(_releaseStartValue * (1.0 - progress));
}
else
{
Value = 0;
CurrentStage = Stage.Idle;
_stageSampleCount = 0;
}
break;
}
// Update counters
_stageSampleCount++;
if (CurrentStage != Stage.Idle && CurrentStage != Stage.Release)
{
_totalSamplesPlayed++;
}
Value = Math.Clamp(Value, 0f, 1f);
return Value;
}
/// <summary>
/// Updates the envelope state based on the current gate/trigger input.
/// Call this once per frame with the current time and parameters (for UI/visual use).
/// </summary>
/// <param name="gate">Gate/trigger input (true = on)</param>
/// <param name="currentTime">Current time in seconds (e.g., context.LocalFxTime)</param>
/// <param name="attack">Attack time in seconds</param>
/// <param name="decay">Decay time in seconds</param>
/// <param name="sustain">Sustain level (0-1)</param>
/// <param name="release">Release time in seconds</param>
/// <param name="mode">Trigger mode (Gate or Trigger)</param>
/// <param name="duration">Duration for Trigger mode (ignored in Gate mode)</param>
public void Update(bool gate, double currentTime, float attack, float decay, float sustain, float release, TriggerMode mode, float duration = float.MaxValue)
{
// Store parameters
SetParameters(attack, decay, sustain, release);
_mode = mode;
_duration = duration > 0 ? duration : float.MaxValue;
// Detect edges
var risingEdge = gate && !_previousGate;
var fallingEdge = !gate && _previousGate;
_previousGate = gate;
// Get time delta
var deltaTime = (float)(currentTime - _lastTime);
_lastTime = currentTime;
// Clamp delta time to avoid huge jumps
if (deltaTime < 0 || deltaTime > 1.0f)
deltaTime = 0.016f; // ~60fps fallback
if (mode == TriggerMode.Gate)
{
// GATE MODE: Envelope follows the input signal
if (risingEdge)
{
CurrentStage = Stage.Attack;
_stageTime = 0;
_releaseStartValue = Value;
}
else if (fallingEdge && CurrentStage != Stage.Idle)
{
CurrentStage = Stage.Release;
_stageTime = 0;
_releaseStartValue = Value;
}
}
else // Trigger mode
{
// TRIGGER MODE: Rising edge starts full ADSR cycle
if (risingEdge)
{
CurrentStage = Stage.Attack;
_stageTime = 0;
_totalTime = 0;
_releaseStartValue = Value;
}
// Check if duration reached (trigger release)
if (CurrentStage != Stage.Idle &&
CurrentStage != Stage.Release &&
_duration < float.MaxValue &&
_totalTime >= _duration)
{
CurrentStage = Stage.Release;
_stageTime = 0;
_releaseStartValue = Value;
}
}
// Update timing
_stageTime += deltaTime;
if (CurrentStage != Stage.Release && CurrentStage != Stage.Idle)
{
_totalTime += deltaTime;
}
// Calculate envelope value based on current stage
switch (CurrentStage)
{
case Stage.Idle:
Value = 0;
break;
case Stage.Attack:
if (_stageTime < _attackTime)
{
Value = _stageTime / _attackTime;
}
else
{
Value = 1.0f;
CurrentStage = Stage.Decay;
_stageTime = 0;
}
break;
case Stage.Decay:
if (_stageTime < _decayTime)
{
var progress = _stageTime / _decayTime;
Value = 1.0f - progress * (1.0f - _sustainLevel);
}
else
{
Value = _sustainLevel;
CurrentStage = Stage.Sustain;
_stageTime = 0;
}
break;
case Stage.Sustain:
Value = _sustainLevel;
// Stay in sustain until release is triggered
break;
case Stage.Release:
if (_stageTime < _releaseTime)
{
var progress = _stageTime / _releaseTime;
Value = (float)(_releaseStartValue * (1.0f - progress));
}
else
{
Value = 0;
CurrentStage = Stage.Idle;
_stageTime = 0;
}
break;
}
Value = Math.Clamp(Value, 0f, 1f);
}
/// <summary>
/// Manually trigger the release phase (e.g., when gate goes low in gate mode)
/// For frame-based mode. For sample-based, use TriggerRelease().
/// </summary>
public void StartRelease()
{
if (CurrentStage != Stage.Idle && CurrentStage != Stage.Release)
{
_releaseStartValue = Value;
_stageTime = 0;
_stageSampleCount = 0;
CurrentStage = Stage.Release;
}
}
/// <summary>
/// Reset the envelope to idle state
/// </summary>
public void Reset()
{
Value = 0;
CurrentStage = Stage.Idle;
_stageTime = 0;
_totalTime = 0;
_stageSampleCount = 0;
_totalSamplesPlayed = 0;
_releaseStartValue = 0;
_previousGate = false;
System.Threading.Interlocked.Exchange(ref _triggerAttackSignal, 0);
System.Threading.Interlocked.Exchange(ref _triggerReleaseSignal, 0);
}
}