diff --git a/dependencies/osu.Game.Rulesets.OverlayAPI b/dependencies/osu.Game.Rulesets.OverlayAPI index 0be625d0..731aa9f6 160000 --- a/dependencies/osu.Game.Rulesets.OverlayAPI +++ b/dependencies/osu.Game.Rulesets.OverlayAPI @@ -1 +1 @@ -Subproject commit 0be625d0a95809a5dc03aecba67426ec6d50a535 +Subproject commit 731aa9f658e69ce94c0bd6ef0a7b7c215081275c diff --git a/src/Apps/KeyAsio/Converters/AsioToAudioConverter.cs b/src/Apps/KeyAsio/Converters/AsioToAudioConverter.cs new file mode 100644 index 00000000..8e2b5598 --- /dev/null +++ b/src/Apps/KeyAsio/Converters/AsioToAudioConverter.cs @@ -0,0 +1,19 @@ +using System.Globalization; +using Avalonia.Data.Converters; + +namespace KeyAsio.Converters; + +public sealed class AsioToAudioConverter : IValueConverter +{ + public static readonly AsioToAudioConverter Instance = new(); + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value is string text ? text.Replace("ASIO", "Audio", StringComparison.Ordinal) : value; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } +} diff --git a/src/Apps/KeyAsio/DependencyInjectionExtensions.cs b/src/Apps/KeyAsio/DependencyInjectionExtensions.cs index 1c0c20fc..3517e97b 100644 --- a/src/Apps/KeyAsio/DependencyInjectionExtensions.cs +++ b/src/Apps/KeyAsio/DependencyInjectionExtensions.cs @@ -38,6 +38,7 @@ public static IServiceCollection AddGuiModule(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Apps/KeyAsio/Lang/SR.Designer.cs b/src/Apps/KeyAsio/Lang/SR.Designer.cs index 70150d8e..eed74f90 100644 --- a/src/Apps/KeyAsio/Lang/SR.Designer.cs +++ b/src/Apps/KeyAsio/Lang/SR.Designer.cs @@ -935,7 +935,7 @@ internal static string Preset_Fast { } /// - /// 查找类似 Reduces system overhead, suitable for low-end devices or basic gameplay needs. 的本地化字符串。 + /// 查找类似 Standard resource usage to deliver the best listening experience, suitable for most gameplay scenarios. 的本地化字符串。 /// internal static string Preset_FastDescription { get { @@ -953,7 +953,7 @@ internal static string Preset_SelectionDescription { } /// - /// 查找类似 Select Preset Mode 的本地化字符串。 + /// 查找类似 Select Preset 的本地化字符串。 /// internal static string Preset_SelectionTitle { get { @@ -961,24 +961,6 @@ internal static string Preset_SelectionTitle { } } - /// - /// 查找类似 Balanced 的本地化字符串。 - /// - internal static string Preset_Standard { - get { - return ResourceManager.GetString("Preset_Standard", resourceCulture); - } - } - - /// - /// 查找类似 Balances resource usage to deliver the best listening experience, suitable for most gameplay scenarios. 的本地化字符串。 - /// - internal static string Preset_StandardDescription { - get { - return ResourceManager.GetString("Preset_StandardDescription", resourceCulture); - } - } - /// /// 查找类似 About 的本地化字符串。 /// @@ -1780,6 +1762,15 @@ internal static string Wizard_Start { } } + /// + /// 查找类似 a 的本地化字符串。 + /// + internal static string Wizard_SubTitle { + get { + return ResourceManager.GetString("Wizard_SubTitle", resourceCulture); + } + } + /// /// 查找类似 Test Key Sound 的本地化字符串。 /// diff --git a/src/Apps/KeyAsio/Lang/SR.en.resx b/src/Apps/KeyAsio/Lang/SR.en.resx index e98b72f0..befe5605 100644 --- a/src/Apps/KeyAsio/Lang/SR.en.resx +++ b/src/Apps/KeyAsio/Lang/SR.en.resx @@ -404,19 +404,13 @@ You can change option later. Efficient - Reduces system overhead, suitable for low-end devices or basic gameplay needs. + Standard resource usage to deliver the best listening experience, suitable for most gameplay scenarios. Please choose an appropriate mode based on your device performance and usage needs. - Select Preset Mode - - - Balanced - - - Balances resource usage to deliver the best listening experience, suitable for most gameplay scenarios. + Select Preset About @@ -721,4 +715,7 @@ You can change option later. Alpha + + Configure KeyASIO in just a few steps. + \ No newline at end of file diff --git a/src/Apps/KeyAsio/Lang/SR.ja.resx b/src/Apps/KeyAsio/Lang/SR.ja.resx index 2b1cdfbc..0dca2501 100644 --- a/src/Apps/KeyAsio/Lang/SR.ja.resx +++ b/src/Apps/KeyAsio/Lang/SR.ja.resx @@ -411,13 +411,7 @@ デバイス性能と使用目的に応じて適切なモードを選択してください。 - プリセットモードを選択 - - - 標準バランス - - - リソース使用をバランスさせ、最適な聴覚体験を提供し、ほとんどのプレイ場面に適しています。 + プリセットを選択 概要 @@ -710,4 +704,7 @@ KeyASIO へようこそ + + いくつかの手順で KeyASIO を設定できます。 + \ No newline at end of file diff --git a/src/Apps/KeyAsio/Lang/SR.resx b/src/Apps/KeyAsio/Lang/SR.resx index 4c825e30..66087454 100644 --- a/src/Apps/KeyAsio/Lang/SR.resx +++ b/src/Apps/KeyAsio/Lang/SR.resx @@ -410,19 +410,13 @@ You can change option later. Efficient - Reduces system overhead, suitable for low-end devices or basic gameplay needs. + Standard resource usage to deliver the best listening experience, suitable for most gameplay scenarios. Please choose an appropriate mode based on your device performance and usage needs. - Select Preset Mode - - - Balanced - - - Balances resource usage to deliver the best listening experience, suitable for most gameplay scenarios. + Select Preset About @@ -727,4 +721,7 @@ You can change option later. Alpha + + Configure KeyASIO in just a few steps. + \ No newline at end of file diff --git a/src/Apps/KeyAsio/Lang/SR.zh-hans.resx b/src/Apps/KeyAsio/Lang/SR.zh-hans.resx index 6b7f852c..47e88e92 100644 --- a/src/Apps/KeyAsio/Lang/SR.zh-hans.resx +++ b/src/Apps/KeyAsio/Lang/SR.zh-hans.resx @@ -411,19 +411,13 @@ 高效轻量 - 简化系统开销,适合低配设备或基本游玩需求。 + 标准资源占用,提供最佳听觉体验,适合大多数游玩场景。 请根据您的设备性能和使用需求选择合适的模式。 - 选择预设模式 - - - 标准均衡 - - - 均衡资源占用,提供最佳听觉体验,适合大多数游玩场景。 + 选择预设 关于 @@ -728,4 +722,7 @@ 开发版 (Alpha) + + 只需几个步骤,即可完成 KeyASIO 配置。 + \ No newline at end of file diff --git a/src/Apps/KeyAsio/Services/PresetManager.cs b/src/Apps/KeyAsio/Services/PresetManager.cs index 0a68edfc..e4bba1a6 100644 --- a/src/Apps/KeyAsio/Services/PresetManager.cs +++ b/src/Apps/KeyAsio/Services/PresetManager.cs @@ -1,7 +1,7 @@ +using KeyAsio.Configuration; using KeyAsio.Core.Audio; using KeyAsio.Core.Audio.SampleProviders.BalancePans; using KeyAsio.Lang; -using KeyAsio.Configuration; using KeyAsio.ViewModels; using Material.Icons; @@ -9,7 +9,6 @@ namespace KeyAsio.Services; public enum PresetMode { - Standard, Fast, Extreme } @@ -47,13 +46,6 @@ public void Initialize() { AvailablePresets = [ - new PresetModel( - PresetMode.Standard, - SRKeys.Preset_Standard, - SRKeys.Preset_StandardDescription, - MaterialIconKind.ScaleBalance, - "SukiInformationColor" - ), new PresetModel( PresetMode.Fast, SRKeys.Preset_Fast, @@ -82,21 +74,12 @@ public void Initialize() } // Fast - if (_appSettings.Sync.Scanning.GeneralScanInterval == 50 && - _appSettings.Sync.Scanning.TimingScanInterval == 2 && - _appSettings.Sync.Playback.LimiterType == LimiterType.Soft && - _appSettings.Sync.Playback.BalanceMode == BalanceMode.ConstantPower) - { - return PresetMode.Fast; - } - - // Standard if (_appSettings.Sync.Scanning.GeneralScanInterval == 50 && _appSettings.Sync.Scanning.TimingScanInterval == 2 && _appSettings.Sync.Playback.LimiterType == LimiterType.Peak && _appSettings.Sync.Playback.BalanceMode == BalanceMode.ProMixFocus) { - return PresetMode.Standard; + return PresetMode.Fast; } return null; @@ -106,9 +89,6 @@ public async Task ApplyPreset(PresetMode mode, AudioSettingsViewModel audioSetti { switch (mode) { - case PresetMode.Standard: - ApplyStandard(); - break; case PresetMode.Fast: ApplyLightweight(); break; @@ -120,26 +100,11 @@ public async Task ApplyPreset(PresetMode mode, AudioSettingsViewModel audioSetti //await audioSettingsViewModel.ReloadAudioDevice(); } - private void ApplyStandard() + private void ApplyLightweight() { - //_appSettings.Input.UseRawInput = true; - _appSettings.Sync.Playback.LimiterType = LimiterType.Peak; _appSettings.Sync.Playback.BalanceMode = BalanceMode.ProMixFocus; - //_appSettings.Performance.EnableAvx512 = true; - - _appSettings.Sync.Scanning.GeneralScanInterval = 50; - _appSettings.Sync.Scanning.TimingScanInterval = 2; - - // todo: 平衡器算法、限频器算法、无视所有音量与声道变化等 - } - - private void ApplyLightweight() - { - _appSettings.Sync.Playback.LimiterType = LimiterType.Soft; - _appSettings.Sync.Playback.BalanceMode = BalanceMode.ConstantPower; - _appSettings.Sync.Scanning.GeneralScanInterval = 50; _appSettings.Sync.Scanning.TimingScanInterval = 2; } @@ -151,5 +116,7 @@ private void ApplyExtreme() _appSettings.Sync.Scanning.GeneralScanInterval = 50; _appSettings.Sync.Scanning.TimingScanInterval = 1; + + // todo: 平衡器算法、限频器算法、无视所有音量与声道变化等 } } diff --git a/src/Apps/KeyAsio/Services/WizardTestSoundService.cs b/src/Apps/KeyAsio/Services/WizardTestSoundService.cs new file mode 100644 index 00000000..24d8bcc1 --- /dev/null +++ b/src/Apps/KeyAsio/Services/WizardTestSoundService.cs @@ -0,0 +1,106 @@ +using KeyAsio.Core.Audio; +using KeyAsio.Core.Audio.Caching; +using KeyAsio.Sync.Services; +using Microsoft.Extensions.Logging; + +namespace KeyAsio.Services; + +public interface IWizardTestSoundService +{ + bool IsPlaying { get; } + + void Start(); + + void Stop(); +} + +public sealed class WizardTestSoundService : IWizardTestSoundService, IDisposable +{ + private const string TestSoundKey = "internal://dynamic/normal-hitnormal"; + + private readonly AudioCacheManager _audioCacheManager; + private readonly IPlaybackEngine _playbackEngine; + private readonly SfxPlaybackService _sfxPlaybackService; + private readonly ILogger _logger; + private readonly Lock _stateLock = new(); + + private CancellationTokenSource? _playbackCts; + + public WizardTestSoundService( + AudioCacheManager audioCacheManager, + IPlaybackEngine playbackEngine, + SfxPlaybackService sfxPlaybackService, + ILogger logger) + { + _audioCacheManager = audioCacheManager; + _playbackEngine = playbackEngine; + _sfxPlaybackService = sfxPlaybackService; + _logger = logger; + } + + public bool IsPlaying + { + get + { + lock (_stateLock) + { + return _playbackCts is { IsCancellationRequested: false }; + } + } + } + + public void Start() + { + lock (_stateLock) + { + StopCore(); + + if (_playbackEngine.CurrentDevice is null) + { + throw new InvalidOperationException("Audio device must be active before starting the wizard test sound."); + } + + var cachedAudio = _audioCacheManager.CreateDynamic(TestSoundKey, _playbackEngine.EngineWaveFormat); + _playbackCts = new CancellationTokenSource(); + _sfxPlaybackService.PlayEffectsAudio(cachedAudio, 0.8f, 0f); + _ = RunPlaybackLoopAsync(cachedAudio, _playbackCts.Token); + } + } + + public void Stop() + { + lock (_stateLock) + { + StopCore(); + } + } + + private async Task RunPlaybackLoopAsync(CachedAudio cachedAudio, CancellationToken cancellationToken) + { + try + { + using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1)); + while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false)) + { + _sfxPlaybackService.PlayEffectsAudio(cachedAudio, 0.8f, 0f); + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + } + catch (Exception exception) + { + _logger.LogError(exception, "Wizard test sound playback failed"); + Stop(); + } + } + + private void StopCore() + { + _playbackCts?.Cancel(); + _playbackCts?.Dispose(); + _playbackCts = null; + } + + public void Dispose() => Stop(); +} diff --git a/src/Apps/KeyAsio/ViewModels/WizardAudioConfigViewModel.cs b/src/Apps/KeyAsio/ViewModels/WizardAudioConfigViewModel.cs index 64b622b4..d614f59c 100644 --- a/src/Apps/KeyAsio/ViewModels/WizardAudioConfigViewModel.cs +++ b/src/Apps/KeyAsio/ViewModels/WizardAudioConfigViewModel.cs @@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.Input; using KeyAsio.Configuration; using KeyAsio.Core.Audio; +using KeyAsio.Plugins.Contracts; using KeyAsio.Services; using SukiUI.Toasts; @@ -21,37 +22,52 @@ public enum AudioSubStep { Selection, Configuration, + ConcurrencyCheck, + AlternativeDeviceCheck, + ProMixRequired, Validation } public partial class WizardAudioConfigViewModel : ViewModelBase { + private const string ProMixPluginId = "KeyAsio.Plugins.ProMix"; + private readonly IAudioDeviceManager _audioDeviceManager; private readonly IAudioDeviceOperationCoordinator _deviceOperations; + private readonly IWizardTestSoundService _testSoundService; private readonly ISukiToastManager _toastManager; private readonly AppSettings _appSettings; + private IReadOnlyList _allAudioDevices = []; public WizardAudioConfigViewModel( IAudioDeviceManager audioDeviceManager, IAudioDeviceOperationCoordinator deviceOperations, ISukiToastManager toastManager, - AppSettings appSettings) + AppSettings appSettings, + IPluginManager pluginManager, + IWizardTestSoundService testSoundService) { _audioDeviceManager = audioDeviceManager; _deviceOperations = deviceOperations; + _testSoundService = testSoundService; _toastManager = toastManager; _appSettings = appSettings; + IsProMixAvailable = pluginManager.GetAllPlugins() + .Any(plugin => string.Equals(plugin.Id, ProMixPluginId, StringComparison.Ordinal)); - AvailableDriverTypes = new ObservableCollection(Enum.GetValues()); + AvailableDriverTypes = [WavePlayerType.ASIO, WavePlayerType.WASAPI]; SelectedDriverType = WavePlayerType.ASIO; - LoadDevices(); + _ = LoadDevicesAsync(); } + public bool IsProMixAvailable { get; } + [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsHardwareConfig))] [NotifyPropertyChangedFor(nameof(IsSoftwareConfig))] [NotifyPropertyChangedFor(nameof(IsHardwareMode))] + [NotifyPropertyChangedFor(nameof(CanGoForward))] public partial WizardMode SelectedMode { get; set; } = WizardMode.NotSelected; // Config Page @@ -78,6 +94,9 @@ public WizardAudioConfigViewModel( [NotifyPropertyChangedFor(nameof(IsSelectionMode))] [NotifyPropertyChangedFor(nameof(IsHardwareConfig))] [NotifyPropertyChangedFor(nameof(IsSoftwareConfig))] + [NotifyPropertyChangedFor(nameof(IsConcurrencyCheck))] + [NotifyPropertyChangedFor(nameof(IsAlternativeDeviceCheck))] + [NotifyPropertyChangedFor(nameof(IsProMixRequired))] [NotifyPropertyChangedFor(nameof(IsValidationStep))] [NotifyPropertyChangedFor(nameof(CanGoForward))] public partial AudioSubStep CurrentAudioSubStep { get; set; } = AudioSubStep.Selection; @@ -90,6 +109,12 @@ public WizardAudioConfigViewModel( public bool IsSoftwareConfig => CurrentAudioSubStep == AudioSubStep.Configuration && SelectedMode == WizardMode.Software; + public bool IsConcurrencyCheck => CurrentAudioSubStep == AudioSubStep.ConcurrencyCheck; + + public bool IsAlternativeDeviceCheck => CurrentAudioSubStep == AudioSubStep.AlternativeDeviceCheck; + + public bool IsProMixRequired => CurrentAudioSubStep == AudioSubStep.ProMixRequired; + public bool IsHardwareMode => SelectedMode == WizardMode.Hardware; public bool IsValidationStep => CurrentAudioSubStep == AudioSubStep.Validation; @@ -113,6 +138,19 @@ public WizardAudioConfigViewModel( [ObservableProperty] public partial string ValidationMessage { get; set; } = ""; + [ObservableProperty] + public partial string ValidationInstruction { get; set; } = + "请在 osu! 选图页播放音乐并按键,确认音乐与软件音效均正常。"; + + [ObservableProperty] + public partial string ProMixRequiredMessage { get; set; } = ""; + + [ObservableProperty] + public partial bool HasAlternativeGameDevices { get; set; } + + [ObservableProperty] + public partial bool IsConcurrencyTestSoundPlaying { get; set; } + public async Task TryGoBackAsync() { @@ -122,13 +160,12 @@ public async Task TryGoBackAsync() return true; } - if (CurrentAudioSubStep == AudioSubStep.Validation) + if (CurrentAudioSubStep is AudioSubStep.ConcurrencyCheck + or AudioSubStep.AlternativeDeviceCheck + or AudioSubStep.ProMixRequired + or AudioSubStep.Validation) { - CurrentAudioSubStep = AudioSubStep.Configuration; - IsValidationRunning = false; - ValidationSuccess = false; - IsAudioConfigFinished = false; - await _deviceOperations.DeactivateAsync(); + await ReturnToConfigurationAsync(); return true; } @@ -173,42 +210,37 @@ public bool CanGoForward } } + public void StopTestSound() + { + _testSoundService.Stop(); + IsConcurrencyTestSoundPlaying = false; + } + [ObservableProperty] public partial bool ShowVirtualDriverWarning { get; set; } [ObservableProperty] public partial string VirtualDriverWarning { get; set; } = ""; - [RelayCommand] + private bool CanSelectMode(WizardMode mode) => mode != WizardMode.Software || IsProMixAvailable; + + [RelayCommand(CanExecute = nameof(CanSelectMode))] private void SelectMode(WizardMode mode) { + StopTestSound(); SelectedMode = mode; if (mode == WizardMode.Hardware) { _appSettings.Sync.EnableMixSync = false; - Dispatcher.UIThread.InvokeAsync(async () => - { - var devices = await _audioDeviceManager.GetCachedAvailableDevicesAsync(); - var asioCount = devices.Count(d => d.WavePlayerType == WavePlayerType.ASIO); - if (asioCount < 1) - { - ShowHardwareDriverWarning = true; - HardwareDriverWarning = "未检测到支持的驱动,建议切换到软件模式"; - } - else - { - ShowHardwareDriverWarning = false; - } - }); SelectedDriverType = WavePlayerType.ASIO; + UpdateDeviceList(_allAudioDevices); } else if (mode == WizardMode.Software) { _appSettings.Sync.EnableMixSync = true; CheckVirtualDriver(); - // Software mode (ProMix) typically outputs to a physical device via WASAPI or ASIO - // For now default to WASAPI as it is more common for physical outputs SelectedDriverType = WavePlayerType.WASAPI; + UpdateDeviceList(_allAudioDevices); } CurrentAudioSubStep = AudioSubStep.Configuration; @@ -217,19 +249,21 @@ private void SelectMode(WizardMode mode) [RelayCommand] private async Task BackToSelection() { + StopTestSound(); SelectedMode = WizardMode.NotSelected; CurrentAudioSubStep = AudioSubStep.Selection; IsAudioConfigFinished = false; - // Stop any playing audio + ValidationSuccess = false; await _deviceOperations.DeactivateAsync(); } [RelayCommand] private async Task ApplyAndTestConfig() { + StopTestSound(); CurrentAudioSubStep = AudioSubStep.Validation; IsValidationRunning = true; - ValidationMessage = "正在初始化音频引擎..."; + ValidationMessage = "正在初始化音频引擎…"; ValidationSuccess = false; if (SelectedAudioDevice is null) @@ -242,20 +276,124 @@ private async Task ApplyAndTestConfig() var result = await _deviceOperations.ApplyAsync(SelectedAudioDevice, _appSettings.Audio.SampleRate); if (result.IsSuccess) { - ValidationSuccess = true; - IsAudioConfigFinished = true; - ValidationMessage = "配置成功"; + if (SelectedMode == WizardMode.Hardware) + { + try + { + _testSoundService.Start(); + IsConcurrencyTestSoundPlaying = true; + CurrentAudioSubStep = AudioSubStep.ConcurrencyCheck; + ValidationMessage = "独占设备已就绪"; + } + catch (Exception exception) + { + ValidationSuccess = false; + ValidationMessage = $"测试音播放失败:{exception.Message}"; + } + } + else + { + ValidationSuccess = true; + IsAudioConfigFinished = true; + ValidationMessage = "配置成功"; + ValidationInstruction = + "请在 osu! 中选择虚拟声卡并进入选图页,确认音乐自动播放后按键,验证软件音效正常。"; + } } else { ValidationSuccess = false; - ValidationMessage = $"初始化失败: {result.Error?.Message ?? "未知错误"}"; + ValidationMessage = $"初始化失败:{result.Error?.Message ?? "未知错误"}"; IsAudioConfigFinished = false; } IsValidationRunning = false; } + [RelayCommand] + private void ConfirmSameDeviceAudio() + { + CompleteHardwareRouting( + "设备配置完成", + "保持 osu! 使用当前设备,将其全局延迟调整至 -40ms 左右,选一张谱面用 Auto 游玩,确认软件音效与游戏音乐均正常。"); + } + + [RelayCommand] + private async Task ReportSameDeviceSilent() + { + if (HasAlternativeGameDevices) + { + CurrentAudioSubStep = AudioSubStep.AlternativeDeviceCheck; + return; + } + + await RequireProMixAsync("系统只检测到一个播放设备,且不支持 ASIO/WASAPI 同时共享。"); + } + + [RelayCommand] + private void ConfirmAlternativeDeviceAudio() + { + CompleteHardwareRouting( + "设备配置完成", + "保持 osu! 使用当前能听到声音的设备,选一张谱面用 Auto 游玩,确认软件音效与游戏音乐在两个设备上均正常。\n\n注意:需自行 DIY(如使用外部混音器)将两路输出合并到耳机。"); + } + + [RelayCommand] + private async Task ReportNoAlternativeDevice() + { + await RequireProMixAsync("未找到能让 osu! 正常播放的其他设备,当前硬件组合无法手动分流。"); + } + + [RelayCommand] + private async Task RetryHardwareSetup() + { + await ReturnToConfigurationAsync(); + } + + [RelayCommand(CanExecute = nameof(IsProMixAvailable))] + private async Task SwitchToProMix() + { + StopTestSound(); + await _deviceOperations.DeactivateAsync(); + + SelectedMode = WizardMode.Software; + _appSettings.Sync.EnableMixSync = true; + SelectedDriverType = WavePlayerType.WASAPI; + UpdateDeviceList(_allAudioDevices); + CheckVirtualDriver(); + CurrentAudioSubStep = AudioSubStep.Configuration; + } + + private void CompleteHardwareRouting(string title, string instruction) + { + StopTestSound(); + ValidationSuccess = true; + IsAudioConfigFinished = true; + ValidationMessage = title; + ValidationInstruction = instruction; + CurrentAudioSubStep = AudioSubStep.Validation; + } + + private async Task RequireProMixAsync(string message) + { + StopTestSound(); + await _deviceOperations.DeactivateAsync(); + ProMixRequiredMessage = message; + ValidationSuccess = false; + IsAudioConfigFinished = false; + CurrentAudioSubStep = AudioSubStep.ProMixRequired; + } + + private async Task ReturnToConfigurationAsync() + { + StopTestSound(); + IsValidationRunning = false; + ValidationSuccess = false; + IsAudioConfigFinished = false; + await _deviceOperations.DeactivateAsync(); + CurrentAudioSubStep = AudioSubStep.Configuration; + } + [RelayCommand] private void DownloadVirtualDriver() { @@ -285,24 +423,49 @@ private void RetryVirtualDriverCheck() } - private async void LoadDevices() + private async Task LoadDevicesAsync() { var devices = await _audioDeviceManager.GetCachedAvailableDevicesAsync(); + _allAudioDevices = devices; + HasAlternativeGameDevices = devices + .Where(device => device.WavePlayerType == WavePlayerType.WASAPI && device.DeviceId is not null) + .Select(device => device.DeviceId) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Count() > 1; UpdateDeviceList(devices); } partial void OnSelectedDriverTypeChanged(WavePlayerType value) { - LoadDevices(); + UpdateDeviceList(_allAudioDevices); } private void UpdateDeviceList(IReadOnlyList allDevices) { - var filtered = allDevices.Where(d => d.WavePlayerType == SelectedDriverType).ToList(); + var filtered = allDevices + .Where(device => device.WavePlayerType == SelectedDriverType) + .Where(device => SelectedMode != WizardMode.Hardware || + SelectedDriverType != WavePlayerType.WASAPI || + device.DeviceId is not null) + .Select(device => SelectedMode == WizardMode.Hardware && + device.WavePlayerType == WavePlayerType.WASAPI + ? device with { IsExclusive = true, Latency = 3 } + : device) + .ToList(); + AvailableAudioDevices = new ObservableCollection(filtered); - if (AvailableAudioDevices.Any()) + SelectedAudioDevice = AvailableAudioDevices.FirstOrDefault(); + + if (SelectedMode == WizardMode.Hardware) + { + ShowHardwareDriverWarning = AvailableAudioDevices.Count == 0; + HardwareDriverWarning = SelectedDriverType == WavePlayerType.ASIO + ? "未检测到 ASIO 驱动,请切换为 WASAPI 独占。" + : "未检测到可用的 WASAPI 播放设备,请尝试 ASIO 或改用 ProMix。"; + } + else { - SelectedAudioDevice = AvailableAudioDevices.First(); + ShowHardwareDriverWarning = false; } } @@ -321,7 +484,7 @@ private void CheckVirtualDriver() if (!IsVirtualDriverDetected) { ShowVirtualDriverWarning = true; - VirtualDriverWarning = "未检测到虚拟声卡驱动,建议安装以获得最佳体验"; + VirtualDriverWarning = "未检测到虚拟声卡,建议安装以获得完整体验"; } else { diff --git a/src/Apps/KeyAsio/ViewModels/WizardViewModel.cs b/src/Apps/KeyAsio/ViewModels/WizardViewModel.cs index 04c7ffc3..6b4ba661 100644 --- a/src/Apps/KeyAsio/ViewModels/WizardViewModel.cs +++ b/src/Apps/KeyAsio/ViewModels/WizardViewModel.cs @@ -235,7 +235,15 @@ private void UpdateNavigationState() if (WizardAudioConfigViewModel.CurrentAudioSubStep == AudioSubStep.Configuration) { prevText = SRKeys.Wizard_BackToSelection; - nextText = SRKeys.Wizard_ApplyAndTest; + nextText = WizardAudioConfigViewModel.SelectedMode == WizardMode.Hardware + ? "创建设备并试听" + : SRKeys.Wizard_ApplyAndTest; + } + else if (WizardAudioConfigViewModel.CurrentAudioSubStep is AudioSubStep.ConcurrencyCheck + or AudioSubStep.AlternativeDeviceCheck + or AudioSubStep.ProMixRequired) + { + prevText = "重新选择设备"; } else if (WizardAudioConfigViewModel.CurrentAudioSubStep == AudioSubStep.Validation) { @@ -309,6 +317,8 @@ private void Skip() private void Finish() { + WizardAudioConfigViewModel.StopTestSound(); + // Save settings AppSettings.Logging.EnableErrorReporting = EnableCrashReport; // _appSettings.Update.EnableAutoUpdate = EnableUpdates; // TODO: will be added diff --git a/src/Apps/KeyAsio/Views/Controls/WizardAudioConfigView.axaml b/src/Apps/KeyAsio/Views/Controls/WizardAudioConfigView.axaml index ef908365..fbe783dc 100644 --- a/src/Apps/KeyAsio/Views/Controls/WizardAudioConfigView.axaml +++ b/src/Apps/KeyAsio/Views/Controls/WizardAudioConfigView.axaml @@ -3,11 +3,18 @@ xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="using:KeyAsio.Views.Controls" + xmlns:converters="using:KeyAsio.Converters" xmlns:lang="clr-namespace:KeyAsio.Lang" xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:suki="https://github.com/kikipoulet/SukiUI" xmlns:vm="using:KeyAsio.ViewModels" x:DataType="vm:WizardAudioConfigViewModel"> + + + + Text="音频配置:选择模式" /> - + Software @@ -49,19 +60,46 @@ + + CornerRadius="5" + IsVisible="{Binding IsProMixAvailable}"> + Text="含 ProMix 试用" /> + + + + + + @@ -94,7 +132,7 @@ @@ -106,101 +144,114 @@ + Spacing="12"> + Text="手动模式:选择设备" /> + + + + + + + + + + - + Margin="16" + ColumnDefinitions="Auto,*,220" + ColumnSpacing="12"> + Width="22" + Height="22" + Foreground="{DynamicResource SukiPrimaryColor}" + Kind="Tune" /> - + Spacing="2"> + + FontSize="12" + Opacity="0.6" + Text="ASIO 或 WASAPI 独占" /> - + ItemsSource="{Binding AvailableDriverTypes}" + SelectedItem="{Binding SelectedDriverType}" /> - - + - + Spacing="2"> + + Text="KeyASIO 将以此设备独占播放" /> @@ -211,43 +262,214 @@ - + + + + + - - + + + + + + + + + + + + + CornerRadius="22" + Opacity="0.15" /> + Kind="VolumeHigh" /> - - + Spacing="2"> + + Opacity="0.65" + Text="KeyASIO 已独占所选设备" /> + + + + + + + + + + + + +