重构ASIO机制

This commit is contained in:
LA
2026-01-16 12:28:33 +08:00
parent 0c98f9c0a8
commit a3b8cae7af
19 changed files with 465 additions and 430 deletions

View File

@@ -987,6 +987,8 @@ private void load()
$END$
};</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Approximator/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=bassasio/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=basswasapi/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Beatmap/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=beatmaps/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=beatmap_0027s/@EntryIndexedValue">True</s:Boolean>

View File

@@ -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)
{

View File

@@ -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
{

View File

@@ -16,13 +16,6 @@ namespace osu.Framework.Audio.Asio
/// </summary>
public static class AsioDeviceManager
{
// /// <summary>
// /// 常见采样率列表, 用于尝试验证设备支持的采样率。
// /// 按优先级排序48kHz优先于44.1kHz。
// /// 使用int类型因为采样率通常是整数值double仅在底层API交互时使用。
// /// </summary>
// 删除 SUPPORTED_SAMPLE_RATES 列表,默认使用 48000
/// <summary>
/// 未指定、或设置采样率失败时使用默认采样率48kHz性能较好。
/// </summary>
@@ -31,32 +24,32 @@ namespace osu.Framework.Audio.Asio
/// <summary>
/// 最大重试次数,用于处理设备繁忙的情况。
/// </summary>
private const int MAX_RETRY_COUNT = 3;
private const int max_retry_count = 3;
/// <summary>
/// 重试间隔时间(毫秒)。
/// </summary>
private const int RETRY_DELAY_MS = 100;
private const int retry_delay_ms = 100;
/// <summary>
/// 设备释放延迟时间(毫秒)。
/// </summary>
private const int DEVICE_FREE_DELAY_MS = 100;
private const int device_free_delay_ms = 100;
/// <summary>
/// 强制重置延迟时间(毫秒)。
/// </summary>
private const int FORCE_RESET_DELAY_MS = 200;
private const int force_reset_delay_ms = 200;
/// <summary>
/// 采样率容差,用于验证设置是否成功。
/// </summary>
private const double SAMPLE_RATE_TOLERANCE = 1.0;
private const double sample_rate_tolerance = 1.0;
/// <summary>
/// 静音帧日志间隔。
/// </summary>
private const int SILENCE_LOG_INTERVAL = 200;
private const int silence_log_interval = 200;
/// <summary>
/// ASIO音频路由的全局混音器句柄。
@@ -117,7 +110,7 @@ namespace osu.Framework.Audio.Asio
/// <returns>如果初始化成功则为true否则为false。</returns>
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
}
}
/// <summary>
/// 获取带有支持采样率的可用ASIO设备列表。
/// 注意:此操作可能较慢,因为需要为每个设备查询支持的采样率。
/// </summary>
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);
}
}
}
/// <summary>
/// 初始化ASIO设备。
/// </summary>
/// <param name="deviceIndex">要初始化的ASIO设备的索引。</param>
/// <param name="sampleRateToTry">要尝试的采样率。如果为null则使用默认48000Hz。</param>
/// <param name="bufferSize">ASIO缓冲区大小。如果为null则使用默认128。</param>
/// <returns>如果初始化成功则为true否则为false。</returns>
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
}
}
/// <summary>
/// 安全地切换ASIO设备先完全释放当前设备再初始化新设备。
/// </summary>
/// <param name="newDeviceIndex">新设备的索引</param>
/// <param name="sampleRateToTry">要尝试的采样率。如果为null则使用默认48000Hz。</param>
/// <param name="bufferSize">ASIO缓冲区大小。如果为null则使用默认128。</param>
/// <returns>如果切换成功则为true否则为false。</returns>
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);
}
/// <summary>
/// 启动ASIO设备处理。
/// </summary>
@@ -421,61 +427,6 @@ namespace osu.Framework.Audio.Asio
}
}
/// <summary>
/// 获取指定ASIO设备支持的采样率列表。
/// 此方法临时初始化设备以查询支持的速率,然后释放它。
/// </summary>
/// <param name="deviceIndex">要查询的ASIO设备的索引。</param>
/// <returns>支持的采样率列表,如果无法查询设备则为空列表。</returns>
public static IEnumerable<double> GetSupportedSampleRates(int deviceIndex)
{
var supportedRates = new List<double>();
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;
}
/// <summary>
/// 为ASIO设备配置默认输入和输出通道。
/// </summary>
@@ -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
/// <summary>
/// 为ASIO设备配置输出通道。
/// </summary>
/// <param name="info">ASIO设备信息。</param>
/// <returns>如果输出通道成功配置则为true否则为false。</returns>
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
/// <summary>
/// ASIO过程回调用于通道处理。
/// </summary>
@@ -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
}
}
/// <summary>
/// 安全枚举ASIO设备并确保正确处理错误。
/// </summary>
/// <returns> 一个可枚举的ASIO设备列表。</returns>
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)>();
}
}
/// <summary>
/// Finds the ASIO device index for a given device name.
/// </summary>
/// <param name="deviceName">The name of the ASIO device to find.</param>
/// <returns>The device index if found, null otherwise.</returns>
public static int? FindAsioDeviceIndex(string deviceName)
{
foreach (var device in EnumerateAsioDevices())
{
if (device.Name == deviceName)
return device.Index;
}
return null;
}
/// <summary>
/// 从音频线程获取全局混音器句柄。
/// </summary>
@@ -683,3 +678,79 @@ namespace osu.Framework.Audio.Asio
}
}
}
// #region 获取受支持的采样率
// /// <summary>
// /// 获取带有支持采样率的可用ASIO设备列表。
// /// 注意:此操作可能较慢,因为需要为每个设备查询支持的采样率。
// /// </summary>
// 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);
// }
// }
// }
// /// <summary>
// /// 获取指定ASIO设备支持的采样率列表。
// /// 此方法临时初始化设备以查询支持的速率,然后释放它。
// /// </summary>
// /// <param name="deviceIndex">要查询的ASIO设备的索引。</param>
// /// <returns>支持的采样率列表,如果无法查询设备则为空列表。</returns>
// public static IEnumerable<double> GetSupportedSampleRates(int deviceIndex)
// {
// var supportedRates = new List<double>();
//
// 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

