SHA256
Emulate AudioSource Playback State For Effect Sounds
This commit is contained in:
1 parent
5136127e39
commit
69d098e04c
3 files changed
+290
-95
No files matched your search
@@ -50,7 +50,7 @@ internal sealed class OfflineAudioMixer : IDisposable
|
||||
private readonly int _latencySamples;
|
||||
|
||||
// Main thread.
|
||||
private ConditionalWeakTable<AudioSource, Voice> _sourceVoices = new();
|
||||
private ConditionalWeakTable<AudioSource, SourceState> _sourceStates = new();
|
||||
private readonly List<Voice> _effectVoices = new();
|
||||
private int _effectVoicePruneAt = EffectVoicePruneCount;
|
||||
private AudioClipSource? _bgmSource;
|
||||
@@ -191,9 +191,9 @@ internal sealed class OfflineAudioMixer : IDisposable
|
||||
|
||||
/// <summary>
|
||||
/// Starts an effect sound at <paramref name="startTime"/>. An <paramref name="endTime"/> of 0 or more makes it loop until then.
|
||||
/// <paramref name="source"/> (when given) is mapped to the voice for later changes through the AudioSource.
|
||||
/// <paramref name="source"/> (when given) is mapped to the returned state, which reports and applies later changes through the AudioSource.
|
||||
/// </summary>
|
||||
public Voice RegisterSoundEvent(AudioSource? source, string clipName, double startTime, double endTime, float volume, float pitch, double now)
|
||||
public SourceState RegisterSoundEvent(AudioSource? source, string clipName, double startTime, double endTime, float volume, float pitch, double now)
|
||||
{
|
||||
var clipSource = GetOrAddSource(clipName);
|
||||
var loop = endTime >= 0;
|
||||
@@ -207,26 +207,30 @@ internal sealed class OfflineAudioMixer : IDisposable
|
||||
}
|
||||
_effectVoices.Add(voice);
|
||||
|
||||
if (source) MapSource(source!, voice);
|
||||
return voice;
|
||||
var state = new SourceState(voice, Math.Max(startTime, now), pitch, loop, loop ? endTime : double.PositiveInfinity,
|
||||
clipSource?.Duration ?? 0.0, _sampleRate);
|
||||
if (source) MapSource(source!, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>Maps <paramref name="source"/> to <paramref name="voice"/> for later changes through the AudioSource (replaces an earlier mapping).</summary>
|
||||
public void MapSource(AudioSource source, Voice voice)
|
||||
/// <summary>
|
||||
/// Maps <paramref name="source"/> to <paramref name="state"/> (replaces an earlier mapping) and takes the clip length and
|
||||
/// sample rate from the clip of the source.
|
||||
/// </summary>
|
||||
public void MapSource(AudioSource source, SourceState state)
|
||||
{
|
||||
_sourceVoices.Remove(source);
|
||||
_sourceVoices.Add(source, voice);
|
||||
state.BindClip(source.clip);
|
||||
_sourceStates.Remove(source);
|
||||
_sourceStates.Add(source, state);
|
||||
}
|
||||
|
||||
/// <summary>Voice started for <paramref name="source"/> by <see cref="RegisterSoundEvent"/>.</summary>
|
||||
public bool TryGetVoice(AudioSource? source, out Voice voice)
|
||||
/// <summary>State of the effect sound last started for <paramref name="source"/>. The mapping stays after the sound ends or stops.</summary>
|
||||
public bool TryGetSourceState(AudioSource? source, out SourceState state)
|
||||
{
|
||||
voice = null!;
|
||||
return source != null && _sourceVoices.TryGetValue(source, out voice);
|
||||
state = null!;
|
||||
return source != null && _sourceStates.TryGetValue(source, out state);
|
||||
}
|
||||
|
||||
public bool RemoveSourceMapping(AudioSource source) => _sourceVoices.Remove(source);
|
||||
|
||||
/// <summary>
|
||||
/// Stops every effect sound at <paramref name="now"/>, including ones scheduled to start later
|
||||
/// (AudioManager.StopAllSounds destroys every AudioSource in liveSources, which holds the sources of
|
||||
@@ -237,7 +241,7 @@ internal sealed class OfflineAudioMixer : IDisposable
|
||||
foreach (var voice in _effectVoices)
|
||||
voice.Stop(now);
|
||||
_effectVoices.Clear();
|
||||
_sourceVoices = new ConditionalWeakTable<AudioSource, Voice>();
|
||||
_sourceStates = new ConditionalWeakTable<AudioSource, SourceState>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -348,7 +352,7 @@ internal sealed class OfflineAudioMixer : IDisposable
|
||||
foreach (var filter in _filters)
|
||||
filter.Reset();
|
||||
}
|
||||
_sourceVoices = new ConditionalWeakTable<AudioSource, Voice>();
|
||||
_sourceStates = new ConditionalWeakTable<AudioSource, SourceState>();
|
||||
_effectVoices.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user