SHA256
141 lines
5.4 KiB
C#
141 lines
5.4 KiB
C#
using System;
|
|
using ADOFAIRenderer.Util.Audio.Mixer;
|
|
using UnityEngine;
|
|
|
|
namespace ADOFAIRenderer.Util.Audio;
|
|
|
|
/// <summary>
|
|
/// Playback state of a game AudioSource mapped to an effect <see cref="Voice"/>, 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.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <param name="voice">Voice that plays the sound.</param>
|
|
/// <param name="start">Start of playback (seconds relative to the render start, not before the time it was scheduled).</param>
|
|
/// <param name="pitch">Pitch at the start.</param>
|
|
/// <param name="loop">Repeats the clip.</param>
|
|
/// <param name="endTime">Scheduled end time (seconds relative to the render start), or infinity.</param>
|
|
/// <param name="length">Clip length in seconds (replaced by the game clip in <see cref="BindClip"/>).</param>
|
|
/// <param name="frequency">Clip sample rate for timeSamples (replaced by the game clip in <see cref="BindClip"/>).</param>
|
|
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; }
|
|
|
|
/// <summary>Takes the clip length and sample rate from the clip of the game AudioSource.</summary>
|
|
public void BindClip(AudioClip? clip)
|
|
{
|
|
if (!clip) return;
|
|
_length = clip!.length;
|
|
_frequency = Math.Max(1, clip.frequency);
|
|
}
|
|
|
|
/// <summary>Scheduled end time (see AudioSource.SetScheduledEndTime), seconds relative to the render start.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Clip position at <paramref name="now"/> (the start position before the start), wrapped when looping.</summary>
|
|
private double PositionAt(double now)
|
|
{
|
|
var position = _anchorPosition + (Math.Max(now, _anchorTime) - _anchorTime) * PlaybackPitch;
|
|
if (_loop && _length > 0 && position >= _length) position %= _length;
|
|
return position;
|
|
}
|
|
|
|
/// <summary>Time at which the clip position reaches the clip length (infinity when looping or at pitch 0).</summary>
|
|
private double NaturalEnd()
|
|
{
|
|
if (_loop || PlaybackPitch <= 0) return double.PositiveInfinity;
|
|
return _anchorTime + Math.Max(0.0, _length - _anchorPosition) / PlaybackPitch;
|
|
}
|
|
}
|