View File

@@ -1,4 +1,4 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#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
/// <summary>
/// 采样率用于ASIO设备的初始化和运行时更改。
/// </summary>
public readonly Bindable<int> SampleRate = new Bindable<int>(48000);
public readonly Bindable<int> SAMPLE_RATE = new Bindable<int>(48000);
/// <summary>
/// ASIO设备的首选采样率, 用于设备的初始化。
/// ASIO缓冲区大小默认为128用于ASIO设备的初始化。
/// </summary>
private static int? preferredAsioSampleRate;
public readonly Bindable<int> ASIO_BUFFER_SIZE = new Bindable<int>(128);
/// <summary>
/// The names of all available audio devices.
@@ -172,14 +173,6 @@ namespace osu.Framework.Audio
private ImmutableArray<DeviceInfo> audioDevices = ImmutableArray<DeviceInfo>.Empty;
private ImmutableList<string> audioDeviceNames = ImmutableList<string>.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;
/// <summary>
/// Safely enumerates ASIO devices with proper error handling.
/// </summary>
/// <returns>An enumerable of ASIO devices, or empty if ASIO is not available.</returns>
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)>();
}
}
/// <summary>
/// Finds the ASIO device index for a given device name.
/// </summary>
/// <param name="deviceName">The name of the ASIO device to find.</param>
/// <returns>The device index if found, null otherwise.</returns>
private int? findAsioDeviceIndex(string deviceName)
{
foreach (var device in enumerateAsioDevices())
{
if (device.Name == deviceName)
return device.Index;
}
return null;
}
private void setUserBindableValueLeaseSafe<T>(Bindable<T> bindable, T newValue)
{
if (EqualityComparer<T>.Default.Equals(bindable.Value, newValue))
@@ -283,32 +229,21 @@ namespace osu.Framework.Audio
private readonly Lazy<SampleStore> globalSampleStore;
/// <summary>
/// Sets the preferred sample rate for ASIO devices.
/// This will be used during ASIO device initialization.
/// </summary>
/// <param name="sampleRate">The preferred sample rate in Hz, or null to use default.</param>
public static void SetPreferredAsioSampleRate(int? sampleRate)
{
preferredAsioSampleRate = sampleRate;
}
/// <summary>
/// Sets the preferred sample rate for ASIO devices.
/// This will be used during ASIO device initialization.
/// 设置ASIO采样率对外接口。
/// </summary>
/// <param name="sampleRate">The preferred sample rate in Hz.</param>
public void SetPreferredAsioSampleRate(int sampleRate)
{
SampleRate.Value = sampleRate;
SAMPLE_RATE.Value = sampleRate;
}
/// <summary>
/// Gets the preferred sample rate for ASIO devices.
/// 设置ASIO缓冲区大小对外接口。
/// </summary>
/// <returns>The preferred sample rate in Hz, or null if not set.</returns>
public static int? GetPreferredAsioSampleRate()
/// <param name="bufferSize">The preferred buffer size for ASIO device.</param>
public void SetAsioBufferSize(int bufferSize)
{
return preferredAsioSampleRate;
ASIO_BUFFER_SIZE.Value = bufferSize;
}
/// <summary>
@@ -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
/// </summary>
/// <param name="device">The device to initialise.</param>
/// <param name="outputMode">The output mode to use for playback.</param>
/// <param name="asioDeviceIndex">When <paramref name="outputMode"/> is ASIO, the selected ASIO device index.</param>
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"})";
}
/// <summary>
/// Gets the supported sample rates for a specific ASIO device.
/// This is useful for populating UI dropdowns with device-specific sample rate options.
/// </summary>
/// <param name="deviceName">The name of the ASIO device.</param>
/// <returns>An array of supported sample rates, or an empty array if the device is not found or not an ASIO device.</returns>
public double[] GetAsioDeviceSupportedSampleRates(string deviceName)
{
if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
return Array.Empty<double>();
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<double>();
}
/// <summary>
/// Gets all available ASIO devices with their supported sample rates.
/// This is useful for populating UI with device-specific sample rate options.
/// </summary>
/// <returns>An enumerable of tuples containing device index, name, and supported sample rates.</returns>
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 受支持的采样率设置
// /// <summary>
// /// Gets the supported sample rates for a specific ASIO device.
// /// This is useful for populating UI dropdowns with device-specific sample rate options.
// /// </summary>
// /// <param name="deviceName">The name of the ASIO device.</param>
// /// <returns>An array of supported sample rates, or an empty array if the device is not found or not an ASIO device.</returns>
// public double[] GetAsioDeviceSupportedSampleRates(string deviceName)
// {
// if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
// return Array.Empty<double>();
//
// 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<double>();
// }
// /// <summary>
// /// Gets all available ASIO devices with their supported sample rates.
// /// This is useful for populating UI with device-specific sample rate options.
// /// </summary>
// /// <returns>An enumerable of tuples containing device index, name, and supported sample rates.</returns>
// 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

