修改架构\过滤规则\调整记录结构\修改单位

增加开关机制
增强延迟检测框架
This commit is contained in:
LA
2026-01-15 17:08:42 +08:00
parent 37b61ab3ef
commit 0c98f9c0a8
16 changed files with 1185 additions and 3 deletions

View 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; // 估算值
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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() { }
}
}

View 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; }
}
}

View 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;
}
}

View 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 { }
}
}
}

View 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;
}
}

View 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);
}
}

View 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);
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View File

@@ -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;
}

View File

@@ -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,