mirror of
https://github.com/SK-la/osu-framework.git
synced 2026-03-13 11:20:31 +00:00
大范围重构
This commit is contained in:
@@ -1,83 +0,0 @@
|
||||
namespace osu.Framework.Audio.EzLatency
|
||||
{
|
||||
#nullable disable
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Basic tracker that exposes an API to record measurements.
|
||||
/// This implementation does not perform audio operations; it merely collects and forwards measurements.
|
||||
/// </summary>
|
||||
public class BasicEzLatencyTracker : IEzLatencyTracker
|
||||
{
|
||||
public event Action<EzLatencyRecord> OnMeasurement;
|
||||
|
||||
private int sampleRate = 48000;
|
||||
private readonly EzLatencyAnalyzer analyzer;
|
||||
|
||||
public BasicEzLatencyTracker()
|
||||
{
|
||||
analyzer = new EzLatencyAnalyzer();
|
||||
analyzer.OnNewRecord += record => OnMeasurement?.Invoke(record);
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
analyzer.Enabled = true;
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
analyzer.Enabled = false;
|
||||
}
|
||||
|
||||
public void SetSampleRate(int sampleRate) => this.sampleRate = sampleRate;
|
||||
|
||||
public void PushMeasurement(EzLatencyRecord record) => OnMeasurement?.Invoke(record);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
analyzer.Enabled = false;
|
||||
analyzer.OnNewRecord -= record => OnMeasurement?.Invoke(record);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录输入数据
|
||||
/// </summary>
|
||||
/// <param name="inputTime">输入时间</param>
|
||||
/// <param name="keyValue">按键值</param>
|
||||
public void RecordInputData(double inputTime, object keyValue = null)
|
||||
{
|
||||
analyzer.RecordInputData(inputTime, keyValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录判定数据
|
||||
/// </summary>
|
||||
/// <param name="judgeTime">判定时间</param>
|
||||
public void RecordJudgeData(double judgeTime)
|
||||
{
|
||||
analyzer.RecordJudgeData(judgeTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录播放数据
|
||||
/// </summary>
|
||||
/// <param name="playbackTime">播放时间</param>
|
||||
public void RecordPlaybackData(double playbackTime)
|
||||
{
|
||||
analyzer.RecordPlaybackData(playbackTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录硬件数据
|
||||
/// </summary>
|
||||
/// <param name="driverTime">驱动时间</param>
|
||||
/// <param name="outputHardwareTime">输出硬件时间</param>
|
||||
/// <param name="inputHardwareTime">输入硬件时间</param>
|
||||
/// <param name="latencyDifference">延迟差异</param>
|
||||
public void RecordHardwareData(double driverTime, double outputHardwareTime, double inputHardwareTime, double latencyDifference)
|
||||
{
|
||||
analyzer.RecordHardwareData(driverTime, outputHardwareTime, inputHardwareTime, latencyDifference);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
// 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.Logging;
|
||||
|
||||
namespace osu.Framework.Audio.EzLatency
|
||||
{
|
||||
/// <summary>
|
||||
/// 延迟检测管理器,负责协调输入和硬件数据的收集
|
||||
/// </summary>
|
||||
public class EzLatencyAnalyzer
|
||||
{
|
||||
private readonly Stopwatch stopwatch;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用延迟测量
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 当有新的延迟记录时触发
|
||||
/// </summary>
|
||||
public event Action<EzLatencyRecord>? OnNewRecord;
|
||||
|
||||
// 当前收集的数据
|
||||
private EzLatencyInputData currentInputData;
|
||||
private EzLatencyHardwareData currentHardwareData;
|
||||
|
||||
public EzLatencyAnalyzer()
|
||||
{
|
||||
stopwatch = Stopwatch.StartNew();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录输入数据
|
||||
/// </summary>
|
||||
/// <param name="inputTime">输入时间</param>
|
||||
/// <param name="keyValue">按键值</param>
|
||||
public void RecordInputData(double inputTime, object? keyValue = null)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
|
||||
currentInputData = new EzLatencyInputData
|
||||
{
|
||||
InputTime = inputTime,
|
||||
KeyValue = keyValue,
|
||||
JudgeTime = currentInputData.JudgeTime, // 保持已有的值
|
||||
PlaybackTime = currentInputData.PlaybackTime // 保持已有的值
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录判定数据
|
||||
/// </summary>
|
||||
/// <param name="judgeTime">判定时间</param>
|
||||
public void RecordJudgeData(double judgeTime)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
|
||||
currentInputData = new EzLatencyInputData
|
||||
{
|
||||
InputTime = currentInputData.InputTime,
|
||||
KeyValue = currentInputData.KeyValue,
|
||||
JudgeTime = judgeTime,
|
||||
PlaybackTime = currentInputData.PlaybackTime
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录播放数据
|
||||
/// </summary>
|
||||
/// <param name="playbackTime">播放时间</param>
|
||||
public void RecordPlaybackData(double playbackTime)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
|
||||
currentInputData = new EzLatencyInputData
|
||||
{
|
||||
InputTime = currentInputData.InputTime,
|
||||
KeyValue = currentInputData.KeyValue,
|
||||
JudgeTime = currentInputData.JudgeTime,
|
||||
PlaybackTime = playbackTime
|
||||
};
|
||||
|
||||
// 尝试生成完整记录
|
||||
TryGenerateCompleteRecord();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录硬件数据
|
||||
/// </summary>
|
||||
/// <param name="driverTime">驱动时间</param>
|
||||
/// <param name="outputHardwareTime">输出硬件时间</param>
|
||||
/// <param name="inputHardwareTime">输入硬件时间</param>
|
||||
/// <param name="latencyDifference">延迟差异</param>
|
||||
public void RecordHardwareData(double driverTime, double outputHardwareTime, double inputHardwareTime, double latencyDifference)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
|
||||
currentHardwareData = new EzLatencyHardwareData
|
||||
{
|
||||
DriverTime = driverTime,
|
||||
OutputHardwareTime = outputHardwareTime,
|
||||
InputHardwareTime = inputHardwareTime,
|
||||
LatencyDifference = latencyDifference
|
||||
};
|
||||
|
||||
// 尝试生成完整记录
|
||||
TryGenerateCompleteRecord();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试生成完整的延迟记录(当输入数据和硬件数据都有效时)
|
||||
/// </summary>
|
||||
private void TryGenerateCompleteRecord()
|
||||
{
|
||||
if (currentInputData.IsValid && currentHardwareData.IsValid)
|
||||
{
|
||||
var record = new EzLatencyRecord
|
||||
{
|
||||
Timestamp = DateTimeOffset.Now,
|
||||
InputTime = currentInputData.InputTime,
|
||||
JudgeTime = currentInputData.JudgeTime,
|
||||
PlaybackTime = currentInputData.PlaybackTime,
|
||||
DriverTime = currentHardwareData.DriverTime,
|
||||
OutputHardwareTime = currentHardwareData.OutputHardwareTime,
|
||||
InputHardwareTime = currentHardwareData.InputHardwareTime,
|
||||
LatencyDifference = currentHardwareData.LatencyDifference,
|
||||
MeasuredMs = currentInputData.PlaybackTime - currentInputData.InputTime,
|
||||
Note = "complete-latency-measurement",
|
||||
InputData = currentInputData,
|
||||
HardwareData = currentHardwareData
|
||||
};
|
||||
|
||||
// 触发事件
|
||||
try
|
||||
{
|
||||
OnNewRecord?.Invoke(record);
|
||||
|
||||
// 同时推送到中央服务
|
||||
EzLatencyService.Instance.PushRecord(record);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log($"延迟记录事件处理出错: {ex.Message}", LoggingTarget.Runtime, LogLevel.Error);
|
||||
}
|
||||
|
||||
// 清空已使用的数据,等待下一轮收集
|
||||
currentInputData = default;
|
||||
currentHardwareData = default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前时间戳(毫秒)
|
||||
/// </summary>
|
||||
/// <returns>当前时间戳</returns>
|
||||
public double GetCurrentTimestamp() => stopwatch.Elapsed.TotalMilliseconds;
|
||||
}
|
||||
}
|
||||
375
osu.Framework/Audio/EzLatency/EzLatencyCore.cs
Normal file
375
osu.Framework/Audio/EzLatency/EzLatencyCore.cs
Normal file
@@ -0,0 +1,375 @@
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Threading;
|
||||
|
||||
namespace osu.Framework.Audio.EzLatency
|
||||
{
|
||||
#nullable disable
|
||||
|
||||
// --- Structures and Record -------------------------------------------------
|
||||
|
||||
public struct EzLatencyInputData
|
||||
{
|
||||
public double InputTime;
|
||||
public object? KeyValue;
|
||||
public double JudgeTime;
|
||||
public double PlaybackTime;
|
||||
public bool IsValid => InputTime > 0 && JudgeTime > 0 && PlaybackTime > 0;
|
||||
}
|
||||
|
||||
public struct EzLatencyHardwareData
|
||||
{
|
||||
public double DriverTime;
|
||||
public double OutputHardwareTime;
|
||||
public double InputHardwareTime;
|
||||
public double LatencyDifference;
|
||||
public bool IsValid => OutputHardwareTime > 0 && DriverTime > 0;
|
||||
}
|
||||
|
||||
public class EzLatencyRecord
|
||||
{
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
public double MeasuredMs { get; set; }
|
||||
public string Note { get; set; }
|
||||
|
||||
public double InputTime { get; set; }
|
||||
public double JudgeTime { get; set; }
|
||||
public double PlaybackTime { get; set; }
|
||||
public double DriverTime { get; set; }
|
||||
public double OutputHardwareTime { get; set; }
|
||||
public double InputHardwareTime { get; set; }
|
||||
public double LatencyDifference { get; set; }
|
||||
|
||||
public EzLatencyInputData InputData { get; set; }
|
||||
public EzLatencyHardwareData HardwareData { get; set; }
|
||||
|
||||
public bool IsComplete => InputData.IsValid && HardwareData.IsValid;
|
||||
}
|
||||
|
||||
public enum AudioOutputMode
|
||||
{
|
||||
Default,
|
||||
WasapiShared,
|
||||
WasapiExclusive,
|
||||
Asio,
|
||||
}
|
||||
|
||||
// --- Analyzer --------------------------------------------------------------
|
||||
|
||||
public class EzLatencyAnalyzer
|
||||
{
|
||||
private readonly Stopwatch stopwatch;
|
||||
public bool Enabled { get; set; } = false;
|
||||
public event Action<EzLatencyRecord>? OnNewRecord;
|
||||
|
||||
private EzLatencyInputData currentInputData;
|
||||
private EzLatencyHardwareData currentHardwareData;
|
||||
private double recordStartTime;
|
||||
private const double timeout_ms = 5000;
|
||||
|
||||
public EzLatencyAnalyzer()
|
||||
{
|
||||
stopwatch = Stopwatch.StartNew();
|
||||
}
|
||||
|
||||
public void RecordInputData(double inputTime, object? keyValue = null)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
if (currentInputData.InputTime > 0)
|
||||
{
|
||||
currentInputData = default;
|
||||
currentHardwareData = default;
|
||||
}
|
||||
|
||||
currentInputData.InputTime = inputTime;
|
||||
currentInputData.KeyValue = keyValue;
|
||||
recordStartTime = stopwatch.Elapsed.TotalMilliseconds;
|
||||
}
|
||||
|
||||
public void RecordJudgeData(double judgeTime)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
currentInputData.JudgeTime = judgeTime;
|
||||
checkTimeout();
|
||||
}
|
||||
|
||||
public void RecordPlaybackData(double playbackTime)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
currentInputData.PlaybackTime = playbackTime;
|
||||
tryGenerateCompleteRecord();
|
||||
}
|
||||
|
||||
public void RecordHardwareData(double driverTime, double outputHardwareTime, double inputHardwareTime, double latencyDifference)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
currentHardwareData = new EzLatencyHardwareData
|
||||
{
|
||||
DriverTime = driverTime,
|
||||
OutputHardwareTime = outputHardwareTime,
|
||||
InputHardwareTime = inputHardwareTime,
|
||||
LatencyDifference = latencyDifference
|
||||
};
|
||||
|
||||
tryGenerateCompleteRecord();
|
||||
}
|
||||
|
||||
private void tryGenerateCompleteRecord()
|
||||
{
|
||||
if (!currentInputData.IsValid || !currentHardwareData.IsValid)
|
||||
{
|
||||
checkTimeout();
|
||||
return;
|
||||
}
|
||||
|
||||
var record = new EzLatencyRecord
|
||||
{
|
||||
Timestamp = DateTimeOffset.Now,
|
||||
InputTime = currentInputData.InputTime,
|
||||
JudgeTime = currentInputData.JudgeTime,
|
||||
PlaybackTime = currentInputData.PlaybackTime,
|
||||
DriverTime = currentHardwareData.DriverTime,
|
||||
OutputHardwareTime = currentHardwareData.OutputHardwareTime,
|
||||
InputHardwareTime = currentHardwareData.InputHardwareTime,
|
||||
LatencyDifference = currentHardwareData.LatencyDifference,
|
||||
MeasuredMs = currentInputData.PlaybackTime - currentInputData.InputTime,
|
||||
Note = "complete-latency-measurement",
|
||||
InputData = currentInputData,
|
||||
HardwareData = currentHardwareData
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
OnNewRecord?.Invoke(record);
|
||||
EzLatencyService.Instance.PushRecord(record);
|
||||
Logger.Log(string.Format("EzLatency 完整记录已生成: Input→Playback={0:F2}ms", record.PlaybackTime - record.InputTime), LoggingTarget.Runtime, LogLevel.Debug);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(string.Format("延迟记录事件处理出错: {0}", ex.Message), LoggingTarget.Runtime, LogLevel.Error);
|
||||
}
|
||||
|
||||
ClearCurrentData();
|
||||
}
|
||||
|
||||
public double GetCurrentTimestamp() => stopwatch.Elapsed.TotalMilliseconds;
|
||||
|
||||
private void checkTimeout()
|
||||
{
|
||||
if (recordStartTime > 0)
|
||||
{
|
||||
double elapsed = stopwatch.Elapsed.TotalMilliseconds - recordStartTime;
|
||||
if (elapsed > timeout_ms)
|
||||
{
|
||||
Logger.Log(string.Format("EzLatency 数据收集超时 ({0:F0}ms),清除旧数据", elapsed), LoggingTarget.Runtime, LogLevel.Debug);
|
||||
ClearCurrentData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearCurrentData()
|
||||
{
|
||||
currentInputData = default;
|
||||
currentHardwareData = default;
|
||||
recordStartTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Service ---------------------------------------------------------------
|
||||
|
||||
public class EzLatencyService
|
||||
{
|
||||
private static readonly Lazy<EzLatencyService> lazy = new Lazy<EzLatencyService>(() => new EzLatencyService());
|
||||
public static EzLatencyService Instance => lazy.Value;
|
||||
private EzLatencyService() { }
|
||||
|
||||
public void PushRecord(EzLatencyRecord record)
|
||||
{
|
||||
try { OnMeasurement?.Invoke(record); } catch (Exception ex) { Logger.Log($"EzLatencyService.PushRecord 异常: {ex.Message}", LoggingTarget.Runtime, LogLevel.Error); }
|
||||
}
|
||||
|
||||
public event Action<EzLatencyRecord> OnMeasurement;
|
||||
}
|
||||
|
||||
// --- Logger types ---------------------------------------------------------
|
||||
|
||||
public interface IEzLatencyLogger : IDisposable
|
||||
{
|
||||
void Log(EzLatencyRecord record);
|
||||
void Flush();
|
||||
event Action<EzLatencyRecord> OnRecord;
|
||||
}
|
||||
|
||||
public class EzLoggerAdapter : IEzLatencyLogger
|
||||
{
|
||||
private readonly Scheduler scheduler;
|
||||
private readonly string filePath;
|
||||
private StreamWriter fileWriter;
|
||||
public event Action<EzLatencyRecord> OnRecord;
|
||||
|
||||
public EzLoggerAdapter(Scheduler scheduler = null, string filePath = null)
|
||||
{
|
||||
this.scheduler = scheduler;
|
||||
this.filePath = filePath;
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
try { fileWriter = new StreamWriter(File.Open(filePath, FileMode.Append, FileAccess.Write, FileShare.Read)) { AutoFlush = true }; }
|
||||
catch (Exception ex) { Logger.Log($"EzLoggerAdapter: failed to open file {filePath}: {ex.Message}", LoggingTarget.Runtime, LogLevel.Error); }
|
||||
}
|
||||
}
|
||||
|
||||
public void Log(EzLatencyRecord record)
|
||||
{
|
||||
try { if (scheduler != null) scheduler.Add(() => OnRecord?.Invoke(record)); else OnRecord?.Invoke(record); } catch { }
|
||||
try { string line = $"[{record.Timestamp:O}] {record.MeasuredMs} ms - {record.Note}"; fileWriter?.WriteLine(line); } catch { }
|
||||
}
|
||||
|
||||
public void Flush() { try { fileWriter?.Flush(); } catch { } }
|
||||
public void Dispose() { try { fileWriter?.Dispose(); } catch { } }
|
||||
}
|
||||
|
||||
// --- Interfaces & basic tracker ------------------------------------------
|
||||
|
||||
public interface IEzLatencyTracker : IDisposable
|
||||
{
|
||||
void Start();
|
||||
void Stop();
|
||||
event Action<EzLatencyRecord> OnMeasurement;
|
||||
void SetSampleRate(int sampleRate);
|
||||
}
|
||||
|
||||
public interface IEzLatencyPlayback : IDisposable
|
||||
{
|
||||
void PlayTestTone();
|
||||
void StopTestTone();
|
||||
void SetSampleRate(int sampleRate);
|
||||
}
|
||||
|
||||
public class BasicEzLatencyTracker : IEzLatencyTracker
|
||||
{
|
||||
public event Action<EzLatencyRecord> OnMeasurement;
|
||||
private readonly EzLatencyAnalyzer analyzer;
|
||||
private readonly Action<EzLatencyRecord> recordHandler;
|
||||
|
||||
public BasicEzLatencyTracker()
|
||||
{
|
||||
analyzer = new EzLatencyAnalyzer();
|
||||
recordHandler = record => OnMeasurement?.Invoke(record);
|
||||
analyzer.OnNewRecord += recordHandler;
|
||||
}
|
||||
|
||||
public void Start() { analyzer.Enabled = true; }
|
||||
public void Stop() { analyzer.Enabled = false; }
|
||||
public void SetSampleRate(int sampleRate) { }
|
||||
public void PushMeasurement(EzLatencyRecord record) => OnMeasurement?.Invoke(record);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
analyzer.Enabled = false;
|
||||
if (recordHandler != null) analyzer.OnNewRecord -= recordHandler;
|
||||
}
|
||||
|
||||
public void RecordInputData(double inputTime, object keyValue = null) => analyzer.RecordInputData(inputTime, keyValue);
|
||||
public void RecordJudgeData(double judgeTime) => analyzer.RecordJudgeData(judgeTime);
|
||||
public void RecordPlaybackData(double playbackTime) => analyzer.RecordPlaybackData(playbackTime);
|
||||
public void RecordHardwareData(double driverTime, double outputHardwareTime, double inputHardwareTime, double latencyDifference) => analyzer.RecordHardwareData(driverTime, outputHardwareTime, inputHardwareTime, latencyDifference);
|
||||
}
|
||||
|
||||
// --- Statistics & Collector ----------------------------------------------
|
||||
|
||||
public class EzLatencyStatistics
|
||||
{
|
||||
public bool HasData => RecordCount > 0;
|
||||
public int RecordCount { get; set; }
|
||||
public double AvgInputToJudge { get; set; }
|
||||
public double AvgInputToPlayback { get; set; }
|
||||
public double AvgPlaybackToJudge { get; set; }
|
||||
public double MinInputToJudge { get; set; }
|
||||
public double MaxInputToJudge { get; set; }
|
||||
public double AvgHardwareLatency { get; set; }
|
||||
}
|
||||
|
||||
internal class EzLatencyCollector
|
||||
{
|
||||
private readonly List<EzLatencyRecord> records = new List<EzLatencyRecord>();
|
||||
private readonly object lockObject = new object();
|
||||
|
||||
public void AddRecord(EzLatencyRecord record)
|
||||
{
|
||||
if (!record.IsComplete)
|
||||
return;
|
||||
|
||||
lock (lockObject)
|
||||
{
|
||||
records.Add(record);
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (lockObject)
|
||||
{
|
||||
records.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public EzLatencyStatistics GetStatistics()
|
||||
{
|
||||
lock (lockObject)
|
||||
{
|
||||
if (records.Count == 0)
|
||||
return new EzLatencyStatistics { RecordCount = 0 };
|
||||
|
||||
var inputToJudge = records.Select(r => (double)(r.JudgeTime - r.InputTime)).ToList();
|
||||
var inputToPlayback = records.Select(r => (double)(r.PlaybackTime - r.InputTime)).ToList();
|
||||
var playbackToJudge = records.Select(r => (double)(r.JudgeTime - r.PlaybackTime)).ToList();
|
||||
var hardwareLatency = records.Select(r => (double)r.OutputHardwareTime).Where(h => h > 0).ToList();
|
||||
|
||||
return new EzLatencyStatistics
|
||||
{
|
||||
RecordCount = records.Count,
|
||||
AvgInputToJudge = inputToJudge.Average(),
|
||||
AvgInputToPlayback = inputToPlayback.Average(),
|
||||
AvgPlaybackToJudge = playbackToJudge.Average(),
|
||||
MinInputToJudge = inputToJudge.Min(),
|
||||
MaxInputToJudge = inputToJudge.Max(),
|
||||
AvgHardwareLatency = hardwareLatency.Count > 0 ? hardwareLatency.Average() : 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (lockObject)
|
||||
{
|
||||
return records.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Hardware timestamp provider (pluggable) -----------------------------
|
||||
|
||||
public interface IHwTimestampProvider
|
||||
{
|
||||
bool TryGetHardwareTimestamps(int channelHandle, out double driverTimeMs, out double outputHardwareTimeMs, out double inputHardwareTimeMs, out double latencyDifferenceMs);
|
||||
}
|
||||
|
||||
public class NullHwTimestampProvider : IHwTimestampProvider
|
||||
{
|
||||
public bool TryGetHardwareTimestamps(int channelHandle, out double driverTimeMs, out double outputHardwareTimeMs, out double inputHardwareTimeMs, out double latencyDifferenceMs)
|
||||
{
|
||||
driverTimeMs = outputHardwareTimeMs = inputHardwareTimeMs = latencyDifferenceMs = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,25 @@ namespace osu.Framework.Audio.EzLatency
|
||||
/// </summary>
|
||||
public class EzLatencyManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Pluggable hardware timestamp provider. Defaults to a null provider which indicates no hardware timestamps available.
|
||||
/// Override this with a platform-specific provider if desired.
|
||||
/// </summary>
|
||||
public static IHwTimestampProvider HwProvider = new NullHwTimestampProvider();
|
||||
|
||||
/// <summary>
|
||||
/// Global lightweight manager instance for best-effort framework instrumentation.
|
||||
/// Use this to record input/playback events from low-level framework code without resolving instances.
|
||||
/// </summary>
|
||||
public static readonly EzLatencyManager GLOBAL = new EzLatencyManager();
|
||||
|
||||
/// <summary>
|
||||
/// 延迟测试启用状态的绑定值,可与程序设置关联
|
||||
/// </summary>
|
||||
public readonly BindableBool Enabled = new BindableBool(false);
|
||||
|
||||
private readonly EzLatencyAnalyzer analyzer;
|
||||
private readonly EzLatencyCollector collector = new EzLatencyCollector();
|
||||
|
||||
public EzLatencyManager()
|
||||
{
|
||||
@@ -29,7 +42,19 @@ namespace osu.Framework.Audio.EzLatency
|
||||
}, true);
|
||||
|
||||
// 将分析器的记录事件转发给外部
|
||||
analyzer.OnNewRecord += record => OnNewRecord?.Invoke(record);
|
||||
analyzer.OnNewRecord += record =>
|
||||
{
|
||||
// add to collector for statistics
|
||||
try
|
||||
{
|
||||
collector.AddRecord(record);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
OnNewRecord?.Invoke(record);
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -91,6 +116,23 @@ namespace osu.Framework.Audio.EzLatency
|
||||
/// <returns>当前时间戳(毫秒)</returns>
|
||||
public double GetCurrentTimestamp() => analyzer.GetCurrentTimestamp();
|
||||
|
||||
/// <summary>
|
||||
/// 获取聚合的延迟统计数据(best-effort)。
|
||||
/// </summary>
|
||||
public EzLatencyStatistics GetStatistics() => collector.GetStatistics();
|
||||
|
||||
/// <summary>
|
||||
/// 清空统计收集器
|
||||
/// </summary>
|
||||
public void ClearStatistics() => collector.Clear();
|
||||
|
||||
/// <summary>
|
||||
/// 已收集的完整记录数量
|
||||
/// </summary>
|
||||
public int RecordCount => collector.Count;
|
||||
|
||||
// (Statistics, collector, and hardware provider types are defined in EzLatencyCore.cs)
|
||||
|
||||
/// <summary>
|
||||
/// 在gameplay开始时启用延迟测试
|
||||
/// </summary>
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
// 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;
|
||||
|
||||
namespace osu.Framework.Audio.EzLatency
|
||||
{
|
||||
#nullable disable
|
||||
public class EzLatencyRecord
|
||||
{
|
||||
// 高级字段
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
public double MeasuredMs { get; set; }
|
||||
public string Note { get; set; }
|
||||
|
||||
// 低级字段
|
||||
public double InputTime { get; set; }
|
||||
public double JudgeTime { get; set; }
|
||||
public double PlaybackTime { get; set; }
|
||||
public double DriverTime { get; set; }
|
||||
public double OutputHardwareTime { get; set; }
|
||||
public double InputHardwareTime { get; set; }
|
||||
public double LatencyDifference { get; set; }
|
||||
|
||||
// 可选的低级结构体,包含完整的输入和硬件数据
|
||||
public EzLatencyInputData InputData { get; set; }
|
||||
public EzLatencyHardwareData HardwareData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 检查记录是否包含完整的输入和硬件数据
|
||||
/// </summary>
|
||||
public bool IsComplete => InputData.IsValid && HardwareData.IsValid;
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// 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;
|
||||
|
||||
namespace osu.Framework.Audio.EzLatency
|
||||
{
|
||||
#nullable disable
|
||||
/// <summary>
|
||||
/// Standalone singleton service which collects low-level framework latency records
|
||||
/// and exposes higher-level EzLatencyRecord events for consumers in the osu layer.
|
||||
/// This keeps AudioManager untouched and centralizes latency collection in one place.
|
||||
/// </summary>
|
||||
public class EzLatencyService
|
||||
{
|
||||
private static readonly Lazy<EzLatencyService> lazy = new Lazy<EzLatencyService>(() => new EzLatencyService());
|
||||
|
||||
public static EzLatencyService Instance => lazy.Value;
|
||||
|
||||
private EzLatencyService()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allows producers (framework playback module) to push a fully-populated record
|
||||
/// into the service for forwarding to osu layer subscribers.
|
||||
/// </summary>
|
||||
public void PushRecord(EzLatencyRecord record)
|
||||
{
|
||||
try
|
||||
{
|
||||
OnMeasurement?.Invoke(record);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 记录异常但不抛出,以避免影响主程序流程
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a new high-level latency record is available.
|
||||
/// osu! layer should subscribe to this event and decide how to output logs.
|
||||
/// </summary>
|
||||
public event Action<EzLatencyRecord> OnMeasurement;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Framework.Audio.EzLatency
|
||||
{
|
||||
/// <summary>
|
||||
/// EzOsuLatency 输入数据结构体
|
||||
/// </summary>
|
||||
public struct EzLatencyInputData
|
||||
{
|
||||
public double InputTime;
|
||||
public object KeyValue;
|
||||
public double JudgeTime;
|
||||
public double PlaybackTime;
|
||||
|
||||
/// <summary>
|
||||
/// 检查输入数据是否完整
|
||||
/// </summary>
|
||||
public bool IsValid => InputTime > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EzOsuLatency 硬件数据结构体
|
||||
/// </summary>
|
||||
public struct EzLatencyHardwareData
|
||||
{
|
||||
public double DriverTime;
|
||||
public double OutputHardwareTime;
|
||||
public double InputHardwareTime;
|
||||
public double LatencyDifference;
|
||||
|
||||
/// <summary>
|
||||
/// 检查硬件数据是否完整
|
||||
/// </summary>
|
||||
public bool IsValid => OutputHardwareTime > 0 && DriverTime > 0;
|
||||
}
|
||||
|
||||
public enum AudioOutputMode
|
||||
{
|
||||
Default,
|
||||
WasapiShared,
|
||||
WasapiExclusive,
|
||||
Asio,
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
// 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.IO;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Threading;
|
||||
|
||||
namespace osu.Framework.Audio.EzLatency
|
||||
{
|
||||
#nullable disable
|
||||
public class EzLoggerAdapter : IEzLatencyLogger
|
||||
{
|
||||
private readonly Scheduler scheduler;
|
||||
private readonly string filePath;
|
||||
private StreamWriter fileWriter;
|
||||
|
||||
public event Action<EzLatencyRecord> OnRecord;
|
||||
|
||||
public EzLoggerAdapter(Scheduler scheduler = null, string filePath = null)
|
||||
{
|
||||
this.scheduler = scheduler;
|
||||
this.filePath = filePath;
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
fileWriter = new StreamWriter(File.Open(filePath, FileMode.Append, FileAccess.Write, FileShare.Read)) { AutoFlush = true };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log($"EzLoggerAdapter: failed to open file {filePath}: {ex.Message}", LoggingTarget.Runtime, LogLevel.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Log(EzLatencyRecord record)
|
||||
{
|
||||
// Raise OnRecord for consumers to handle (osu! will subscribe and use Logger there)
|
||||
try
|
||||
{
|
||||
if (scheduler != null)
|
||||
scheduler.Add(() => OnRecord?.Invoke(record));
|
||||
else
|
||||
OnRecord?.Invoke(record);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
// Also write to file if enabled (diagnostic only)
|
||||
try
|
||||
{
|
||||
string line = $"[{record.Timestamp:O}] {record.MeasuredMs} ms - {record.Note}";
|
||||
fileWriter?.WriteLine(line);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public void Flush()
|
||||
{
|
||||
try
|
||||
{
|
||||
fileWriter?.Flush();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
fileWriter?.Dispose();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace osu.Framework.Audio.EzLatency
|
||||
{
|
||||
public interface IEzLatencyLogger : IDisposable
|
||||
{
|
||||
void Log(EzLatencyRecord record);
|
||||
void Flush();
|
||||
|
||||
/// <summary>
|
||||
/// Fired whenever a new latency record is logged.
|
||||
/// Consumers (e.g. osu! layer) should subscribe to this to perform application-level logging/output.
|
||||
/// </summary>
|
||||
event Action<EzLatencyRecord> OnRecord;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace osu.Framework.Audio.EzLatency
|
||||
{
|
||||
#nullable disable
|
||||
using System;
|
||||
|
||||
public interface IEzLatencyPlayback : IDisposable
|
||||
{
|
||||
void PlayTestTone();
|
||||
void StopTestTone();
|
||||
void SetSampleRate(int sampleRate);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace osu.Framework.Audio.EzLatency
|
||||
{
|
||||
#nullable disable
|
||||
using System;
|
||||
|
||||
public interface IEzLatencyTracker : IDisposable
|
||||
{
|
||||
void Start();
|
||||
void Stop();
|
||||
event Action<EzLatencyRecord> OnMeasurement;
|
||||
void SetSampleRate(int sampleRate);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using System.Runtime.InteropServices;
|
||||
using ManagedBass;
|
||||
using ManagedBass.Mix;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Audio.EzLatency;
|
||||
using osu.Framework.Statistics;
|
||||
|
||||
namespace osu.Framework.Audio.Mixing.Bass
|
||||
@@ -175,7 +176,9 @@ namespace osu.Framework.Audio.Mixing.Bass
|
||||
/// If successful, the position is returned.
|
||||
/// </returns>
|
||||
public long ChannelGetPosition(IBassAudioChannel channel, PositionFlags mode = PositionFlags.Bytes)
|
||||
=> BassMix.ChannelGetPosition(channel.Handle, mode);
|
||||
{
|
||||
return BassMix.ChannelGetPosition(channel.Handle, mode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the playback position of a channel.
|
||||
@@ -209,7 +212,9 @@ namespace osu.Framework.Audio.Mixing.Bass
|
||||
/// <param name="flags">What levels to retrieve.</param>
|
||||
/// <returns><c>true</c> if successful, false otherwise.</returns>
|
||||
public bool ChannelGetLevel(IBassAudioChannel channel, [In, Out] float[] levels, float length, LevelRetrievalFlags flags)
|
||||
=> BassMix.ChannelGetLevel(channel.Handle, levels, length, flags) != -1;
|
||||
{
|
||||
return BassMix.ChannelGetLevel(channel.Handle, levels, length, flags) != -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the immediate sample data (or an FFT representation of it) of a channel.
|
||||
@@ -224,7 +229,9 @@ namespace osu.Framework.Audio.Mixing.Bass
|
||||
/// <para>When using the <see cref="DataFlags.Available"/> flag, the number of bytes in the channel's buffer is returned.</para>
|
||||
/// </returns>
|
||||
public int ChannelGetData(IBassAudioChannel channel, float[] buffer, int length)
|
||||
=> BassMix.ChannelGetData(channel.Handle, buffer, length);
|
||||
{
|
||||
return BassMix.ChannelGetData(channel.Handle, buffer, length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up a synchroniser on a mixer source channel.
|
||||
@@ -237,7 +244,9 @@ namespace osu.Framework.Audio.Mixing.Bass
|
||||
/// <param name="user">User instance data to pass to the callback function.</param>
|
||||
/// <returns>If successful, then the new synchroniser's handle is returned, else 0 is returned. Use <see cref="ManagedBass.Bass.LastError" /> to get the error code.</returns>
|
||||
public int ChannelSetSync(IBassAudioChannel channel, SyncFlags type, long parameter, SyncProcedure procedure, IntPtr user = default)
|
||||
=> BassMix.ChannelSetSync(channel.Handle, type, parameter, procedure, user);
|
||||
{
|
||||
return BassMix.ChannelSetSync(channel.Handle, type, parameter, procedure, user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a synchroniser from a mixer source channel.
|
||||
@@ -246,7 +255,9 @@ namespace osu.Framework.Audio.Mixing.Bass
|
||||
/// <param name="sync">Handle of the synchroniser to remove (return value of a previous <see cref="BassMix.ChannelSetSync(int,SyncFlags,long,SyncProcedure,IntPtr)" /> call).</param>
|
||||
/// <returns>If successful, <see langword="true" /> is returned, else <see langword="false" /> is returned. Use <see cref="ManagedBass.Bass.LastError" /> to get the error code.</returns>
|
||||
public bool ChannelRemoveSync(IBassAudioChannel channel, int sync)
|
||||
=> BassMix.ChannelRemoveSync(channel.Handle, sync);
|
||||
{
|
||||
return BassMix.ChannelRemoveSync(channel.Handle, sync);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Frees a channel's resources.
|
||||
@@ -330,7 +341,28 @@ namespace osu.Framework.Audio.Mixing.Bass
|
||||
flags |= BassFlags.MixerChanPause;
|
||||
|
||||
if (BassMix.MixerAddChannel(Handle, channel.Handle, flags))
|
||||
{
|
||||
activeChannels.Add(channel);
|
||||
|
||||
// Best-effort: ask the pluggable hardware timestamp provider for timestamps for this channel
|
||||
try
|
||||
{
|
||||
if (EzLatencyManager.HwProvider.TryGetHardwareTimestamps(channel.Handle, out double driverTimeMs, out double outputHardwareTimeMs, out double inputHardwareTimeMs, out double latencyDiff))
|
||||
{
|
||||
// Push the hardware data into the global manager (non-blocking and swallow any errors)
|
||||
try
|
||||
{
|
||||
EzLatencyManager.GLOBAL.RecordHardwareData(driverTimeMs, outputHardwareTimeMs, inputHardwareTimeMs, latencyDiff);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -25,6 +25,15 @@ namespace osu.Framework.Audio.Sample
|
||||
ObjectDisposedException.ThrowIf(IsDisposed, this);
|
||||
|
||||
Played = true;
|
||||
try
|
||||
{
|
||||
// Best-effort: notify global latency manager that a sample playback was requested.
|
||||
osu.Framework.Audio.EzLatency.EzLatencyManager.GLOBAL.RecordPlaybackEvent();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
OnPlay?.Invoke(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ using ManagedBass.Fx;
|
||||
using osu.Framework.IO;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Audio.Callbacks;
|
||||
using osu.Framework.Audio.EzLatency;
|
||||
using osu.Framework.Audio.Mixing;
|
||||
using osu.Framework.Audio.Mixing.Bass;
|
||||
using osu.Framework.Extensions;
|
||||
@@ -267,7 +268,19 @@ namespace osu.Framework.Audio.Track
|
||||
public override Task StartAsync() => EnqueueAction(() =>
|
||||
{
|
||||
if (startInternal())
|
||||
{
|
||||
isRunning = isPlayed = true;
|
||||
|
||||
// Record playback event for latency measurement (best-effort, non-blocking)
|
||||
try
|
||||
{
|
||||
EzLatencyManager.GLOBAL.RecordPlaybackEvent();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// swallow to avoid impacting playback
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
private bool startInternal()
|
||||
|
||||
@@ -12,6 +12,7 @@ using osu.Framework.Input.States;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
using JoystickState = osu.Framework.Input.States.JoystickState;
|
||||
using osu.Framework.Audio.EzLatency;
|
||||
|
||||
namespace osu.Framework.Input
|
||||
{
|
||||
@@ -149,8 +150,20 @@ namespace osu.Framework.Input
|
||||
case KeyDownEvent keyDown:
|
||||
if (keyDown.Repeat)
|
||||
return false;
|
||||
|
||||
// Apply framework input state update
|
||||
new KeyboardKeyInput(keyDown.Key, true).Apply(CurrentState, this);
|
||||
|
||||
// Record raw input timestamp at the earliest point possible for latency measurement.
|
||||
try
|
||||
{
|
||||
// Use global manager to avoid needing to resolve instances here.
|
||||
EzLatencyManager.GLOBAL.RecordInputEvent(keyDown.Key);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Swallow any errors to avoid affecting input handling.
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case KeyUpEvent keyUp:
|
||||
@@ -233,20 +246,33 @@ namespace osu.Framework.Input
|
||||
return;
|
||||
|
||||
var parentState = parentInputManager.CurrentState;
|
||||
var mouseDiff = (parentState?.Mouse?.Buttons ?? new ButtonStates<MouseButton>()).EnumerateDifference(CurrentState.Mouse.Buttons);
|
||||
var keyDiff = (parentState?.Keyboard.Keys ?? new ButtonStates<Key>()).EnumerateDifference(CurrentState.Keyboard.Keys);
|
||||
var touchDiff = (parentState?.Touch ?? new TouchState()).EnumerateDifference(CurrentState.Touch);
|
||||
var joyButtonDiff = (parentState?.Joystick?.Buttons ?? new ButtonStates<JoystickButton>()).EnumerateDifference(CurrentState.Joystick.Buttons);
|
||||
var midiDiff = (parentState?.Midi?.Keys ?? new ButtonStates<MidiKey>()).EnumerateDifference(CurrentState.Midi.Keys);
|
||||
var tabletPenDiff = (parentState?.Tablet?.PenButtons ?? new ButtonStates<TabletPenButton>()).EnumerateDifference(CurrentState.Tablet.PenButtons);
|
||||
var tabletAuxiliaryDiff = (parentState?.Tablet?.AuxiliaryButtons ?? new ButtonStates<TabletAuxiliaryButton>()).EnumerateDifference(CurrentState.Tablet.AuxiliaryButtons);
|
||||
var mouseButtons = parentState?.Mouse?.Buttons ?? new ButtonStates<MouseButton>();
|
||||
var mouseDiff = mouseButtons.EnumerateDifference(CurrentState.Mouse.Buttons);
|
||||
|
||||
var keyboardKeys = parentState?.Keyboard.Keys ?? new ButtonStates<Key>();
|
||||
var keyDiff = keyboardKeys.EnumerateDifference(CurrentState.Keyboard.Keys);
|
||||
|
||||
var touches = parentState?.Touch ?? new TouchState();
|
||||
var (touchesDeactivated, _) = touches.EnumerateDifference(CurrentState.Touch);
|
||||
|
||||
var joystickButtons = parentState?.Joystick?.Buttons ?? new ButtonStates<JoystickButton>();
|
||||
var joyButtonDiff = joystickButtons.EnumerateDifference(CurrentState.Joystick.Buttons);
|
||||
|
||||
var midiKeys = parentState?.Midi?.Keys ?? new ButtonStates<MidiKey>();
|
||||
var midiDiff = midiKeys.EnumerateDifference(CurrentState.Midi.Keys);
|
||||
|
||||
var penButtons = parentState?.Tablet?.PenButtons ?? new ButtonStates<TabletPenButton>();
|
||||
var tabletPenDiff = penButtons.EnumerateDifference(CurrentState.Tablet.PenButtons);
|
||||
|
||||
var auxButtons = parentState?.Tablet?.AuxiliaryButtons ?? new ButtonStates<TabletAuxiliaryButton>();
|
||||
var tabletAuxiliaryDiff = auxButtons.EnumerateDifference(CurrentState.Tablet.AuxiliaryButtons);
|
||||
|
||||
if (mouseDiff.Released.Length > 0)
|
||||
new MouseButtonInput(mouseDiff.Released.Select(button => new ButtonInputEntry<MouseButton>(button, false))).Apply(CurrentState, this);
|
||||
foreach (var key in keyDiff.Released)
|
||||
new KeyboardKeyInput(key, false).Apply(CurrentState, this);
|
||||
if (touchDiff.deactivated.Length > 0)
|
||||
new TouchInput(touchDiff.deactivated, false).Apply(CurrentState, this);
|
||||
if (touchesDeactivated.Length > 0)
|
||||
new TouchInput(touchesDeactivated, false).Apply(CurrentState, this);
|
||||
foreach (var button in joyButtonDiff.Released)
|
||||
new JoystickButtonInput(button, false).Apply(CurrentState, this);
|
||||
foreach (var key in midiDiff.Released)
|
||||
|
||||
Reference in New Issue
Block a user