forked from ppy/osu
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathFramedBeatmapClock.cs
More file actions
261 lines (201 loc) · 9.01 KB
/
Copy pathFramedBeatmapClock.cs
File metadata and controls
261 lines (201 loc) · 9.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Timing;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Screens.Play;
namespace osu.Game.Beatmaps
{
/// <summary>
/// A clock intended to be the single source-of-truth for beatmap timing.
///
/// It provides some functionality:
/// - Optionally applies (and tracks changes of) beatmap, user, and platform offsets (see ctor argument applyOffsets).
/// - Adjusts <see cref="Seek"/> operations to account for any applied offsets, seeking in raw "beatmap" time values.
/// - Exposes track length.
/// - Allows changing the source to a new track (for cases like editor track updating).
/// </summary>
public partial class FramedBeatmapClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock
{
private readonly bool applyOffsets;
private readonly OffsetCorrectionClock? userGlobalOffsetClock;
private readonly OffsetCorrectionClock? platformOffsetClock;
private readonly FramedOffsetClock? userBeatmapOffsetClock;
private readonly IFrameBasedClock finalClockSource;
private Bindable<double>? userAudioOffset;
private IDisposable? beatmapOffsetSubscription;
private readonly DecouplingFramedClock decoupledTrack;
private readonly InterpolatingFramedClock interpolatedTrack;
[Resolved]
private OsuConfigManager config { get; set; } = null!;
[Resolved]
private RealmAccess realm { get; set; } = null!;
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
[Resolved]
private AudioManager audioManager { get; set; } = null!;
private Bindable<bool> experimentalAudio = null!;
public bool IsRewinding { get; private set; }
public FramedBeatmapClock(bool applyOffsets, bool requireDecoupling, IClock? source = null)
{
this.applyOffsets = applyOffsets;
decoupledTrack = new DecouplingFramedClock(source) { AllowDecoupling = requireDecoupling };
// An interpolating clock is used to ensure precise time values even when the host audio subsystem is not reporting
// high precision times (on windows there's generally only 5-10ms reporting intervals, as an example).
interpolatedTrack = new InterpolatingFramedClock(decoupledTrack)
{
DriftRecoveryHalfLife = 80,
};
if (applyOffsets)
{
platformOffsetClock = new OffsetCorrectionClock(interpolatedTrack);
// User global offset (set in settings) should also be applied.
userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock);
// User per-beatmap offset will be applied to this final clock.
finalClockSource = userBeatmapOffsetClock = new FramedOffsetClock(userGlobalOffsetClock);
}
else
{
finalClockSource = interpolatedTrack;
}
}
protected override void LoadComplete()
{
base.LoadComplete();
if (applyOffsets)
{
Debug.Assert(userBeatmapOffsetClock != null);
Debug.Assert(userGlobalOffsetClock != null);
userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true);
experimentalAudio = audioManager.UseExperimentalWasapi.GetBoundCopy();
experimentalAudio.BindValueChanged(_ => updatePlatformOffset());
// TODO: this doesn't update when using ChangeSource() to change beatmap.
beatmapOffsetSubscription = realm.SubscribeToPropertyChanged(
r => r.Find<BeatmapInfo>(beatmap.Value.BeatmapInfo.ID)?.UserSettings,
settings => settings.Offset,
val =>
{
userBeatmapOffsetClock.Offset = val;
});
}
}
/// <summary>
/// Audio timings in general with newer BASS versions don't match stable.
/// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
/// </summary>
public const double WINDOWS_BASE_AUDIO_OFFSET = 15;
/// <summary>
/// An additional offset applied to account for experimental mode being much better.
/// </summary>
public const double WINDOWS_EXPERIMENTAL_AUDIO_OFFSET = -25;
private void updatePlatformOffset()
{
if (!applyOffsets)
return;
Debug.Assert(platformOffsetClock != null);
switch (RuntimeInfo.OS)
{
case RuntimeInfo.Platform.Windows:
platformOffsetClock.Offset = WINDOWS_BASE_AUDIO_OFFSET;
if (audioManager.UseExperimentalWasapi.Value)
platformOffsetClock.Offset += WINDOWS_EXPERIMENTAL_AUDIO_OFFSET;
return;
default:
platformOffsetClock.Offset = 0;
break;
}
}
protected override void Update()
{
base.Update();
finalClockSource.ProcessFrame();
if (Clock.ElapsedFrameTime != 0)
IsRewinding = Clock.ElapsedFrameTime < 0;
}
public double TotalAppliedOffset
{
get
{
if (!applyOffsets)
return 0;
Debug.Assert(userGlobalOffsetClock != null);
Debug.Assert(userBeatmapOffsetClock != null);
Debug.Assert(platformOffsetClock != null);
return userGlobalOffsetClock.RateAdjustedOffset + userBeatmapOffsetClock.Offset + platformOffsetClock.RateAdjustedOffset;
}
}
#region Delegation of IAdjustableClock / ISourceChangeableClock to decoupled clock.
public void ChangeSource(IClock? source) => decoupledTrack.ChangeSource(source);
public IClock Source => decoupledTrack.Source;
public void Reset()
{
decoupledTrack.Reset();
finalClockSource.ProcessFrame();
}
public void Start()
{
decoupledTrack.Start();
finalClockSource.ProcessFrame();
}
public void Stop()
{
decoupledTrack.Stop();
finalClockSource.ProcessFrame();
}
public bool Seek(double position)
{
bool success = decoupledTrack.Seek(position - TotalAppliedOffset);
finalClockSource.ProcessFrame();
return success;
}
public void ResetSpeedAdjustments() => decoupledTrack.ResetSpeedAdjustments();
public double Rate
{
get => decoupledTrack.Rate;
set => decoupledTrack.Rate = value;
}
#endregion
#region Delegation of IFrameBasedClock to clock with all offsets applied
public double CurrentTime => finalClockSource.CurrentTime;
public bool IsRunning => finalClockSource.IsRunning;
public void ProcessFrame()
{
// Noop to ensure an external consumer doesn't process the internal clock an extra time.
}
public double ElapsedFrameTime => finalClockSource.ElapsedFrameTime;
public double FramesPerSecond => finalClockSource.FramesPerSecond;
#endregion
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
beatmapOffsetSubscription?.Dispose();
}
public string GetSnapshot()
{
return
$"originalSource: {output(Source)}\n" +
$"userGlobalOffsetClock: {output(userGlobalOffsetClock)}\n" +
$"platformOffsetClock: {output(platformOffsetClock)}\n" +
$"userBeatmapOffsetClock: {output(userBeatmapOffsetClock)}\n" +
$"interpolatedTrack: {output(interpolatedTrack)}\n" +
$"decoupledTrack: {output(decoupledTrack)}\n" +
$"finalClockSource: {output(finalClockSource)}\n";
static string output(IClock? clock)
{
if (clock == null)
return "null";
if (clock is IFrameBasedClock framed)
return $"current: {clock.CurrentTime:N2} running: {clock.IsRunning} rate: {clock.Rate} elapsed: {framed.ElapsedFrameTime:N2}";
return $"current: {clock.CurrentTime:N2} running: {clock.IsRunning} rate: {clock.Rate}";
}
}
}
}