重构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$ $END$
};</s:String> };</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Approximator/@EntryIndexedValue">True</s:Boolean> <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/=Beatmap/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=beatmaps/@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> <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 try
{ {
var rates = audioManager.GetAsioDeviceSupportedSampleRates(deviceName); // var rates = audioManager.GetAsioDeviceSupportedSampleRates(deviceName);
TestContext.WriteLine($" Supported rates: {(rates != null ? string.Join(", ", rates) : "null")}"); // TestContext.WriteLine($" Supported rates: {(rates != null ? string.Join(", ", rates) : "null")}");
//
if (rates != null && rates.Length > 0) // if (rates != null && rates.Length > 0)
{ // {
TestContext.WriteLine($" Rate count: {rates.Length}"); // TestContext.WriteLine($" Rate count: {rates.Length}");
} // }
else // else
{ // {
TestContext.WriteLine(" No supported rates found!"); // TestContext.WriteLine(" No supported rates found!");
} // }
} }
catch (System.Exception ex) catch (System.Exception ex)
{ {

View File

@@ -5,6 +5,7 @@ using System.Collections.Immutable;
using System.Linq; using System.Linq;
using ManagedBass; using ManagedBass;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.EzLatency;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Threading; using osu.Framework.Threading;
@@ -25,14 +26,14 @@ namespace osu.Framework.Tests.Audio
private volatile bool simulateLoss; private volatile bool simulateLoss;
protected override bool InitBass(int device, AudioOutputMode outputMode, int? asioDeviceIndex) protected override bool InitBass(int device, AudioOutputMode outputMode)
{ {
try try
{ {
if (simulateLoss) 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 finally
{ {

View File

@@ -16,13 +16,6 @@ namespace osu.Framework.Audio.Asio
/// </summary> /// </summary>
public static class AsioDeviceManager public static class AsioDeviceManager
{ {
// /// <summary>
// /// 常见采样率列表, 用于尝试验证设备支持的采样率。
// /// 按优先级排序48kHz优先于44.1kHz。
// /// 使用int类型因为采样率通常是整数值double仅在底层API交互时使用。
// /// </summary>
// 删除 SUPPORTED_SAMPLE_RATES 列表,默认使用 48000
/// <summary> /// <summary>
/// 未指定、或设置采样率失败时使用默认采样率48kHz性能较好。 /// 未指定、或设置采样率失败时使用默认采样率48kHz性能较好。
/// </summary> /// </summary>
@@ -31,32 +24,32 @@ namespace osu.Framework.Audio.Asio
/// <summary> /// <summary>
/// 最大重试次数,用于处理设备繁忙的情况。 /// 最大重试次数,用于处理设备繁忙的情况。
/// </summary> /// </summary>
private const int MAX_RETRY_COUNT = 3; private const int max_retry_count = 3;
/// <summary> /// <summary>
/// 重试间隔时间(毫秒)。 /// 重试间隔时间(毫秒)。
/// </summary> /// </summary>
private const int RETRY_DELAY_MS = 100; private const int retry_delay_ms = 100;
/// <summary> /// <summary>
/// 设备释放延迟时间(毫秒)。 /// 设备释放延迟时间(毫秒)。
/// </summary> /// </summary>
private const int DEVICE_FREE_DELAY_MS = 100; private const int device_free_delay_ms = 100;
/// <summary> /// <summary>
/// 强制重置延迟时间(毫秒)。 /// 强制重置延迟时间(毫秒)。
/// </summary> /// </summary>
private const int FORCE_RESET_DELAY_MS = 200; private const int force_reset_delay_ms = 200;
/// <summary> /// <summary>
/// 采样率容差,用于验证设置是否成功。 /// 采样率容差,用于验证设置是否成功。
/// </summary> /// </summary>
private const double SAMPLE_RATE_TOLERANCE = 1.0; private const double sample_rate_tolerance = 1.0;
/// <summary> /// <summary>
/// 静音帧日志间隔。 /// 静音帧日志间隔。
/// </summary> /// </summary>
private const int SILENCE_LOG_INTERVAL = 200; private const int silence_log_interval = 200;
/// <summary> /// <summary>
/// ASIO音频路由的全局混音器句柄。 /// ASIO音频路由的全局混音器句柄。
@@ -117,7 +110,7 @@ namespace osu.Framework.Audio.Asio
/// <returns>如果初始化成功则为true否则为false。</returns> /// <returns>如果初始化成功则为true否则为false。</returns>
private static bool tryInitializeDevice(int deviceIndex, AsioInitFlags flags) 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)) if (BassAsio.Init(deviceIndex, flags))
{ {
@@ -131,10 +124,10 @@ namespace osu.Framework.Audio.Asio
// 如果设备繁忙,等待并重试 // 如果设备繁忙,等待并重试
if ((int)bassError == 3 || bassError == Errors.Busy) 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); 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); Thread.Sleep(retry_delay_ms);
continue; continue;
} }
} }
@@ -170,7 +163,7 @@ namespace osu.Framework.Audio.Asio
double actualRate = BassAsio.Rate; 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); Logger.Log($"Failed to set ASIO device sample rate to {rate}Hz (actual: {actualRate}Hz)", LoggingTarget.Runtime, LogLevel.Error);
return false; 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> /// <summary>
/// 初始化ASIO设备。 /// 初始化ASIO设备。
/// </summary> /// </summary>
/// <param name="deviceIndex">要初始化的ASIO设备的索引。</param> /// <param name="deviceIndex">要初始化的ASIO设备的索引。</param>
/// <param name="sampleRateToTry">要尝试的采样率。如果为null则使用默认48000Hz。</param> /// <param name="sampleRateToTry">要尝试的采样率。如果为null则使用默认48000Hz。</param>
/// <param name="bufferSize">ASIO缓冲区大小。如果为null则使用默认128。</param>
/// <returns>如果初始化成功则为true否则为false。</returns> /// <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 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); 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); Logger.Log($"Initializing ASIO device: {deviceInfo.Name} (Driver: {deviceInfo.Driver})", LoggingTarget.Runtime, LogLevel.Debug);
// 释放之前的设备,确保完全清理
FreeDevice(); 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)) if (!tryInitializeDevice(deviceIndex, AsioInitFlags.Thread))
{ {
Logger.Log($"Failed to initialize ASIO device {deviceIndex} with Thread flag", LoggingTarget.Runtime, LogLevel.Error); 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 // 尝试采样率使用传入的值或默认48000
double rateToTry = sampleRateToTry ?? 48000.0; double rateToTry = sampleRateToTry ?? 48000.0;
if (!trySetSampleRate(rateToTry)) if (!trySetSampleRate(rateToTry))
{ {
Logger.Log($"Failed to set sample rate {rateToTry}Hz for ASIO device {deviceIndex}", LoggingTarget.Runtime, LogLevel.Error); Logger.Log($"Failed to set sample rate {rateToTry}Hz for ASIO device {deviceIndex}", LoggingTarget.Runtime, LogLevel.Error);
BassAsio.Free(); FreeDevice();
return false; return false;
} }
double successfulRate = rateToTry; 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; return true;
} }
catch (Exception ex) catch (Exception ex)
@@ -285,7 +270,7 @@ namespace osu.Framework.Audio.Asio
{ {
BassAsio.Stop(); BassAsio.Stop();
BassAsio.Free(); BassAsio.Free();
Thread.Sleep(DEVICE_FREE_DELAY_MS); Thread.Sleep(device_free_delay_ms);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -302,7 +287,7 @@ namespace osu.Framework.Audio.Asio
{ {
FreeDevice(); FreeDevice();
globalMixerHandle = 0; globalMixerHandle = 0;
Thread.Sleep(FORCE_RESET_DELAY_MS); Thread.Sleep(force_reset_delay_ms);
Logger.Log("ASIO Force Reset", LoggingTarget.Runtime, LogLevel.Debug); Logger.Log("ASIO Force Reset", LoggingTarget.Runtime, LogLevel.Debug);
} }
catch (Exception ex) 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> /// <summary>
/// 启动ASIO设备处理。 /// 启动ASIO设备处理。
/// </summary> /// </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> /// <summary>
/// 为ASIO设备配置默认输入和输出通道。 /// 为ASIO设备配置默认输入和输出通道。
/// </summary> /// </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); 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) if (channelsConfigured)
{ {
@@ -514,24 +465,19 @@ namespace osu.Framework.Audio.Asio
} }
} }
#region
/// <summary> /// <summary>
/// 为ASIO设备配置输出通道。 /// 为ASIO设备配置输出通道。
/// </summary> /// </summary>
/// <param name="info">ASIO设备信息。</param> /// <param name="info">ASIO设备信息。</param>
/// <returns>如果输出通道成功配置则为true否则为false。</returns> /// <returns>如果输出通道成功配置则为true否则为false。</returns>
private static bool configureOutputChannels(AsioInfo info) private static bool configureStereoChannels(AsioInfo info)
{ {
if (info.Outputs < 2) if (info.Outputs == 2) return true;
{
Logger.Log($"Insufficient output channels available ({info.Outputs}), cannot configure stereo output", LoggingTarget.Runtime, LogLevel.Important);
return false;
}
try try
{ {
// 配置第一个立体声输出对通道0和1
Logger.Log("Configuring stereo output channels (0 and 1) for ASIO device", LoggingTarget.Runtime, LogLevel.Debug);
// 创建ASIO过程回调 // 创建ASIO过程回调
AsioProcedure asioCallback = asioProcedure; AsioProcedure asioCallback = asioProcedure;
@@ -579,6 +525,8 @@ namespace osu.Framework.Audio.Asio
} }
} }
#endregion
/// <summary> /// <summary>
/// ASIO过程回调用于通道处理。 /// ASIO过程回调用于通道处理。
/// </summary> /// </summary>
@@ -613,7 +561,7 @@ namespace osu.Framework.Audio.Asio
{ {
// 没有音频数据可用,用静音填充 // 没有音频数据可用,用静音填充
fillBufferWithSilence(buffer, length); 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); Logger.Log($"[AudioDebug] ASIO callback silence count={silenceFrames}, globalMixer={mixerHandle}", LoggingTarget.Runtime, LogLevel.Debug);
} }
else if (bytesRead < length) 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>
/// 从音频线程获取全局混音器句柄。 /// 从音频线程获取全局混音器句柄。
/// </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. // See the LICENCE file in the repository root for full licence text.
#nullable disable #nullable disable
@@ -13,6 +13,7 @@ using JetBrains.Annotations;
using ManagedBass; using ManagedBass;
using ManagedBass.Fx; using ManagedBass.Fx;
using ManagedBass.Mix; using ManagedBass.Mix;
using osu.Framework.Audio.EzLatency;
using AsioDeviceManager = osu.Framework.Audio.Asio.AsioDeviceManager; using AsioDeviceManager = osu.Framework.Audio.Asio.AsioDeviceManager;
using osu.Framework.Audio.Mixing; using osu.Framework.Audio.Mixing;
using osu.Framework.Audio.Mixing.Bass; using osu.Framework.Audio.Mixing.Bass;
@@ -72,12 +73,12 @@ namespace osu.Framework.Audio
/// <summary> /// <summary>
/// 采样率用于ASIO设备的初始化和运行时更改。 /// 采样率用于ASIO设备的初始化和运行时更改。
/// </summary> /// </summary>
public readonly Bindable<int> SampleRate = new Bindable<int>(48000); public readonly Bindable<int> SAMPLE_RATE = new Bindable<int>(48000);
/// <summary> /// <summary>
/// ASIO设备的首选采样率, 用于设备的初始化。 /// ASIO缓冲区大小默认为128用于ASIO设备的初始化。
/// </summary> /// </summary>
private static int? preferredAsioSampleRate; public readonly Bindable<int> ASIO_BUFFER_SIZE = new Bindable<int>(128);
/// <summary> /// <summary>
/// The names of all available audio devices. /// The names of all available audio devices.
@@ -172,14 +173,6 @@ namespace osu.Framework.Audio
private ImmutableArray<DeviceInfo> audioDevices = ImmutableArray<DeviceInfo>.Empty; private ImmutableArray<DeviceInfo> audioDevices = ImmutableArray<DeviceInfo>.Empty;
private ImmutableList<string> audioDeviceNames = ImmutableList<string>.Empty; private ImmutableList<string> audioDeviceNames = ImmutableList<string>.Empty;
protected enum AudioOutputMode
{
Default,
WasapiShared,
WasapiExclusive,
Asio,
}
private static int asioNativeUnavailableLogged; private static int asioNativeUnavailableLogged;
private const string legacy_type_bass = "BASS"; private const string legacy_type_bass = "BASS";
@@ -189,53 +182,6 @@ namespace osu.Framework.Audio
private bool syncingSelection; 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) private void setUserBindableValueLeaseSafe<T>(Bindable<T> bindable, T newValue)
{ {
if (EqualityComparer<T>.Default.Equals(bindable.Value, newValue)) if (EqualityComparer<T>.Default.Equals(bindable.Value, newValue))
@@ -283,32 +229,21 @@ namespace osu.Framework.Audio
private readonly Lazy<SampleStore> globalSampleStore; private readonly Lazy<SampleStore> globalSampleStore;
/// <summary> /// <summary>
/// Sets the preferred sample rate for ASIO devices. /// 设置ASIO采样率对外接口。
/// 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.
/// </summary> /// </summary>
/// <param name="sampleRate">The preferred sample rate in Hz.</param> /// <param name="sampleRate">The preferred sample rate in Hz.</param>
public void SetPreferredAsioSampleRate(int sampleRate) public void SetPreferredAsioSampleRate(int sampleRate)
{ {
SampleRate.Value = sampleRate; SAMPLE_RATE.Value = sampleRate;
} }
/// <summary> /// <summary>
/// Gets the preferred sample rate for ASIO devices. /// 设置ASIO缓冲区大小对外接口。
/// </summary> /// </summary>
/// <returns>The preferred sample rate in Hz, or null if not set.</returns> /// <param name="bufferSize">The preferred buffer size for ASIO device.</param>
public static int? GetPreferredAsioSampleRate() public void SetAsioBufferSize(int bufferSize)
{ {
return preferredAsioSampleRate; ASIO_BUFFER_SIZE.Value = bufferSize;
} }
/// <summary> /// <summary>
@@ -390,7 +325,7 @@ namespace osu.Framework.Audio
GlobalMixerHandle.ValueChanged += handle => usingGlobalMixer.Value = handle.NewValue.HasValue; GlobalMixerHandle.ValueChanged += handle => usingGlobalMixer.Value = handle.NewValue.HasValue;
// Listen for unified sample rate changes and reinitialize device if supported // Listen for unified sample rate changes and reinitialize device if supported
SampleRate.ValueChanged += e => SAMPLE_RATE.ValueChanged += e =>
{ {
if (syncingSelection) if (syncingSelection)
return; 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) // 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 _)) 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); Logger.Log($"Current audio device before reinitialization: {AudioDevice.Value}", name: "audio", level: LogLevel.Debug);
scheduler.AddOnce(() => scheduler.AddOnce(() =>
@@ -419,7 +354,47 @@ namespace osu.Framework.Audio
} }
else 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; syncingSelection = false;
} }
} }
@@ -563,7 +538,7 @@ namespace osu.Framework.Audio
// Note: normalisation may write back to bindables; ensure those writes are update-thread-safe. // Note: normalisation may write back to bindables; ensure those writes are update-thread-safe.
normaliseLegacySelection(); normaliseLegacySelection();
var (mode, deviceName, asioIndex) = parseSelection(AudioDevice.Value); var (mode, deviceName) = parseSelection(AudioDevice.Value);
bool isExplicitSelection = !string.IsNullOrEmpty(AudioDevice.Value); bool isExplicitSelection = !string.IsNullOrEmpty(AudioDevice.Value);
bool isTypedSelection = hasTypeSuffix(AudioDevice.Value); bool isTypedSelection = hasTypeSuffix(AudioDevice.Value);
@@ -589,35 +564,28 @@ namespace osu.Framework.Audio
// try using the specified device // try using the specified device
if (mode == AudioOutputMode.Asio) 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. // 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. // Use the OS default BASS device as a fallback initialisation target.
// For ASIO mode, add retry logic since device initialization can be flaky // 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; bool asioInitSuccess = false;
for (int retry = 0; retry < max_asio_retries; retry++) 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))
if (trySetDevice(bass_default_device, mode, asioIndex))
{ {
Logger.Log($"ASIO device initialization successful on attempt {retry + 1}", name: "audio", level: LogLevel.Important);
asioInitSuccess = true; asioInitSuccess = true;
break;
} }
else
if (retry < max_asio_retries - 1)
{ {
Logger.Log($"ASIO device initialization failed, retrying in 1 second (attempt {retry + 1}/{max_asio_retries})", name: "audio", level: LogLevel.Important); Logger.Log($"ASIO device initialization failed, retrying in 300ms (attempt {retry + 1}/{max_asio_retries})", name: "audio", level: LogLevel.Important);
Thread.Sleep(1000); Thread.Sleep(200);
// Force cleanup between retries to prevent resource conflicts // Force cleanup between retries to prevent resource conflicts
scheduler.Add(() => scheduler.Add(() =>
{ {
thread.FreeDevice(bass_default_device); thread.FreeDevice(bass_default_device);
Thread.Sleep(200); Thread.Sleep(100);
}); });
} }
} }
@@ -625,7 +593,6 @@ namespace osu.Framework.Audio
if (!asioInitSuccess) if (!asioInitSuccess)
{ {
Logger.Log("ASIO device initialization failed after all retries, falling back to default device", name: "audio", level: LogLevel.Important); 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; return;
@@ -634,19 +601,19 @@ namespace osu.Framework.Audio
{ {
// try using the specified device // try using the specified device
int deviceIndex = audioDeviceNames.FindIndex(d => d == deviceName); 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. // 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, // 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". // 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. // If an explicit selection failed, revert to Default and try again in default output mode.
// Keep checkbox state unless Exclusive/ASIO was chosen. // Keep checkbox state unless Exclusive/ASIO was chosen.
if (isExplicitSelection) if (isExplicitSelection)
{ {
if (trySetDevice(bass_default_device, AudioOutputMode.Default, null)) if (trySetDevice(bass_default_device, AudioOutputMode.Default))
{ {
revertSelectionToDefault(); revertSelectionToDefault();
return; 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. // 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. // we're boned. even "No sound" device won't initialise.
return; return;
bool trySetDevice(int deviceId, AudioOutputMode outputMode, int? asioDeviceIndex) bool trySetDevice(int deviceId, AudioOutputMode outputMode)
{ {
var device = audioDevices.ElementAtOrDefault(deviceId); var device = audioDevices.ElementAtOrDefault(deviceId);
@@ -675,7 +642,7 @@ namespace osu.Framework.Audio
return false; return false;
// initialize new device // initialize new device
if (!InitBass(deviceId, outputMode, asioDeviceIndex)) if (!InitBass(deviceId, outputMode))
return false; return false;
//we have successfully initialised a new device. //we have successfully initialised a new device.
@@ -759,8 +726,7 @@ namespace osu.Framework.Audio
/// </summary> /// </summary>
/// <param name="device">The device to initialise.</param> /// <param name="device">The device to initialise.</param>
/// <param name="outputMode">The output mode to use for playback.</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)
protected virtual bool InitBass(int device, AudioOutputMode outputMode, int? asioDeviceIndex)
{ {
// this likely doesn't help us but also doesn't seem to cause any issues or any cpu increase. // this likely doesn't help us but also doesn't seem to cause any issues or any cpu increase.
Bass.UpdatePeriod = 5; Bass.UpdatePeriod = 5;
@@ -797,7 +763,7 @@ namespace osu.Framework.Audio
try 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) catch (Exception e)
{ {
@@ -855,24 +821,6 @@ namespace osu.Framework.Audio
return attemptInit(); 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() private void syncAudioDevices()
{ {
audioDevices = GetAllDevices(); audioDevices = GetAllDevices();
@@ -926,7 +874,7 @@ namespace osu.Framework.Audio
// ASIO drivers. // ASIO drivers.
int asioCount = 0; int asioCount = 0;
foreach (var device in enumerateAsioDevices()) foreach (var device in AsioDeviceManager.EnumerateAsioDevices())
{ {
entries.Add(formatEntry(device.Name, type_asio)); entries.Add(formatEntry(device.Name, type_asio));
asioCount++; asioCount++;
@@ -940,14 +888,14 @@ namespace osu.Framework.Audio
private static string formatEntry(string name, string type) => $"{name} ({type})"; 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. // Default device.
if (string.IsNullOrEmpty(selection)) if (string.IsNullOrEmpty(selection))
{ {
return (UseExperimentalWasapi.Value && RuntimeInfo.OS == RuntimeInfo.Platform.Windows return (UseExperimentalWasapi.Value && RuntimeInfo.OS == RuntimeInfo.Platform.Windows
? AudioOutputMode.WasapiShared ? 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. // 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 (UseExperimentalWasapi.Value && RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
{ {
if (tryParseSuffixed(selection, type_wasapi_exclusive, out string baseName)) if (tryParseSuffixed(selection, type_wasapi_exclusive, out string baseName))
return (AudioOutputMode.WasapiShared, baseName, null); return (AudioOutputMode.WasapiShared, baseName);
if (tryParseSuffixed(selection, type_asio, out _)) 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)) 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)) if (tryParseSuffixed(selection, type_asio, out name))
{ {
int? index = findAsioDeviceIndex(name); return (AudioOutputMode.Asio, name);
return (AudioOutputMode.Asio, name, index);
} }
// Legacy value (raw BASS device name). Keep old behaviour: the experimental flag decides shared WASAPI. // Legacy value (raw BASS device name). Keep old behaviour: the experimental flag decides shared WASAPI.
if (UseExperimentalWasapi.Value && RuntimeInfo.OS == RuntimeInfo.Platform.Windows) 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) private static bool tryParseSuffixed(string value, string type, out string baseName)
@@ -1050,7 +997,7 @@ namespace osu.Framework.Audio
protected virtual bool IsCurrentDeviceValid() protected virtual bool IsCurrentDeviceValid()
{ {
var device = audioDevices.ElementAtOrDefault(Bass.CurrentDevice); 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. // ASIO output selection does not map to a BASS device name; ensure we're initialised and ASIO is working.
if (mode == AudioOutputMode.Asio) if (mode == AudioOutputMode.Asio)
@@ -1082,72 +1029,75 @@ namespace osu.Framework.Audio
string deviceName = audioDevices.ElementAtOrDefault(Bass.CurrentDevice).Name; string deviceName = audioDevices.ElementAtOrDefault(Bass.CurrentDevice).Name;
return $@"{GetType().ReadableName()} ({deviceName ?? "Unknown"})"; 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. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Diagnostics;
using osu.Framework.Logging; using osu.Framework.Logging;
namespace osu.Framework.Audio namespace osu.Framework.Audio.EzLatency
{ {
/// <summary> /// <summary>
/// EzDriverModule 负责音频驱动的设置和管理。 /// EzDriverModule 负责音频驱动的设置和管理。
@@ -17,7 +18,7 @@ namespace osu.Framework.Audio
/// </summary> /// </summary>
public bool Enabled { get; set; } = false; 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; internal double DriverTime;

View File

@@ -5,7 +5,7 @@ using System;
using System.Diagnostics; using System.Diagnostics;
using osu.Framework.Logging; using osu.Framework.Logging;
namespace osu.Framework.Audio namespace osu.Framework.Audio.EzLatency
{ {
/// <summary> /// <summary>
/// EzHardwareModule 负责硬件层延迟的测量和管理。 /// EzHardwareModule 负责硬件层延迟的测量和管理。
@@ -58,7 +58,8 @@ namespace osu.Framework.Audio
InputHardwareTime = stopwatch.Elapsed.TotalMilliseconds; InputHardwareTime = stopwatch.Elapsed.TotalMilliseconds;
LatencyDifference = OutputHardwareTime - EzInputModule.InputTime; // 首尾延迟差值T_out - T_in 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. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Diagnostics;
using osu.Framework.Logging; using osu.Framework.Logging;
namespace osu.Framework.Audio namespace osu.Framework.Audio.EzLatency
{ {
/// <summary> /// <summary>
/// EzInputModule 负责处理输入事件的捕获和时间戳记录。 /// EzInputModule 负责处理输入事件的捕获和时间戳记录。
@@ -17,7 +18,7 @@ namespace osu.Framework.Audio
/// </summary> /// </summary>
public static bool Enabled { get; set; } = false; 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; public static double InputTime;

View File

@@ -2,9 +2,10 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Diagnostics;
using osu.Framework.Logging; using osu.Framework.Logging;
namespace osu.Framework.Audio namespace osu.Framework.Audio.EzLatency
{ {
/// <summary> /// <summary>
/// EzJudgeModule 负责处理判定时间的计算和记录。 /// EzJudgeModule 负责处理判定时间的计算和记录。
@@ -17,7 +18,7 @@ namespace osu.Framework.Audio
/// </summary> /// </summary>
public static bool Enabled { get; set; } = false; 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; 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 namespace osu.Framework.Audio.EzLatency
{ {
#nullable disable #nullable disable
using System;
public class EzLatencyRecord public class EzLatencyRecord
{ {
// high-level fields // high-level fields
@@ -20,7 +23,7 @@ namespace osu.Framework.Audio.EzLatency
public double LatencyDifference { get; set; } public double LatencyDifference { get; set; }
// optional low-level structs copied from AudioThread for richer diagnostics // optional low-level structs copied from AudioThread for richer diagnostics
public osu.Framework.Threading.EzLatencyInputData InputData { get; set; } public EzLatencyInputData InputData { get; set; }
public osu.Framework.Threading.EzLatencyHardwareData HardwareData { 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 namespace osu.Framework.Audio.EzLatency
{ {
#nullable disable #nullable disable
using System;
/// <summary> /// <summary>
/// Standalone singleton service which collects low-level framework latency records /// Standalone singleton service which collects low-level framework latency records
/// and exposes higher-level EzLatencyRecord events for consumers in the osu layer. /// 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.Bindables;
using osu.Framework.Logging; using osu.Framework.Logging;
namespace osu.Framework.Audio namespace osu.Framework.Audio.EzLatency
{ {
/// <summary> /// <summary>
/// EzOsuLatency 延迟测试模块 /// EzOsuLatency 延迟测试模块
@@ -75,7 +75,7 @@ namespace osu.Framework.Audio
// 5. 计算 T_out - T_call // 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; const double hardware_latency = 1.5;
double simulatedLatency = bufferLatency + hardware_latency; double simulatedLatency = bufferLatency + hardware_latency;
@@ -133,6 +133,7 @@ namespace osu.Framework.Audio
public void RunPeriodicTest() public void RunPeriodicTest()
{ {
double currentTime = RecordTimestamp(); double currentTime = RecordTimestamp();
if (currentTime - lastTestTime > 1000) // 每秒测试一次 if (currentTime - lastTestTime > 1000) // 每秒测试一次
{ {
RunTest(); RunTest();
@@ -155,7 +156,7 @@ namespace osu.Framework.Audio
{ {
// 基于配置参数模拟延迟 // 基于配置参数模拟延迟
double baseLatency = DriverType.Value == "ASIO" ? 2.0 : 5.0; // ASIO通常延迟更低 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; // 硬件层延迟 const double hardware_latency = 1.5; // 硬件层延迟
return baseLatency + bufferLatency + hardware_latency; return baseLatency + bufferLatency + hardware_latency;

View File

@@ -6,7 +6,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Logging; using osu.Framework.Logging;
namespace osu.Framework.Audio namespace osu.Framework.Audio.EzLatency
{ {
/// <summary> /// <summary>
/// EzOsuLatency 日志模块 /// EzOsuLatency 日志模块
@@ -26,9 +26,9 @@ namespace osu.Framework.Audio
public bool Enabled { get; set; } = false; 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> /// <summary>
/// 延迟统计数据结构体 /// 延迟统计数据结构体
@@ -45,12 +45,13 @@ namespace osu.Framework.Audio
/// <summary> /// <summary>
/// 记录单个延迟事件并返回延迟信息(用于实时输出) /// 记录单个延迟事件并返回延迟信息(用于实时输出)
/// </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) if (!Enabled)
return null; return null;
var record = new osu.Framework.Audio.EzLatency.EzLatencyRecord var record = new EzLatencyRecord
{ {
Timestamp = DateTimeOffset.Now, Timestamp = DateTimeOffset.Now,
InputTime = inputTime, InputTime = inputTime,
@@ -128,8 +129,10 @@ namespace osu.Framework.Audio
return; 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 message1 =
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)"; $"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: {message1}");
Logger.Log($"[EzOsuLatency] Analysis: \n{message2}", LoggingTarget.Runtime, LogLevel.Important); 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) public void LogLatency(double timestamp, string driverType, int sampleRate, int bufferSize, double inputLatency, double playbackLatency, double totalLatency, double uncontrollableLatency)
{ {
if (Enabled) 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> /// <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 namespace osu.Framework.Audio.EzLatency
{ {
#nullable disable #nullable disable
using System;
using System.IO;
using osu.Framework.Logging;
using osu.Framework.Threading;
public class EzLoggerAdapter : IEzLatencyLogger public class EzLoggerAdapter : IEzLatencyLogger
{ {
private readonly Scheduler scheduler; private readonly Scheduler scheduler;

View File

@@ -5,7 +5,7 @@ using System;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Threading; using osu.Framework.Threading;
namespace osu.Framework.Audio namespace osu.Framework.Audio.EzLatency
{ {
/// <summary> /// <summary>
/// EzPlaybackModule 负责处理播放事件的触发和时间戳记录。 /// EzPlaybackModule 负责处理播放事件的触发和时间戳记录。
@@ -46,8 +46,8 @@ namespace osu.Framework.Audio
if (EzInputModule.InputTime > 0) if (EzInputModule.InputTime > 0)
{ {
// build local low-level structs for richer diagnostics (do not mutate AudioThread fields) // build local low-level structs for richer diagnostics (do not mutate AudioThread fields)
osu.Framework.Threading.EzLatencyInputData inputData = default; EzLatencyInputData inputData = default;
osu.Framework.Threading.EzLatencyHardwareData hardwareData = default; EzLatencyHardwareData hardwareData = default;
try try
{ {

View File

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

View File

@@ -1426,7 +1426,7 @@ namespace osu.Framework.Platform
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxTexturesUploadedPerFrame); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxTexturesUploadedPerFrame);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxPixelsUploadedPerFrame); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxPixelsUploadedPerFrame);
if (Renderer is not osu.Framework.Graphics.Rendering.Renderer renderer) if (Renderer is not Renderer renderer)
return; return;
DrawThread?.Scheduler.Add(() => DrawThread?.Scheduler.Add(() =>
@@ -1447,7 +1447,7 @@ namespace osu.Framework.Platform
/// </summary> /// </summary>
public void RestoreTextureUploadLimits() public void RestoreTextureUploadLimits()
{ {
if (Renderer is not osu.Framework.Graphics.Rendering.Renderer renderer) if (Renderer is not Renderer renderer)
return; return;
if (originalMaxTexturesUploadedPerFrame == null || originalMaxPixelsUploadedPerFrame == null) 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. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Statistics; using osu.Framework.Statistics;
@@ -16,6 +16,7 @@ using ManagedBass.Mix;
using ManagedBass.Wasapi; using ManagedBass.Wasapi;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Asio; using osu.Framework.Audio.Asio;
using osu.Framework.Audio.EzLatency;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Development; using osu.Framework.Development;
using osu.Framework.Logging; using osu.Framework.Logging;
@@ -23,28 +24,6 @@ using osu.Framework.Platform.Linux.Native;
namespace osu.Framework.Threading 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 public class AudioThread : GameThread
{ {
private static int wasapiNativeUnavailableLogged; private static int wasapiNativeUnavailableLogged;
@@ -241,7 +220,7 @@ namespace osu.Framework.Threading
/// </summary> /// </summary>
private readonly Bindable<int?> globalMixerHandle = new Bindable<int?>(); 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); 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. 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(); freeWasapi();
// 对于ASIO模式在初始化前添加额外延迟以确保设备完全释放 // 对于ASIO模式在初始化前添加额外延迟以确保设备完全释放
if (outputMode == AudioThreadOutputMode.Asio) if (outputMode == AudioOutputMode.Asio)
{ {
Logger.Log("检测到ASIO模式在设备初始化前添加额外延迟", name: "audio", level: LogLevel.Debug); Logger.Log("检测到ASIO模式在设备初始化前添加额外延迟", name: "audio", level: LogLevel.Debug);
// 增加延迟以确保设备完全释放 // 增加延迟以确保设备完全释放
Thread.Sleep(500); Thread.Sleep(200);
} }
// Try to initialise the device, or request a re-initialise. // Try to initialise the device, or request a re-initialise.
@@ -272,10 +251,10 @@ namespace osu.Framework.Threading
switch (outputMode) switch (outputMode)
{ {
case AudioThreadOutputMode.Default: case AudioOutputMode.Default:
break; break;
case AudioThreadOutputMode.WasapiShared: case AudioOutputMode.WasapiShared:
if (!attemptWasapiInitialisation(deviceId, exclusive: false)) if (!attemptWasapiInitialisation(deviceId, exclusive: false))
{ {
Logger.Log($"BassWasapi initialisation failed (shared mode). BASS error: {Bass.LastError}", name: "audio", level: LogLevel.Error); 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; break;
case AudioThreadOutputMode.WasapiExclusive: case AudioOutputMode.WasapiExclusive:
if (!attemptWasapiInitialisation(deviceId, exclusive: true)) if (!attemptWasapiInitialisation(deviceId, exclusive: true))
{ {
Logger.Log($"BassWasapi initialisation failed (exclusive mode). BASS error: {Bass.LastError}", name: "audio", level: LogLevel.Error); 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; break;
case AudioThreadOutputMode.Asio: case AudioOutputMode.Asio:
if (asioDeviceIndex == null) if (!initAsio(deviceId, preferredSampleRate))
{
Logger.Log("选择了ASIO输出模式但未提供ASIO设备索引。", name: "audio", level: LogLevel.Error);
return false;
}
if (!initAsio(asioDeviceIndex.Value, preferredSampleRate))
return false; return false;
break; 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. // 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. // 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",
{
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); level: LogLevel.Verbose);
return false;
} }
} }
else else
@@ -649,20 +616,19 @@ namespace osu.Framework.Threading
Logger.Log(message, name: "audio", level: LogLevel.Error); 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(); freeAsio();
// 使用来自AudioManager的统一采样率 // 使用来自AudioManager的统一采样率和缓冲区大小
if (Manager != null) 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处理回退决策 // 不要自动释放BASS设备 - 让AudioManager处理回退决策
// 这可以防止过度激进的设备切换导致设备可用性降低 // 这可以防止过度激进的设备切换导致设备可用性降低
return false; return false;
@@ -844,15 +810,15 @@ namespace osu.Framework.Threading
/// </summary> /// </summary>
/// <param name="outputMode">输出模式</param> /// <param name="outputMode">输出模式</param>
/// <returns>缓冲区延迟(毫秒),失败返回-1</returns> /// <returns>缓冲区延迟(毫秒),失败返回-1</returns>
internal double GetDriverBufferLatency(AudioThreadOutputMode outputMode) internal double GetDriverBufferLatency(AudioOutputMode outputMode)
{ {
switch (outputMode) switch (outputMode)
{ {
case AudioThreadOutputMode.Asio: case AudioOutputMode.Asio:
return GetAsioOutputLatency(); return GetAsioOutputLatency();
case AudioThreadOutputMode.WasapiExclusive: case AudioOutputMode.WasapiExclusive:
case AudioThreadOutputMode.WasapiShared: case AudioOutputMode.WasapiShared:
return GetWasapiStreamLatency(); return GetWasapiStreamLatency();
default: default:
@@ -869,14 +835,14 @@ namespace osu.Framework.Threading
/// <param name="uncontrollableLatency">不可控延迟</param> /// <param name="uncontrollableLatency">不可控延迟</param>
internal void LogLatencyData(double inputLatency, double playbackLatency, double totalLatency, double uncontrollableLatency) internal void LogLatencyData(double inputLatency, double playbackLatency, double totalLatency, double uncontrollableLatency)
{ {
const string driverType = "Unknown"; const string driver_type = "Unknown";
int sampleRate = 44100; int sampleRate = 44100;
const int bufferSize = 256; const int buffer_size = 128;
// 获取当前驱动信息 // 获取当前驱动信息
if (Manager != null) if (Manager != null)
{ {
sampleRate = Manager.SampleRate.Value; sampleRate = Manager.SAMPLE_RATE.Value;
// TODO: 获取bufferSize // TODO: 获取bufferSize
} }
@@ -885,9 +851,9 @@ namespace osu.Framework.Threading
LogModule.LogLatency( LogModule.LogLatency(
LatencyTestModule.RecordTimestamp(), LatencyTestModule.RecordTimestamp(),
driverType, driver_type,
sampleRate, sampleRate,
bufferSize, buffer_size,
inputLatency, inputLatency,
playbackLatency, playbackLatency,
totalLatency, totalLatency,
@@ -895,14 +861,6 @@ namespace osu.Framework.Threading
); );
} }
internal enum AudioThreadOutputMode
{
Default,
WasapiShared,
WasapiExclusive,
Asio,
}
#endregion #endregion
} }
} }