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 { /// Start of playback, seconds relative to the render start. private readonly double _start; /// Render time of the last position or pitch change (at least ), seconds. private double _anchorTime; /// Unwrapped clip position at , seconds into the clip. private double _anchorPosition; /// Last finite pitch set (reported as is; negative values play as 0). private double _pitch; private readonly bool _loop; /// Scheduled end time, seconds relative to the render start; infinity when not set. private double _endTime = double.PositiveInfinity; /// True after ; playback does not resume. private bool _stopped; /// timeSamples reported while not playing: the last value set through time/timeSamples, 0 after Stop. private int _offsetSamples; /// Clip length in seconds. private double _length; /// Clip sample rate in Hz (at least 1), used to convert between time and timeSamples. 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); } /// Voice of the that plays the sound; changes made while playing are queued on it. 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); } /// AudioSource.isPlaying at : not stopped and before both the scheduled end and the natural end. public bool IsPlaying(double now) => !_stopped && now < Math.Min(_endTime, NaturalEnd()); /// AudioSource.time at : clip position in seconds while playing, otherwise 0. public float GetTime(double now) => IsPlaying(now) ? (float)PositionAt(now) : 0f; /// AudioSource.timeSamples at : floor(position * frequency) while playing, otherwise the stored offset. public int GetTimeSamples(double now) => IsPlaying(now) ? (int)Math.Floor(PositionAt(now) * _frequency) : _offsetSamples; /// AudioSource.pitch: the last finite value set. public float GetPitch() => (float)_pitch; /// /// AudioSource.time setter: stores floor(seconds * frequency) as the offset and, while playing, moves the position to /// (negative values become 0) from (or from the start when not started yet). /// public void SetTime(float seconds, double now) { _offsetSamples = (int)Math.Floor((double)seconds * _frequency); Seek(seconds, now); } /// AudioSource.timeSamples setter: as with samples / frequency seconds. public void SetTimeSamples(int samples, double now) { _offsetSamples = samples; Seek((double)samples / _frequency, now); } /// /// AudioSource.pitch setter: ignores NaN and infinity. While playing, the position up to is kept /// (new anchor) and the voice changes rate at ; otherwise only the reported value changes. /// 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); } /// AudioSource.Stop(): stops the voice at when playing, marks the state stopped and clears the offset. public void Stop(double now) { if (IsPlaying(now)) Voice.Stop(now); _stopped = true; _offsetSamples = 0; } /// Rate used for playback: with negative values treated as 0 (as in ). private double PlaybackPitch => Math.Max(0.0, _pitch); /// /// While playing, sets the clip position to (at least 0) from max(now, start) and queues the same /// position change on the voice. /// 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); } /// Moves the anchor to max(now, start), keeping the current position, before a pitch change. 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; } }