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