mirror of
https://github.com/SK-la/osu-framework.git
synced 2026-03-13 11:20:31 +00:00
维护代码质量
This commit is contained in:
@@ -44,6 +44,7 @@ namespace osu.Framework.Tests.Audio
|
||||
|
||||
// Create a track
|
||||
var track = audioManager.Tracks.Get("Resources.Tracks.sample-track.mp3");
|
||||
|
||||
if (track == null)
|
||||
{
|
||||
TestContext.WriteLine("Test audio file not available, but device switching worked");
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace osu.Framework.Tests.Audio
|
||||
Assert.Ignore("No ASIO devices available for testing");
|
||||
|
||||
// Test switching to each ASIO device
|
||||
foreach (var asioDevice in asioDevices)
|
||||
foreach (string? asioDevice in asioDevices)
|
||||
{
|
||||
TestContext.WriteLine($"Testing switch to ASIO device: {asioDevice}");
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace osu.Framework.Tests.Audio
|
||||
Assert.Ignore("No ASIO devices available for testing");
|
||||
|
||||
// Get current device before test
|
||||
string originalDevice = audioManager.AudioDevice.Value;
|
||||
// string originalDevice = audioManager.AudioDevice.Value;
|
||||
|
||||
// Try to switch to an ASIO device that might fail
|
||||
// We'll use the first one, but the test environment might succeed
|
||||
@@ -53,14 +53,7 @@ namespace osu.Framework.Tests.Audio
|
||||
|
||||
// In test environment, it might succeed, but in real world with failing ASIO,
|
||||
// it should fallback to default (empty string) or original device
|
||||
if (currentDevice != testDevice)
|
||||
{
|
||||
TestContext.WriteLine("ASIO device failed and fell back to another device - this is expected behavior");
|
||||
}
|
||||
else
|
||||
{
|
||||
TestContext.WriteLine("ASIO device was successfully selected");
|
||||
}
|
||||
TestContext.WriteLine(currentDevice != testDevice ? "ASIO device failed and fell back to another device - this is expected behavior" : "ASIO device was successfully selected");
|
||||
|
||||
// The key test is that the system doesn't crash and audio still works
|
||||
// We can't easily test the actual fallback in test environment since ASIO works here
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace osu.Framework.Tests.Audio
|
||||
{
|
||||
Console.WriteLine("Available audio devices:");
|
||||
var devices = audioManager.AudioDeviceNames.ToList();
|
||||
|
||||
for (int i = 0; i < devices.Count; i++)
|
||||
{
|
||||
Console.WriteLine($"{i}: {devices[i]}");
|
||||
@@ -42,7 +43,7 @@ namespace osu.Framework.Tests.Audio
|
||||
var asioDevices = devices.Where(d => d.Contains("(ASIO)")).ToList();
|
||||
Console.WriteLine($"\nFound {asioDevices.Count} ASIO devices:");
|
||||
|
||||
foreach (var device in asioDevices)
|
||||
foreach (string? device in asioDevices)
|
||||
{
|
||||
Console.WriteLine($" - {device}");
|
||||
}
|
||||
@@ -54,7 +55,7 @@ namespace osu.Framework.Tests.Audio
|
||||
}
|
||||
|
||||
// Test switching to first ASIO device
|
||||
var testDevice = asioDevices.First();
|
||||
string? testDevice = asioDevices.First();
|
||||
Console.WriteLine($"\nTesting ASIO device: {testDevice}");
|
||||
|
||||
audioManager.AudioDevice.Value = testDevice;
|
||||
|
||||
@@ -73,6 +73,7 @@ namespace osu.Framework.Tests.Audio
|
||||
try
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
foreach (var device in AsioDeviceManager.AvailableDevices)
|
||||
{
|
||||
Console.WriteLine($"ASIO Device {device.Index}: {device.Name}");
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedBass;
|
||||
using ManagedBass.Asio;
|
||||
using osu.Framework.Logging;
|
||||
@@ -57,13 +58,15 @@ namespace osu.Framework.Audio.Asio
|
||||
/// 当ASIO设备初始化时,由音频线程设置。
|
||||
/// </summary>
|
||||
private static int globalMixerHandle;
|
||||
|
||||
// 自动重新初始化监控
|
||||
private static System.Threading.CancellationTokenSource? reinitMonitorCts;
|
||||
private static CancellationTokenSource? reinitMonitorCts;
|
||||
private static int? lastInitializedDeviceIndex;
|
||||
private static double? lastRequestedSampleRate;
|
||||
private static int? lastRequestedBufferSize;
|
||||
|
||||
// 用于防止重复的自动重新初始化同时进行
|
||||
private static int reinitInProgress = 0;
|
||||
private static int reinitInProgress;
|
||||
private static Action? notifierHandler;
|
||||
|
||||
/// <summary>
|
||||
@@ -116,6 +119,7 @@ namespace osu.Framework.Audio.Asio
|
||||
/// </summary>
|
||||
/// <param name="deviceIndex">设备索引。</param>
|
||||
/// <param name="flags">初始化标志。</param>
|
||||
/// <param name="allowRetries"></param>
|
||||
/// <returns>如果初始化成功则为true,否则为false。</returns>
|
||||
private static bool tryInitializeDevice(int deviceIndex, AsioInitFlags flags, bool allowRetries = true)
|
||||
{
|
||||
@@ -284,7 +288,7 @@ namespace osu.Framework.Audio.Asio
|
||||
|
||||
if (tryInitializeDevice(deviceIndex, flags, allowRetries: false))
|
||||
{
|
||||
initialized = true;
|
||||
// initialized = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -359,7 +363,7 @@ namespace osu.Framework.Audio.Asio
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Log($"ASIO device did not report a usable rate; failing initialization", LoggingTarget.Runtime, LogLevel.Error);
|
||||
Logger.Log("ASIO device did not report a usable rate; failing initialization", LoggingTarget.Runtime, LogLevel.Error);
|
||||
FreeDevice();
|
||||
return false;
|
||||
}
|
||||
@@ -375,25 +379,24 @@ namespace osu.Framework.Audio.Asio
|
||||
lastInitializedDeviceIndex = deviceIndex;
|
||||
lastRequestedSampleRate = sampleRateToTry ?? successfulRate;
|
||||
lastRequestedBufferSize = bufferSize;
|
||||
|
||||
// 先尝试使用 CoreAudio 通知(MMDevice)进行事件驱动的变更检测
|
||||
try
|
||||
{
|
||||
notifierHandler = () =>
|
||||
{
|
||||
// 确保不会并发执行多个重新初始化
|
||||
if (System.Threading.Interlocked.CompareExchange(ref reinitInProgress, 1, 0) != 0)
|
||||
if (Interlocked.CompareExchange(ref reinitInProgress, 1, 0) != 0)
|
||||
return;
|
||||
|
||||
System.Threading.Tasks.Task.Run(() =>
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.Log("ASIO device notification received from system, triggering reinit.", LoggingTarget.Runtime, LogLevel.Important);
|
||||
FreeDevice();
|
||||
if (lastInitializedDeviceIndex.HasValue)
|
||||
{
|
||||
InitializeDevice(lastInitializedDeviceIndex.Value, lastRequestedSampleRate, lastRequestedBufferSize);
|
||||
}
|
||||
|
||||
InitializeDevice(lastInitializedDeviceIndex.Value, lastRequestedSampleRate, lastRequestedBufferSize);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -401,7 +404,7 @@ namespace osu.Framework.Audio.Asio
|
||||
}
|
||||
finally
|
||||
{
|
||||
System.Threading.Interlocked.Exchange(ref reinitInProgress, 0);
|
||||
Interlocked.Exchange(ref reinitInProgress, 0);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -730,6 +733,7 @@ namespace osu.Framework.Audio.Asio
|
||||
AsioProcedure asioCallback = asioProcedure;
|
||||
|
||||
int outputs = Math.Max(0, info.Outputs);
|
||||
|
||||
if (outputs < 2)
|
||||
{
|
||||
Logger.Log($"Not enough ASIO outputs ({outputs}) to configure stereo", LoggingTarget.Runtime, LogLevel.Error);
|
||||
@@ -817,7 +821,7 @@ namespace osu.Framework.Audio.Asio
|
||||
{
|
||||
foreach (ProcessModule mod in proc.Modules)
|
||||
{
|
||||
if (mod.ModuleName?.Equals("bassasio.dll", StringComparison.OrdinalIgnoreCase) == true)
|
||||
if (mod.ModuleName.Equals("bassasio.dll", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result.Add((proc.Id, proc.ProcessName));
|
||||
break;
|
||||
@@ -844,12 +848,13 @@ namespace osu.Framework.Audio.Asio
|
||||
{
|
||||
stopReinitMonitor();
|
||||
|
||||
reinitMonitorCts = new System.Threading.CancellationTokenSource();
|
||||
reinitMonitorCts = new CancellationTokenSource();
|
||||
var token = reinitMonitorCts.Token;
|
||||
|
||||
System.Threading.Tasks.Task.Run(() =>
|
||||
Task.Run(() =>
|
||||
{
|
||||
AsioInfo lastInfo = default;
|
||||
|
||||
try
|
||||
{
|
||||
BassAsio.GetInfo(out lastInfo);
|
||||
@@ -857,7 +862,9 @@ namespace osu.Framework.Audio.Asio
|
||||
catch { }
|
||||
|
||||
double lastRate = 0;
|
||||
try { lastRate = BassAsio.Rate; } catch { }
|
||||
|
||||
try { lastRate = BassAsio.Rate; }
|
||||
catch { }
|
||||
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
@@ -865,14 +872,19 @@ namespace osu.Framework.Audio.Asio
|
||||
{
|
||||
// 检查采样率或通道数变化
|
||||
AsioInfo info;
|
||||
|
||||
if (BassAsio.GetInfo(out info))
|
||||
{
|
||||
double rate = 0;
|
||||
try { rate = BassAsio.Rate; } catch { }
|
||||
|
||||
try { rate = BassAsio.Rate; }
|
||||
catch { }
|
||||
|
||||
if (info.Inputs != lastInfo.Inputs || info.Outputs != lastInfo.Outputs || Math.Abs(rate - lastRate) > 0.5)
|
||||
{
|
||||
Logger.Log($"ASIO device parameters changed (inputs:{lastInfo.Inputs}->{info.Inputs}, outputs:{lastInfo.Outputs}->{info.Outputs}, rate:{lastRate}->{rate}). Triggering reinit.", LoggingTarget.Runtime, LogLevel.Important);
|
||||
Logger.Log(
|
||||
$"ASIO device parameters changed (inputs:{lastInfo.Inputs}->{info.Inputs}, outputs:{lastInfo.Outputs}->{info.Outputs}, rate:{lastRate}->{rate}). Triggering reinit.",
|
||||
LoggingTarget.Runtime, LogLevel.Important);
|
||||
|
||||
// 执行重新初始化:释放并使用原参数重新初始化
|
||||
try
|
||||
@@ -900,7 +912,7 @@ namespace osu.Framework.Audio.Asio
|
||||
// 忽略单次检测错误
|
||||
}
|
||||
|
||||
System.Threading.Thread.Sleep(checkIntervalMs);
|
||||
Thread.Sleep(checkIntervalMs);
|
||||
}
|
||||
}, token);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// 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.Runtime.InteropServices;
|
||||
|
||||
@@ -9,8 +12,8 @@ namespace osu.Framework.Audio.Asio
|
||||
{
|
||||
public static event Action? DeviceChanged;
|
||||
|
||||
private static IMMNotificationClientImpl? client;
|
||||
private static IMMDeviceEnumerator? enumerator;
|
||||
private static ImmNotificationClientImpl? client;
|
||||
private static IMmDeviceEnumerator? enumerator;
|
||||
|
||||
public static void Start()
|
||||
{
|
||||
@@ -18,9 +21,9 @@ namespace osu.Framework.Audio.Asio
|
||||
|
||||
try
|
||||
{
|
||||
enumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
|
||||
client = new IMMNotificationClientImpl();
|
||||
client.DeviceChanged += () => DeviceChanged?.Invoke();
|
||||
enumerator = (IMmDeviceEnumerator)new MMDeviceEnumerator();
|
||||
client = new ImmNotificationClientImpl();
|
||||
|
||||
try
|
||||
{
|
||||
// RegisterEndpointNotificationCallback returns an HRESULT; ignore non-zero gracefully
|
||||
@@ -30,7 +33,6 @@ namespace osu.Framework.Audio.Asio
|
||||
{
|
||||
// swallow registry failures and stop notifier to allow fallback polling
|
||||
Stop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -42,14 +44,14 @@ namespace osu.Framework.Audio.Asio
|
||||
|
||||
public static void Stop()
|
||||
{
|
||||
try
|
||||
try
|
||||
{
|
||||
if (enumerator != null && client != null)
|
||||
{
|
||||
if (enumerator != null && client != null)
|
||||
{
|
||||
_ = enumerator.UnregisterEndpointNotificationCallback(client);
|
||||
}
|
||||
_ = enumerator.UnregisterEndpointNotificationCallback(client);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
client = null;
|
||||
@@ -57,10 +59,8 @@ namespace osu.Framework.Audio.Asio
|
||||
}
|
||||
}
|
||||
|
||||
private class IMMNotificationClientImpl : IMMNotificationClient
|
||||
private class ImmNotificationClientImpl : IMmNotificationClient
|
||||
{
|
||||
public event Action? DeviceChanged;
|
||||
|
||||
public void OnDeviceStateChanged(string pwstrDeviceId, int dwNewState)
|
||||
{
|
||||
DeviceChanged?.Invoke();
|
||||
@@ -81,7 +81,7 @@ namespace osu.Framework.Audio.Asio
|
||||
DeviceChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void OnPropertyValueChanged(string pwstrDeviceId, PROPERTYKEY key)
|
||||
public void OnPropertyValueChanged(string pwstrDeviceId, PropertyKey key)
|
||||
{
|
||||
DeviceChanged?.Invoke();
|
||||
}
|
||||
@@ -90,31 +90,38 @@ namespace osu.Framework.Audio.Asio
|
||||
#region COM interop
|
||||
|
||||
[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
|
||||
private class MMDeviceEnumerator { }
|
||||
private class MMDeviceEnumerator
|
||||
{
|
||||
}
|
||||
|
||||
private enum EDataFlow { eRender = 0, eCapture = 1, eAll = 2 }
|
||||
private enum ERole { eConsole = 0, eMultimedia = 1, eCommunications = 2 }
|
||||
private enum EDataFlow { ERender = 0, ECapture = 1, EAll = 2 }
|
||||
|
||||
private enum ERole { EConsole = 0, EMultimedia = 1, ECommunications = 2 }
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct PROPERTYKEY { public Guid fmtid; public int pid; }
|
||||
private struct PropertyKey
|
||||
{
|
||||
public Guid fmtID;
|
||||
public int pid;
|
||||
}
|
||||
|
||||
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface IMMDeviceEnumerator
|
||||
private interface IMmDeviceEnumerator
|
||||
{
|
||||
int NotImpl1();
|
||||
int NotImpl2();
|
||||
int RegisterEndpointNotificationCallback(IMMNotificationClient client);
|
||||
int UnregisterEndpointNotificationCallback(IMMNotificationClient client);
|
||||
int RegisterEndpointNotificationCallback(IMmNotificationClient client);
|
||||
int UnregisterEndpointNotificationCallback(IMmNotificationClient client);
|
||||
}
|
||||
|
||||
[Guid("7991EEC9-7E89-4D85-8390-6C703CEC60C0"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface IMMNotificationClient
|
||||
private interface IMmNotificationClient
|
||||
{
|
||||
void OnDeviceStateChanged([MarshalAs(UnmanagedType.LPWStr)] string pwstrDeviceId, int dwNewState);
|
||||
void OnDeviceAdded([MarshalAs(UnmanagedType.LPWStr)] string pwstrDeviceId);
|
||||
void OnDeviceRemoved([MarshalAs(UnmanagedType.LPWStr)] string pwstrDeviceId);
|
||||
void OnDefaultDeviceChanged(EDataFlow flow, ERole role, [MarshalAs(UnmanagedType.LPWStr)] string pwstrDefaultDeviceId);
|
||||
void OnPropertyValueChanged([MarshalAs(UnmanagedType.LPWStr)] string pwstrDeviceId, PROPERTYKEY key);
|
||||
void OnPropertyValueChanged([MarshalAs(UnmanagedType.LPWStr)] string pwstrDeviceId, PropertyKey key);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -73,12 +73,12 @@ namespace osu.Framework.Audio
|
||||
/// <summary>
|
||||
/// 采样率,用于ASIO设备的初始化和运行时更改。
|
||||
/// </summary>
|
||||
public readonly Bindable<int> SAMPLE_RATE = new Bindable<int>(48000);
|
||||
public readonly Bindable<int> SampleRate = new Bindable<int>(48000);
|
||||
|
||||
/// <summary>
|
||||
/// ASIO缓冲区大小,默认为128,用于ASIO设备的初始化。
|
||||
/// </summary>
|
||||
public readonly Bindable<int> ASIO_BUFFER_SIZE = new Bindable<int>(128);
|
||||
public readonly Bindable<int> AsioBufferSize = new Bindable<int>(128);
|
||||
|
||||
/// <summary>
|
||||
/// The names of all available audio devices.
|
||||
@@ -237,7 +237,7 @@ namespace osu.Framework.Audio
|
||||
/// <param name="sampleRate">The preferred sample rate in Hz.</param>
|
||||
public void SetPreferredAsioSampleRate(int sampleRate)
|
||||
{
|
||||
SAMPLE_RATE.Value = sampleRate;
|
||||
SampleRate.Value = sampleRate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -246,7 +246,7 @@ namespace osu.Framework.Audio
|
||||
/// <param name="bufferSize">The preferred buffer size for ASIO device.</param>
|
||||
public void SetAsioBufferSize(int bufferSize)
|
||||
{
|
||||
ASIO_BUFFER_SIZE.Value = bufferSize;
|
||||
AsioBufferSize.Value = bufferSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -328,7 +328,7 @@ namespace osu.Framework.Audio
|
||||
GlobalMixerHandle.ValueChanged += handle => usingGlobalMixer.Value = handle.NewValue.HasValue;
|
||||
|
||||
// Listen for unified sample rate changes and reinitialize device if supported
|
||||
SAMPLE_RATE.ValueChanged += e =>
|
||||
SampleRate.ValueChanged += e =>
|
||||
{
|
||||
if (syncingSelection)
|
||||
return;
|
||||
@@ -340,7 +340,7 @@ namespace osu.Framework.Audio
|
||||
// Only reinitialize if we're currently using an ASIO device (only ASIO supports runtime sample rate changes)
|
||||
if (hasTypeSuffix(AudioDevice.Value) && tryParseSuffixed(AudioDevice.Value, type_asio, out string _))
|
||||
{
|
||||
Logger.Log($"Sample rate changed to {SAMPLE_RATE.Value}Hz, reinitializing ASIO device", name: "audio", level: LogLevel.Important);
|
||||
Logger.Log($"Sample rate changed to {SampleRate.Value}Hz, reinitializing ASIO device", name: "audio", level: LogLevel.Important);
|
||||
Logger.Log($"Current audio device before reinitialization: {AudioDevice.Value}", name: "audio", level: LogLevel.Debug);
|
||||
|
||||
scheduler.AddOnce(() =>
|
||||
@@ -358,7 +358,7 @@ namespace osu.Framework.Audio
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Log($"Sample rate changed to {SAMPLE_RATE.Value}Hz, but current device ({AudioDevice.Value}) does not support runtime sample rate changes", name: "audio",
|
||||
Logger.Log($"Sample rate changed to {SampleRate.Value}Hz, but current device ({AudioDevice.Value}) does not support runtime sample rate changes", name: "audio",
|
||||
level: LogLevel.Debug);
|
||||
syncingSelection = false;
|
||||
isChangingAsioSettings = false;
|
||||
@@ -372,7 +372,7 @@ namespace osu.Framework.Audio
|
||||
};
|
||||
|
||||
// Listen for ASIO buffer size changes and reinitialize device if supported
|
||||
ASIO_BUFFER_SIZE.ValueChanged += e =>
|
||||
AsioBufferSize.ValueChanged += e =>
|
||||
{
|
||||
if (syncingSelection)
|
||||
return;
|
||||
@@ -401,7 +401,7 @@ namespace osu.Framework.Audio
|
||||
// Only reinitialize if we're currently using an ASIO device (only ASIO supports runtime buffer size changes)
|
||||
if (hasTypeSuffix(AudioDevice.Value) && tryParseSuffixed(AudioDevice.Value, type_asio, out string _))
|
||||
{
|
||||
Logger.Log($"ASIO buffer size changed to {ASIO_BUFFER_SIZE.Value}, reinitializing ASIO device", name: "audio", level: LogLevel.Important);
|
||||
Logger.Log($"ASIO buffer size changed to {AsioBufferSize.Value}, reinitializing ASIO device", name: "audio", level: LogLevel.Important);
|
||||
Logger.Log($"Current audio device before reinitialization: {AudioDevice.Value}", name: "audio", level: LogLevel.Debug);
|
||||
|
||||
scheduler.AddOnce(() =>
|
||||
@@ -419,7 +419,7 @@ namespace osu.Framework.Audio
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Log($"ASIO buffer size changed to {ASIO_BUFFER_SIZE.Value}, but current device ({AudioDevice.Value}) does not support runtime buffer size changes", name: "audio", level: LogLevel.Debug);
|
||||
Logger.Log($"ASIO buffer size changed to {AsioBufferSize.Value}, but current device ({AudioDevice.Value}) does not support runtime buffer size changes", name: "audio", level: LogLevel.Debug);
|
||||
syncingSelection = false;
|
||||
isChangingAsioSettings = false;
|
||||
}
|
||||
@@ -787,7 +787,7 @@ namespace osu.Framework.Audio
|
||||
|
||||
try
|
||||
{
|
||||
innerSuccess = thread.InitDevice(device, outputMode, SAMPLE_RATE.Value);
|
||||
innerSuccess = thread.InitDevice(device, outputMode, SampleRate.Value);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
@@ -2,25 +2,25 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Threading;
|
||||
|
||||
namespace osu.Framework.Audio.EzLatency
|
||||
{
|
||||
#nullable disable
|
||||
|
||||
// --- Structures and Record -------------------------------------------------
|
||||
#nullable disable
|
||||
|
||||
public struct EzLatencyInputData
|
||||
{
|
||||
public double InputTime;
|
||||
public object KeyValue;
|
||||
public double JudgeTime;
|
||||
|
||||
public double PlaybackTime;
|
||||
|
||||
// Consider input+playback or input+judge as valid for best-effort measurements.
|
||||
public bool IsValid => InputTime > 0 && (PlaybackTime > 0 || JudgeTime > 0);
|
||||
}
|
||||
@@ -67,7 +67,7 @@ namespace osu.Framework.Audio.EzLatency
|
||||
public class EzLatencyAnalyzer
|
||||
{
|
||||
private readonly Stopwatch stopwatch;
|
||||
public bool Enabled { get; set; } = false;
|
||||
public bool Enabled { get; set; }
|
||||
public event Action<EzLatencyRecord> OnNewRecord;
|
||||
|
||||
private EzLatencyInputData currentInputData;
|
||||
@@ -83,6 +83,7 @@ namespace osu.Framework.Audio.EzLatency
|
||||
public void RecordInputData(double inputTime, object keyValue = null)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
|
||||
if (currentInputData.InputTime > 0)
|
||||
{
|
||||
currentInputData = default;
|
||||
@@ -97,6 +98,7 @@ namespace osu.Framework.Audio.EzLatency
|
||||
public void RecordJudgeData(double judgeTime)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
|
||||
currentInputData.JudgeTime = judgeTime;
|
||||
checkTimeout();
|
||||
}
|
||||
@@ -104,6 +106,7 @@ namespace osu.Framework.Audio.EzLatency
|
||||
public void RecordPlaybackData(double playbackTime)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
|
||||
currentInputData.PlaybackTime = playbackTime;
|
||||
tryGenerateCompleteRecord();
|
||||
}
|
||||
@@ -111,6 +114,7 @@ namespace osu.Framework.Audio.EzLatency
|
||||
public void RecordHardwareData(double driverTime, double outputHardwareTime, double inputHardwareTime, double latencyDifference)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
|
||||
currentHardwareData = new EzLatencyHardwareData
|
||||
{
|
||||
DriverTime = driverTime,
|
||||
@@ -142,9 +146,11 @@ namespace osu.Framework.Audio.EzLatency
|
||||
InputHardwareTime = currentHardwareData.InputHardwareTime,
|
||||
LatencyDifference = currentHardwareData.LatencyDifference,
|
||||
// MeasuredMs: prefer Playback - Input when available, otherwise use Judge - Input as a best-effort.
|
||||
MeasuredMs = (currentInputData.PlaybackTime > 0)
|
||||
MeasuredMs = currentInputData.PlaybackTime > 0
|
||||
? currentInputData.PlaybackTime - currentInputData.InputTime
|
||||
: (currentInputData.JudgeTime > 0 ? currentInputData.JudgeTime - currentInputData.InputTime : 0),
|
||||
: currentInputData.JudgeTime > 0
|
||||
? currentInputData.JudgeTime - currentInputData.InputTime
|
||||
: 0,
|
||||
Note = currentHardwareData.IsValid ? "complete-latency-measurement" : "best-effort-no-hw",
|
||||
InputData = currentInputData,
|
||||
HardwareData = currentHardwareData
|
||||
@@ -152,17 +158,17 @@ namespace osu.Framework.Audio.EzLatency
|
||||
|
||||
try
|
||||
{
|
||||
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);
|
||||
OnNewRecord?.Invoke(record);
|
||||
EzLatencyService.Instance.PushRecord(record);
|
||||
Logger.Log(
|
||||
currentHardwareData.IsValid
|
||||
? $"EzLatency 完整记录已生成: Input→Playback={record.PlaybackTime - record.InputTime:F2}ms"
|
||||
: $"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);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log($"EzLatencyAnalyzer: tryGenerateCompleteRecord failed: {ex.Message}", LoggingTarget.Runtime, LogLevel.Error);
|
||||
}
|
||||
|
||||
ClearCurrentData();
|
||||
}
|
||||
@@ -174,6 +180,7 @@ namespace osu.Framework.Audio.EzLatency
|
||||
if (recordStartTime > 0)
|
||||
{
|
||||
double elapsed = stopwatch.Elapsed.TotalMilliseconds - recordStartTime;
|
||||
|
||||
if (elapsed > timeout_ms)
|
||||
{
|
||||
Logger.Log($"EzLatency 数据收集超时 ({elapsed:F0}ms),清除旧数据", LoggingTarget.Runtime, LogLevel.Debug);
|
||||
@@ -225,14 +232,14 @@ namespace osu.Framework.Audio.EzLatency
|
||||
public class EzLoggerAdapter : IEzLatencyLogger
|
||||
{
|
||||
private readonly Scheduler scheduler;
|
||||
private StreamWriter fileWriter;
|
||||
private readonly StreamWriter fileWriter;
|
||||
public event Action<EzLatencyRecord> OnRecord;
|
||||
|
||||
public EzLoggerAdapter(Scheduler scheduler = null, string filePath = null)
|
||||
{
|
||||
this.scheduler = scheduler;
|
||||
|
||||
this.scheduler = scheduler as Scheduler;
|
||||
this.scheduler = scheduler;
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -343,7 +350,9 @@ namespace osu.Framework.Audio.EzLatency
|
||||
public void RecordInputData(double inputTime, object keyValue = null) => analyzer.RecordInputData(inputTime, keyValue);
|
||||
public void RecordJudgeData(double judgeTime) => analyzer.RecordJudgeData(judgeTime);
|
||||
public void RecordPlaybackData(double playbackTime) => analyzer.RecordPlaybackData(playbackTime);
|
||||
public void RecordHardwareData(double driverTime, double outputHardwareTime, double inputHardwareTime, double latencyDifference) => analyzer.RecordHardwareData(driverTime, outputHardwareTime, inputHardwareTime, latencyDifference);
|
||||
|
||||
public void RecordHardwareData(double driverTime, double outputHardwareTime, double inputHardwareTime, double latencyDifference) =>
|
||||
analyzer.RecordHardwareData(driverTime, outputHardwareTime, inputHardwareTime, latencyDifference);
|
||||
}
|
||||
|
||||
// --- Statistics & Collector ----------------------------------------------
|
||||
@@ -394,22 +403,22 @@ namespace osu.Framework.Audio.EzLatency
|
||||
|
||||
// Filter out invalid or incomplete differences (<= 0) to avoid extreme/garbage values.
|
||||
var inputToJudge = records
|
||||
.Where(r => r.InputTime > 0 && r.JudgeTime > 0)
|
||||
.Select(r => r.JudgeTime - r.InputTime)
|
||||
.Where(d => Math.Abs(d) <= 1000) // sanity cap: ignore absurdly large diffs (>1000ms)
|
||||
.ToList();
|
||||
.Where(r => r.InputTime > 0 && r.JudgeTime > 0)
|
||||
.Select(r => r.JudgeTime - r.InputTime)
|
||||
.Where(d => Math.Abs(d) <= 1000) // sanity cap: ignore absurdly large diffs (>1000ms)
|
||||
.ToList();
|
||||
|
||||
var inputToPlayback = records
|
||||
.Where(r => r.InputTime > 0 && r.PlaybackTime > 0)
|
||||
.Select(r => r.PlaybackTime - r.InputTime)
|
||||
.Where(d => Math.Abs(d) <= 1000)
|
||||
.ToList();
|
||||
.Where(r => r.InputTime > 0 && r.PlaybackTime > 0)
|
||||
.Select(r => r.PlaybackTime - r.InputTime)
|
||||
.Where(d => Math.Abs(d) <= 1000)
|
||||
.ToList();
|
||||
|
||||
var playbackToJudge = records
|
||||
.Where(r => r.PlaybackTime > 0 && r.JudgeTime > 0)
|
||||
.Select(r => r.JudgeTime - r.PlaybackTime)
|
||||
.Where(d => Math.Abs(d) <= 1000)
|
||||
.ToList();
|
||||
.Where(r => r.PlaybackTime > 0 && r.JudgeTime > 0)
|
||||
.Select(r => r.JudgeTime - r.PlaybackTime)
|
||||
.Where(d => Math.Abs(d) <= 1000)
|
||||
.ToList();
|
||||
|
||||
var hardwareLatency = records.Select(r => r.OutputHardwareTime).Where(h => h > 0).ToList();
|
||||
|
||||
|
||||
@@ -25,10 +25,11 @@ namespace osu.Framework.Audio.Sample
|
||||
ObjectDisposedException.ThrowIf(IsDisposed, this);
|
||||
|
||||
Played = true;
|
||||
|
||||
try
|
||||
{
|
||||
// Best-effort: notify global latency manager that a sample playback was requested.
|
||||
osu.Framework.Audio.EzLatency.EzLatencyManager.GLOBAL.RecordPlaybackEvent();
|
||||
EzLatency.EzLatencyManager.GLOBAL.RecordPlaybackEvent();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -132,14 +132,14 @@ namespace osu.Framework.Input
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
#if DEBUG
|
||||
if (handledBy != null)
|
||||
{
|
||||
Logger.Log(SuppressLoggingEventInformation(handledBy)
|
||||
? $"{e.GetType().Name} handled by {handledBy}."
|
||||
: $"{e} handled by {handledBy}.", LoggingTarget.Runtime, LogLevel.Debug);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
return handledBy;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
// using osu.Framework.Extensions.TypeExtensions;
|
||||
|
||||
using osu.Framework.Input.States;
|
||||
using osuTK.Input;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ using JetBrains.Annotations;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Input.States;
|
||||
|
||||
// using osu.Framework.Logging;
|
||||
|
||||
namespace osu.Framework.Input
|
||||
|
||||
@@ -311,7 +311,7 @@ namespace osu.Framework.Threading
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Log($"无法找到ASIO设备: {deviceName}", name: "audio", level: LogLevel.Error);
|
||||
Logger.Log($"无法找到ASIO设备: {deviceName}, Mode: {mode}", name: "audio", level: LogLevel.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -687,8 +687,8 @@ namespace osu.Framework.Threading
|
||||
// 使用来自AudioManager的统一采样率和缓冲区大小
|
||||
if (Manager != null)
|
||||
{
|
||||
preferredSampleRate = Manager.SAMPLE_RATE.Value;
|
||||
int bufferSize = Manager.ASIO_BUFFER_SIZE.Value;
|
||||
preferredSampleRate = Manager.SampleRate.Value == 0 ? preferredSampleRate : Manager.SampleRate.Value;
|
||||
int bufferSize = Manager.AsioBufferSize.Value;
|
||||
|
||||
// 验证当前设备是否已在运行,如果是则先停止
|
||||
if (AsioDeviceManager.IsDeviceRunning())
|
||||
@@ -718,7 +718,7 @@ namespace osu.Framework.Threading
|
||||
}
|
||||
|
||||
int outputChannels = Math.Max(1, deviceInfo.Value.Outputs);
|
||||
int inputChannels = Math.Max(0, deviceInfo.Value.Inputs);
|
||||
// int inputChannels = Math.Max(0, deviceInfo.Value.Inputs);
|
||||
|
||||
// 验证设备信息
|
||||
if (outputChannels < 2)
|
||||
@@ -898,8 +898,6 @@ namespace osu.Framework.Threading
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
@@ -918,7 +916,7 @@ namespace osu.Framework.Threading
|
||||
}
|
||||
|
||||
// 检查是否为ASIO设备
|
||||
string suffix = $" ({type_asio})";
|
||||
const string suffix = $" ({type_asio})";
|
||||
|
||||
if (selection.EndsWith(suffix, StringComparison.Ordinal))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user