From 6854b13ca655995802eea2e63e624950816e930a Mon Sep 17 00:00:00 2001 From: Milkitic Date: Wed, 15 Jul 2026 23:06:58 +0800 Subject: [PATCH 01/13] Update MyYamlConfigurationConverter.cs --- .../Serialization/MyYamlConfigurationConverter.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Common/KeyAsio.Configuration/Serialization/MyYamlConfigurationConverter.cs b/src/Common/KeyAsio.Configuration/Serialization/MyYamlConfigurationConverter.cs index 1a73faa7..ceea1a25 100644 --- a/src/Common/KeyAsio.Configuration/Serialization/MyYamlConfigurationConverter.cs +++ b/src/Common/KeyAsio.Configuration/Serialization/MyYamlConfigurationConverter.cs @@ -49,6 +49,10 @@ public override object DeserializeSettings(string content, content, "(?im)^(\\s*limiterType\\s*:\\s*)[\"']?Polynomial[\"']?(\\s*(?:#.*)?)$", "$1Soft$2"); + content = Regex.Replace( + content, + "(?im)^(\\s*limiterType\\s*:\\s*)[\"']?Master[\"']?(\\s*(?:#.*)?)$", + "$1Peak$2"); if (!content.StartsWith("default:") && !LooksLikeNewYaml(content)) { From 980f03f98458d7482a9f2c8de89cad4bfc4c5ead Mon Sep 17 00:00:00 2001 From: Milkitic Date: Wed, 15 Jul 2026 23:07:11 +0800 Subject: [PATCH 02/13] Update MainWindow.axaml.cs --- src/Apps/KeyAsio/Views/MainWindow.axaml.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Apps/KeyAsio/Views/MainWindow.axaml.cs b/src/Apps/KeyAsio/Views/MainWindow.axaml.cs index c9dc98bf..006b86db 100644 --- a/src/Apps/KeyAsio/Views/MainWindow.axaml.cs +++ b/src/Apps/KeyAsio/Views/MainWindow.axaml.cs @@ -106,14 +106,14 @@ private async void Control_OnLoaded(object? sender, RoutedEventArgs e) _viewModel.SettingsPageItem = SettingsMenuItem; _viewModel.AudioEnginePageItem = AudioEngineMenuItem; - //_viewModel.RequestShowWizard += async () => await ShowWizardAsync(); + _viewModel.RequestShowWizard += async () => await ShowWizardAsync(); await Dispatcher.UIThread.InvokeAsync(() => UpdateThemeByDevice(null)); - //if (_viewModel.AppSettings.General.IsFirstRun) - //{ - // await ShowWizardAsync(); - //} + if (_viewModel.AppSettings.General.IsFirstRun) + { + await ShowWizardAsync(); + } InitializeStartupLogic(); } From 4eda45a1f814a51efa30bebacbbbbb68c33d57fb Mon Sep 17 00:00:00 2001 From: Milkitic Date: Fri, 17 Jul 2026 13:13:55 +0800 Subject: [PATCH 03/13] =?UTF-8?q?feat:=20=E6=A0=B9=E6=8D=AEProMix=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E5=8F=AF=E7=94=A8=E6=80=A7=E6=8E=A7=E5=88=B6=E8=BD=AF?= =?UTF-8?q?=E4=BB=B6=E6=B7=B7=E9=9F=B3=E6=A8=A1=E5=BC=8F=E9=80=89=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在音频配置向导中,软件混音模式依赖于ProMix插件。通过IPluginManager检测插件是否加载,若未加载则禁用该选项并显示提示,避免用户选择不可用的模式。同时添加了对应的单元测试验证。 --- .../ViewModels/WizardAudioConfigViewModel.cs | 14 +++++- .../Controls/WizardAudioConfigView.axaml | 43 +++++++++++++++++-- .../WizardAudioConfigViewModelTests.cs | 23 +++++++++- 3 files changed, 74 insertions(+), 6 deletions(-) diff --git a/src/Apps/KeyAsio/ViewModels/WizardAudioConfigViewModel.cs b/src/Apps/KeyAsio/ViewModels/WizardAudioConfigViewModel.cs index 64b622b4..e6ca970f 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; @@ -26,6 +27,8 @@ public enum AudioSubStep public partial class WizardAudioConfigViewModel : ViewModelBase { + private const string ProMixPluginId = "KeyAsio.Plugins.ProMix"; + private readonly IAudioDeviceManager _audioDeviceManager; private readonly IAudioDeviceOperationCoordinator _deviceOperations; private readonly ISukiToastManager _toastManager; @@ -35,12 +38,15 @@ public WizardAudioConfigViewModel( IAudioDeviceManager audioDeviceManager, IAudioDeviceOperationCoordinator deviceOperations, ISukiToastManager toastManager, - AppSettings appSettings) + AppSettings appSettings, + IPluginManager pluginManager) { _audioDeviceManager = audioDeviceManager; _deviceOperations = deviceOperations; _toastManager = toastManager; _appSettings = appSettings; + IsProMixAvailable = pluginManager.GetAllPlugins() + .Any(plugin => string.Equals(plugin.Id, ProMixPluginId, StringComparison.Ordinal)); AvailableDriverTypes = new ObservableCollection(Enum.GetValues()); SelectedDriverType = WavePlayerType.ASIO; @@ -48,6 +54,8 @@ public WizardAudioConfigViewModel( LoadDevices(); } + public bool IsProMixAvailable { get; } + [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsHardwareConfig))] [NotifyPropertyChangedFor(nameof(IsSoftwareConfig))] @@ -179,7 +187,9 @@ public bool CanGoForward [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) { SelectedMode = mode; diff --git a/src/Apps/KeyAsio/Views/Controls/WizardAudioConfigView.axaml b/src/Apps/KeyAsio/Views/Controls/WizardAudioConfigView.axaml index ef908365..6162dc62 100644 --- a/src/Apps/KeyAsio/Views/Controls/WizardAudioConfigView.axaml +++ b/src/Apps/KeyAsio/Views/Controls/WizardAudioConfigView.axaml @@ -8,6 +8,12 @@ xmlns:suki="https://github.com/kikipoulet/SukiUI" xmlns:vm="using:KeyAsio.ViewModels" x:DataType="vm:WizardAudioConfigViewModel"> + + + - + Software @@ -49,20 +59,47 @@ + + CornerRadius="5" + IsVisible="{Binding IsProMixAvailable}"> + + + + + + @@ -477,4 +514,4 @@ - \ No newline at end of file + diff --git a/tests/KeyAsio.UnitTests/WizardAudioConfigViewModelTests.cs b/tests/KeyAsio.UnitTests/WizardAudioConfigViewModelTests.cs index 779d3b52..58f8bb2f 100644 --- a/tests/KeyAsio.UnitTests/WizardAudioConfigViewModelTests.cs +++ b/tests/KeyAsio.UnitTests/WizardAudioConfigViewModelTests.cs @@ -3,6 +3,7 @@ using KeyAsio.Core.Audio; using KeyAsio.Configuration; using KeyAsio.Services; +using KeyAsio.Plugins.Contracts; using KeyAsio.ViewModels; using Moq; using SukiUI.Toasts; @@ -14,6 +15,7 @@ public class WizardAudioConfigViewModelTests private readonly Mock _mockDeviceManager; private readonly Mock _mockDeviceOperations; private readonly Mock _mockToastManager; + private readonly Mock _mockPluginManager; private readonly AppSettings _appSettings; public WizardAudioConfigViewModelTests() @@ -21,8 +23,13 @@ public WizardAudioConfigViewModelTests() _mockDeviceManager = new Mock(); _mockDeviceOperations = new Mock(); _mockToastManager = new Mock(); + _mockPluginManager = new Mock(); _appSettings = new AppSettings(); + var proMixPlugin = new Mock(); + proMixPlugin.SetupGet(plugin => plugin.Id).Returns("KeyAsio.Plugins.ProMix"); + _mockPluginManager.Setup(manager => manager.GetAllPlugins()).Returns([proMixPlugin.Object]); + // Default setup for device manager _mockDeviceManager.Setup(m => m.GetCachedAvailableDevicesAsync()) .ReturnsAsync(new List()); @@ -41,7 +48,8 @@ private WizardAudioConfigViewModel CreateViewModel() _mockDeviceManager.Object, _mockDeviceOperations.Object, _mockToastManager.Object, - _appSettings); + _appSettings, + _mockPluginManager.Object); } [AvaloniaFact] @@ -52,6 +60,19 @@ public void Constructor_InitializesDefaults() Assert.Equal(AudioSubStep.Selection, vm.CurrentAudioSubStep); Assert.Equal(WavePlayerType.ASIO, vm.SelectedDriverType); Assert.Empty(vm.AvailableAudioDevices); + Assert.True(vm.IsProMixAvailable); + } + + [AvaloniaFact] + public void ProMixOption_IsDisabled_WhenPluginIsNotLoaded() + { + _mockPluginManager.Setup(manager => manager.GetAllPlugins()).Returns([]); + + var vm = CreateViewModel(); + + Assert.False(vm.IsProMixAvailable); + Assert.False(vm.SelectModeCommand.CanExecute(WizardMode.Software)); + Assert.True(vm.SelectModeCommand.CanExecute(WizardMode.Hardware)); } [AvaloniaFact] From af9b7c59a377a12488596bebeb58bc9b93f25c29 Mon Sep 17 00:00:00 2001 From: Milkitic Date: Fri, 17 Jul 2026 13:48:55 +0800 Subject: [PATCH 04/13] =?UTF-8?q?feat:=20=E7=A7=BB=E9=99=A4=E6=A0=87?= =?UTF-8?q?=E5=87=86=E9=A2=84=E8=AE=BE=E6=A8=A1=E5=BC=8F=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E9=A2=84=E8=AE=BE=E9=80=89=E6=8B=A9=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将预设模式从三种精简为两种(高效/极致),移除冗余的"标准均衡"模式。更新高效模式描述以更准确反映其定位,并调整预设选择对话框布局为两列。 --- src/Apps/KeyAsio/Lang/SR.Designer.cs | 20 +-------- src/Apps/KeyAsio/Lang/SR.en.resx | 8 +--- src/Apps/KeyAsio/Lang/SR.ja.resx | 6 --- src/Apps/KeyAsio/Lang/SR.resx | 8 +--- src/Apps/KeyAsio/Lang/SR.zh-hans.resx | 8 +--- src/Apps/KeyAsio/Services/PresetManager.cs | 41 ++----------------- .../Views/Dialogs/PresetSelectionDialog.axaml | 2 +- .../Views/Dialogs/WizardDialogView.axaml | 4 +- 8 files changed, 11 insertions(+), 86 deletions(-) diff --git a/src/Apps/KeyAsio/Lang/SR.Designer.cs b/src/Apps/KeyAsio/Lang/SR.Designer.cs index 70150d8e..d151c41c 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 { @@ -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 的本地化字符串。 /// diff --git a/src/Apps/KeyAsio/Lang/SR.en.resx b/src/Apps/KeyAsio/Lang/SR.en.resx index e98b72f0..abf84a37 100644 --- a/src/Apps/KeyAsio/Lang/SR.en.resx +++ b/src/Apps/KeyAsio/Lang/SR.en.resx @@ -404,7 +404,7 @@ 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. @@ -412,12 +412,6 @@ You can change option later. Select Preset Mode - - Balanced - - - Balances resource usage to deliver the best listening experience, suitable for most gameplay scenarios. - About diff --git a/src/Apps/KeyAsio/Lang/SR.ja.resx b/src/Apps/KeyAsio/Lang/SR.ja.resx index 2b1cdfbc..8977974e 100644 --- a/src/Apps/KeyAsio/Lang/SR.ja.resx +++ b/src/Apps/KeyAsio/Lang/SR.ja.resx @@ -413,12 +413,6 @@ プリセットモードを選択 - - 標準バランス - - - リソース使用をバランスさせ、最適な聴覚体験を提供し、ほとんどのプレイ場面に適しています。 - 概要 diff --git a/src/Apps/KeyAsio/Lang/SR.resx b/src/Apps/KeyAsio/Lang/SR.resx index 4c825e30..b90f6898 100644 --- a/src/Apps/KeyAsio/Lang/SR.resx +++ b/src/Apps/KeyAsio/Lang/SR.resx @@ -410,7 +410,7 @@ 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. @@ -418,12 +418,6 @@ You can change option later. Select Preset Mode - - Balanced - - - Balances resource usage to deliver the best listening experience, suitable for most gameplay scenarios. - About diff --git a/src/Apps/KeyAsio/Lang/SR.zh-hans.resx b/src/Apps/KeyAsio/Lang/SR.zh-hans.resx index 6b7f852c..a66122de 100644 --- a/src/Apps/KeyAsio/Lang/SR.zh-hans.resx +++ b/src/Apps/KeyAsio/Lang/SR.zh-hans.resx @@ -411,7 +411,7 @@ 高效轻量 - 简化系统开销,适合低配设备或基本游玩需求。 + 标准资源占用,提供最佳听觉体验,适合大多数游玩场景。 请根据您的设备性能和使用需求选择合适的模式。 @@ -419,12 +419,6 @@ 选择预设模式 - - 标准均衡 - - - 均衡资源占用,提供最佳听觉体验,适合大多数游玩场景。 - 关于 diff --git a/src/Apps/KeyAsio/Services/PresetManager.cs b/src/Apps/KeyAsio/Services/PresetManager.cs index 0a68edfc..84999c5c 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, @@ -90,15 +82,6 @@ public void Initialize() 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 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/Views/Dialogs/PresetSelectionDialog.axaml b/src/Apps/KeyAsio/Views/Dialogs/PresetSelectionDialog.axaml index bcf5d71b..4c361ac1 100644 --- a/src/Apps/KeyAsio/Views/Dialogs/PresetSelectionDialog.axaml +++ b/src/Apps/KeyAsio/Views/Dialogs/PresetSelectionDialog.axaml @@ -61,7 +61,7 @@ diff --git a/src/Apps/KeyAsio/Views/Dialogs/WizardDialogView.axaml b/src/Apps/KeyAsio/Views/Dialogs/WizardDialogView.axaml index 8fa70503..e39371ca 100644 --- a/src/Apps/KeyAsio/Views/Dialogs/WizardDialogView.axaml +++ b/src/Apps/KeyAsio/Views/Dialogs/WizardDialogView.axaml @@ -1,4 +1,4 @@ - From 132a5a9b0c0d7a8bb04e6c0368a819c55cf61855 Mon Sep 17 00:00:00 2001 From: Milkitic Date: Fri, 17 Jul 2026 17:27:50 +0800 Subject: [PATCH 05/13] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E7=A1=AC?= =?UTF-8?q?=E4=BB=B6=E6=A8=A1=E5=BC=8F=E9=85=8D=E7=BD=AE=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=B9=B6=E5=8F=91=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E4=B8=8E=E8=AE=BE=E5=A4=87=E5=88=86=E6=B5=81=E5=BC=95=E5=AF=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为硬件模式引入分步配置流程:创建独占设备后播放测试音,引导用户验证声卡并发能力;若不支持并发则引导选择其他播放设备或切换到ProMix模式。同时优化设备列表过滤逻辑,支持WASAPI独占模式选择。 --- .../KeyAsio/DependencyInjectionExtensions.cs | 1 + .../Services/WizardTestSoundService.cs | 106 +++++++ .../ViewModels/WizardAudioConfigViewModel.cs | 221 +++++++++++-- .../KeyAsio/ViewModels/WizardViewModel.cs | 12 +- .../Controls/WizardAudioConfigView.axaml | 300 +++++++++++++----- .../WizardAudioConfigViewModelTests.cs | 120 ++++++- 6 files changed, 634 insertions(+), 126 deletions(-) create mode 100644 src/Apps/KeyAsio/Services/WizardTestSoundService.cs 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/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 e6ca970f..5a51cddd 100644 --- a/src/Apps/KeyAsio/ViewModels/WizardAudioConfigViewModel.cs +++ b/src/Apps/KeyAsio/ViewModels/WizardAudioConfigViewModel.cs @@ -22,6 +22,9 @@ public enum AudioSubStep { Selection, Configuration, + ConcurrencyCheck, + AlternativeDeviceCheck, + ProMixRequired, Validation } @@ -31,27 +34,31 @@ public partial class WizardAudioConfigViewModel : ViewModelBase 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, - IPluginManager pluginManager) + 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; } @@ -60,6 +67,7 @@ public WizardAudioConfigViewModel( [NotifyPropertyChangedFor(nameof(IsHardwareConfig))] [NotifyPropertyChangedFor(nameof(IsSoftwareConfig))] [NotifyPropertyChangedFor(nameof(IsHardwareMode))] + [NotifyPropertyChangedFor(nameof(CanGoForward))] public partial WizardMode SelectedMode { get; set; } = WizardMode.NotSelected; // Config Page @@ -86,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; @@ -98,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; @@ -121,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() { @@ -130,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; } @@ -181,6 +210,12 @@ public bool CanGoForward } } + public void StopTestSound() + { + _testSoundService.Stop(); + IsConcurrencyTestSoundPlaying = false; + } + [ObservableProperty] public partial bool ShowVirtualDriverWarning { get; set; } @@ -192,33 +227,20 @@ public bool CanGoForward [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; @@ -227,16 +249,18 @@ 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 = "正在初始化音频引擎..."; @@ -252,9 +276,29 @@ 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 { @@ -266,6 +310,90 @@ private async Task ApplyAndTestConfig() IsValidationRunning = false; } + [RelayCommand] + private void ConfirmSameDeviceAudio() + { + CompleteHardwareRouting( + "设备支持并发", + "保持 osu! 使用这块声卡对应的同一播放设备,在选图页确认音乐自动播放,再按键确认软件音效正常。"); + } + + [RelayCommand] + private async Task ReportSameDeviceSilent() + { + if (HasAlternativeGameDevices) + { + CurrentAudioSubStep = AudioSubStep.AlternativeDeviceCheck; + return; + } + + await RequireProMixAsync("系统只检测到一个物理播放设备,而且它不支持 ASIO/WASAPI 并发。"); + } + + [RelayCommand] + private void ConfirmAlternativeDeviceAudio() + { + CompleteHardwareRouting( + "设备分流配置完成", + "保持 osu! 使用刚才确认有声音的其他播放设备,在选图页确认音乐自动播放,再按键确认软件音效正常。"); + } + + [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() { @@ -295,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; } } diff --git a/src/Apps/KeyAsio/ViewModels/WizardViewModel.cs b/src/Apps/KeyAsio/ViewModels/WizardViewModel.cs index 04c7ffc3..a5dd627f 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 6162dc62..1a9f13ff 100644 --- a/src/Apps/KeyAsio/Views/Controls/WizardAudioConfigView.axaml +++ b/src/Apps/KeyAsio/Views/Controls/WizardAudioConfigView.axaml @@ -143,101 +143,99 @@ + Spacing="12"> + Text="手动模式 · 选择独占设备" /> + + + + + + + + + + - - + + 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}" /> - + - - + - - + + + Text="WASAPI 设备会以独占模式创建" /> @@ -248,43 +246,177 @@ - + + + + + - - + + + + + + + + + + + + + Width="44" + Height="44" + Background="{DynamicResource SukiSuccessColor}" + CornerRadius="22" + Opacity="0.15" /> + Foreground="{DynamicResource SukiSuccessColor}" + Kind="VolumeHigh" /> - - - + + + Opacity="0.65" + Text="KeyASIO 已独占所选设备;离开此步骤后测试音会自动停止。" /> + + + + + + + + + + +