Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
c3c73fe
refactor: add layered shared components alongside legacy project
Milkitic Jul 11, 2026
146e547
refactor: coordinate audio settings transitions transactionally
Milkitic Jul 11, 2026
bf78583
refactor: migrate applications to plugin contracts and sync sources
Milkitic Jul 11, 2026
9bed2c6
refactor: add .editorconfig for consistent coding style
Milkitic Jul 11, 2026
2995d73
refactor: standardize package references by removing version numbers
Milkitic Jul 11, 2026
ea3595f
refactor: add CI workflow for build and test processes
Milkitic Jul 11, 2026
b3e3b03
refactor: add architecture documentation outlining project boundaries…
Milkitic Jul 11, 2026
7371c4a
Retry StopDevice; make OutputWaveFormat nullable
Milkitic Jul 12, 2026
b05e434
Add NeedsShutdown flag and robust plugin shutdown
Milkitic Jul 12, 2026
586eed5
Refactor file layout, clean up usings and small fixes
Milkitic Jul 13, 2026
6208196
Add pause/resume for loop providers
Milkitic Jul 13, 2026
13c096f
fix: 修正持久化 ClientType 过时问题并更新子模块
Milkitic Jul 14, 2026
def81d9
fix: 调整BalanceFactor默认值为0.2
Milkitic Jul 14, 2026
815e415
fix: 修正ProfessionalBalanceProvider的平衡算法
Milkitic Jul 14, 2026
6fb2c9b
feat: 将 Mid-Side 平衡模式重命名为 ProMix Focus
Milkitic Jul 14, 2026
8535e53
feat: 重构平衡模式,新增线性立体声像并移除旧模式
Milkitic Jul 14, 2026
fc582d4
refactor: 移除 AntiClipStrategy 防削波策略,统一由 MasterLimiterProvider 处理
Milkitic Jul 14, 2026
28f72d1
feat: 移除键盘绑定初始化器中不必要的同步会话暂停检查
Milkitic Jul 14, 2026
ee1a43e
feat: 按客户端类型分离皮肤选择字段
Milkitic Jul 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
root = true

[*]
insert_final_newline = true
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: ci

on:
push:
branches: [master]
pull_request:

permissions:
contents: read

jobs:
build-and-test:
runs-on: windows-latest

steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive

- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x

- name: Restore
run: dotnet restore KeyAsio.Net.slnx

- name: Build
run: dotnet build KeyAsio.Net.slnx --configuration Release --no-restore

- name: Test application behavior
run: dotnet test tests/KeyAsio.UnitTests/KeyAsio.UnitTests.csproj --configuration Release --no-build

- name: Test audio primitives
run: dotnet test tests/AudioTests/AudioTests.csproj --configuration Release --no-build
5 changes: 5 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ jobs:
if ($env:OFFICIAL_PRIVATE_KEY) {
Set-Content -Path "src/Apps/KeyAsio/keyasio_private.key" -Value $env:OFFICIAL_PRIVATE_KEY
}

- name: Verify release candidate
run: |
dotnet test tests/KeyAsio.UnitTests/KeyAsio.UnitTests.csproj --configuration Release
dotnet test tests/AudioTests/AudioTests.csproj --configuration Release

