From a3b8cae7af38d2d46d5cc2723ec44b667c0f00f8 Mon Sep 17 00:00:00 2001
From: LA <1245661240@qq.com>
Date: Fri, 16 Jan 2026 12:28:33 +0800
Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84ASIO=E6=9C=BA=E5=88=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
osu-framework.sln.DotSettings | 2 +
.../AudioManagerDeviceEnumerationTest.cs | 22 +-
.../Audio/AudioManagerWithDeviceLoss.cs | 7 +-
osu.Framework/Audio/Asio/AsioDeviceManager.cs | 285 ++++++++------
osu.Framework/Audio/AudioManager.cs | 348 ++++++++----------
.../Audio/{ => EzLatency}/EzDriverModule.cs | 5 +-
.../Audio/{ => EzLatency}/EzHardwareModule.cs | 5 +-
.../Audio/{ => EzLatency}/EzInputModule.cs | 5 +-
.../Audio/{ => EzLatency}/EzJudgeModule.cs | 5 +-
.../Audio/EzLatency/EzLatencyRecord.cs | 13 +-
.../Audio/EzLatency/EzLatencyService.cs | 9 +-
.../Audio/EzLatency/EzLatencyStruct.cs | 35 ++
.../{ => EzLatency}/EzLatencyTestModule.cs | 7 +-
.../Audio/{ => EzLatency}/EzLogModule.cs | 21 +-
.../Audio/EzLatency/EzLoggerAdapter.cs | 15 +-
.../Audio/{ => EzLatency}/EzPlaybackModule.cs | 6 +-
.../Audio/EzLatency/IEzLatencyLogger.cs | 5 +-
osu.Framework/Platform/GameHost.cs | 4 +-
osu.Framework/Threading/AudioThread.cs | 96 ++---
19 files changed, 465 insertions(+), 430 deletions(-)
rename osu.Framework/Audio/{ => EzLatency}/EzDriverModule.cs (95%)
rename osu.Framework/Audio/{ => EzLatency}/EzHardwareModule.cs (97%)
rename osu.Framework/Audio/{ => EzLatency}/EzInputModule.cs (94%)
rename osu.Framework/Audio/{ => EzLatency}/EzJudgeModule.cs (94%)
create mode 100644 osu.Framework/Audio/EzLatency/EzLatencyStruct.cs
rename osu.Framework/Audio/{ => EzLatency}/EzLatencyTestModule.cs (95%)
rename osu.Framework/Audio/{ => EzLatency}/EzLogModule.cs (78%)
rename osu.Framework/Audio/{ => EzLatency}/EzPlaybackModule.cs (96%)
diff --git a/osu-framework.sln.DotSettings b/osu-framework.sln.DotSettings
index bad7c7804..e5b76aeb8 100644
--- a/osu-framework.sln.DotSettings
+++ b/osu-framework.sln.DotSettings
@@ -987,6 +987,8 @@ private void load()
$END$
};
True
+ True
+ True
True
True
True
diff --git a/osu.Framework.Tests/Audio/AudioManagerDeviceEnumerationTest.cs b/osu.Framework.Tests/Audio/AudioManagerDeviceEnumerationTest.cs
index 3b76f681f..306f8e1cb 100644
--- a/osu.Framework.Tests/Audio/AudioManagerDeviceEnumerationTest.cs
+++ b/osu.Framework.Tests/Audio/AudioManagerDeviceEnumerationTest.cs
@@ -72,17 +72,17 @@ namespace osu.Framework.Tests.Audio
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)
{
diff --git a/osu.Framework.Tests/Audio/AudioManagerWithDeviceLoss.cs b/osu.Framework.Tests/Audio/AudioManagerWithDeviceLoss.cs
index 567254c14..0a4191803 100644
--- a/osu.Framework.Tests/Audio/AudioManagerWithDeviceLoss.cs
+++ b/osu.Framework.Tests/Audio/AudioManagerWithDeviceLoss.cs
@@ -5,6 +5,7 @@ using System.Collections.Immutable;
using System.Linq;
using ManagedBass;
using osu.Framework.Audio;
+using osu.Framework.Audio.EzLatency;
using osu.Framework.IO.Stores;
using osu.Framework.Threading;
@@ -25,14 +26,14 @@ namespace osu.Framework.Tests.Audio
private volatile bool simulateLoss;
- protected override bool InitBass(int device, AudioOutputMode outputMode, int? asioDeviceIndex)
+ protected override bool InitBass(int device, AudioOutputMode outputMode)
{
try
{
if (simulateLoss)
- return device == Bass.NoSoundDevice && base.InitBass(device, outputMode, asioDeviceIndex);
+ return device == Bass.NoSoundDevice && base.InitBass(device, outputMode);
- return base.InitBass(device, outputMode, asioDeviceIndex);
+ return base.InitBass(device, outputMode);
}
finally
{
diff --git a/osu.Framework/Audio/Asio/AsioDeviceManager.cs b/osu.Framework/Audio/Asio/AsioDeviceManager.cs
index d6f1506f6..76d020977 100644
--- a/osu.Framework/Audio/Asio/AsioDeviceManager.cs
+++ b/osu.Framework/Audio/Asio/AsioDeviceManager.cs
@@ -16,13 +16,6 @@ namespace osu.Framework.Audio.Asio
///
public static class AsioDeviceManager
{
- // ///
- // /// 常见采样率列表, 用于尝试验证设备支持的采样率。
- // /// 按优先级排序,48kHz优先于44.1kHz。
- // /// 使用int类型因为采样率通常是整数值,double仅在底层API交互时使用。
- // ///
- // 删除 SUPPORTED_SAMPLE_RATES 列表,默认使用 48000
-
///
/// 未指定、或设置采样率失败时,使用默认采样率48kHz,性能较好。
///
@@ -31,32 +24,32 @@ namespace osu.Framework.Audio.Asio
///
/// 最大重试次数,用于处理设备繁忙的情况。
///
- private const int MAX_RETRY_COUNT = 3;
+ private const int max_retry_count = 3;
///
/// 重试间隔时间(毫秒)。
///
- private const int RETRY_DELAY_MS = 100;
+ private const int retry_delay_ms = 100;
///
/// 设备释放延迟时间(毫秒)。
///
- private const int DEVICE_FREE_DELAY_MS = 100;
+ private const int device_free_delay_ms = 100;
///
/// 强制重置延迟时间(毫秒)。
///
- private const int FORCE_RESET_DELAY_MS = 200;
+ private const int force_reset_delay_ms = 200;
///
/// 采样率容差,用于验证设置是否成功。
///
- private const double SAMPLE_RATE_TOLERANCE = 1.0;
+ private const double sample_rate_tolerance = 1.0;
///
/// 静音帧日志间隔。
///
- private const int SILENCE_LOG_INTERVAL = 200;
+ private const int silence_log_interval = 200;
///
/// ASIO音频路由的全局混音器句柄。
@@ -117,7 +110,7 @@ namespace osu.Framework.Audio.Asio
/// 如果初始化成功则为true,否则为false。
private static bool tryInitializeDevice(int deviceIndex, AsioInitFlags flags)
{
- for (int retryCount = 0; retryCount < MAX_RETRY_COUNT; retryCount++)
+ for (int retryCount = 0; retryCount < max_retry_count; retryCount++)
{
if (BassAsio.Init(deviceIndex, flags))
{
@@ -131,10 +124,10 @@ namespace osu.Framework.Audio.Asio
// 如果设备繁忙,等待并重试
if ((int)bassError == 3 || bassError == Errors.Busy)
{
- if (retryCount < MAX_RETRY_COUNT - 1)
+ if (retryCount < max_retry_count - 1)
{
- Logger.Log($"Device busy, waiting {RETRY_DELAY_MS}ms before retry {retryCount + 1}/{MAX_RETRY_COUNT}", LoggingTarget.Runtime, LogLevel.Important);
- Thread.Sleep(RETRY_DELAY_MS);
+ Logger.Log($"Device busy, waiting {retry_delay_ms}ms before retry {retryCount + 1}/{max_retry_count}", LoggingTarget.Runtime, LogLevel.Important);
+ Thread.Sleep(retry_delay_ms);
continue;
}
}
@@ -170,7 +163,7 @@ namespace osu.Framework.Audio.Asio
double actualRate = BassAsio.Rate;
- if (Math.Abs(actualRate - rate) >= SAMPLE_RATE_TOLERANCE)
+ if (Math.Abs(actualRate - rate) >= sample_rate_tolerance)
{
Logger.Log($"Failed to set ASIO device sample rate to {rate}Hz (actual: {actualRate}Hz)", LoggingTarget.Runtime, LogLevel.Error);
return false;
@@ -211,33 +204,18 @@ namespace osu.Framework.Audio.Asio
}
}
- ///
- /// 获取带有支持采样率的可用ASIO设备列表。
- /// 注意:此操作可能较慢,因为需要为每个设备查询支持的采样率。
- ///
- public static IEnumerable<(int Index, string Name, double[] SupportedSampleRates)> AvailableDevicesWithSampleRates
- {
- get
- {
- foreach (var (index, name) in AvailableDevices)
- {
- double[] supportedRates = GetSupportedSampleRates(index).ToArray();
- yield return (index, name, supportedRates);
- }
- }
- }
-
///
/// 初始化ASIO设备。
///
/// 要初始化的ASIO设备的索引。
/// 要尝试的采样率。如果为null,则使用默认48000Hz。
+ /// ASIO缓冲区大小。如果为null,则使用默认128。
/// 如果初始化成功则为true,否则为false。
- public static bool InitializeDevice(int deviceIndex, double? sampleRateToTry = null)
+ public static bool InitializeDevice(int deviceIndex, double? sampleRateToTry = null, int? bufferSize = null)
{
try
{
- Logger.Log($"InitializeDevice called with deviceIndex={deviceIndex}, sampleRateToTry={sampleRateToTry}", LoggingTarget.Runtime,
+ Logger.Log($"InitializeDevice called with deviceIndex={deviceIndex}, sampleRateToTry={sampleRateToTry}, bufferSize={bufferSize}", LoggingTarget.Runtime,
LogLevel.Debug);
// 获取设备信息
@@ -246,9 +224,15 @@ namespace osu.Framework.Audio.Asio
Logger.Log($"Initializing ASIO device: {deviceInfo.Name} (Driver: {deviceInfo.Driver})", LoggingTarget.Runtime, LogLevel.Debug);
+ // 释放之前的设备,确保完全清理
FreeDevice();
- // 删除重试机制,直接尝试初始化
+ // 注意:BassAsio没有直接的方法来设置缓冲区大小
+ // ASIO缓冲区大小主要由驱动程序决定,无法在运行时动态设置
+ // 我们只能记录期望的缓冲区大小用于日志目的
+ Logger.Log($"Attempting to initialize ASIO device with sample rate {sampleRateToTry ?? 48000.0}Hz. Requested buffer size: {bufferSize ?? 128} (note: actual buffer size is determined by driver)", LoggingTarget.Runtime, LogLevel.Debug);
+
+ // 初始化设备
if (!tryInitializeDevice(deviceIndex, AsioInitFlags.Thread))
{
Logger.Log($"Failed to initialize ASIO device {deviceIndex} with Thread flag", LoggingTarget.Runtime, LogLevel.Error);
@@ -257,16 +241,17 @@ namespace osu.Framework.Audio.Asio
// 尝试采样率:使用传入的值或默认48000
double rateToTry = sampleRateToTry ?? 48000.0;
+
if (!trySetSampleRate(rateToTry))
{
Logger.Log($"Failed to set sample rate {rateToTry}Hz for ASIO device {deviceIndex}", LoggingTarget.Runtime, LogLevel.Error);
- BassAsio.Free();
+ FreeDevice();
return false;
}
double successfulRate = rateToTry;
- Logger.Log($"ASIO device {deviceIndex} initialized successfully with sample rate {successfulRate}Hz", LoggingTarget.Runtime, LogLevel.Important);
+ Logger.Log($"ASIO device {deviceIndex} initialized successfully with sample rate {successfulRate}Hz, buffer size {(bufferSize ?? 128)}", LoggingTarget.Runtime, LogLevel.Important);
return true;
}
catch (Exception ex)
@@ -285,7 +270,7 @@ namespace osu.Framework.Audio.Asio
{
BassAsio.Stop();
BassAsio.Free();
- Thread.Sleep(DEVICE_FREE_DELAY_MS);
+ Thread.Sleep(device_free_delay_ms);
}
catch (Exception ex)
{
@@ -302,7 +287,7 @@ namespace osu.Framework.Audio.Asio
{
FreeDevice();
globalMixerHandle = 0;
- Thread.Sleep(FORCE_RESET_DELAY_MS);
+ Thread.Sleep(force_reset_delay_ms);
Logger.Log("ASIO Force Reset", LoggingTarget.Runtime, LogLevel.Debug);
}
catch (Exception ex)
@@ -311,6 +296,27 @@ namespace osu.Framework.Audio.Asio
}
}
+ ///
+ /// 安全地切换ASIO设备,先完全释放当前设备再初始化新设备。
+ ///
+ /// 新设备的索引
+ /// 要尝试的采样率。如果为null,则使用默认48000Hz。
+ /// ASIO缓冲区大小。如果为null,则使用默认128。
+ /// 如果切换成功则为true,否则为false。
+ public static bool SwitchToDevice(int newDeviceIndex, double? sampleRateToTry = null, int? bufferSize = null)
+ {
+ Logger.Log($"Switching ASIO device to index {newDeviceIndex}, sampleRate: {sampleRateToTry}, bufferSize: {bufferSize}", LoggingTarget.Runtime, LogLevel.Important);
+
+ // 先完全释放当前设备
+ FreeDevice();
+
+ // 添加额外延迟确保设备完全释放
+ Thread.Sleep(500);
+
+ // 然后初始化新设备
+ return InitializeDevice(newDeviceIndex, sampleRateToTry, bufferSize);
+ }
+
///
/// 启动ASIO设备处理。
///
@@ -421,61 +427,6 @@ namespace osu.Framework.Audio.Asio
}
}
- ///
- /// 获取指定ASIO设备支持的采样率列表。
- /// 此方法临时初始化设备以查询支持的速率,然后释放它。
- ///
- /// 要查询的ASIO设备的索引。
- /// 支持的采样率列表,如果无法查询设备则为空列表。
- public static IEnumerable GetSupportedSampleRates(int deviceIndex)
- {
- var supportedRates = new List();
-
- try
- {
- if (!tryGetDeviceInfo(deviceIndex, out AsioDeviceInfo deviceInfo))
- {
- Logger.Log($"Failed to get device info for ASIO device index {deviceIndex}", LoggingTarget.Runtime, LogLevel.Error);
- return supportedRates;
- }
-
- FreeDevice();
-
- // 临时初始化设备进行查询
- if (!tryInitializeDevice(deviceIndex, AsioInitFlags.Thread))
- {
- Logger.Log($"Failed to temporarily initialize ASIO device {deviceIndex} for rate querying", LoggingTarget.Runtime, LogLevel.Error);
- return supportedRates;
- }
-
- // 只检查默认 48000 是否支持
- int rate = 48000;
- try
- {
- if (BassAsio.CheckRate(rate))
- {
- supportedRates.Add(rate);
- }
- }
- catch (Exception ex)
- {
- Logger.Log($"Exception while checking sample rate {rate}Hz: {ex.Message}", LoggingTarget.Runtime, LogLevel.Error);
- }
-
- FreeDevice();
-
- Logger.Log($"Found {supportedRates.Count} supported sample rates for ASIO device {deviceInfo.Name}: {string.Join(", ", supportedRates)}", LoggingTarget.Runtime, LogLevel.Important);
- }
- catch (Exception ex)
- {
- Logger.Log($"Exception querying ASIO device sample rates: {ex.Message}", LoggingTarget.Runtime, LogLevel.Error);
-
- FreeDevice();
- }
-
- return supportedRates;
- }
-
///
/// 为ASIO设备配置默认输入和输出通道。
///
@@ -492,7 +443,7 @@ namespace osu.Framework.Audio.Asio
Logger.Log($"ASIO device has {info.Value.Inputs} inputs and {info.Value.Outputs} outputs available", LoggingTarget.Runtime, LogLevel.Debug);
// 尝试配置至少一个立体声输出通道对
- bool channelsConfigured = configureOutputChannels(info.Value);
+ bool channelsConfigured = configureStereoChannels(info.Value);
if (channelsConfigured)
{
@@ -514,24 +465,19 @@ namespace osu.Framework.Audio.Asio
}
}
+ #region 配置双通道输出
+
///
/// 为ASIO设备配置输出通道。
///
/// ASIO设备信息。
/// 如果输出通道成功配置则为true,否则为false。
- private static bool configureOutputChannels(AsioInfo info)
+ private static bool configureStereoChannels(AsioInfo info)
{
- if (info.Outputs < 2)
- {
- Logger.Log($"Insufficient output channels available ({info.Outputs}), cannot configure stereo output", LoggingTarget.Runtime, LogLevel.Important);
- return false;
- }
+ if (info.Outputs == 2) return true;
try
{
- // 配置第一个立体声输出对(通道0和1)
- Logger.Log("Configuring stereo output channels (0 and 1) for ASIO device", LoggingTarget.Runtime, LogLevel.Debug);
-
// 创建ASIO过程回调
AsioProcedure asioCallback = asioProcedure;
@@ -579,6 +525,8 @@ namespace osu.Framework.Audio.Asio
}
}
+ #endregion
+
///
/// ASIO过程回调,用于通道处理。
///
@@ -613,7 +561,7 @@ namespace osu.Framework.Audio.Asio
{
// 没有音频数据可用,用静音填充
fillBufferWithSilence(buffer, length);
- if (++silenceFrames % SILENCE_LOG_INTERVAL == 0)
+ if (++silenceFrames % silence_log_interval == 0)
Logger.Log($"[AudioDebug] ASIO callback silence count={silenceFrames}, globalMixer={mixerHandle}", LoggingTarget.Runtime, LogLevel.Debug);
}
else if (bytesRead < length)
@@ -642,6 +590,53 @@ namespace osu.Framework.Audio.Asio
}
}
+ ///
+ /// 安全枚举ASIO设备,并确保正确处理错误。
+ ///
+ /// 一个可枚举的ASIO设备列表。
+ public static IEnumerable<(int Index, string Name)> EnumerateAsioDevices()
+ {
+ if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
+ return Enumerable.Empty<(int, string)>();
+
+ try
+ {
+ return AvailableDevices;
+ }
+ catch (DllNotFoundException)
+ {
+ // ASIO native library not available - this is expected in some test environments
+ return Enumerable.Empty<(int, string)>();
+ }
+ catch (EntryPointNotFoundException)
+ {
+ // ASIO native library not available - this is expected in some test environments
+ return Enumerable.Empty<(int, string)>();
+ }
+ catch (Exception ex)
+ {
+ // Log other unexpected exceptions but don't fail
+ Logger.Log($"Unexpected error enumerating ASIO devices: {ex.Message}", LoggingTarget.Runtime, LogLevel.Error);
+ return Enumerable.Empty<(int, string)>();
+ }
+ }
+
+ ///
+ /// Finds the ASIO device index for a given device name.
+ ///
+ /// The name of the ASIO device to find.
+ /// The device index if found, null otherwise.
+ public static int? FindAsioDeviceIndex(string deviceName)
+ {
+ foreach (var device in EnumerateAsioDevices())
+ {
+ if (device.Name == deviceName)
+ return device.Index;
+ }
+
+ return null;
+ }
+
///
/// 从音频线程获取全局混音器句柄。
///
@@ -683,3 +678,79 @@ namespace osu.Framework.Audio.Asio
}
}
}
+
+// #region 获取受支持的采样率
+
+// ///
+// /// 获取带有支持采样率的可用ASIO设备列表。
+// /// 注意:此操作可能较慢,因为需要为每个设备查询支持的采样率。
+// ///
+// public static IEnumerable<(int Index, string Name, double[] SupportedSampleRates)> AvailableDevicesWithSampleRates
+// {
+// get
+// {
+// foreach (var (index, name) in AvailableDevices)
+// {
+// double[] supportedRates = GetSupportedSampleRates(index).ToArray();
+// yield return (index, name, supportedRates);
+// }
+// }
+// }
+
+ // ///
+ // /// 获取指定ASIO设备支持的采样率列表。
+ // /// 此方法临时初始化设备以查询支持的速率,然后释放它。
+ // ///
+ // /// 要查询的ASIO设备的索引。
+ // /// 支持的采样率列表,如果无法查询设备则为空列表。
+ // public static IEnumerable GetSupportedSampleRates(int deviceIndex)
+ // {
+ // var supportedRates = new List();
+ //
+ // try
+ // {
+ // if (!tryGetDeviceInfo(deviceIndex, out AsioDeviceInfo deviceInfo))
+ // {
+ // Logger.Log($"Failed to get device info for ASIO device index {deviceIndex}", LoggingTarget.Runtime, LogLevel.Error);
+ // return supportedRates;
+ // }
+ //
+ // FreeDevice();
+ //
+ // // 临时初始化设备进行查询
+ // if (!tryInitializeDevice(deviceIndex, AsioInitFlags.Thread))
+ // {
+ // Logger.Log($"Failed to temporarily initialize ASIO device {deviceIndex} for rate querying", LoggingTarget.Runtime, LogLevel.Error);
+ // return supportedRates;
+ // }
+ //
+ // // 只检查默认 48000 是否支持
+ // int rate = 48000;
+ //
+ // try
+ // {
+ // if (BassAsio.CheckRate(rate))
+ // {
+ // supportedRates.Add(rate);
+ // }
+ // }
+ // catch (Exception ex)
+ // {
+ // Logger.Log($"Exception while checking sample rate {rate}Hz: {ex.Message}", LoggingTarget.Runtime, LogLevel.Error);
+ // }
+ //
+ // FreeDevice();
+ //
+ // Logger.Log($"Found {supportedRates.Count} supported sample rates for ASIO device {deviceInfo.Name}: {string.Join(", ", supportedRates)}", LoggingTarget.Runtime, LogLevel.Important);
+ // }
+ // catch (Exception ex)
+ // {
+ // Logger.Log($"Exception querying ASIO device sample rates: {ex.Message}", LoggingTarget.Runtime, LogLevel.Error);
+ //
+ // FreeDevice();
+ // }
+ //
+ // return supportedRates;
+ // }
+
+// #endregion
diff --git a/osu.Framework/Audio/AudioManager.cs b/osu.Framework/Audio/AudioManager.cs
index f246085ac..9bf180f52 100644
--- a/osu.Framework/Audio/AudioManager.cs
+++ b/osu.Framework/Audio/AudioManager.cs
@@ -1,4 +1,4 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
@@ -13,6 +13,7 @@ using JetBrains.Annotations;
using ManagedBass;
using ManagedBass.Fx;
using ManagedBass.Mix;
+using osu.Framework.Audio.EzLatency;
using AsioDeviceManager = osu.Framework.Audio.Asio.AsioDeviceManager;
using osu.Framework.Audio.Mixing;
using osu.Framework.Audio.Mixing.Bass;
@@ -72,12 +73,12 @@ namespace osu.Framework.Audio
///
/// 采样率,用于ASIO设备的初始化和运行时更改。
///
- public readonly Bindable SampleRate = new Bindable(48000);
+ public readonly Bindable SAMPLE_RATE = new Bindable(48000);
///
- /// ASIO设备的首选采样率, 用于设备的初始化。
+ /// ASIO缓冲区大小,默认为128,用于ASIO设备的初始化。
///
- private static int? preferredAsioSampleRate;
+ public readonly Bindable ASIO_BUFFER_SIZE = new Bindable(128);
///
/// The names of all available audio devices.
@@ -172,14 +173,6 @@ namespace osu.Framework.Audio
private ImmutableArray audioDevices = ImmutableArray.Empty;
private ImmutableList audioDeviceNames = ImmutableList.Empty;
- protected enum AudioOutputMode
- {
- Default,
- WasapiShared,
- WasapiExclusive,
- Asio,
- }
-
private static int asioNativeUnavailableLogged;
private const string legacy_type_bass = "BASS";
@@ -189,53 +182,6 @@ namespace osu.Framework.Audio
private bool syncingSelection;
- ///
- /// Safely enumerates ASIO devices with proper error handling.
- ///
- /// An enumerable of ASIO devices, or empty if ASIO is not available.
- private IEnumerable<(int Index, string Name)> enumerateAsioDevices()
- {
- if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
- return Enumerable.Empty<(int, string)>();
-
- try
- {
- return AsioDeviceManager.AvailableDevices;
- }
- catch (DllNotFoundException)
- {
- // ASIO native library not available - this is expected in some test environments
- return Enumerable.Empty<(int, string)>();
- }
- catch (EntryPointNotFoundException)
- {
- // ASIO native library not available - this is expected in some test environments
- return Enumerable.Empty<(int, string)>();
- }
- catch (Exception ex)
- {
- // Log other unexpected exceptions but don't fail
- Logger.Log($"Unexpected error enumerating ASIO devices: {ex.Message}", LoggingTarget.Runtime, LogLevel.Error);
- return Enumerable.Empty<(int, string)>();
- }
- }
-
- ///
- /// Finds the ASIO device index for a given device name.
- ///
- /// The name of the ASIO device to find.
- /// The device index if found, null otherwise.
- private int? findAsioDeviceIndex(string deviceName)
- {
- foreach (var device in enumerateAsioDevices())
- {
- if (device.Name == deviceName)
- return device.Index;
- }
-
- return null;
- }
-
private void setUserBindableValueLeaseSafe(Bindable bindable, T newValue)
{
if (EqualityComparer.Default.Equals(bindable.Value, newValue))
@@ -283,32 +229,21 @@ namespace osu.Framework.Audio
private readonly Lazy globalSampleStore;
///
- /// Sets the preferred sample rate for ASIO devices.
- /// This will be used during ASIO device initialization.
- ///
- /// The preferred sample rate in Hz, or null to use default.
- public static void SetPreferredAsioSampleRate(int? sampleRate)
- {
- preferredAsioSampleRate = sampleRate;
- }
-
- ///
- /// Sets the preferred sample rate for ASIO devices.
- /// This will be used during ASIO device initialization.
+ /// 设置ASIO采样率,对外接口。
///
/// The preferred sample rate in Hz.
public void SetPreferredAsioSampleRate(int sampleRate)
{
- SampleRate.Value = sampleRate;
+ SAMPLE_RATE.Value = sampleRate;
}
///
- /// Gets the preferred sample rate for ASIO devices.
+ /// 设置ASIO缓冲区大小,对外接口。
///
- /// The preferred sample rate in Hz, or null if not set.
- public static int? GetPreferredAsioSampleRate()
+ /// The preferred buffer size for ASIO device.
+ public void SetAsioBufferSize(int bufferSize)
{
- return preferredAsioSampleRate;
+ ASIO_BUFFER_SIZE.Value = bufferSize;
}
///
@@ -390,7 +325,7 @@ namespace osu.Framework.Audio
GlobalMixerHandle.ValueChanged += handle => usingGlobalMixer.Value = handle.NewValue.HasValue;
// Listen for unified sample rate changes and reinitialize device if supported
- SampleRate.ValueChanged += e =>
+ SAMPLE_RATE.ValueChanged += e =>
{
if (syncingSelection)
return;
@@ -402,7 +337,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 {SampleRate.Value}Hz, reinitializing ASIO device", name: "audio", level: LogLevel.Important);
+ Logger.Log($"Sample rate changed to {SAMPLE_RATE.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(() =>
@@ -419,7 +354,47 @@ namespace osu.Framework.Audio
}
else
{
- 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);
+ Logger.Log($"Sample rate changed to {SAMPLE_RATE.Value}Hz, but current device ({AudioDevice.Value}) does not support runtime sample rate changes", name: "audio", level: LogLevel.Debug);
+ syncingSelection = false;
+ }
+ }
+ catch
+ {
+ syncingSelection = false;
+ }
+ };
+
+ // Listen for ASIO buffer size changes and reinitialize device if supported
+ ASIO_BUFFER_SIZE.ValueChanged += e =>
+ {
+ if (syncingSelection)
+ return;
+
+ syncingSelection = true;
+
+ try
+ {
+ // 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($"Current audio device before reinitialization: {AudioDevice.Value}", name: "audio", level: LogLevel.Debug);
+
+ scheduler.AddOnce(() =>
+ {
+ initCurrentDevice();
+
+ if (!IsCurrentDeviceValid())
+ {
+ Logger.Log("Buffer size setting failed, device invalid", name: "audio", level: LogLevel.Error);
+ }
+
+ syncingSelection = false;
+ });
+ }
+ 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);
syncingSelection = false;
}
}
@@ -563,7 +538,7 @@ namespace osu.Framework.Audio
// Note: normalisation may write back to bindables; ensure those writes are update-thread-safe.
normaliseLegacySelection();
- var (mode, deviceName, asioIndex) = parseSelection(AudioDevice.Value);
+ var (mode, deviceName) = parseSelection(AudioDevice.Value);
bool isExplicitSelection = !string.IsNullOrEmpty(AudioDevice.Value);
bool isTypedSelection = hasTypeSuffix(AudioDevice.Value);
@@ -589,35 +564,28 @@ namespace osu.Framework.Audio
// try using the specified device
if (mode == AudioOutputMode.Asio)
{
- Logger.Log($"Initializing ASIO device with index {asioIndex}, preferred sample rate {SampleRate.Value}Hz", name: "audio", level: LogLevel.Important);
-
// ASIO output still requires BASS to be initialised, but output is performed by BassAsio.
// Use the OS default BASS device as a fallback initialisation target.
// For ASIO mode, add retry logic since device initialization can be flaky
- const int max_asio_retries = 3;
+ const int max_asio_retries = 2;
bool asioInitSuccess = false;
for (int retry = 0; retry < max_asio_retries; retry++)
{
- Logger.Log($"ASIO initialization attempt {retry + 1}/{max_asio_retries}", name: "audio", level: LogLevel.Debug);
-
- if (trySetDevice(bass_default_device, mode, asioIndex))
+ if (trySetDevice(bass_default_device, mode))
{
- Logger.Log($"ASIO device initialization successful on attempt {retry + 1}", name: "audio", level: LogLevel.Important);
asioInitSuccess = true;
- break;
}
-
- if (retry < max_asio_retries - 1)
+ else
{
- Logger.Log($"ASIO device initialization failed, retrying in 1 second (attempt {retry + 1}/{max_asio_retries})", name: "audio", level: LogLevel.Important);
- Thread.Sleep(1000);
+ Logger.Log($"ASIO device initialization failed, retrying in 300ms (attempt {retry + 1}/{max_asio_retries})", name: "audio", level: LogLevel.Important);
+ Thread.Sleep(200);
// Force cleanup between retries to prevent resource conflicts
scheduler.Add(() =>
{
thread.FreeDevice(bass_default_device);
- Thread.Sleep(200);
+ Thread.Sleep(100);
});
}
}
@@ -625,7 +593,6 @@ namespace osu.Framework.Audio
if (!asioInitSuccess)
{
Logger.Log("ASIO device initialization failed after all retries, falling back to default device", name: "audio", level: LogLevel.Important);
- // Fall back to default - don't try other devices as they may also fail
}
return;
@@ -634,19 +601,19 @@ namespace osu.Framework.Audio
{
// try using the specified device
int deviceIndex = audioDeviceNames.FindIndex(d => d == deviceName);
- if (deviceIndex >= 0 && trySetDevice(BASS_INTERNAL_DEVICE_COUNT + deviceIndex, mode, asioIndex)) return;
+ if (deviceIndex >= 0 && trySetDevice(BASS_INTERNAL_DEVICE_COUNT + deviceIndex, mode)) return;
}
// try using the system default if there is any device present.
// mobiles are an exception as the built-in speakers may not be provided as an audio device name,
// but they are still provided by BASS under the internal device name "Default".
- if ((audioDeviceNames.Count > 0 || RuntimeInfo.IsMobile) && trySetDevice(bass_default_device, mode, asioIndex)) return;
+ if ((audioDeviceNames.Count > 0 || RuntimeInfo.IsMobile) && trySetDevice(bass_default_device, mode)) return;
// If an explicit selection failed, revert to Default and try again in default output mode.
// Keep checkbox state unless Exclusive/ASIO was chosen.
if (isExplicitSelection)
{
- if (trySetDevice(bass_default_device, AudioOutputMode.Default, null))
+ if (trySetDevice(bass_default_device, AudioOutputMode.Default))
{
revertSelectionToDefault();
return;
@@ -657,12 +624,12 @@ namespace osu.Framework.Audio
}
// no audio devices can be used, so try using Bass-provided "No sound" device as last resort.
- trySetDevice(Bass.NoSoundDevice, AudioOutputMode.Default, null);
+ trySetDevice(Bass.NoSoundDevice, AudioOutputMode.Default);
// we're boned. even "No sound" device won't initialise.
return;
- bool trySetDevice(int deviceId, AudioOutputMode outputMode, int? asioDeviceIndex)
+ bool trySetDevice(int deviceId, AudioOutputMode outputMode)
{
var device = audioDevices.ElementAtOrDefault(deviceId);
@@ -675,7 +642,7 @@ namespace osu.Framework.Audio
return false;
// initialize new device
- if (!InitBass(deviceId, outputMode, asioDeviceIndex))
+ if (!InitBass(deviceId, outputMode))
return false;
//we have successfully initialised a new device.
@@ -759,8 +726,7 @@ namespace osu.Framework.Audio
///
/// The device to initialise.
/// The output mode to use for playback.
- /// When is ASIO, the selected ASIO device index.
- protected virtual bool InitBass(int device, AudioOutputMode outputMode, int? asioDeviceIndex)
+ protected virtual bool InitBass(int device, AudioOutputMode outputMode)
{
// this likely doesn't help us but also doesn't seem to cause any issues or any cpu increase.
Bass.UpdatePeriod = 5;
@@ -797,7 +763,7 @@ namespace osu.Framework.Audio
try
{
- innerSuccess = thread.InitDevice(device, toThreadOutputMode(outputMode), asioDeviceIndex, outputMode == AudioOutputMode.Asio ? SampleRate.Value : null);
+ innerSuccess = thread.InitDevice(device, outputMode, SAMPLE_RATE.Value);
}
catch (Exception e)
{
@@ -855,24 +821,6 @@ namespace osu.Framework.Audio
return attemptInit();
}
- private static AudioThread.AudioThreadOutputMode toThreadOutputMode(AudioOutputMode mode)
- {
- switch (mode)
- {
- case AudioOutputMode.WasapiShared:
- return AudioThread.AudioThreadOutputMode.WasapiShared;
-
- case AudioOutputMode.WasapiExclusive:
- return AudioThread.AudioThreadOutputMode.WasapiExclusive;
-
- case AudioOutputMode.Asio:
- return AudioThread.AudioThreadOutputMode.Asio;
-
- default:
- return AudioThread.AudioThreadOutputMode.Default;
- }
- }
-
private void syncAudioDevices()
{
audioDevices = GetAllDevices();
@@ -926,7 +874,7 @@ namespace osu.Framework.Audio
// ASIO drivers.
int asioCount = 0;
- foreach (var device in enumerateAsioDevices())
+ foreach (var device in AsioDeviceManager.EnumerateAsioDevices())
{
entries.Add(formatEntry(device.Name, type_asio));
asioCount++;
@@ -940,14 +888,14 @@ namespace osu.Framework.Audio
private static string formatEntry(string name, string type) => $"{name} ({type})";
- private (AudioOutputMode mode, string deviceName, int? asioDeviceIndex) parseSelection(string selection)
+ private (AudioOutputMode mode, string deviceName) parseSelection(string selection)
{
// Default device.
if (string.IsNullOrEmpty(selection))
{
return (UseExperimentalWasapi.Value && RuntimeInfo.OS == RuntimeInfo.Platform.Windows
? AudioOutputMode.WasapiShared
- : AudioOutputMode.Default, string.Empty, null);
+ : AudioOutputMode.Default, string.Empty);
}
// Option (1): if experimental(shared) is enabled, typed entries should be treated as legacy/config leftovers.
@@ -955,26 +903,25 @@ namespace osu.Framework.Audio
if (UseExperimentalWasapi.Value && RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
{
if (tryParseSuffixed(selection, type_wasapi_exclusive, out string baseName))
- return (AudioOutputMode.WasapiShared, baseName, null);
+ return (AudioOutputMode.WasapiShared, baseName);
if (tryParseSuffixed(selection, type_asio, out _))
- return (AudioOutputMode.WasapiShared, string.Empty, null);
+ return (AudioOutputMode.WasapiShared, string.Empty);
}
if (tryParseSuffixed(selection, type_wasapi_exclusive, out string name))
- return (AudioOutputMode.WasapiExclusive, name, null);
+ return (AudioOutputMode.WasapiExclusive, name);
if (tryParseSuffixed(selection, type_asio, out name))
{
- int? index = findAsioDeviceIndex(name);
- return (AudioOutputMode.Asio, name, index);
+ return (AudioOutputMode.Asio, name);
}
// Legacy value (raw BASS device name). Keep old behaviour: the experimental flag decides shared WASAPI.
if (UseExperimentalWasapi.Value && RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
- return (AudioOutputMode.WasapiShared, selection, null);
+ return (AudioOutputMode.WasapiShared, selection);
- return (AudioOutputMode.Default, selection, null);
+ return (AudioOutputMode.Default, selection);
}
private static bool tryParseSuffixed(string value, string type, out string baseName)
@@ -1050,7 +997,7 @@ namespace osu.Framework.Audio
protected virtual bool IsCurrentDeviceValid()
{
var device = audioDevices.ElementAtOrDefault(Bass.CurrentDevice);
- var (mode, selectedName, _) = parseSelection(AudioDevice.Value);
+ var (mode, selectedName) = parseSelection(AudioDevice.Value);
// ASIO output selection does not map to a BASS device name; ensure we're initialised and ASIO is working.
if (mode == AudioOutputMode.Asio)
@@ -1082,72 +1029,75 @@ namespace osu.Framework.Audio
string deviceName = audioDevices.ElementAtOrDefault(Bass.CurrentDevice).Name;
return $@"{GetType().ReadableName()} ({deviceName ?? "Unknown"})";
}
-
- ///
- /// Gets the supported sample rates for a specific ASIO device.
- /// This is useful for populating UI dropdowns with device-specific sample rate options.
- ///
- /// The name of the ASIO device.
- /// An array of supported sample rates, or an empty array if the device is not found or not an ASIO device.
- public double[] GetAsioDeviceSupportedSampleRates(string deviceName)
- {
- if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
- return Array.Empty();
-
- try
- {
- foreach (var device in AsioDeviceManager.AvailableDevicesWithSampleRates)
- {
- if (device.Name == deviceName)
- {
- return device.SupportedSampleRates;
- }
- }
- }
- catch (DllNotFoundException e)
- {
- logAsioNativeUnavailableOnce(e);
- }
- catch (EntryPointNotFoundException e)
- {
- logAsioNativeUnavailableOnce(e);
- }
- catch (Exception ex)
- {
- Logger.Log($"Error getting ASIO device sample rates for {deviceName}: {ex.Message}", LoggingTarget.Runtime, LogLevel.Error);
- }
-
- return Array.Empty();
- }
-
- ///
- /// Gets all available ASIO devices with their supported sample rates.
- /// This is useful for populating UI with device-specific sample rate options.
- ///
- /// An enumerable of tuples containing device index, name, and supported sample rates.
- public IEnumerable<(int Index, string Name, double[] SupportedSampleRates)> GetAvailableAsioDevicesWithSampleRates()
- {
- if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
- return Enumerable.Empty<(int, string, double[])>();
-
- try
- {
- return AsioDeviceManager.AvailableDevicesWithSampleRates;
- }
- catch (DllNotFoundException e)
- {
- logAsioNativeUnavailableOnce(e);
- }
- catch (EntryPointNotFoundException e)
- {
- logAsioNativeUnavailableOnce(e);
- }
- catch (Exception ex)
- {
- Logger.Log($"Error getting ASIO devices with sample rates: {ex.Message}", LoggingTarget.Runtime, LogLevel.Error);
- }
-
- return Enumerable.Empty<(int, string, double[])>();
- }
}
}
+
+// #region 受支持的采样率设置
+
+// ///
+// /// Gets the supported sample rates for a specific ASIO device.
+// /// This is useful for populating UI dropdowns with device-specific sample rate options.
+// ///
+// /// The name of the ASIO device.
+// /// An array of supported sample rates, or an empty array if the device is not found or not an ASIO device.
+// public double[] GetAsioDeviceSupportedSampleRates(string deviceName)
+// {
+// if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
+// return Array.Empty();
+//
+// try
+// {
+// foreach (var device in AsioDeviceManager.AvailableDevicesWithSampleRates)
+// {
+// if (device.Name == deviceName)
+// {
+// return device.SupportedSampleRates;
+// }
+// }
+// }
+// catch (DllNotFoundException e)
+// {
+// logAsioNativeUnavailableOnce(e);
+// }
+// catch (EntryPointNotFoundException e)
+// {
+// logAsioNativeUnavailableOnce(e);
+// }
+// catch (Exception ex)
+// {
+// Logger.Log($"Error getting ASIO device sample rates for {deviceName}: {ex.Message}", LoggingTarget.Runtime, LogLevel.Error);
+// }
+//
+// return Array.Empty();
+// }
+// ///
+// /// Gets all available ASIO devices with their supported sample rates.
+// /// This is useful for populating UI with device-specific sample rate options.
+// ///
+// /// An enumerable of tuples containing device index, name, and supported sample rates.
+// public IEnumerable<(int Index, string Name, double[] SupportedSampleRates)> GetAvailableAsioDevicesWithSampleRates()
+// {
+// if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
+// return Enumerable.Empty<(int, string, double[])>();
+//
+// try
+// {
+// return AsioDeviceManager.AvailableDevicesWithSampleRates;
+// }
+// catch (DllNotFoundException e)
+// {
+// logAsioNativeUnavailableOnce(e);
+// }
+// catch (EntryPointNotFoundException e)
+// {
+// logAsioNativeUnavailableOnce(e);
+// }
+// catch (Exception ex)
+// {
+// Logger.Log($"Error getting ASIO devices with sample rates: {ex.Message}", LoggingTarget.Runtime, LogLevel.Error);
+// }
+//
+// return Enumerable.Empty<(int, string, double[])>();
+// }
+
+// #endregion
diff --git a/osu.Framework/Audio/EzDriverModule.cs b/osu.Framework/Audio/EzLatency/EzDriverModule.cs
similarity index 95%
rename from osu.Framework/Audio/EzDriverModule.cs
rename to osu.Framework/Audio/EzLatency/EzDriverModule.cs
index 9f2ae5a4e..bba028b36 100644
--- a/osu.Framework/Audio/EzDriverModule.cs
+++ b/osu.Framework/Audio/EzLatency/EzDriverModule.cs
@@ -2,9 +2,10 @@
// 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
+namespace osu.Framework.Audio.EzLatency
{
///
/// EzDriverModule 负责音频驱动的设置和管理。
@@ -17,7 +18,7 @@ namespace osu.Framework.Audio
///
public bool Enabled { get; set; } = false;
- private readonly System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
+ private readonly Stopwatch stopwatch = new Stopwatch();
// 存储驱动时间
internal double DriverTime;
diff --git a/osu.Framework/Audio/EzHardwareModule.cs b/osu.Framework/Audio/EzLatency/EzHardwareModule.cs
similarity index 97%
rename from osu.Framework/Audio/EzHardwareModule.cs
rename to osu.Framework/Audio/EzLatency/EzHardwareModule.cs
index f7cb5fe78..64a159377 100644
--- a/osu.Framework/Audio/EzHardwareModule.cs
+++ b/osu.Framework/Audio/EzLatency/EzHardwareModule.cs
@@ -5,7 +5,7 @@ using System;
using System.Diagnostics;
using osu.Framework.Logging;
-namespace osu.Framework.Audio
+namespace osu.Framework.Audio.EzLatency
{
///
/// EzHardwareModule 负责硬件层延迟的测量和管理。
@@ -58,7 +58,8 @@ namespace osu.Framework.Audio
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);
+ Logger.Log($"[EzOsuLatency] Driver: {driverTime:F2}ms, OutputHW: {OutputHardwareTime:F2}ms, InputHW: {InputHardwareTime:F2}ms, LatencyDiff: {LatencyDifference:F2}ms", name: "audio",
+ level: LogLevel.Debug);
}
}
diff --git a/osu.Framework/Audio/EzInputModule.cs b/osu.Framework/Audio/EzLatency/EzInputModule.cs
similarity index 94%
rename from osu.Framework/Audio/EzInputModule.cs
rename to osu.Framework/Audio/EzLatency/EzInputModule.cs
index 8509fa4ba..ecd4d5039 100644
--- a/osu.Framework/Audio/EzInputModule.cs
+++ b/osu.Framework/Audio/EzLatency/EzInputModule.cs
@@ -2,9 +2,10 @@
// 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
+namespace osu.Framework.Audio.EzLatency
{
///
/// EzInputModule 负责处理输入事件的捕获和时间戳记录。
@@ -17,7 +18,7 @@ namespace osu.Framework.Audio
///
public static bool Enabled { get; set; } = false;
- private static readonly System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
+ private static readonly Stopwatch stopwatch = new Stopwatch();
// 存储输入数据
public static double InputTime;
diff --git a/osu.Framework/Audio/EzJudgeModule.cs b/osu.Framework/Audio/EzLatency/EzJudgeModule.cs
similarity index 94%
rename from osu.Framework/Audio/EzJudgeModule.cs
rename to osu.Framework/Audio/EzLatency/EzJudgeModule.cs
index bb314e52e..4cb489c16 100644
--- a/osu.Framework/Audio/EzJudgeModule.cs
+++ b/osu.Framework/Audio/EzLatency/EzJudgeModule.cs
@@ -2,9 +2,10 @@
// 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
+namespace osu.Framework.Audio.EzLatency
{
///
/// EzJudgeModule 负责处理判定时间的计算和记录。
@@ -17,7 +18,7 @@ namespace osu.Framework.Audio
///
public static bool Enabled { get; set; } = false;
- private static readonly System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
+ private static readonly Stopwatch stopwatch = new Stopwatch();
// 存储判定时间
internal static double JudgeTime;
diff --git a/osu.Framework/Audio/EzLatency/EzLatencyRecord.cs b/osu.Framework/Audio/EzLatency/EzLatencyRecord.cs
index 348768bd6..dc3501eb9 100644
--- a/osu.Framework/Audio/EzLatency/EzLatencyRecord.cs
+++ b/osu.Framework/Audio/EzLatency/EzLatencyRecord.cs
@@ -1,8 +1,11 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+
namespace osu.Framework.Audio.EzLatency
{
- #nullable disable
- using System;
-
+#nullable disable
public class EzLatencyRecord
{
// high-level fields
@@ -20,7 +23,7 @@ namespace osu.Framework.Audio.EzLatency
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; }
+ public EzLatencyInputData InputData { get; set; }
+ public EzLatencyHardwareData HardwareData { get; set; }
}
}
diff --git a/osu.Framework/Audio/EzLatency/EzLatencyService.cs b/osu.Framework/Audio/EzLatency/EzLatencyService.cs
index e351d7a24..3c9e60b67 100644
--- a/osu.Framework/Audio/EzLatency/EzLatencyService.cs
+++ b/osu.Framework/Audio/EzLatency/EzLatencyService.cs
@@ -1,8 +1,11 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+
namespace osu.Framework.Audio.EzLatency
{
- #nullable disable
- using System;
-
+#nullable disable
///
/// Standalone singleton service which collects low-level framework latency records
/// and exposes higher-level EzLatencyRecord events for consumers in the osu layer.
diff --git a/osu.Framework/Audio/EzLatency/EzLatencyStruct.cs b/osu.Framework/Audio/EzLatency/EzLatencyStruct.cs
new file mode 100644
index 000000000..4c5172818
--- /dev/null
+++ b/osu.Framework/Audio/EzLatency/EzLatencyStruct.cs
@@ -0,0 +1,35 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Framework.Audio.EzLatency
+{
+ ///
+ /// EzOsuLatency 输入数据结构体
+ ///
+ public struct EzLatencyInputData
+ {
+ public double InputTime;
+ public object KeyValue;
+ public double JudgeTime;
+ public double PlaybackTime;
+ }
+
+ ///
+ /// EzOsuLatency 硬件数据结构体
+ ///
+ public struct EzLatencyHardwareData
+ {
+ public double DriverTime;
+ public double OutputHardwareTime;
+ public double InputHardwareTime;
+ public double LatencyDifference;
+ }
+
+ public enum AudioOutputMode
+ {
+ Default,
+ WasapiShared,
+ WasapiExclusive,
+ Asio,
+ }
+}
diff --git a/osu.Framework/Audio/EzLatencyTestModule.cs b/osu.Framework/Audio/EzLatency/EzLatencyTestModule.cs
similarity index 95%
rename from osu.Framework/Audio/EzLatencyTestModule.cs
rename to osu.Framework/Audio/EzLatency/EzLatencyTestModule.cs
index 058e036b7..f7f901db2 100644
--- a/osu.Framework/Audio/EzLatencyTestModule.cs
+++ b/osu.Framework/Audio/EzLatency/EzLatencyTestModule.cs
@@ -5,7 +5,7 @@ using System.Diagnostics;
using osu.Framework.Bindables;
using osu.Framework.Logging;
-namespace osu.Framework.Audio
+namespace osu.Framework.Audio.EzLatency
{
///
/// EzOsuLatency 延迟测试模块
@@ -75,7 +75,7 @@ namespace osu.Framework.Audio
// 5. 计算 T_out - T_call
// 临时模拟:假设延迟为缓冲区延迟 + 硬件延迟
- double bufferLatency = (BufferSize.Value / (double)SampleRate.Value) * 1000;
+ double bufferLatency = BufferSize.Value / (double)SampleRate.Value * 1000;
const double hardware_latency = 1.5;
double simulatedLatency = bufferLatency + hardware_latency;
@@ -133,6 +133,7 @@ namespace osu.Framework.Audio
public void RunPeriodicTest()
{
double currentTime = RecordTimestamp();
+
if (currentTime - lastTestTime > 1000) // 每秒测试一次
{
RunTest();
@@ -155,7 +156,7 @@ namespace osu.Framework.Audio
{
// 基于配置参数模拟延迟
double baseLatency = DriverType.Value == "ASIO" ? 2.0 : 5.0; // ASIO通常延迟更低
- double bufferLatency = (BufferSize.Value / (double)SampleRate.Value) * 1000; // 缓冲区延迟
+ double bufferLatency = BufferSize.Value / (double)SampleRate.Value * 1000; // 缓冲区延迟
const double hardware_latency = 1.5; // 硬件层延迟
return baseLatency + bufferLatency + hardware_latency;
diff --git a/osu.Framework/Audio/EzLogModule.cs b/osu.Framework/Audio/EzLatency/EzLogModule.cs
similarity index 78%
rename from osu.Framework/Audio/EzLogModule.cs
rename to osu.Framework/Audio/EzLatency/EzLogModule.cs
index c39f66ed7..8737980fc 100644
--- a/osu.Framework/Audio/EzLogModule.cs
+++ b/osu.Framework/Audio/EzLatency/EzLogModule.cs
@@ -6,7 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Logging;
-namespace osu.Framework.Audio
+namespace osu.Framework.Audio.EzLatency
{
///
/// EzOsuLatency 日志模块
@@ -26,9 +26,9 @@ namespace osu.Framework.Audio
public bool Enabled { get; set; } = false;
// 存储延迟记录用于统计
- private readonly List latencyRecords = new List();
+ private readonly List latencyRecords = new List();
- public event System.Action? OnNewRecord;
+ public event Action? OnNewRecord;
///
/// 延迟统计数据结构体
@@ -45,12 +45,13 @@ namespace osu.Framework.Audio
///
/// 记录单个延迟事件并返回延迟信息(用于实时输出)
///
- 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)
+ public EzLatencyRecord? RecordLatencyEventAndGet(double inputTime, double judgeTime, double playbackTime, double driverTime, double outputHardwareTime, double inputHardwareTime,
+ double latencyDifference, EzLatencyInputData inputData = default, EzLatencyHardwareData hardwareData = default)
{
if (!Enabled)
return null;
- var record = new osu.Framework.Audio.EzLatency.EzLatencyRecord
+ var record = new EzLatencyRecord
{
Timestamp = DateTimeOffset.Now,
InputTime = inputTime,
@@ -128,8 +129,10 @@ namespace osu.Framework.Audio
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)";
+ 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);
@@ -149,7 +152,9 @@ namespace osu.Framework.Audio
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);
+ 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);
}
///
diff --git a/osu.Framework/Audio/EzLatency/EzLoggerAdapter.cs b/osu.Framework/Audio/EzLatency/EzLoggerAdapter.cs
index 7d557be86..e0d56572c 100644
--- a/osu.Framework/Audio/EzLatency/EzLoggerAdapter.cs
+++ b/osu.Framework/Audio/EzLatency/EzLoggerAdapter.cs
@@ -1,11 +1,14 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.IO;
+using osu.Framework.Logging;
+using osu.Framework.Threading;
+
namespace osu.Framework.Audio.EzLatency
{
- #nullable disable
- using System;
- using System.IO;
- using osu.Framework.Logging;
- using osu.Framework.Threading;
-
+#nullable disable
public class EzLoggerAdapter : IEzLatencyLogger
{
private readonly Scheduler scheduler;
diff --git a/osu.Framework/Audio/EzPlaybackModule.cs b/osu.Framework/Audio/EzLatency/EzPlaybackModule.cs
similarity index 96%
rename from osu.Framework/Audio/EzPlaybackModule.cs
rename to osu.Framework/Audio/EzLatency/EzPlaybackModule.cs
index 71e643efb..f1ef7005f 100644
--- a/osu.Framework/Audio/EzPlaybackModule.cs
+++ b/osu.Framework/Audio/EzLatency/EzPlaybackModule.cs
@@ -5,7 +5,7 @@ using System;
using osu.Framework.Logging;
using osu.Framework.Threading;
-namespace osu.Framework.Audio
+namespace osu.Framework.Audio.EzLatency
{
///
/// EzPlaybackModule 负责处理播放事件的触发和时间戳记录。
@@ -46,8 +46,8 @@ namespace osu.Framework.Audio
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;
+ EzLatencyInputData inputData = default;
+ EzLatencyHardwareData hardwareData = default;
try
{
diff --git a/osu.Framework/Audio/EzLatency/IEzLatencyLogger.cs b/osu.Framework/Audio/EzLatency/IEzLatencyLogger.cs
index d025619cb..78a7434b9 100644
--- a/osu.Framework/Audio/EzLatency/IEzLatencyLogger.cs
+++ b/osu.Framework/Audio/EzLatency/IEzLatencyLogger.cs
@@ -1,8 +1,7 @@
+using System;
+
namespace osu.Framework.Audio.EzLatency
{
- #nullable disable
- using System;
-
public interface IEzLatencyLogger : IDisposable
{
void Log(EzLatencyRecord record);
diff --git a/osu.Framework/Platform/GameHost.cs b/osu.Framework/Platform/GameHost.cs
index bcbc9eb41..5caa33275 100644
--- a/osu.Framework/Platform/GameHost.cs
+++ b/osu.Framework/Platform/GameHost.cs
@@ -1426,7 +1426,7 @@ namespace osu.Framework.Platform
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxTexturesUploadedPerFrame);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxPixelsUploadedPerFrame);
- if (Renderer is not osu.Framework.Graphics.Rendering.Renderer renderer)
+ if (Renderer is not Renderer renderer)
return;
DrawThread?.Scheduler.Add(() =>
@@ -1447,7 +1447,7 @@ namespace osu.Framework.Platform
///
public void RestoreTextureUploadLimits()
{
- if (Renderer is not osu.Framework.Graphics.Rendering.Renderer renderer)
+ if (Renderer is not Renderer renderer)
return;
if (originalMaxTexturesUploadedPerFrame == null || originalMaxPixelsUploadedPerFrame == null)
diff --git a/osu.Framework/Threading/AudioThread.cs b/osu.Framework/Threading/AudioThread.cs
index 674c697e8..0abd5b00d 100644
--- a/osu.Framework/Threading/AudioThread.cs
+++ b/osu.Framework/Threading/AudioThread.cs
@@ -1,4 +1,4 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Statistics;
@@ -16,6 +16,7 @@ using ManagedBass.Mix;
using ManagedBass.Wasapi;
using osu.Framework.Audio;
using osu.Framework.Audio.Asio;
+using osu.Framework.Audio.EzLatency;
using osu.Framework.Bindables;
using osu.Framework.Development;
using osu.Framework.Logging;
@@ -23,28 +24,6 @@ using osu.Framework.Platform.Linux.Native;
namespace osu.Framework.Threading
{
- ///
- /// EzOsuLatency 输入数据结构体
- ///
- public struct EzLatencyInputData
- {
- public double InputTime;
- public object KeyValue;
- public double JudgeTime;
- public double PlaybackTime;
- }
-
- ///
- /// EzOsuLatency 硬件数据结构体
- ///
- public struct EzLatencyHardwareData
- {
- public double DriverTime;
- public double OutputHardwareTime;
- public double InputHardwareTime;
- public double LatencyDifference;
- }
-
public class AudioThread : GameThread
{
private static int wasapiNativeUnavailableLogged;
@@ -241,7 +220,7 @@ namespace osu.Framework.Threading
///
private readonly Bindable globalMixerHandle = new Bindable();
- internal bool InitDevice(int deviceId, AudioThreadOutputMode outputMode, int? asioDeviceIndex = null, double? preferredSampleRate = null)
+ internal bool InitDevice(int deviceId, AudioOutputMode outputMode, double preferredSampleRate)
{
Debug.Assert(ThreadSafety.IsAudioThread);
Trace.Assert(deviceId != -1); // The real device ID should always be used, as the -1 device has special cases which are hard to work with.
@@ -253,11 +232,11 @@ namespace osu.Framework.Threading
freeWasapi();
// 对于ASIO模式,在初始化前添加额外延迟以确保设备完全释放
- if (outputMode == AudioThreadOutputMode.Asio)
+ if (outputMode == AudioOutputMode.Asio)
{
Logger.Log("检测到ASIO模式,在设备初始化前添加额外延迟", name: "audio", level: LogLevel.Debug);
// 增加延迟以确保设备完全释放
- Thread.Sleep(500);
+ Thread.Sleep(200);
}
// Try to initialise the device, or request a re-initialise.
@@ -272,10 +251,10 @@ namespace osu.Framework.Threading
switch (outputMode)
{
- case AudioThreadOutputMode.Default:
+ case AudioOutputMode.Default:
break;
- case AudioThreadOutputMode.WasapiShared:
+ case AudioOutputMode.WasapiShared:
if (!attemptWasapiInitialisation(deviceId, exclusive: false))
{
Logger.Log($"BassWasapi initialisation failed (shared mode). BASS error: {Bass.LastError}", name: "audio", level: LogLevel.Error);
@@ -284,7 +263,7 @@ namespace osu.Framework.Threading
break;
- case AudioThreadOutputMode.WasapiExclusive:
+ case AudioOutputMode.WasapiExclusive:
if (!attemptWasapiInitialisation(deviceId, exclusive: true))
{
Logger.Log($"BassWasapi initialisation failed (exclusive mode). BASS error: {Bass.LastError}", name: "audio", level: LogLevel.Error);
@@ -293,14 +272,8 @@ namespace osu.Framework.Threading
break;
- case AudioThreadOutputMode.Asio:
- if (asioDeviceIndex == null)
- {
- Logger.Log("选择了ASIO输出模式但未提供ASIO设备索引。", name: "audio", level: LogLevel.Error);
- return false;
- }
-
- if (!initAsio(asioDeviceIndex.Value, preferredSampleRate))
+ case AudioOutputMode.Asio:
+ if (!initAsio(deviceId, preferredSampleRate))
return false;
break;
@@ -506,15 +479,9 @@ namespace osu.Framework.Threading
{
// If the user selected a specific non-default device, do not fall back to system default.
// Fallback would likely be busy (e.g. browser playing on default), and would mask the real issue.
- if (bassDeviceId != Bass.DefaultDevice)
- {
- Logger.Log($"Could not map BASS device {bassDeviceId} (driver '{driver}') to a WASAPI output device; refusing to fall back to default (-1).", name: "audio",
- level: LogLevel.Verbose);
- return false;
- }
-
- Logger.Log($"Could not map BASS default device (driver '{driver}') to a WASAPI output device; falling back to default WASAPI device (-1).", name: "audio",
+ Logger.Log($"Could not map BASS device {bassDeviceId} (driver '{driver}') to a WASAPI output device; refusing to fall back to default (-1).", name: "audio",
level: LogLevel.Verbose);
+ return false;
}
}
else
@@ -649,20 +616,19 @@ namespace osu.Framework.Threading
Logger.Log(message, name: "audio", level: LogLevel.Error);
}
- private bool initAsio(int asioDeviceIndex, double? preferredSampleRate = null)
+ private bool initAsio(int asioDeviceIndex, double preferredSampleRate = 48000)
{
- Logger.Log($"Initializing ASIO device {asioDeviceIndex} with preferred sample rate {preferredSampleRate}Hz", name: "audio", level: LogLevel.Important);
-
freeAsio();
- // 使用来自AudioManager的统一采样率
+ // 使用来自AudioManager的统一采样率和缓冲区大小
if (Manager != null)
{
- preferredSampleRate = Manager.SampleRate.Value;
+ preferredSampleRate = Manager.SAMPLE_RATE.Value;
+ int bufferSize = Manager.ASIO_BUFFER_SIZE.Value;
- if (!AsioDeviceManager.InitializeDevice(asioDeviceIndex, preferredSampleRate))
+ if (!AsioDeviceManager.InitializeDevice(asioDeviceIndex, preferredSampleRate, bufferSize))
{
- Logger.Log($"AsioDeviceManager.InitializeDevice({asioDeviceIndex}, {preferredSampleRate}) 失败", name: "audio", level: LogLevel.Error);
+ Logger.Log($"AsioDeviceManager.InitializeDevice({asioDeviceIndex}, {preferredSampleRate}, {bufferSize}) 失败", name: "audio", level: LogLevel.Error);
// 不要自动释放BASS设备 - 让AudioManager处理回退决策
// 这可以防止过度激进的设备切换导致设备可用性降低
return false;
@@ -844,15 +810,15 @@ namespace osu.Framework.Threading
///
/// 输出模式
/// 缓冲区延迟(毫秒),失败返回-1
- internal double GetDriverBufferLatency(AudioThreadOutputMode outputMode)
+ internal double GetDriverBufferLatency(AudioOutputMode outputMode)
{
switch (outputMode)
{
- case AudioThreadOutputMode.Asio:
+ case AudioOutputMode.Asio:
return GetAsioOutputLatency();
- case AudioThreadOutputMode.WasapiExclusive:
- case AudioThreadOutputMode.WasapiShared:
+ case AudioOutputMode.WasapiExclusive:
+ case AudioOutputMode.WasapiShared:
return GetWasapiStreamLatency();
default:
@@ -869,14 +835,14 @@ namespace osu.Framework.Threading
/// 不可控延迟
internal void LogLatencyData(double inputLatency, double playbackLatency, double totalLatency, double uncontrollableLatency)
{
- const string driverType = "Unknown";
+ const string driver_type = "Unknown";
int sampleRate = 44100;
- const int bufferSize = 256;
+ const int buffer_size = 128;
// 获取当前驱动信息
if (Manager != null)
{
- sampleRate = Manager.SampleRate.Value;
+ sampleRate = Manager.SAMPLE_RATE.Value;
// TODO: 获取bufferSize
}
@@ -885,9 +851,9 @@ namespace osu.Framework.Threading
LogModule.LogLatency(
LatencyTestModule.RecordTimestamp(),
- driverType,
+ driver_type,
sampleRate,
- bufferSize,
+ buffer_size,
inputLatency,
playbackLatency,
totalLatency,
@@ -895,14 +861,6 @@ namespace osu.Framework.Threading
);
}
- internal enum AudioThreadOutputMode
- {
- Default,
- WasapiShared,
- WasapiExclusive,
- Asio,
- }
-
#endregion
}
}