using System; using ADOFAIRenderer.Util.Audio.Mixer; using UnityEngine; namespace ADOFAIRenderer.Util.Audio; /// /// Playback state of a game AudioSource mapped to an effect , as the AudioSource reports it to the game /// (isPlaying, time, timeSamples, pitch). Main thread only. Times are seconds relative to the render start (the render clock that /// also drives AudioSettings.dspTime during rendering); clip positions are seconds into the clip. /// /// Reported values: /// - Playing: from the scheduled start until Stop(), the scheduled end time, or the natural end of the clip (not looping). /// Before the start, isPlaying is true and time is the position playback will start from. /// - time while playing: start position + elapsed time * pitch since the last change, wrapped at the clip length when looping. /// time is 0 while not playing. /// - timeSamples while playing: floor(time * clip frequency). While not playing: the last value set through time or timeSamples, /// reset to 0 by Stop(). /// - pitch: the last finite value set (NaN and infinity are ignored). Playback uses max(0, pitch). /// Changes made while not playing are stored and do not reach the voice. /// internal sealed class SourceState { private readonly double _start; private double _anchorTime; private double _anchorPosition; private double _pitch; private readonly bool _loop; private double _endTime = double.PositiveInfinity; private bool _stopped; private int _offsetSamples; private double _length; private int _frequency; /// Voice that plays the sound. /// Start of playback (seconds relative to the render start, not before the time it was scheduled). /// Pitch at the start. /// Repeats the clip. /// Scheduled end time (seconds relative to the render start), or infinity. /// Clip length in seconds (replaced by the game clip in ). /// Clip sample rate for timeSamples (replaced by the game clip in ). public SourceState(Voice voice, double start, double pitch, bool loop, double endTime, double length, int frequency) { Voice = voice; _start = start; _anchorTime = start; _pitch = pitch; _loop = loop; _endTime = endTime; _length = length; _frequency = Math.Max(1, frequency); } public Voice Voice { get; } /// Takes the clip length and sample rate from the clip of the game AudioSource. public void BindClip(AudioClip? clip) { if (!clip) return; _length = clip!.length; _frequency = Math.Max(1, clip.frequency); } /// Scheduled end time (see AudioSource.SetScheduledEndTime), seconds relative to the render start. public void SetEndTime(double endTime, double now) { if (!IsPlaying(now)) return; _endTime = endTime; Voice.SetEndTime(endTime, now); } public bool IsPlaying(double now) => !_stopped && now < Math.Min(_endTime, NaturalEnd()); public float GetTime(double now) => IsPlaying(now) ? (float)PositionAt(now) : 0f; public int GetTimeSamples(double now) => IsPlaying(now) ? (int)Math.Floor(PositionAt(now) * _frequency) : _offsetSamples; public float GetPitch() => (float)_pitch; public void SetTime(float seconds, double now) { _offsetSamples = (int)Math.Floor((double)seconds * _frequency); Seek(seconds, now); } public void SetTimeSamples(int samples, double now) { _offsetSamples = samples; Seek((double)samples / _frequency, now); } public void SetPitch(float pitch, double now) { if (float.IsNaN(pitch) || float.IsInfinity(pitch)) return; var playing = IsPlaying(now); if (playing) Reanchor(now); _pitch = pitch; if (playing) Voice.SetPitch(pitch, now); } public void Stop(double now) { if (IsPlaying(now)) Voice.Stop(now); _stopped = true; _offsetSamples = 0; } private double PlaybackPitch => Math.Max(0.0, _pitch); private void Seek(double seconds, double now) { if (!IsPlaying(now)) return; var position = Math.Max(0.0, seconds); _anchorTime = Math.Max(now, _start); _anchorPosition = position; Voice.SetPosition(position, now); } private void Reanchor(double now) { var time = Math.Max(now, _start); _anchorPosition = PositionAt(time); _anchorTime = time; } /// Clip position at (the start position before the start), wrapped when looping. private double PositionAt(double now) { var position = _anchorPosition + (Math.Max(now, _anchorTime) - _anchorTime) * PlaybackPitch; if (_loop && _length > 0 && position >= _length) position %= _length; return position; } /// Time at which the clip position reaches the clip length (infinity when looping or at pitch 0). private double NaturalEnd() { if (_loop || PlaybackPitch <= 0) return double.PositiveInfinity; return _anchorTime + Math.Max(0.0, _length - _anchorPosition) / PlaybackPitch; } }