• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

orion-ecs / keen-eye / 30113831081

24 Jul 2026 05:39PM UTC coverage: 64.224% (-0.07%) from 64.291%
30113831081

push

github

tyevco
fix(audio): Fix non-looping replay, WAV validation, paused-source recycling, double master volume (#1185,#1186,#1188,#1189)

- #1185: AudioSourceSystem now restarts a stopped backend source when State is set back to Playing (distinguishes a replay request from a natural finish via CurrentSound validity), so non-looping sources are replayable per the AudioSource.State contract.
- #1186: WavDecoder validates each chunk size against the remaining bytes and throws AudioLoadException on negative/oversized sizes, preventing the ArgumentOutOfRangeException slice crash and the negative-size infinite loop on untrusted files.
- #1188: SourcePool.Update only recycles Stopped sources (Paused treated as still-active), so Pause/PauseAll no longer kills pooled one-shots on the next frame.
- #1189: SilkAudioContext applies master + Master-channel volume only via the listener gain; ComputeEffectiveVolume no longer re-multiplies them into the per-source gain, eliminating the squared master volume and the new-vs-playing inconsistency.

Adds KeenEyes.Audio.Tests (AudioSourceSystem replay/finish, via fake IAudioDevice/IAudioContext) and KeenEyes.Audio.Silk.Tests (WavDecoder malformed-input validation). #1188/#1189 are OpenAL-device-bound and verified by code trace.

Closes #1185
Closes #1186
Closes #1188
Closes #1189

8612 of 12731 branches covered (67.65%)

Branch coverage included in aggregate %.

10 of 15 new or added lines in 4 files covered. (66.67%)

368 existing lines in 26 files now uncovered.

51312 of 80574 relevant lines covered (63.68%)

0.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.36
/src/KeenEyes.Animation/Systems/AnimationPlayerSystem.cs
1
using KeenEyes.Animation.Components;
2
using KeenEyes.Animation.Data;
3

4
namespace KeenEyes.Animation.Systems;
5

6
/// <summary>
7
/// System that updates animation playback state for entities with AnimationPlayer components.
8
/// </summary>
9
/// <remarks>
10
/// <para>
11
/// This system advances the playback time for all active AnimationPlayer components
12
/// and handles wrap modes (loop, ping-pong, once).
13
/// </para>
14
/// <para>
15
/// For skeletal animation, a separate system should sample the clip data and
16
/// apply poses to bone entities based on the current playback time.
17
/// </para>
18
/// </remarks>
19
public sealed class AnimationPlayerSystem : SystemBase
20
{
21
    private AnimationManager? manager;
22

23
    /// <inheritdoc />
24
    protected override void OnInitialize()
25
    {
26
        World.TryGetExtension<AnimationManager>(out manager);
2✔
27
    }
2✔
28

29
    /// <inheritdoc />
30
    public override void Update(float deltaTime)
31
    {
32
        manager ??= World.TryGetExtension<AnimationManager>(out var m) ? m : null;
1✔
33
        if (manager == null)
1✔
34
        {
35
            return;
1✔
36
        }
37

38
        foreach (var entity in World.Query<AnimationPlayer>())
1✔
39
        {
40
            ref var player = ref World.Get<AnimationPlayer>(entity);
1✔
41

42
            if (!player.IsPlaying || player.ClipId < 0)
1✔
43
            {
44
                continue;
45
            }
46

47
            if (!manager.TryGetClip(player.ClipId, out var clip) || clip == null)
1✔
48
            {
49
                continue;
50
            }
51

52
            // Save previous time for event detection
53
            player.PreviousTime = player.Time;
1✔
54

55
            // Signed timeline step for this frame
56
            var step = deltaTime * player.Speed * clip.Speed;
1✔
57

58
            // Get wrap mode
59
            var wrapMode = player.WrapModeOverride ?? clip.WrapMode;
1✔
60

61
            // Handle completion and wrap
62
            if (clip.Duration <= 0f)
1✔
63
            {
64
                player.Time = 0f;
1✔
65
                player.IsComplete = true;
1✔
66
                player.IsPlaying = false;
1✔
67
            }
68
            else if (wrapMode == WrapMode.PingPong)
1✔
69
            {
70
                // Ping-pong reflects the timeline, so the stored Time is not monotonic.
71
                // Advance from the current reflected position using the tracked direction
72
                // so the half-cycle parity survives across frames.
73
                (player.Time, player.PingPongReversed) =
1✔
74
                    AdvancePingPong(player.Time, player.PingPongReversed, step, clip.Duration);
1✔
75
                player.IsComplete = false;
1✔
76
            }
77
            else
78
            {
79
                player.Time += step;
1✔
80

81
                if (wrapMode == WrapMode.Once && player.Time >= clip.Duration)
1✔
82
                {
83
                    player.Time = clip.Duration;
1✔
84
                    player.IsComplete = true;
1✔
85
                    player.IsPlaying = false;
1✔
86
                }
87
                else if (wrapMode == WrapMode.ClampForever)
1✔
88
                {
89
                    player.Time = Math.Max(player.Time, 0f);
1✔
90
                    player.IsComplete = player.Time >= clip.Duration;
1✔
91
                }
92
                else
93
                {
94
                    // Wrap the time based on the effective wrap mode
95
                    player.Time = WrapTime(player.Time, clip.Duration, wrapMode);
1✔
96
                    player.IsComplete = false;
1✔
97
                }
98
            }
99
        }
100
    }
1✔
101

102
    private static float WrapTime(float time, float duration, WrapMode wrapMode)
103
    {
104
        return wrapMode switch
1✔
105
        {
1✔
106
            WrapMode.Once => Math.Clamp(time, 0f, duration),
1✔
107
            WrapMode.Loop => WrapMath.Repeat(time, duration),
1✔
UNCOV
108
            WrapMode.ClampForever => Math.Max(time, 0f),
×
UNCOV
109
            _ => time
×
110
        };
1✔
111
    }
112

113
    /// <summary>
114
    /// Advances a ping-pong timeline by a signed step, reflecting at both boundaries.
115
    /// </summary>
116
    /// <returns>The new reflected time and whether travel is now in reverse.</returns>
117
    private static (float Time, bool Reversed) AdvancePingPong(float time, bool reversed, float step, float duration)
118
    {
119
        var period = duration * 2f;
1✔
120

121
        // Reconstruct the monotonic phase from the reflected position + direction,
122
        // advance it, then fold back into a triangle wave.
123
        var phase = reversed ? period - time : time;
1✔
124
        var folded = WrapMath.Repeat(phase + step, period);
1✔
125

126
        return folded <= duration ? (folded, false) : (period - folded, true);
1✔
127
    }
128
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc