mirror of
https://github.com/SK-la/osu-framework.git
synced 2026-03-13 11:20:31 +00:00
修改调用和测试
This commit is contained in:
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user