// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Statistics; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Threading; using ManagedBass; using ManagedBass.Asio; 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; using osu.Framework.Platform.Linux.Native; namespace osu.Framework.Threading { public class AudioThread : GameThread { private static int wasapiNativeUnavailableLogged; private static int asioResolverRegistered; // EzOsuLatency 模块 internal readonly EzLatencyTestModule LatencyTestModule; internal readonly EzLogModule LogModule; internal readonly EzDriverModule DriverModule; internal readonly EzHardwareModule HardwareModule; // EzOsuLatency 数据结构体 // NOTE: Previously these were present as placeholders; we now pass local structs from the playback path. // Keep no instance fields to avoid misleading shared mutable state. public AudioThread() : base(name: "Audio") { // Ensure this AudioThread instance is always reachable from native WASAPI callbacks. wasapiUserHandle = GCHandle.Alloc(this, GCHandleType.Normal); wasapiUserPtr = GCHandle.ToIntPtr(wasapiUserHandle); OnNewFrame += onNewFrame; PreloadBass(); // 初始化EzOsuLatency模块 LatencyTestModule = new EzLatencyTestModule(); LogModule = new EzLogModule(); DriverModule = new EzDriverModule(); HardwareModule = new EzHardwareModule(); } public override bool IsCurrent => ThreadSafety.IsAudioThread; internal sealed override void MakeCurrent() { base.MakeCurrent(); ThreadSafety.IsAudioThread = true; } internal override IEnumerable StatisticsCounters => new[] { StatisticsCounterType.TasksRun, StatisticsCounterType.Tracks, StatisticsCounterType.Samples, StatisticsCounterType.SChannels, StatisticsCounterType.Components, StatisticsCounterType.MixChannels, }; private readonly List managers = new List(); private static readonly HashSet initialised_devices = new HashSet(); private static readonly GlobalStatistic cpu_usage = GlobalStatistics.Get("Audio", "Bass CPU%"); private long frameCount; private void onNewFrame() { if (frameCount++ % 1000 == 0) cpu_usage.Value = Bass.CPUUsage; lock (managers) { for (int i = 0; i < managers.Count; i++) { var m = managers[i]; m.Update(); } } // EzOsuLatency: 定期运行延迟测试 LatencyTestModule.RunPeriodicTest(); } internal void RegisterManager(AudioManager manager) { lock (managers) { if (managers.Contains(manager)) throw new InvalidOperationException($"{manager} was already registered"); managers.Add(manager); } // Set the manager reference for event triggering Manager ??= manager; manager.GlobalMixerHandle.BindTo(globalMixerHandle); } internal void UnregisterManager(AudioManager manager) { lock (managers) managers.Remove(manager); manager.GlobalMixerHandle.UnbindFrom(globalMixerHandle); } protected override void OnExit() { base.OnExit(); lock (managers) { // AudioManagers are iterated over backwards since disposal will unregister and remove them from the list. for (int i = managers.Count - 1; i >= 0; i--) { var m = managers[i]; m.Dispose(); // Audio component disposal (including the AudioManager itself) is scheduled and only runs when the AudioThread updates. // But the AudioThread won't run another update since it's exiting, so an update must be performed manually in order to finish the disposal. m.Update(); } managers.Clear(); } // Safety net to ensure we have freed all devices before exiting. // This is mainly required for device-lost scenarios. // See https://github.com/ppy/osu-framework/pull/3378 for further discussion. foreach (int d in initialised_devices.ToArray()) FreeDevice(d); if (wasapiUserHandle.IsAllocated) wasapiUserHandle.Free(); } #region BASS Initialisation // TODO: All this bass init stuff should probably not be in this class. // WASAPI callbacks must never be allowed to be GC'd while native code may still call into them. // Use static delegates with a stable user pointer back to this thread instance. private static readonly WasapiProcedure wasapi_procedure_static = (buffer, length, user) => { var thread = getWasapiOwner(user); int? mixer = thread?.globalMixerHandle.Value; if (mixer == null) return 0; return Bass.ChannelGetData(mixer.Value, buffer, length); }; private static readonly WasapiNotifyProcedure wasapi_notify_procedure_static = (notify, device, user) => { var thread = getWasapiOwner(user); if (thread == null) return; thread.Scheduler.Add(() => { if (notify == WasapiNotificationType.DefaultOutput) { thread.freeWasapi(); thread.initWasapi(device, thread.wasapiExclusiveActive); } }); }; private static AudioThread? getWasapiOwner(IntPtr user) { if (user == IntPtr.Zero) return null; try { return (AudioThread?)GCHandle.FromIntPtr(user).Target; } catch { return null; } } private GCHandle wasapiUserHandle; private readonly IntPtr wasapiUserPtr; private bool wasapiExclusiveActive; /// /// Reference to the AudioManager that owns this AudioThread. /// Used to trigger events when ASIO devices are initialized. /// internal AudioManager? Manager { get; set; } /// /// If a global mixer is being used, this will be the BASS handle for it. /// If non-null, all game mixers should be added to this mixer. /// private readonly Bindable globalMixerHandle = new Bindable(); 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. // Important: stop any existing output first. // In particular, WASAPI exclusive can hold the device such that a subsequent Bass.Init() returns Busy. // If we can't initialise BASS, we also won't get a chance to clean up the previous output mode. freeAsio(); freeWasapi(); // 对于ASIO模式,在初始化前添加额外延迟以确保设备完全释放 if (outputMode == AudioOutputMode.Asio) { Logger.Log("检测到ASIO模式,在设备初始化前添加额外延迟", name: "audio", level: LogLevel.Debug); // 增加延迟以确保设备完全释放 Thread.Sleep(200); } // Try to initialise the device, or request a re-initialise. // 128 == BASS_DEVICE_REINIT. Only use it when the device is already initialised. var initFlags = initialised_devices.Contains(deviceId) ? (DeviceInitFlags)128 : 0; if (!Bass.Init(deviceId, Flags: initFlags)) { Logger.Log($"BASS.Init({deviceId}) failed: {Bass.LastError}", name: "audio", level: LogLevel.Error); return false; } switch (outputMode) { case AudioOutputMode.Default: break; case AudioOutputMode.WasapiShared: if (!attemptWasapiInitialisation(deviceId, exclusive: false)) { Logger.Log($"BassWasapi initialisation failed (shared mode). BASS error: {Bass.LastError}", name: "audio", level: LogLevel.Error); return false; } break; case AudioOutputMode.WasapiExclusive: if (!attemptWasapiInitialisation(deviceId, exclusive: true)) { Logger.Log($"BassWasapi initialisation failed (exclusive mode). BASS error: {Bass.LastError}", name: "audio", level: LogLevel.Error); return false; } break; case AudioOutputMode.Asio: if (!initAsio(deviceId, preferredSampleRate)) return false; break; } initialised_devices.Add(deviceId); return true; } internal void FreeDevice(int deviceId) { Debug.Assert(ThreadSafety.IsAudioThread); int selectedDevice = Bass.CurrentDevice; // 对于ASIO设备,先释放ASIO再释放BASS以确保正确的清理顺序 freeAsio(); if (canSelectDevice(deviceId)) { Bass.CurrentDevice = deviceId; Bass.Free(); } freeWasapi(); if (selectedDevice != deviceId && canSelectDevice(selectedDevice)) Bass.CurrentDevice = selectedDevice; initialised_devices.Remove(deviceId); static bool canSelectDevice(int deviceId) => Bass.GetDeviceInfo(deviceId, out var deviceInfo) && deviceInfo.IsInitialized; } /// /// 使BASS库可用以供使用。 /// internal static void PreloadBass() { if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) { if (Interlocked.Exchange(ref asioResolverRegistered, 1) == 1) return; NativeLibrary.SetDllImportResolver(typeof(BassAsio).Assembly, resolveBassAsio); } if (RuntimeInfo.OS == RuntimeInfo.Platform.Linux) { // 暂时需要此操作来解决libbass_fx.so加载失败问题 (see https://github.com/ppy/osu/issues/2852) Library.Load("libbass.so", Library.LoadFlags.RTLD_LAZY | Library.LoadFlags.RTLD_GLOBAL); } } private static IntPtr resolveBassAsio(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) { if (!libraryName.Equals("bassasio", StringComparison.OrdinalIgnoreCase) && !libraryName.Equals("bassasio.dll", StringComparison.OrdinalIgnoreCase)) return IntPtr.Zero; // 从架构特定的目录加载bassasio.dll try { // 首先尝试使用默认搜索路径加载 IntPtr result = NativeLibrary.Load(libraryName, assembly, searchPath ?? DllImportSearchPath.UseDllDirectoryForDependencies | DllImportSearchPath.SafeDirectories); if (result != IntPtr.Zero) return result; } catch { // 忽略并尝试其他加载方式 } // 如果是64位进程,尝试从x64目录加载 if (Environment.Is64BitProcess) { try { string dllPath = Path.Combine(AppContext.BaseDirectory, "x64", "bassasio.dll"); if (File.Exists(dllPath)) return NativeLibrary.Load(dllPath); } catch { // 忽略并继续 } } else { // 如果是32位进程,尝试从x86目录加载 try { string dllPath = Path.Combine(AppContext.BaseDirectory, "x86", "bassasio.dll"); if (File.Exists(dllPath)) return NativeLibrary.Load(dllPath); } catch { // 忽略并继续 } } // 如果所有尝试都失败,让运行时使用默认解析 return IntPtr.Zero; } private bool attemptWasapiInitialisation() => attemptWasapiInitialisation(Bass.CurrentDevice, exclusive: false); private bool attemptWasapiInitialisation(int bassDeviceId, bool exclusive) { if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows) return false; Logger.Log("Attempting local BassWasapi initialisation"); try { return attemptWasapiInitialisationInternal(bassDeviceId, exclusive); } catch (DllNotFoundException e) { logWasapiNativeUnavailableOnce($"WASAPI output is unavailable because basswasapi.dll could not be loaded ({e.Message})."); return false; } catch (EntryPointNotFoundException e) { logWasapiNativeUnavailableOnce($"WASAPI output is unavailable because basswasapi.dll is incompatible/mismatched ({e.Message})."); return false; } catch (Exception e) { Logger.Log($"WASAPI initialisation failed with exception: {e}", name: "audio", level: LogLevel.Error); return false; } } private bool attemptWasapiInitialisationInternal(int bassDeviceId, bool exclusive) { Logger.Log($"Attempting local BassWasapi initialisation (exclusive: {exclusive})", name: "audio", level: LogLevel.Verbose); int wasapiDevice = -1; // WASAPI device indices don't match normal BASS devices. // Each device is listed multiple times with each supported channel/frequency pair. // // Working backwards to find the correct device is how bass does things internally (see BassWasapi.GetBassDevice). if (bassDeviceId > 0) { string driver = Bass.GetDeviceInfo(bassDeviceId).Driver; if (!string.IsNullOrEmpty(driver)) { var candidates = new List<(int index, int freq, int chans)>(); // WASAPI device indices don't match normal BASS devices. // Each device is listed multiple times with each supported channel/frequency pair. // // Working backwards to find the correct device is how bass does things internally (see BassWasapi.GetBassDevice). // We replicate this by scanning the full list and remembering the last match. for (int i = 0; i < 16384; i++) { if (!BassWasapi.GetDeviceInfo(i, out WasapiDeviceInfo info)) break; // Only consider output devices (not input/loopback), since we're initialising audio output. if (info.ID == driver && info.IsEnabled && !info.IsInput && !info.IsLoopback) candidates.Add((i, info.MixFrequency, info.MixChannels)); } if (candidates.Count > 0) { // Prefer common stereo formats. var best = candidates .OrderBy(c => c.chans == 2 ? 0 : 1) .ThenBy(c => Math.Abs(c.freq - 48000)) .ThenBy(c => Math.Abs(c.freq - 44100)) .First(); wasapiDevice = best.index; Logger.Log($"Mapped BASS device {bassDeviceId} (driver '{driver}') to WASAPI device {wasapiDevice} (mix: {best.freq}Hz/{best.chans}ch).", name: "audio", level: LogLevel.Verbose); // In exclusive mode, the chosen (freq/chans) pair matters. If the preferred candidate fails, // we will retry other candidates below. if (exclusive) { foreach (var candidate in candidates .OrderBy(c => c.chans == 2 ? 0 : 1) .ThenBy(c => Math.Abs(c.freq - 48000)) .ThenBy(c => Math.Abs(c.freq - 44100))) { freeWasapi(); if (initWasapi(candidate.index, exclusive)) return true; } Logger.Log($"All WASAPI exclusive format candidates failed for BASS device {bassDeviceId} (driver '{driver}').", name: "audio", level: LogLevel.Verbose); return false; } } else { // 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. Logger.Log($"Could not map BASS device {bassDeviceId} (driver '{driver}') to a WASAPI output device; refusing to fall back to default (-1).", name: "audio", level: LogLevel.Verbose); return false; } } else { Logger.Log($"BASS device {bassDeviceId} did not provide a driver identifier; falling back to default WASAPI device (-1).", name: "audio", level: LogLevel.Verbose); } } if (wasapiDevice == -1) Logger.Log("Using default WASAPI device (-1).", name: "audio", level: LogLevel.Verbose); // To keep things in a sane state let's only keep one device initialised via wasapi. freeWasapi(); return initWasapi(wasapiDevice, exclusive); } private bool initWasapi(int wasapiDevice, bool exclusive) { try { wasapiExclusiveActive = exclusive; // BASSWASAPI flags: // - 0x1 = EXCLUSIVE // - 0x10 = EVENT (event-driven) // ManagedBass bindings used here do not currently expose Exclusive, so we use the documented value. const WasapiInitFlags exclusive_flag = (WasapiInitFlags)0x1; int requestedFrequency = 0; int requestedChannels = 0; // Shared mode can use event-driven callbacks and auto-format. // Exclusive mode should use an explicit supported format (freq/chans) from the chosen WASAPI entry. var flags = (WasapiInitFlags)0; if (exclusive) { flags |= exclusive_flag; if (wasapiDevice >= 0 && BassWasapi.GetDeviceInfo(wasapiDevice, out WasapiDeviceInfo selectedInfo)) { requestedFrequency = selectedInfo.MixFrequency; requestedChannels = selectedInfo.MixChannels; } } else { flags |= WasapiInitFlags.EventDriven | WasapiInitFlags.AutoFormat; } // Important: in exclusive mode, the underlying implementation may not support event-driven callbacks // and can fall back to polling. Using a near-zero period (float.Epsilon) can then cause a busy-loop, // leading to time running far too fast (and eventual instability elsewhere). float bufferSeconds = exclusive ? 0.05f : 0f; float periodSeconds = exclusive ? 0.01f : float.Epsilon; bool initialised = BassWasapi.Init(wasapiDevice, Frequency: requestedFrequency, Channels: requestedChannels, Procedure: wasapi_procedure_static, Flags: flags, Buffer: bufferSeconds, Period: periodSeconds, User: wasapiUserPtr); Logger.Log( $"Initialising BassWasapi for device {wasapiDevice} (exclusive: {exclusive}, buffer: {bufferSeconds:0.###}s, period: {periodSeconds:0.###}s)...{(initialised ? "success!" : "FAILED")}", name: "audio", level: LogLevel.Verbose); if (!initialised) return false; BassWasapi.GetInfo(out var wasapiInfo); Logger.Log($"WASAPI info: Freq={wasapiInfo.Frequency}, Chans={wasapiInfo.Channels}, Format={wasapiInfo.Format}", name: "audio", level: LogLevel.Verbose); globalMixerHandle.Value = BassMix.CreateMixerStream(wasapiInfo.Frequency, wasapiInfo.Channels, BassFlags.MixerNonStop | BassFlags.Decode | BassFlags.Float); BassWasapi.Start(); BassWasapi.SetNotify(wasapi_notify_procedure_static, wasapiUserPtr); return true; } catch (DllNotFoundException e) { logWasapiNativeUnavailableOnce($"WASAPI output is unavailable because basswasapi.dll could not be loaded ({e.Message})."); freeWasapi(); return false; } catch (EntryPointNotFoundException e) { logWasapiNativeUnavailableOnce($"WASAPI output is unavailable because basswasapi.dll is incompatible/mismatched ({e.Message})."); freeWasapi(); return false; } catch (Exception e) { Logger.Log($"WASAPI init failed with exception: {e}", name: "audio", level: LogLevel.Error); freeWasapi(); return false; } } private void freeWasapi() { int? mixerToFree = globalMixerHandle.Value; try { // The mixer probably doesn't need to be recycled. Just keeping things sane for now. // Stop WASAPI first to prevent callbacks from accessing disposed resources. BassWasapi.SetNotify(null, IntPtr.Zero); BassWasapi.Stop(); BassWasapi.Free(); if (mixerToFree != null) Bass.StreamFree(mixerToFree.Value); } catch (DllNotFoundException e) { logWasapiNativeUnavailableOnce($"WASAPI cleanup failed because basswasapi.dll could not be loaded ({e.Message})."); } catch (EntryPointNotFoundException e) { logWasapiNativeUnavailableOnce($"WASAPI cleanup failed because basswasapi.dll is incompatible/mismatched ({e.Message})."); } catch (Exception e) { Logger.Log($"WASAPI cleanup failed with exception: {e}", name: "audio", level: LogLevel.Error); } finally { globalMixerHandle.Value = null; } } private static void logWasapiNativeUnavailableOnce(string message) { if (Interlocked.Exchange(ref wasapiNativeUnavailableLogged, 1) == 1) return; Logger.Log(message, name: "audio", level: LogLevel.Error); } private bool initAsio(int asioDeviceIndex, double preferredSampleRate = 48000) { freeAsio(); // 使用来自AudioManager的统一采样率和缓冲区大小 if (Manager != null) { preferredSampleRate = Manager.SAMPLE_RATE.Value; int bufferSize = Manager.ASIO_BUFFER_SIZE.Value; if (!AsioDeviceManager.InitializeDevice(asioDeviceIndex, preferredSampleRate, bufferSize)) { Logger.Log($"AsioDeviceManager.InitializeDevice({asioDeviceIndex}, {preferredSampleRate}, {bufferSize}) 失败", name: "audio", level: LogLevel.Error); // 不要自动释放BASS设备 - 让AudioManager处理回退决策 // 这可以防止过度激进的设备切换导致设备可用性降低 return false; } // 初始化后获取设备信息 var deviceInfo = AsioDeviceManager.GetCurrentDeviceInfo(); if (deviceInfo == null) { Logger.Log("初始化后无法获取ASIO设备信息", name: "audio", level: LogLevel.Error); freeAsio(); // 不要自动释放BASS设备 - 让AudioManager处理回退决策 return false; } int outputChannels = Math.Max(1, deviceInfo.Value.Outputs); int inputChannels = Math.Max(0, deviceInfo.Value.Inputs); // 验证设备信息 if (outputChannels < 2) { Logger.Log($"ASIO设备输出通道不足 ({outputChannels}),立体声至少需要2个通道", name: "audio", level: LogLevel.Important); freeAsio(); FreeDevice(Bass.CurrentDevice); return false; } double sampleRate = BassAsio.Rate; Logger.Log($"ASIO设备采样率: {sampleRate}Hz", name: "audio", level: LogLevel.Verbose); // 验证采样率 if (sampleRate <= 0 || sampleRate > 1000000 || double.IsNaN(sampleRate) || double.IsInfinity(sampleRate)) { Logger.Log($"检测到无效采样率 ({sampleRate}Hz),使用 {AsioDeviceManager.DEFAULT_SAMPLE_RATE}Hz 作为回退", name: "audio", level: LogLevel.Important); sampleRate = AsioDeviceManager.DEFAULT_SAMPLE_RATE; } // Logger.Log($"使用ASIO设备配置 - 输出: {outputChannels}, 输入: {inputChannels}, 采样率: {sampleRate}Hz", name: "audio", level: LogLevel.Verbose); // 创建立体声混音器(游戏音频始终是立体声) const int mixer_channels = 2; // Logger.Log($"创建ASIO混音器流: 采样率={sampleRate}, 混音器通道={mixer_channels} (立体声)", name: "audio", level: LogLevel.Verbose); globalMixerHandle.Value = BassMix.CreateMixerStream((int)sampleRate, mixer_channels, BassFlags.MixerNonStop | BassFlags.Decode | BassFlags.Float); if (globalMixerHandle.Value == 0) { var mixerError = Bass.LastError; Logger.Log($"创建ASIO混音器流失败: {(int)mixerError} ({mixerError}), 采样率={sampleRate}, 通道={mixer_channels}", name: "audio", level: LogLevel.Error); freeAsio(); // 释放BASS设备以便AudioManager可以使用不同的设备重试 FreeDevice(Bass.CurrentDevice); return false; } // Logger.Log($"创建了带有 {mixer_channels} 个通道的ASIO混音器流,采样率为 {sampleRate}Hz (句柄: {globalMixerHandle.Value})", name: "audio", level: LogLevel.Verbose); // 为ASIO设备管理器设置全局混音器句柄 AsioDeviceManager.SetGlobalMixerHandle(globalMixerHandle.Value.Value); // 使用设备管理器启动ASIO设备 if (!AsioDeviceManager.StartDevice()) { Logger.Log("AsioDeviceManager.StartDevice() 失败", name: "audio", level: LogLevel.Error); freeAsio(); // 不要自动释放BASS设备 - 让AudioManager处理回退决策 return false; } // Logger.Log($"ASIO设备初始化成功 - 采样率: {sampleRate}Hz, 输出: {outputChannels}, 输入: {inputChannels}", name: "audio", level: LogLevel.Important); // 通知ASIO设备已使用实际采样率初始化 Manager?.OnAsioDeviceInitialized?.Invoke(sampleRate); } return true; } private void freeAsio() { if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows) return; Logger.Log("Freeing ASIO device", name: "audio", level: LogLevel.Important); try { // 首先停止ASIO设备 AsioDeviceManager.StopDevice(); // 停止后稍作延迟让设备稳定 Thread.Sleep(50); // 释放ASIO设备 AsioDeviceManager.FreeDevice(); } catch (Exception ex) { Logger.Log($"ASIO设备清理过程中出现异常: {ex.Message},尝试强制重置", name: "audio", level: LogLevel.Error); try { AsioDeviceManager.ForceReset(); } catch (Exception resetEx) { Logger.Log($"强制重置也失败: {resetEx.Message}", name: "audio", level: LogLevel.Error); } } // 清理混音器句柄 if (globalMixerHandle.Value != null) { try { Bass.StreamFree(globalMixerHandle.Value.Value); } catch (Exception ex) { Logger.Log($"释放ASIO混音器时出现异常: {ex.Message}", name: "audio", level: LogLevel.Error); } globalMixerHandle.Value = null; } // 释放ASIO设备后添加较长延迟 // 这可以防止设备繁忙错误,当在ASIO设备之间切换时 Thread.Sleep(300); } /// /// 获取ASIO驱动的输出延迟 /// /// 输出延迟(毫秒),失败返回-1 internal double GetAsioOutputLatency() { try { if (BassAsio.IsStarted) { // 估算 ASIO 输出延迟 return 2.0; // ASIO 通常延迟较低 } } catch (Exception ex) { Logger.Log($"获取ASIO输出延迟失败: {ex.Message}", name: "audio", level: LogLevel.Error); } return -1; } /// /// 获取WASAPI流的延迟 /// /// 流延迟(毫秒),失败返回-1 internal double GetWasapiStreamLatency() { try { if (BassWasapi.IsStarted) { // 使用 IAudioClient::GetStreamLatency // 注意:BassWasapi 可能不直接暴露这个,需要通过底层 API // 暂时返回估算值 return 10.0; // 估算的 WASAPI 延迟 } } catch (Exception ex) { Logger.Log($"获取WASAPI流延迟失败: {ex.Message}", name: "audio", level: LogLevel.Error); } return -1; } /// /// 获取当前音频驱动的缓冲区延迟 /// /// 输出模式 /// 缓冲区延迟(毫秒),失败返回-1 internal double GetDriverBufferLatency(AudioOutputMode outputMode) { switch (outputMode) { case AudioOutputMode.Asio: return GetAsioOutputLatency(); case AudioOutputMode.WasapiExclusive: case AudioOutputMode.WasapiShared: return GetWasapiStreamLatency(); default: return -1; } } /// /// 记录延迟数据到日志 /// /// 输入延迟 /// 播放延迟 /// 总延迟 /// 不可控延迟 internal void LogLatencyData(double inputLatency, double playbackLatency, double totalLatency, double uncontrollableLatency) { const string driver_type = "Unknown"; int sampleRate = 44100; const int buffer_size = 128; // 获取当前驱动信息 if (Manager != null) { sampleRate = Manager.SAMPLE_RATE.Value; // TODO: 获取bufferSize } // 确定驱动类型 // TODO: 从当前输出模式确定驱动类型 LogModule.LogLatency( LatencyTestModule.RecordTimestamp(), driver_type, sampleRate, buffer_size, inputLatency, playbackLatency, totalLatency, uncontrollableLatency ); } #endregion } }