- name: Publish
run: |
Expand Down
56 changes: 56 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Antelcat.I18N.SourceGenerators" Version="1.1.0" />
<PackageVersion Include="Avalonia" Version="12.1.0" />
<PackageVersion Include="Avalonia.Desktop" Version="12.1.0" />
<PackageVersion Include="Avalonia.Fonts.Inter" Version="12.1.0" />
<PackageVersion Include="Avalonia.Headless.XUnit" Version="12.1.0" />
<PackageVersion Include="Avalonia.Skia" Version="12.1.0" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.1.0" />
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
<PackageVersion Include="BenchmarkDotNet" Version="0.15.6" />
<PackageVersion Include="Blake3" Version="2.2.1" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageVersion Include="Coosu.Beatmap" Version="2.5.1" />
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
<PackageVersion Include="dnlib" Version="4.5.0" />
<PackageVersion Include="FastExpressionCompiler" Version="5.4.1" />
<PackageVersion Include="Fody" Version="6.9.3" />
<PackageVersion Include="LiveChartsCore.SkiaSharpView.Avalonia" Version="2.0.5" />
<PackageVersion Include="Material.Icons.Avalonia" Version="3.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.9" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.298" />
<PackageVersion Include="MinVer" Version="7.0.0" />
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="NAudio.Core" Version="2.3.0" />
<PackageVersion Include="NAudio.Lame" Version="2.1.0" />
<PackageVersion Include="NAudio.Vorbis" Version="1.5.0" />
<PackageVersion Include="NAudio.Wasapi" Version="2.3.0" />
<PackageVersion Include="NAudio.WinMM" Version="2.3.0" />
<PackageVersion Include="NLog.Extensions.Logging" Version="6.1.4" />
<PackageVersion Include="Octokit" Version="14.0.0" />
<PackageVersion Include="PropertyChanged.Fody" Version="4.1.0" />
<PackageVersion Include="Reloaded.Memory.Sigscan" Version="3.1.9" />
<PackageVersion Include="Semver" Version="3.0.0" />
<PackageVersion Include="Sentry" Version="6.6.0" />
<PackageVersion Include="Sentry.Extensions.Logging" Version="6.6.0" />
<PackageVersion Include="SmoothScroll.Avalonia" Version="12.0.0.12" />
<PackageVersion Include="SukiUI" Version="7.0.1" />
<PackageVersion Include="Svg.Controls.Skia.Avalonia" Version="12.0.0.13" />
<PackageVersion Include="System.Management" Version="10.0.9" />
<PackageVersion Include="System.Numerics.Tensors" Version="10.0.9" />
<PackageVersion Include="YamlDotNet" Version="18.1.0" />
<PackageVersion Include="z440.atl.core" Version="7.15.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
</ItemGroup>
</Project>
8 changes: 6 additions & 2 deletions KeyAsio.Net.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<Project Path="benchmarks\SimdBenchmarks\SimdBenchmarks.csproj" />
</Folder>
<Folder Name="/Dependencies/">
<Project Path="dependencies/osu.Game.Rulesets.KeyAsio/KeyAsio.LazerProtocol/KeyAsio.LazerProtocol.csproj" />
<Project Path="dependencies\Milki.Extensions\src\Milki.Extensions.Configuration\Milki.Extensions.Configuration.csproj" />
<Project Path="dependencies\Milki.Extensions\src\Milki.Extensions.MouseKeyHook\Milki.Extensions.MouseKeyHook.csproj" />
<Project Path="dependencies\Milki.Extensions\src\Milki.Extensions.Threading\Milki.Extensions.Threading.csproj" />
Expand All @@ -19,10 +20,13 @@
<Project Path="tests\PlayingTests\PlayingTests.csproj" />
</Folder>
<Project Path="src\Apps\KeyAsio\KeyAsio.csproj" />
<Project Path="src\Common\KeyAsio.Plugins.Abstractions/KeyAsio.Plugins.Abstractions.csproj" />
<Project Path="src\Common\KeyAsio.Application\KeyAsio.Application.csproj" />
<Project Path="src\Common\KeyAsio.Common\KeyAsio.Common.csproj" />
<Project Path="src\Common\KeyAsio.Plugins.Contracts/KeyAsio.Plugins.Contracts.csproj" />
<Project Path="src\Common\KeyAsio.Secrets\KeyAsio.Secrets.csproj" />
<Project Path="src\Common\KeyAsio.Shared\KeyAsio.Shared.csproj" />
<Project Path="src\Common\KeyAsio.Configuration\KeyAsio.Configuration.csproj" />
<Project Path="src\Core\KeyAsio.Core.Audio\KeyAsio.Core.Audio.csproj" />
<Project Path="src\Core\KeyAsio.Core.Memory\KeyAsio.Core.Memory.csproj" />
<Project Path="src\Core\KeyAsio.Core.OsuAudio\KeyAsio.Core.OsuAudio.csproj" />
<Project Path="src\Core\KeyAsio.Sync\KeyAsio.Sync.csproj" />
</Solution>
4 changes: 2 additions & 2 deletions benchmarks/SimdBenchmarks/SimdBenchmarks.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
Expand All @@ -8,7 +8,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.6" />
<PackageReference Include="BenchmarkDotNet" />
</ItemGroup>

<ItemGroup>
Expand Down
6 changes: 6 additions & 0 deletions dependencies/Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Project>
<!-- Third-party submodules keep ownership of their own package versions. -->
<PropertyGroup>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
</PropertyGroup>
</Project>
46 changes: 46 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Architecture

