mirror of
https://github.com/SK-la/osu-framework.git
synced 2026-03-13 11:20:31 +00:00
重构ASIO机制
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
35
osu.Framework/Audio/EzLatency/EzLatencyStruct.cs
Normal file
35
osu.Framework/Audio/EzLatency/EzLatencyStruct.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
@@ -1,8 +1,7 @@
|
||||
using System;
|
||||
|
||||
namespace osu.Framework.Audio.EzLatency
|
||||
{
|
||||
#nullable disable
|
||||
using System;
|
||||
|
||||
public interface IEzLatencyLogger : IDisposable
|
||||
{
|
||||
void Log(EzLatencyRecord record);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user