mirror of
https://github.com/SK-la/osu-framework.git
synced 2026-03-13 11:20:31 +00:00
修改架构\过滤规则\调整记录结构\修改单位
增加开关机制 增强延迟检测框架
This commit is contained in:
95
osu.Framework/Audio/EzDriverModule.cs
Normal file
95
osu.Framework/Audio/EzDriverModule.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
// 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 osu.Framework.Logging;
|
||||
|
||||
namespace osu.Framework.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// EzDriverModule 负责音频驱动的设置和管理。
|
||||
/// 主要职责包括配置 ASIO 和 WASAPI 独占模式,设置采样率和缓冲区大小,并获取驱动延迟反馈。
|
||||
/// </summary>
|
||||
public class EzDriverModule
|
||||
{
|
||||
/// <summary>
|
||||
/// EzOsuLatency 总开关 - 控制是否启用延迟测量
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
private readonly System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
|
||||
|
||||
// 存储驱动时间
|
||||
internal double DriverTime;
|
||||
|
||||
public EzDriverModule()
|
||||
{
|
||||
stopwatch.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录驱动操作的时间戳
|
||||
/// </summary>
|
||||
/// <param name="timestamp">操作的时间戳</param>
|
||||
public void RecordTimestamp(DateTime timestamp)
|
||||
{
|
||||
if (Enabled)
|
||||
{
|
||||
DriverTime = stopwatch.Elapsed.TotalMilliseconds;
|
||||
// 不直接打印日志
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 运行驱动延迟测试
|
||||
/// </summary>
|
||||
public void RunTest()
|
||||
{
|
||||
if (Enabled)
|
||||
Logger.Log("[EzDriver] Run driver latency test", name: "audio", level: LogLevel.Debug);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置缓冲区大小
|
||||
/// </summary>
|
||||
/// <param name="bufferSize">缓冲区大小</param>
|
||||
public void SetBufferSize(int bufferSize)
|
||||
{
|
||||
if (Enabled)
|
||||
Logger.Log($"[EzDriver] Set buffer size: {bufferSize}", name: "audio", level: LogLevel.Debug);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置采样率
|
||||
/// </summary>
|
||||
/// <param name="sampleRate">采样率</param>
|
||||
public void SetSampleRate(int sampleRate)
|
||||
{
|
||||
if (Enabled)
|
||||
Logger.Log($"[EzDriver] Set sample rate: {sampleRate}", name: "audio", level: LogLevel.Debug);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置驱动类型
|
||||
/// </summary>
|
||||
/// <param name="driverType">驱动类型,如 ASIO 或 WASAPIExclusive</param>
|
||||
public void SetDriverType(string driverType)
|
||||
{
|
||||
if (Enabled)
|
||||
Logger.Log($"[EzDriver] Set driver type: {driverType}", name: "audio", level: LogLevel.Debug);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取驱动缓冲延迟(T_buf)
|
||||
/// </summary>
|
||||
/// <returns>缓冲延迟(毫秒),失败返回 -1</returns>
|
||||
public double GetBufferLatency()
|
||||
{
|
||||
// 这里应该调用 AudioThread 的 GetAsioOutputLatency 或 GetWasapiStreamLatency
|
||||
// 暂时返回估算值
|
||||
if (Enabled)
|
||||
Logger.Log("[EzDriver] Get buffer latency", name: "audio", level: LogLevel.Debug);
|
||||
return 5.0; // 估算值
|
||||
}
|
||||
}
|
||||
}
|
||||
108
osu.Framework/Audio/EzHardwareModule.cs
Normal file
108
osu.Framework/Audio/EzHardwareModule.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
// 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
|
||||
{
|
||||
/// <summary>
|
||||
/// EzHardwareModule 负责硬件层延迟的测量和管理。
|
||||
/// 主要职责包括测量 DAC 转换延迟、总线延迟等,并记录不可控延迟(UncontrollableLatency)。
|
||||
/// </summary>
|
||||
public class EzHardwareModule
|
||||
{
|
||||
private readonly Stopwatch stopwatch;
|
||||
|
||||
/// <summary>
|
||||
/// EzOsuLatency 总开关 - 控制是否启用延迟测量
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
// 存储硬件时间
|
||||
internal double OutputHardwareTime;
|
||||
internal double InputHardwareTime;
|
||||
internal double LatencyDifference;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
public EzHardwareModule()
|
||||
{
|
||||
stopwatch = Stopwatch.StartNew();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录输出硬件播放时间戳
|
||||
/// </summary>
|
||||
/// <param name="timestamp">测量的时间戳</param>
|
||||
public void RecordOutputTimestamp(DateTime timestamp)
|
||||
{
|
||||
if (Enabled)
|
||||
{
|
||||
OutputHardwareTime = stopwatch.Elapsed.TotalMilliseconds;
|
||||
// 不直接打印日志
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录输入硬件接收时间戳,并计算首尾延迟差值
|
||||
/// </summary>
|
||||
/// <param name="timestamp">测量的时间戳</param>
|
||||
/// <param name="driverTime">驱动时间</param>
|
||||
public void RecordInputTimestamp(DateTime timestamp, double driverTime)
|
||||
{
|
||||
if (Enabled)
|
||||
{
|
||||
InputHardwareTime = stopwatch.Elapsed.TotalMilliseconds;
|
||||
LatencyDifference = OutputHardwareTime - EzInputModule.InputTime; // 首尾延迟差值:T_out - T_in
|
||||
// 一次性打印硬件数据的日志
|
||||
Logger.Log($"[EzOsuLatency] Driver: {driverTime:F2}ms, OutputHW: {OutputHardwareTime:F2}ms, InputHW: {InputHardwareTime:F2}ms, LatencyDiff: {LatencyDifference:F2}ms", name: "audio", level: LogLevel.Debug);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 运行硬件延迟测试
|
||||
/// </summary>
|
||||
public void RunTest()
|
||||
{
|
||||
if (Enabled)
|
||||
Logger.Log("[EzHardware] Run hardware latency test", name: "audio", level: LogLevel.Debug);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测量硬件延迟
|
||||
/// </summary>
|
||||
/// <returns>不可控延迟值</returns>
|
||||
public double MeasureHardwareLatency()
|
||||
{
|
||||
// 测量 DAC 转换延迟、USB/PCIe 总线延迟等
|
||||
// 由于无法真实测量,返回默认值
|
||||
const double uncontrollable_latency = 2.0; // DAC + 总线延迟估算
|
||||
if (Enabled)
|
||||
Logger.Log($"[EzHardware] Measure hardware latency: {uncontrollable_latency:F2}ms", name: "audio", level: LogLevel.Debug);
|
||||
return uncontrollable_latency;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置不可控延迟值
|
||||
/// </summary>
|
||||
/// <param name="uncontrollableLatency">不可控延迟值</param>
|
||||
public void SetUncontrollableLatency(double uncontrollableLatency)
|
||||
{
|
||||
if (Enabled)
|
||||
Logger.Log($"[EzHardware] Set uncontrollable latency: {uncontrollableLatency:F2}ms", name: "audio", level: LogLevel.Debug);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置采样率
|
||||
/// </summary>
|
||||
/// <param name="sampleRate">采样率</param>
|
||||
public void SetSampleRate(int sampleRate)
|
||||
{
|
||||
if (Enabled)
|
||||
Logger.Log($"[EzHardware] Set sample rate: {sampleRate}", name: "audio", level: LogLevel.Debug);
|
||||
}
|
||||
}
|
||||
}
|
||||
82
osu.Framework/Audio/EzInputModule.cs
Normal file
82
osu.Framework/Audio/EzInputModule.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
// 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 osu.Framework.Logging;
|
||||
|
||||
namespace osu.Framework.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// EzInputModule 负责处理输入事件的捕获和时间戳记录。
|
||||
/// 主要职责包括监听用户输入事件,记录输入发生的时间戳,并为延迟测试提供输入基准。
|
||||
/// </summary>
|
||||
public static class EzInputModule
|
||||
{
|
||||
/// <summary>
|
||||
/// EzOsuLatency 总开关 - 控制是否启用延迟测量
|
||||
/// </summary>
|
||||
public static bool Enabled { get; set; } = false;
|
||||
|
||||
private static readonly System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
|
||||
|
||||
// 存储输入数据
|
||||
public static double InputTime;
|
||||
internal static object? KeyValue;
|
||||
|
||||
static EzInputModule()
|
||||
{
|
||||
stopwatch.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录输入事件的时间戳(T_in)
|
||||
/// </summary>
|
||||
/// <param name="timestamp">输入事件的时间戳</param>
|
||||
/// <param name="keyValue">按键值</param>
|
||||
public static void RecordTimestamp(DateTime timestamp, object keyValue)
|
||||
{
|
||||
if (Enabled)
|
||||
{
|
||||
InputTime = stopwatch.Elapsed.TotalMilliseconds;
|
||||
KeyValue = keyValue;
|
||||
// 不直接打印日志
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空输入数据(用于长按note的情况)
|
||||
/// </summary>
|
||||
public static void ClearInputData()
|
||||
{
|
||||
InputTime = 0;
|
||||
KeyValue = null;
|
||||
}
|
||||
|
||||
public static void RunTest()
|
||||
{
|
||||
// 实现逻辑:执行测试
|
||||
if (Enabled)
|
||||
Logger.Log("[EzInput] Run input latency test", name: "audio", level: LogLevel.Debug);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置输入缓冲区大小
|
||||
/// </summary>
|
||||
/// <param name="bufferSize">缓冲区大小</param>
|
||||
public static void SetBufferSize(int bufferSize)
|
||||
{
|
||||
if (Enabled)
|
||||
Logger.Log($"[EzInput] Set buffer size: {bufferSize}", name: "audio", level: LogLevel.Debug);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置采样率
|
||||
/// </summary>
|
||||
/// <param name="sampleRate">采样率</param>
|
||||
public static void SetSampleRate(int sampleRate)
|
||||
{
|
||||
if (Enabled)
|
||||
Logger.Log($"[EzInput] Set sample rate: {sampleRate}", name: "audio", level: LogLevel.Debug);
|
||||
}
|
||||
}
|
||||
}
|
||||
72
osu.Framework/Audio/EzJudgeModule.cs
Normal file
72
osu.Framework/Audio/EzJudgeModule.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
// 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 osu.Framework.Logging;
|
||||
|
||||
namespace osu.Framework.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// EzJudgeModule 负责处理判定时间的计算和记录。
|
||||
/// 主要职责包括基于输入事件计算判定时间,记录判定延迟,并为整体延迟分析提供数据。
|
||||
/// </summary>
|
||||
public static class EzJudgeModule
|
||||
{
|
||||
/// <summary>
|
||||
/// EzOsuLatency 总开关 - 控制是否启用延迟测量
|
||||
/// </summary>
|
||||
public static bool Enabled { get; set; } = false;
|
||||
|
||||
private static readonly System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
|
||||
|
||||
// 存储判定时间
|
||||
internal static double JudgeTime;
|
||||
|
||||
static EzJudgeModule()
|
||||
{
|
||||
stopwatch.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录判定时间戳(T_judge)
|
||||
/// </summary>
|
||||
/// <param name="timestamp">判定事件的时间戳</param>
|
||||
public static void RecordTimestamp(DateTime timestamp)
|
||||
{
|
||||
if (Enabled)
|
||||
{
|
||||
JudgeTime = stopwatch.Elapsed.TotalMilliseconds;
|
||||
// 不直接打印日志
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 运行判定延迟测试
|
||||
/// </summary>
|
||||
public static void RunTest()
|
||||
{
|
||||
if (Enabled)
|
||||
Logger.Log("[EzJudge] Run judgement latency test", name: "audio", level: LogLevel.Debug);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置判定缓冲区大小
|
||||
/// </summary>
|
||||
/// <param name="bufferSize">缓冲区大小</param>
|
||||
public static void SetBufferSize(int bufferSize)
|
||||
{
|
||||
if (Enabled)
|
||||
Logger.Log($"[EzJudge] Set buffer size: {bufferSize}", name: "audio", level: LogLevel.Debug);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置采样率
|
||||
/// </summary>
|
||||
/// <param name="sampleRate">采样率</param>
|
||||
public static void SetSampleRate(int sampleRate)
|
||||
{
|
||||
if (Enabled)
|
||||
Logger.Log($"[EzJudge] Set sample rate: {sampleRate}", name: "audio", level: LogLevel.Debug);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
osu.Framework/Audio/EzLatency/BasicEzLatencyTracker.cs
Normal file
25
osu.Framework/Audio/EzLatency/BasicEzLatencyTracker.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
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;
|
||||
|
||||
public void Start() { }
|
||||
public void Stop() { }
|
||||
|
||||
public void SetSampleRate(int sampleRate) => this.sampleRate = sampleRate;
|
||||
|
||||
public void PushMeasurement(EzLatencyRecord record) => OnMeasurement?.Invoke(record);
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
26
osu.Framework/Audio/EzLatency/EzLatencyRecord.cs
Normal file
26
osu.Framework/Audio/EzLatency/EzLatencyRecord.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
namespace osu.Framework.Audio.EzLatency
|
||||
{
|
||||
#nullable disable
|
||||
using System;
|
||||
|
||||
public class EzLatencyRecord
|
||||
{
|
||||
// high-level fields
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
public double MeasuredMs { get; set; }
|
||||
public string Note { get; set; }
|
||||
|
||||
// low-level fields (originating from EzLogModule)
|
||||
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; }
|
||||
|
||||
// optional low-level structs copied from AudioThread for richer diagnostics
|
||||
public osu.Framework.Threading.EzLatencyInputData InputData { get; set; }
|
||||
public osu.Framework.Threading.EzLatencyHardwareData HardwareData { get; set; }
|
||||
}
|
||||
}
|
||||
40
osu.Framework/Audio/EzLatency/EzLatencyService.cs
Normal file
40
osu.Framework/Audio/EzLatency/EzLatencyService.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
namespace osu.Framework.Audio.EzLatency
|
||||
{
|
||||
#nullable disable
|
||||
using System;
|
||||
|
||||
/// <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 { }
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
80
osu.Framework/Audio/EzLatency/EzLoggerAdapter.cs
Normal file
80
osu.Framework/Audio/EzLatency/EzLoggerAdapter.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
namespace osu.Framework.Audio.EzLatency
|
||||
{
|
||||
#nullable disable
|
||||
using System;
|
||||
using System.IO;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Threading;
|
||||
|
||||
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 { }
|
||||
}
|
||||
}
|
||||
}
|
||||
17
osu.Framework/Audio/EzLatency/IEzLatencyLogger.cs
Normal file
17
osu.Framework/Audio/EzLatency/IEzLatencyLogger.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace osu.Framework.Audio.EzLatency
|
||||
{
|
||||
#nullable disable
|
||||
using System;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
12
osu.Framework/Audio/EzLatency/IEzLatencyPlayback.cs
Normal file
12
osu.Framework/Audio/EzLatency/IEzLatencyPlayback.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace osu.Framework.Audio.EzLatency
|
||||
{
|
||||
#nullable disable
|
||||
using System;
|
||||
|
||||
public interface IEzLatencyPlayback : IDisposable
|
||||
{
|
||||
void PlayTestTone();
|
||||
void StopTestTone();
|
||||
void SetSampleRate(int sampleRate);
|
||||
}
|
||||
}
|
||||
13
osu.Framework/Audio/EzLatency/IEzLatencyTracker.cs
Normal file
13
osu.Framework/Audio/EzLatency/IEzLatencyTracker.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
164
osu.Framework/Audio/EzLatencyTestModule.cs
Normal file
164
osu.Framework/Audio/EzLatencyTestModule.cs
Normal file
@@ -0,0 +1,164 @@
|
||||
// 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.Diagnostics;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Logging;
|
||||
|
||||
namespace osu.Framework.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// EzOsuLatency 延迟测试模块
|
||||
/// 负责执行虚拟环路测试,量化播放事件触发后到真正发声的延迟。
|
||||
/// 通过播放脉冲信号并回录来计算音频输出延迟。
|
||||
/// </summary>
|
||||
public class EzLatencyTestModule
|
||||
{
|
||||
/// <summary>
|
||||
/// EzOsuLatency 总开关 - 控制是否启用延迟测量
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 延迟测试启用状态
|
||||
/// </summary>
|
||||
public readonly Bindable<bool> LatencyTestEnabled = new Bindable<bool>(false);
|
||||
|
||||
/// <summary>
|
||||
/// 驱动类型配置
|
||||
/// </summary>
|
||||
public readonly Bindable<string> DriverType = new Bindable<string>("ASIO");
|
||||
|
||||
/// <summary>
|
||||
/// 采样率配置
|
||||
/// </summary>
|
||||
public readonly Bindable<int> SampleRate = new Bindable<int>(44100);
|
||||
|
||||
/// <summary>
|
||||
/// 缓冲区大小配置
|
||||
/// </summary>
|
||||
public readonly Bindable<int> BufferSize = new Bindable<int>(256);
|
||||
|
||||
private readonly Stopwatch stopwatch = new Stopwatch();
|
||||
private double lastTestTime;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化延迟测试模块
|
||||
/// </summary>
|
||||
public EzLatencyTestModule()
|
||||
{
|
||||
stopwatch.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 运行延迟测试
|
||||
/// 执行虚拟环路测试:播放脉冲 → 回录 → 计算延迟
|
||||
/// </summary>
|
||||
/// <returns>测试结果的延迟值(毫秒),失败返回-1</returns>
|
||||
public double RunTest()
|
||||
{
|
||||
if (!LatencyTestEnabled.Value)
|
||||
return -1;
|
||||
|
||||
if (Enabled)
|
||||
Logger.Log($"[EzLatencyTest] Starting latency test - Driver: {DriverType.Value}, SampleRate: {SampleRate.Value}, Buffer: {BufferSize.Value}");
|
||||
|
||||
// 记录测试开始时间戳 (T_call)
|
||||
double tCall = RecordTimestamp();
|
||||
|
||||
// 简化实现:模拟播放脉冲并测量延迟
|
||||
// 实际实现需要:
|
||||
// 1. 生成脉冲信号
|
||||
// 2. 播放到音频输出
|
||||
// 3. 通过虚拟环路回录
|
||||
// 4. 检测脉冲到达时间 (T_out)
|
||||
// 5. 计算 T_out - T_call
|
||||
|
||||
// 临时模拟:假设延迟为缓冲区延迟 + 硬件延迟
|
||||
double bufferLatency = (BufferSize.Value / (double)SampleRate.Value) * 1000;
|
||||
const double hardware_latency = 1.5;
|
||||
double simulatedLatency = bufferLatency + hardware_latency;
|
||||
|
||||
if (Enabled)
|
||||
Logger.Log($"[EzLatencyTest] Test completed, latency: {simulatedLatency:F2}ms (T_call: {tCall:F2})");
|
||||
|
||||
return simulatedLatency;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录当前时间戳
|
||||
/// </summary>
|
||||
/// <returns>当前时间戳(毫秒)</returns>
|
||||
public double RecordTimestamp()
|
||||
{
|
||||
return stopwatch.Elapsed.TotalMilliseconds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置缓冲区大小
|
||||
/// </summary>
|
||||
/// <param name="size">缓冲区大小</param>
|
||||
public void SetBufferSize(int size)
|
||||
{
|
||||
BufferSize.Value = size;
|
||||
if (Enabled)
|
||||
Logger.Log($"[EzLatencyTest] Buffer size set to: {size}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置采样率
|
||||
/// </summary>
|
||||
/// <param name="rate">采样率</param>
|
||||
public void SetSampleRate(int rate)
|
||||
{
|
||||
SampleRate.Value = rate;
|
||||
if (Enabled)
|
||||
Logger.Log($"[EzLatencyTest] Sample rate set to: {rate}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置驱动类型
|
||||
/// </summary>
|
||||
/// <param name="driver">驱动类型 ("ASIO" 或 "WASAPIExclusive")</param>
|
||||
public void SetDriverType(string driver)
|
||||
{
|
||||
DriverType.Value = driver;
|
||||
if (Enabled)
|
||||
Logger.Log($"[EzLatencyTest] Driver type set to: {driver}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定期测试(在 AudioThread.onNewFrame 中调用)
|
||||
/// </summary>
|
||||
public void RunPeriodicTest()
|
||||
{
|
||||
double currentTime = RecordTimestamp();
|
||||
if (currentTime - lastTestTime > 1000) // 每秒测试一次
|
||||
{
|
||||
RunTest();
|
||||
lastTestTime = currentTime;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 精确测试(在 AudioManager 播放事件中调用)
|
||||
/// </summary>
|
||||
public void RunPreciseTest()
|
||||
{
|
||||
RunTest();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 临时模拟延迟计算(实际实现需要音频处理逻辑)
|
||||
/// </summary>
|
||||
private double calculateSimulatedLatency()
|
||||
{
|
||||
// 基于配置参数模拟延迟
|
||||
double baseLatency = DriverType.Value == "ASIO" ? 2.0 : 5.0; // ASIO通常延迟更低
|
||||
double bufferLatency = (BufferSize.Value / (double)SampleRate.Value) * 1000; // 缓冲区延迟
|
||||
const double hardware_latency = 1.5; // 硬件层延迟
|
||||
|
||||
return baseLatency + bufferLatency + hardware_latency;
|
||||
}
|
||||
}
|
||||
}
|
||||
175
osu.Framework/Audio/EzLogModule.cs
Normal file
175
osu.Framework/Audio/EzLogModule.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Logging;
|
||||
|
||||
namespace osu.Framework.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// EzOsuLatency 日志模块
|
||||
/// 负责记录延迟测量数据、缓冲区参数和驱动类型信息。
|
||||
/// 使用框架的 Logger 输出日志。
|
||||
/// </summary>
|
||||
public class EzLogModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 静态实例,用于全局访问
|
||||
/// </summary>
|
||||
public static EzLogModule Instance { get; } = new EzLogModule();
|
||||
|
||||
/// <summary>
|
||||
/// EzOsuLatency 总开关 - 控制是否启用延迟测量
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
// 存储延迟记录用于统计
|
||||
private readonly List<osu.Framework.Audio.EzLatency.EzLatencyRecord> latencyRecords = new List<osu.Framework.Audio.EzLatency.EzLatencyRecord>();
|
||||
|
||||
public event System.Action<osu.Framework.Audio.EzLatency.EzLatencyRecord>? OnNewRecord;
|
||||
|
||||
/// <summary>
|
||||
/// 延迟统计数据结构体
|
||||
/// </summary>
|
||||
public struct EzLatencyStatistics
|
||||
{
|
||||
public int RecordCount;
|
||||
public double AvgInputToJudge;
|
||||
public double AvgInputToPlayback;
|
||||
public double AvgPlaybackToJudge;
|
||||
public bool HasData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录单个延迟事件并返回延迟信息(用于实时输出)
|
||||
/// </summary>
|
||||
public osu.Framework.Audio.EzLatency.EzLatencyRecord? RecordLatencyEventAndGet(double inputTime, double judgeTime, double playbackTime, double driverTime, double outputHardwareTime, double inputHardwareTime, double latencyDifference, osu.Framework.Threading.EzLatencyInputData inputData = default, osu.Framework.Threading.EzLatencyHardwareData hardwareData = default)
|
||||
{
|
||||
if (!Enabled)
|
||||
return null;
|
||||
|
||||
var record = new osu.Framework.Audio.EzLatency.EzLatencyRecord
|
||||
{
|
||||
Timestamp = DateTimeOffset.Now,
|
||||
InputTime = inputTime,
|
||||
JudgeTime = judgeTime,
|
||||
PlaybackTime = playbackTime,
|
||||
DriverTime = driverTime,
|
||||
OutputHardwareTime = outputHardwareTime,
|
||||
InputHardwareTime = inputHardwareTime,
|
||||
LatencyDifference = latencyDifference,
|
||||
MeasuredMs = playbackTime - inputTime,
|
||||
Note = "ezlogmodule"
|
||||
};
|
||||
|
||||
// attach optional low-level structs for richer diagnostics
|
||||
try
|
||||
{
|
||||
record.InputData = inputData;
|
||||
record.HardwareData = hardwareData;
|
||||
}
|
||||
catch { }
|
||||
|
||||
latencyRecords.Add(record);
|
||||
|
||||
// notify any subscribers about the new record
|
||||
try
|
||||
{
|
||||
OnNewRecord?.Invoke(record);
|
||||
}
|
||||
catch { }
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取延迟统计数据(不直接输出日志)
|
||||
/// </summary>
|
||||
/// <returns>延迟统计数据结构体</returns>
|
||||
public EzLatencyStatistics GetLatencyStatistics()
|
||||
{
|
||||
if (!Enabled || latencyRecords.Count == 0)
|
||||
{
|
||||
return new EzLatencyStatistics { HasData = false };
|
||||
}
|
||||
|
||||
// 计算各种延迟的平均值
|
||||
double avgInputToJudge = latencyRecords.Average(r => r.JudgeTime - r.InputTime);
|
||||
double avgInputToPlayback = latencyRecords.Average(r => r.PlaybackTime - r.InputTime);
|
||||
double avgPlaybackToJudge = latencyRecords.Average(r => r.JudgeTime - r.PlaybackTime);
|
||||
|
||||
var statistics = new EzLatencyStatistics
|
||||
{
|
||||
RecordCount = latencyRecords.Count,
|
||||
AvgInputToJudge = avgInputToJudge,
|
||||
AvgInputToPlayback = avgInputToPlayback,
|
||||
AvgPlaybackToJudge = avgPlaybackToJudge,
|
||||
HasData = true
|
||||
};
|
||||
|
||||
// 清空记录,为下次统计做准备
|
||||
latencyRecords.Clear();
|
||||
|
||||
return statistics;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成延迟统计报告(兼容旧接口,直接输出日志)
|
||||
/// </summary>
|
||||
public void LogLatencyStatistics()
|
||||
{
|
||||
var stats = GetLatencyStatistics();
|
||||
|
||||
if (!stats.HasData)
|
||||
{
|
||||
Logger.Log($"[EzOsuLatency] No latency data available", LoggingTarget.Runtime, LogLevel.Debug);
|
||||
return;
|
||||
}
|
||||
|
||||
string message1 = $"Input->Judgement: {stats.AvgInputToJudge:F2}ms, Input->Audio: {stats.AvgInputToPlayback:F2}ms, Audio->Judgement: {stats.AvgPlaybackToJudge:F2}ms (based on {stats.RecordCount} complete records)";
|
||||
string message2 = $"Input->Judgement: {stats.AvgInputToJudge:F2}ms, \nInput->Audio: {stats.AvgInputToPlayback:F2}ms, \nAudio->Judgement: {stats.AvgPlaybackToJudge:F2}ms \n(based on {stats.RecordCount} complete records)";
|
||||
|
||||
Logger.Log($"[EzOsuLatency] Analysis: {message1}");
|
||||
Logger.Log($"[EzOsuLatency] Analysis: \n{message2}", LoggingTarget.Runtime, LogLevel.Important);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录延迟数据(兼容旧接口)
|
||||
/// </summary>
|
||||
/// <param name="timestamp">时间戳</param>
|
||||
/// <param name="driverType">驱动类型</param>
|
||||
/// <param name="sampleRate">采样率</param>
|
||||
/// <param name="bufferSize">缓冲区大小</param>
|
||||
/// <param name="inputLatency">输入延迟</param>
|
||||
/// <param name="playbackLatency">播放延迟</param>
|
||||
/// <param name="totalLatency">总延迟</param>
|
||||
/// <param name="uncontrollableLatency">不可控延迟</param>
|
||||
public void LogLatency(double timestamp, string driverType, int sampleRate, int bufferSize, double inputLatency, double playbackLatency, double totalLatency, double uncontrollableLatency)
|
||||
{
|
||||
if (Enabled)
|
||||
Logger.Log($"[EzOsuLatency] Latency data: Timestamp={timestamp:F2}, Driver={driverType}, SampleRate={sampleRate}, Buffer={bufferSize}, Input={inputLatency:F2}ms, Playback={playbackLatency:F2}ms, Total={totalLatency:F2}ms, Uncontrollable={uncontrollableLatency:F2}ms", LoggingTarget.Runtime, LogLevel.Debug);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录一般日志信息
|
||||
/// </summary>
|
||||
/// <param name="message">日志消息</param>
|
||||
public void LogInfo(string message)
|
||||
{
|
||||
if (Enabled)
|
||||
Logger.Log($"[EzOsuLatency] {message}", LoggingTarget.Runtime, LogLevel.Important);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录错误日志
|
||||
/// </summary>
|
||||
/// <param name="message">错误消息</param>
|
||||
public void LogError(string message)
|
||||
{
|
||||
if (Enabled)
|
||||
Logger.Log($"[EzOsuLatency] Error: {message}", LoggingTarget.Runtime, LogLevel.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
130
osu.Framework/Audio/EzPlaybackModule.cs
Normal file
130
osu.Framework/Audio/EzPlaybackModule.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
// 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 osu.Framework.Logging;
|
||||
using osu.Framework.Threading;
|
||||
|
||||
namespace osu.Framework.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// EzPlaybackModule 负责处理播放事件的触发和时间戳记录。
|
||||
/// 主要职责包括监听播放事件,记录播放触发时间,并测量从触发到实际发声的延迟。
|
||||
/// </summary>
|
||||
public class EzPlaybackModule
|
||||
{
|
||||
/// <summary>
|
||||
/// EzOsuLatency 总开关 - 控制是否启用延迟测量
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
private readonly System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
|
||||
|
||||
// 存储播放时间
|
||||
internal double PlaybackTime;
|
||||
|
||||
// AudioThread 引用,用于访问其他模块
|
||||
private readonly AudioThread audioThread;
|
||||
|
||||
public EzPlaybackModule(AudioThread audioThread)
|
||||
{
|
||||
this.audioThread = audioThread;
|
||||
stopwatch.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录播放事件的时间戳(T_call)
|
||||
/// </summary>
|
||||
/// <param name="timestamp">播放事件的时间戳</param>
|
||||
public void RecordTimestamp(DateTime timestamp)
|
||||
{
|
||||
if (Enabled)
|
||||
{
|
||||
PlaybackTime = stopwatch.Elapsed.TotalMilliseconds;
|
||||
|
||||
// 只有在有有效的输入数据时才打印日志(避免长按note的情况)
|
||||
if (EzInputModule.InputTime > 0)
|
||||
{
|
||||
// build local low-level structs for richer diagnostics (do not mutate AudioThread fields)
|
||||
osu.Framework.Threading.EzLatencyInputData inputData = default;
|
||||
osu.Framework.Threading.EzLatencyHardwareData hardwareData = default;
|
||||
|
||||
try
|
||||
{
|
||||
inputData.InputTime = EzInputModule.InputTime;
|
||||
inputData.KeyValue = EzInputModule.KeyValue ?? "";
|
||||
inputData.JudgeTime = EzJudgeModule.JudgeTime;
|
||||
inputData.PlaybackTime = PlaybackTime;
|
||||
|
||||
hardwareData.DriverTime = audioThread.DriverModule.DriverTime;
|
||||
hardwareData.OutputHardwareTime = audioThread.HardwareModule.OutputHardwareTime;
|
||||
hardwareData.InputHardwareTime = audioThread.HardwareModule.InputHardwareTime;
|
||||
hardwareData.LatencyDifference = audioThread.HardwareModule.LatencyDifference;
|
||||
}
|
||||
catch { }
|
||||
|
||||
// 生成最终 EzLatencyRecord 并直接推送到 EzLatencyService
|
||||
var record = new osu.Framework.Audio.EzLatency.EzLatencyRecord
|
||||
{
|
||||
Timestamp = DateTimeOffset.Now,
|
||||
MeasuredMs = PlaybackTime - EzInputModule.InputTime,
|
||||
Note = "playback-record",
|
||||
InputTime = EzInputModule.InputTime,
|
||||
JudgeTime = EzJudgeModule.JudgeTime,
|
||||
PlaybackTime = PlaybackTime,
|
||||
DriverTime = audioThread.DriverModule.DriverTime,
|
||||
OutputHardwareTime = audioThread.HardwareModule.OutputHardwareTime,
|
||||
InputHardwareTime = audioThread.HardwareModule.InputHardwareTime,
|
||||
LatencyDifference = audioThread.HardwareModule.LatencyDifference,
|
||||
InputData = inputData,
|
||||
HardwareData = hardwareData
|
||||
};
|
||||
|
||||
// push to central service for osu layer to consume
|
||||
try
|
||||
{
|
||||
osu.Framework.Audio.EzLatency.EzLatencyService.Instance.PushRecord(record);
|
||||
}
|
||||
catch { }
|
||||
|
||||
// 兼容:仍然把事件记录到 EzLogModule 的内部统计(可选)
|
||||
try
|
||||
{
|
||||
if (EzLogModule.Instance != null && EzLogModule.Instance.Enabled)
|
||||
EzLogModule.Instance.RecordLatencyEventAndGet(record.InputTime, record.JudgeTime, record.PlaybackTime, record.DriverTime, record.OutputHardwareTime, record.InputHardwareTime, record.LatencyDifference, inputData, hardwareData);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 运行播放延迟测试
|
||||
/// </summary>
|
||||
public void RunTest()
|
||||
{
|
||||
if (Enabled)
|
||||
Logger.Log("[EzPlayback] Run playback latency test", name: "audio", level: LogLevel.Debug);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置播放缓冲区大小
|
||||
/// </summary>
|
||||
/// <param name="bufferSize">缓冲区大小</param>
|
||||
public void SetBufferSize(int bufferSize)
|
||||
{
|
||||
if (Enabled)
|
||||
Logger.Log($"[EzPlayback] Set buffer size: {bufferSize}", name: "audio", level: LogLevel.Debug);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置采样率
|
||||
/// </summary>
|
||||
/// <param name="sampleRate">采样率</param>
|
||||
public void SetSampleRate(int sampleRate)
|
||||
{
|
||||
if (Enabled)
|
||||
Logger.Log($"[EzPlayback] Set sample rate: {sampleRate}", name: "audio", level: LogLevel.Debug);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ using JetBrains.Annotations;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Input.States;
|
||||
using osu.Framework.Logging;
|
||||
// using osu.Framework.Logging;
|
||||
|
||||
namespace osu.Framework.Input
|
||||
{
|
||||
@@ -55,8 +55,8 @@ namespace osu.Framework.Input
|
||||
{
|
||||
var handledBy = drawables.FirstOrDefault(target => target.TriggerEvent(e));
|
||||
|
||||
if (handledBy != null)
|
||||
Logger.Log($"{e} handled by {handledBy}.", LoggingTarget.Runtime, LogLevel.Debug);
|
||||
// if (handledBy != null)
|
||||
// Logger.Log($"{e} handled by {handledBy}.", LoggingTarget.Runtime, LogLevel.Debug);
|
||||
|
||||
return handledBy;
|
||||
}
|
||||
|
||||
@@ -23,11 +23,43 @@ using osu.Framework.Platform.Linux.Native;
|
||||
|
||||
namespace osu.Framework.Threading
|
||||
{
|
||||
/// <summary>
|
||||
/// EzOsuLatency 输入数据结构体
|
||||
/// </summary>
|
||||
public struct EzLatencyInputData
|
||||
{
|
||||
public double InputTime;
|
||||
public object KeyValue;
|
||||
public double JudgeTime;
|
||||
public double PlaybackTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EzOsuLatency 硬件数据结构体
|
||||
/// </summary>
|
||||
public struct EzLatencyHardwareData
|
||||
{
|
||||
public double DriverTime;
|
||||
public double OutputHardwareTime;
|
||||
public double InputHardwareTime;
|
||||
public double LatencyDifference;
|
||||
}
|
||||
|
||||
public class AudioThread : GameThread
|
||||
{
|
||||
private static int wasapiNativeUnavailableLogged;
|
||||
private static int asioResolverRegistered;
|
||||
|
||||
// EzOsuLatency 模块
|
||||
internal readonly EzLatencyTestModule LatencyTestModule;
|
||||
internal readonly EzLogModule LogModule;
|
||||
internal readonly EzDriverModule DriverModule;
|
||||
internal readonly EzHardwareModule HardwareModule;
|
||||
|
||||
// EzOsuLatency 数据结构体
|
||||
// NOTE: Previously these were present as placeholders; we now pass local structs from the playback path.
|
||||
// Keep no instance fields to avoid misleading shared mutable state.
|
||||
|
||||
public AudioThread()
|
||||
: base(name: "Audio")
|
||||
{
|
||||
@@ -37,6 +69,12 @@ namespace osu.Framework.Threading
|
||||
|
||||
OnNewFrame += onNewFrame;
|
||||
PreloadBass();
|
||||
|
||||
// 初始化EzOsuLatency模块
|
||||
LatencyTestModule = new EzLatencyTestModule();
|
||||
LogModule = new EzLogModule();
|
||||
DriverModule = new EzDriverModule();
|
||||
HardwareModule = new EzHardwareModule();
|
||||
}
|
||||
|
||||
public override bool IsCurrent => ThreadSafety.IsAudioThread;
|
||||
@@ -79,6 +117,9 @@ namespace osu.Framework.Threading
|
||||
m.Update();
|
||||
}
|
||||
}
|
||||
|
||||
// EzOsuLatency: 定期运行延迟测试
|
||||
LatencyTestModule.RunPeriodicTest();
|
||||
}
|
||||
|
||||
internal void RegisterManager(AudioManager manager)
|
||||
@@ -752,6 +793,108 @@ namespace osu.Framework.Threading
|
||||
Thread.Sleep(300);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取ASIO驱动的输出延迟
|
||||
/// </summary>
|
||||
/// <returns>输出延迟(毫秒),失败返回-1</returns>
|
||||
internal double GetAsioOutputLatency()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (BassAsio.IsStarted)
|
||||
{
|
||||
// 估算 ASIO 输出延迟
|
||||
return 2.0; // ASIO 通常延迟较低
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log($"获取ASIO输出延迟失败: {ex.Message}", name: "audio", level: LogLevel.Error);
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取WASAPI流的延迟
|
||||
/// </summary>
|
||||
/// <returns>流延迟(毫秒),失败返回-1</returns>
|
||||
internal double GetWasapiStreamLatency()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (BassWasapi.IsStarted)
|
||||
{
|
||||
// 使用 IAudioClient::GetStreamLatency
|
||||
// 注意:BassWasapi 可能不直接暴露这个,需要通过底层 API
|
||||
// 暂时返回估算值
|
||||
return 10.0; // 估算的 WASAPI 延迟
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log($"获取WASAPI流延迟失败: {ex.Message}", name: "audio", level: LogLevel.Error);
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前音频驱动的缓冲区延迟
|
||||
/// </summary>
|
||||
/// <param name="outputMode">输出模式</param>
|
||||
/// <returns>缓冲区延迟(毫秒),失败返回-1</returns>
|
||||
internal double GetDriverBufferLatency(AudioThreadOutputMode outputMode)
|
||||
{
|
||||
switch (outputMode)
|
||||
{
|
||||
case AudioThreadOutputMode.Asio:
|
||||
return GetAsioOutputLatency();
|
||||
|
||||
case AudioThreadOutputMode.WasapiExclusive:
|
||||
case AudioThreadOutputMode.WasapiShared:
|
||||
return GetWasapiStreamLatency();
|
||||
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录延迟数据到日志
|
||||
/// </summary>
|
||||
/// <param name="inputLatency">输入延迟</param>
|
||||
/// <param name="playbackLatency">播放延迟</param>
|
||||
/// <param name="totalLatency">总延迟</param>
|
||||
/// <param name="uncontrollableLatency">不可控延迟</param>
|
||||
internal void LogLatencyData(double inputLatency, double playbackLatency, double totalLatency, double uncontrollableLatency)
|
||||
{
|
||||
const string driverType = "Unknown";
|
||||
int sampleRate = 44100;
|
||||
const int bufferSize = 256;
|
||||
|
||||
// 获取当前驱动信息
|
||||
if (Manager != null)
|
||||
{
|
||||
sampleRate = Manager.SampleRate.Value;
|
||||
// TODO: 获取bufferSize
|
||||
}
|
||||
|
||||
// 确定驱动类型
|
||||
// TODO: 从当前输出模式确定驱动类型
|
||||
|
||||
LogModule.LogLatency(
|
||||
LatencyTestModule.RecordTimestamp(),
|
||||
driverType,
|
||||
sampleRate,
|
||||
bufferSize,
|
||||
inputLatency,
|
||||
playbackLatency,
|
||||
totalLatency,
|
||||
uncontrollableLatency
|
||||
);
|
||||
}
|
||||
|
||||
internal enum AudioThreadOutputMode
|
||||
{
|
||||
Default,
|
||||
|
||||
Reference in New Issue
Block a user