The solution uses the application executable as the composition root. Lower layers do not resolve services from an
`IServiceProvider`; dependencies are supplied through constructors or narrow capability interfaces.

## Project boundaries

- `KeyAsio.Common` contains implementation-independent utilities and collections.
- `KeyAsio.Plugins.Contracts` is the only API surface plugins should compile against. It depends only on stable public
data/audio packages and never exposes the host service container.
- `KeyAsio.Configuration` owns persisted settings and their serialization.
- `KeyAsio.Core.*` owns audio, memory and beatmap-audio primitives.
- `KeyAsio.Sync` owns game-state acquisition and synchronized playback orchestration.
- `KeyAsio.Application` owns host runtime services, plugin loading, localization and skin discovery.
- `KeyAsio` owns Avalonia/Suki presentation, OS integration and dependency injection composition.

Dependencies point inward toward contracts and primitives. In particular, `KeyAsio.Sync` has no dependency on
`KeyAsio.Application`, and neither plugin contracts nor application services depend on Avalonia/Suki.

## Audio device transactions

All application-level device changes go through `IAudioDeviceOperationCoordinator`. A transition is serialized,
stops the current device, starts the requested device, and only then commits settings. A failed start or persistence
operation restores both the previous runtime device and the previous persisted configuration. Cache invalidation is a
post-transition concern and cannot corrupt the transaction.

## Plugin contract migration

The contract is intentionally breaking. A plugin project should reference `KeyAsio.Plugins.Contracts` and must not
reference `KeyAsio.Application`, `KeyAsio.Configuration`, `KeyAsio.Sync`, Avalonia host services, Suki managers or
Octokit.

| Previous dependency | Contract capability |
| --- | --- |
| Root `IServiceProvider` | Explicit properties on `IPluginContext` |
| `AppSettings` | `IPluginSettings` |
| `GameplaySessionManager` / `SyncSessionContext` | `IGameplaySession` |
| `AudioCacheManager` / `CachedAudio` | `IGameplaySession.TryCreateCachedAudioProvider` |
| Beatmap resource catalog implementation | `TryResolveResource` / `TryResolveAudioResource` |
| Dispatcher, dialog and toast managers | `IPluginInteractionService` |
| Logger resolved from DI | `IPluginContext.LoggerFactory` |
| `Octokit.Release` | `UpdateRelease` and `UpdateAsset` |

The host shares already-loaded assemblies with each collectible plugin load context and resolves plugin-private managed
and native dependencies from the plugin output directory. This preserves type identity for the contract while keeping
plugin-only dependencies unloadable.
23 changes: 10 additions & 13 deletions src/Apps/KeyAsio/App.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
using Avalonia;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using KeyAsio.Core.Audio;
using KeyAsio.Plugins.Abstractions;
using KeyAsio.Plugins.Contracts;
using KeyAsio.Services;
using KeyAsio.Shared;
using KeyAsio.Shared.Localization;
using KeyAsio.Shared.OsuMemory;
using KeyAsio.Shared.Plugins;
using KeyAsio.Shared.Services;
using KeyAsio.Shared.Sync;
using KeyAsio.Configuration;
using KeyAsio.Application.Localization;
using KeyAsio.Sync.Sources;
using KeyAsio.Application.Plugins;
using KeyAsio.Application.Services;
using KeyAsio.Sync;
using KeyAsio.Views;
using Microsoft.Extensions.DependencyInjection;

namespace KeyAsio;