View File

@@ -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
{
/// <summary>
/// EzDriverModule 负责音频驱动的设置和管理。
@@ -17,7 +18,7 @@ namespace osu.Framework.Audio
/// </summary>
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;

View File

@@ -5,7 +5,7 @@ using System;
using System.Diagnostics;
using osu.Framework.Logging;
namespace osu.Framework.Audio
namespace osu.Framework.Audio.EzLatency
{
/// <summary>
/// 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);
}
}

View File

@@ -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
{
/// <summary>
/// EzInputModule 负责处理输入事件的捕获和时间戳记录。
@@ -17,7 +18,7 @@ namespace osu.Framework.Audio
/// </summary>
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;

View File

@@ -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
{
/// <summary>
/// EzJudgeModule 负责处理判定时间的计算和记录。
@@ -17,7 +18,7 @@ namespace osu.Framework.Audio
/// </summary>
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;

View File

@@ -1,8 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
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; }
}
}

View File

@@ -1,8 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
namespace osu.Framework.Audio.EzLatency
{
#nullable disable
using System;
#nullable disable
/// <summary>
/// Standalone singleton service which collects low-level framework latency records
/// and exposes higher-level EzLatencyRecord events for consumers in the osu layer.

View File

@@ -0,0 +1,35 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Framework.Audio.EzLatency
{
/// <summary>
/// EzOsuLatency 输入数据结构体
/// </summary>
public struct EzLatencyInputData
{
public double InputTime;
public object KeyValue;
public double JudgeTime;
public double PlaybackTime;
}
/// <summary>
/// EzOsuLatency 硬件数据结构体
/// </summary>
public struct EzLatencyHardwareData
{
public double DriverTime;
public double OutputHardwareTime;
public double InputHardwareTime;
public double LatencyDifference;
}
public enum AudioOutputMode
{
Default,
WasapiShared,
WasapiExclusive,
Asio,
}
}

View File

@@ -5,7 +5,7 @@ using System.Diagnostics;
using osu.Framework.Bindables;
using osu.Framework.Logging;
namespace osu.Framework.Audio
namespace osu.Framework.Audio.EzLatency
{
/// <summary>
/// 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;

View File

@@ -6,7 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Logging;
namespace osu.Framework.Audio
namespace osu.Framework.Audio.EzLatency
{
/// <summary>
/// EzOsuLatency 日志模块
@@ -26,9 +26,9 @@ namespace osu.Framework.Audio
public bool Enabled { get; set; } = false;
// 存储延迟记录用于统计
private readonly List<osu.Framework.Audio.EzLatency.EzLatencyRecord> latencyRecords = new List<osu.Framework.Audio.EzLatency.EzLatencyRecord>();
private readonly List<EzLatencyRecord> latencyRecords = new List<EzLatencyRecord>();
public event System.Action<osu.Framework.Audio.EzLatency.EzLatencyRecord>? OnNewRecord;
public event Action<EzLatencyRecord>? OnNewRecord;
/// <summary>
/// 延迟统计数据结构体
@@ -45,12 +45,13 @@ namespace osu.Framework.Audio
/// <summary>
/// 记录单个延迟事件并返回延迟信息(用于实时输出)
/// </summary>
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);
}
/// <summary>

View File

@@ -1,11 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.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;

View File

@@ -5,7 +5,7 @@ using System;
using osu.Framework.Logging;
using osu.Framework.Threading;
namespace osu.Framework.Audio
namespace osu.Framework.Audio.EzLatency
{
/// <summary>
/// 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
{

View File

@@ -1,8 +1,7 @@
using System;
namespace osu.Framework.Audio.EzLatency
{
#nullable disable
using System;
public interface IEzLatencyLogger : IDisposable
{
void Log(EzLatencyRecord record);

View File

@@ -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
/// </summary>
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)

View File

@@ -1,4 +1,4 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using 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
{
/// <summary>
/// EzOsuLatency 输入数据结构体
/// </summary>
public struct EzLatencyInputData
{
public double InputTime;
public object KeyValue;
public double JudgeTime;
public double PlaybackTime;
}
/// <summary>
/// EzOsuLatency 硬件数据结构体
/// </summary>
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
/// </summary>
private readonly Bindable<int?> globalMixerHandle = new Bindable<int?>();
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,16 +479,10 @@ 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",
level: LogLevel.Verbose);
}
}
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
/// </summary>
/// <param name="outputMode">输出模式</param>
/// <returns>缓冲区延迟(毫秒),失败返回-1</returns>
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
/// <param name="uncontrollableLatency">不可控延迟</param>
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
}
}