修改调用和测试

This commit is contained in:
LA
2026-01-17 01:02:10 +08:00
parent b981b5b56d
commit 79a888007f
4 changed files with 143 additions and 87 deletions

View File

@@ -12,39 +12,6 @@ namespace osu.Framework.Tests.Audio
[TestFixture]
public class AudioManagerDeviceEnumerationTest
{
[Test]
public void TestAudioManagerEnumeratesAsioDevices()
{
AudioThread.PreloadBass();
var audioThread = new AudioThread();
var trackStore = new ResourceStore<byte[]>(new DllResourceStore(typeof(AudioManagerDeviceEnumerationTest).Assembly));
var sampleStore = new ResourceStore<byte[]>(new DllResourceStore(typeof(AudioManagerDeviceEnumerationTest).Assembly));
using (var audioManager = new AudioManager(audioThread, trackStore, sampleStore, null))
{
var deviceNames = audioManager.AudioDeviceNames.ToList();
// Check that we have base devices
Assert.That(deviceNames.Count, Is.GreaterThan(0), "Should have at least one audio device");
// Check for ASIO devices if on Windows
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
{
var asioDevices = deviceNames.Where(name => name.Contains("(ASIO)")).ToList();
// ASIO may not be available in test environment, so we just log what we found
TestContext.WriteLine($"Found {asioDevices.Count} ASIO devices");
// Log the devices for debugging
foreach (string? device in deviceNames)
TestContext.WriteLine($"Device: {device}");
// Don't assert on ASIO device count since it may not be available in test environment
}
}
}
[Test]
public void TestAsioDeviceSupportedSampleRates()
{
@@ -65,24 +32,24 @@ namespace osu.Framework.Tests.Audio
TestContext.WriteLine($"Found {asioDevices.Count} ASIO devices");
foreach (string? device in asioDevices)
foreach (var device in asioDevices)
{
string deviceName = device.Replace(" (ASIO)", "");
TestContext.WriteLine($"Testing ASIO device: {deviceName}");
try
{
// var rates = audioManager.GetAsioDeviceSupportedSampleRates(deviceName);
// TestContext.WriteLine($" Supported rates: {(rates != null ? string.Join(", ", rates) : "null")}");
//
// if (rates != null && rates.Length > 0)
// {
// TestContext.WriteLine($" Rate count: {rates.Length}");
// }
// else
// {
// TestContext.WriteLine(" No supported rates found!");
// }
var rates = audioManager.GetAsioDeviceSupportedSampleRates(deviceName);
TestContext.WriteLine($" Supported rates: {(rates != null ? string.Join(", ", rates) : "null")}");
if (rates != null && rates.Length > 0)
{
TestContext.WriteLine($" Rate count: {rates.Length}");
}
else
{
TestContext.WriteLine(" No supported rates found!");
}
}
catch (System.Exception ex)
{

View File

@@ -18,10 +18,11 @@ namespace osu.Framework.Audio.EzLatency
public struct EzLatencyInputData
{
public double InputTime;
public object? KeyValue;
public object KeyValue;
public double JudgeTime;
public double PlaybackTime;
public bool IsValid => InputTime > 0 && JudgeTime > 0 && PlaybackTime > 0;
// Consider input+playback or input+judge as valid for best-effort measurements.
public bool IsValid => InputTime > 0 && (PlaybackTime > 0 || JudgeTime > 0);
}
public struct EzLatencyHardwareData
@@ -67,7 +68,7 @@ namespace osu.Framework.Audio.EzLatency
{
private readonly Stopwatch stopwatch;
public bool Enabled { get; set; } = false;
public event Action<EzLatencyRecord>? OnNewRecord;
public event Action<EzLatencyRecord> OnNewRecord;
private EzLatencyInputData currentInputData;
private EzLatencyHardwareData currentHardwareData;
@@ -79,7 +80,7 @@ namespace osu.Framework.Audio.EzLatency
stopwatch = Stopwatch.StartNew();
}
public void RecordInputData(double inputTime, object? keyValue = null)
public void RecordInputData(double inputTime, object keyValue = null)
{
if (!Enabled) return;
if (currentInputData.InputTime > 0)
@@ -123,12 +124,13 @@ namespace osu.Framework.Audio.EzLatency
private void tryGenerateCompleteRecord()
{
if (!currentInputData.IsValid || !currentHardwareData.IsValid)
if (!currentInputData.IsValid)
{
checkTimeout();
return;
}
// If we have hardware data, emit a full record. Otherwise emit a best-effort record without hw.
var record = new EzLatencyRecord
{
Timestamp = DateTimeOffset.Now,
@@ -140,21 +142,24 @@ namespace osu.Framework.Audio.EzLatency
InputHardwareTime = currentHardwareData.InputHardwareTime,
LatencyDifference = currentHardwareData.LatencyDifference,
MeasuredMs = currentInputData.PlaybackTime - currentInputData.InputTime,
Note = "complete-latency-measurement",
Note = currentHardwareData.IsValid ? "complete-latency-measurement" : "best-effort-no-hw",
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);
OnNewRecord?.Invoke(record);
EzLatencyService.Instance.PushRecord(record);
if (currentHardwareData.IsValid)
Logger.Log($"EzLatency 完整记录已生成: Input→Playback={record.PlaybackTime - record.InputTime:F2}ms", LoggingTarget.Runtime, LogLevel.Debug);
else
Logger.Log($"EzLatency 最佳尝试记录(无硬件时间戳): Input→Playback={record.PlaybackTime - record.InputTime:F2}ms", LoggingTarget.Runtime, LogLevel.Debug);
}
catch (Exception ex)
{
Logger.Log($"EzLatencyAnalyzer: tryGenerateCompleteRecord failed: {ex.Message}", LoggingTarget.Runtime, LogLevel.Error);
}
ClearCurrentData();
}
@@ -168,7 +173,7 @@ namespace osu.Framework.Audio.EzLatency
double elapsed = stopwatch.Elapsed.TotalMilliseconds - recordStartTime;
if (elapsed > timeout_ms)
{
Logger.Log(string.Format("EzLatency 数据收集超时 ({0:F0}ms),清除旧数据", elapsed), LoggingTarget.Runtime, LogLevel.Debug);
Logger.Log($"EzLatency 数据收集超时 ({elapsed:F0}ms),清除旧数据", LoggingTarget.Runtime, LogLevel.Debug);
ClearCurrentData();
}
}
@@ -192,7 +197,14 @@ namespace osu.Framework.Audio.EzLatency
public void PushRecord(EzLatencyRecord record)
{
try { OnMeasurement?.Invoke(record); } catch (Exception ex) { Logger.Log($"EzLatencyService.PushRecord 异常: {ex.Message}", LoggingTarget.Runtime, LogLevel.Error); }
try
{
OnMeasurement?.Invoke(record);
}
catch (Exception ex)
{
Logger.Log($"EzLatencyService.PushRecord 异常: {ex.Message}", LoggingTarget.Runtime, LogLevel.Error);
}
}
public event Action<EzLatencyRecord> OnMeasurement;
@@ -210,29 +222,78 @@ namespace osu.Framework.Audio.EzLatency
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))
this.scheduler = scheduler as Scheduler;
{
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); }
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);
fileWriter = null;
}
}
}
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 { }
try
{
if (scheduler != null)
scheduler.Add(() => OnRecord?.Invoke(record));
else
OnRecord?.Invoke(record);
}
catch (Exception ex)
{
Logger.Log($"EzLoggerAdapter: OnRecord handler threw: {ex.Message}", LoggingTarget.Runtime, LogLevel.Error);
}
try
{
if (fileWriter != null)
{
string line = $"[{record.Timestamp:O}] {record.MeasuredMs} ms - {record.Note}";
fileWriter.WriteLine(line);
}
}
catch (Exception ex)
{
Logger.Log($"EzLoggerAdapter: failed to write to file: {ex.Message}", LoggingTarget.Runtime, LogLevel.Error);
}
}
public void Flush() { try { fileWriter?.Flush(); } catch { } }
public void Dispose() { try { fileWriter?.Dispose(); } catch { } }
public void Flush()
{
try
{
fileWriter?.Flush();
}
catch (Exception ex)
{
Logger.Log($"EzLoggerAdapter: flush failed: {ex.Message}", LoggingTarget.Runtime, LogLevel.Error);
}
}
public void Dispose()
{
try
{
fileWriter?.Dispose();
}
catch (Exception ex)
{
Logger.Log($"EzLoggerAdapter: dispose failed: {ex.Message}", LoggingTarget.Runtime, LogLevel.Error);
}
}
}
// --- Interfaces & basic tracker ------------------------------------------
@@ -303,7 +364,8 @@ namespace osu.Framework.Audio.EzLatency
public void AddRecord(EzLatencyRecord record)
{
if (!record.IsComplete)
// Accept records if input data is valid (best-effort), even if hardware data isn't available.
if (!record.InputData.IsValid)
return;
lock (lockObject)
@@ -327,10 +389,10 @@ namespace osu.Framework.Audio.EzLatency
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();
var inputToJudge = records.Select(r => r.JudgeTime - r.InputTime).ToList();
var inputToPlayback = records.Select(r => r.PlaybackTime - r.InputTime).ToList();
var playbackToJudge = records.Select(r => r.JudgeTime - r.PlaybackTime).ToList();
var hardwareLatency = records.Select(r => r.OutputHardwareTime).Where(h => h > 0).ToList();
return new EzLatencyStatistics
{

View File

@@ -30,31 +30,25 @@ namespace osu.Framework.Audio.EzLatency
private readonly EzLatencyAnalyzer analyzer;
private readonly EzLatencyCollector collector = new EzLatencyCollector();
private readonly Action<EzLatencyRecord> serviceHandler;
public EzLatencyManager()
{
analyzer = new EzLatencyAnalyzer();
// 将启用状态与分析器同步
Enabled.BindValueChanged(v =>
{
analyzer.Enabled = v.NewValue;
}, true);
// 将分析器的记录事件转发给外部
analyzer.OnNewRecord += record =>
// 统一通过 EzLatencyService 的事件通道接收所有记录(包括来自其它分析器/线程的)
serviceHandler = record =>
{
// add to collector for statistics
try
{
collector.AddRecord(record);
}
catch
{
}
collector.AddRecord(record);
OnNewRecord?.Invoke(record);
};
EzLatencyService.Instance.OnMeasurement += serviceHandler;
}
/// <summary>
@@ -126,6 +120,21 @@ namespace osu.Framework.Audio.EzLatency
/// </summary>
public void ClearStatistics() => collector.Clear();
/// <summary>
/// Create a simple file logger for latency records. Convenience factory to make EzLoggerAdapter discoverable.
/// </summary>
public IEzLatencyLogger CreateLogger(string filePath = null) => new EzLoggerAdapter(null, filePath);
/// <summary>
/// Create a basic in-process tracker which emits measurements into the global pipeline.
/// </summary>
public IEzLatencyTracker CreateBasicTracker()
{
var t = new BasicEzLatencyTracker();
t.OnMeasurement += r => EzLatencyService.Instance.PushRecord(r);
return t;
}
/// <summary>
/// 已收集的完整记录数量
/// </summary>
@@ -155,7 +164,7 @@ namespace osu.Framework.Audio.EzLatency
public void Dispose()
{
Enabled.UnbindAll();
analyzer.OnNewRecord -= OnNewRecord;
EzLatencyService.Instance.OnMeasurement -= serviceHandler;
}
}
}

View File

@@ -48,6 +48,24 @@ namespace osu.Framework.Threading
// 初始化EzLatency模块
LatencyAnalyzer = new EzLatencyAnalyzer();
// Forward any analyzer records from this audio thread into the global EzLatencyService
try
{
LatencyAnalyzer.OnNewRecord += r =>
{
try
{
EzLatencyService.Instance.PushRecord(r);
}
catch
{
}
};
}
catch
{
}
}
public override bool IsCurrent => ThreadSafety.IsAudioThread;