public partial class App : Application
public partial class App : Avalonia.Application
{
public override void Initialize()
{
Expand All @@ -25,8 +25,6 @@ public override void Initialize()
public override void OnFrameworkInitializationCompleted()
{
//I18NExtension.Culture = new CultureInfo("en-US");
UiDispatcher.SetUiSynchronizationContext();

LocalizationService.Instance.ConfigureStringResolver(static key => KeyAsio.Lang.SR.ResourceManager.GetString(key) ?? key);
LocalizationService.Instance.ConfigureCultureApplier(static culture => KeyAsio.Lang.SR.Culture = culture);

Expand All @@ -47,7 +45,6 @@ public override void OnFrameworkInitializationCompleted()
keyboardBindingInitializer.RegisterAllKeys();

var pluginManager = services.GetRequiredService<IPluginManager>();
var playbackEngine = services.GetRequiredService<IPlaybackEngine>();

// InternalPlugins
pluginManager.LoadPlugins(AppDomain.CurrentDomain.BaseDirectory, "KeyAsio.Plugins.*.dll",
Expand All @@ -57,7 +54,7 @@ public override void OnFrameworkInitializationCompleted()
// var pluginDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins");
// pluginManager.LoadPlugins(pluginDir, "*.dll", SearchOption.AllDirectories);

pluginManager.InitializePlugins(new AudioEngineWrapper(playbackEngine));
pluginManager.InitializePlugins();
pluginManager.StartupPlugins();

var syncController = services.GetRequiredService<SyncController>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
using System.Runtime.CompilerServices;
using Avalonia.Metadata;

[assembly: XmlnsDefinition("https://github.com/avaloniaui", "KeyAsio.Shared.Localization")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "KeyAsio.Shared.Lang")]

[assembly: InternalsVisibleTo("ColumnTest")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "KeyAsio.Application.Localization")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "KeyAsio.Lang")]
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using KeyAsio.Plugins.Abstractions.OsuMemory;
using KeyAsio.Plugins.Contracts.Sync;

namespace KeyAsio.Converters;

Expand Down
4 changes: 2 additions & 2 deletions src/Apps/KeyAsio/Converters/StringColorToBrushConverter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Globalization;
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data.Converters;
Expand Down Expand Up @@ -28,7 +28,7 @@ public class StringColorToBrushConverter : IValueConverter
return new SolidColorBrush(color, opacity);
}

if (Application.Current?.TryFindResource(colorStr, Application.Current?.ActualThemeVariant, out var res) != true) return null;
if (Avalonia.Application.Current?.TryFindResource(colorStr, Avalonia.Application.Current?.ActualThemeVariant, out var res) != true) return null;
{
switch (res)
{
Expand Down
35 changes: 31 additions & 4 deletions src/Apps/KeyAsio/DependencyInjectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
using KeyAsio.Services;
using KeyAsio.Services;
using KeyAsio.Services.Localization;
using KeyAsio.Shared.Localization;
using KeyAsio.Shared.Models;
using KeyAsio.Application.Localization;
using KeyAsio.Application.Models;
using KeyAsio.Application.Abstractions;
using KeyAsio.Application.Plugins;
using KeyAsio.Application.Services;
using KeyAsio.Configuration;
using KeyAsio.Plugins.Contracts;
using KeyAsio.Sync.Abstractions;
using KeyAsio.ViewModels;
using KeyAsio.ViewModels.Dialogs;
using KeyAsio.Views;
Expand All @@ -15,14 +21,35 @@ public static class DependencyInjectionExtensions
{
public static IServiceCollection AddGuiModule(this IServiceCollection services)
{
services.AddSingleton<SharedViewModel>();
services.AddSingleton<ApplicationState>();
services.AddSingleton<IAppSettingsPersistence, AppSettingsPersistence>();
services.AddSingleton<IPlaybackRuntimeState>(static provider =>
provider.GetRequiredService<ApplicationState>());

services.AddSingleton<IApplicationDispatcher, AvaloniaApplicationDispatcher>();
services.AddSingleton<SkinManager>();
services.AddSingleton<ISkinResourceProvider>(static provider =>
provider.GetRequiredService<SkinManager>());

services.AddSingleton<ISukiDialogManager, SukiDialogManager>();
services.AddSingleton<ISukiToastManager, SafeSukiToastManager>();
services.AddSingleton<IPluginInteractionService, PluginInteractionService>();

services.AddSingleton<AudioEngineWrapper>();
services.AddSingleton<IAudioEngine>(static provider =>
provider.GetRequiredService<AudioEngineWrapper>());
services.AddSingleton<PluginSettingsAdapter>();
services.AddSingleton<IPluginSettings>(static provider =>
provider.GetRequiredService<PluginSettingsAdapter>());
services.AddSingleton<GameplaySessionAdapter>();
services.AddSingleton<IGameplaySession>(static provider =>
provider.GetRequiredService<GameplaySessionAdapter>());
services.AddSingleton<IPluginManager, PluginManager>();

services.AddSingleton<LanguageManager>();
services.AddSingleton<SettingsManager>();
services.AddSingleton<PresetManager>();
services.AddSingleton<IAudioDeviceOperationCoordinator, AudioDeviceOperationCoordinator>();

services.AddTransient<MainWindow>();
services.AddTransient<MainWindowViewModel>();
Expand Down
Loading
Loading