diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..6553990d
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,4 @@
+root = true
+
+[*]
+insert_final_newline = true
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..c2054e41
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -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
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 392530e7..c31695f2 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -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: |
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 00000000..ddd14f74
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,56 @@
+
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/KeyAsio.Net.slnx b/KeyAsio.Net.slnx
index 57c10d0f..92e74354 100644
--- a/KeyAsio.Net.slnx
+++ b/KeyAsio.Net.slnx
@@ -3,6 +3,7 @@
+
@@ -19,10 +20,13 @@
-
+
+
+
-
+
+
diff --git a/benchmarks/SimdBenchmarks/SimdBenchmarks.csproj b/benchmarks/SimdBenchmarks/SimdBenchmarks.csproj
index 72fe3962..91580bdd 100644
--- a/benchmarks/SimdBenchmarks/SimdBenchmarks.csproj
+++ b/benchmarks/SimdBenchmarks/SimdBenchmarks.csproj
@@ -1,4 +1,4 @@
-
+
net10.0
@@ -8,7 +8,7 @@
-
+
diff --git a/dependencies/Directory.Packages.props b/dependencies/Directory.Packages.props
new file mode 100644
index 00000000..1d169061
--- /dev/null
+++ b/dependencies/Directory.Packages.props
@@ -0,0 +1,6 @@
+
+
+
+ false
+
+
diff --git a/dependencies/osu.Game.Rulesets.KeyAsio b/dependencies/osu.Game.Rulesets.KeyAsio
index 88af8661..1f7be98e 160000
--- a/dependencies/osu.Game.Rulesets.KeyAsio
+++ b/dependencies/osu.Game.Rulesets.KeyAsio
@@ -1 +1 @@
-Subproject commit 88af866100dd05b2fa24a80175287cdcd20b65ad
+Subproject commit 1f7be98e1f9308e17c65464783136130e52aabce
diff --git a/docs/architecture.md b/docs/architecture.md
new file mode 100644
index 00000000..e3b4a1d7
--- /dev/null
+++ b/docs/architecture.md
@@ -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.
diff --git a/src/Apps/KeyAsio/App.axaml.cs b/src/Apps/KeyAsio/App.axaml.cs
index afbac6ac..decaf9b3 100644
--- a/src/Apps/KeyAsio/App.axaml.cs
+++ b/src/Apps/KeyAsio/App.axaml.cs
@@ -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()
{
@@ -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);
@@ -47,7 +45,6 @@ public override void OnFrameworkInitializationCompleted()
keyboardBindingInitializer.RegisterAllKeys();
var pluginManager = services.GetRequiredService();
- var playbackEngine = services.GetRequiredService();
// InternalPlugins
pluginManager.LoadPlugins(AppDomain.CurrentDomain.BaseDirectory, "KeyAsio.Plugins.*.dll",
@@ -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();
diff --git a/src/Common/KeyAsio.Shared/AssemblyInfo.cs b/src/Apps/KeyAsio/AssemblyInfo.cs
similarity index 53%
rename from src/Common/KeyAsio.Shared/AssemblyInfo.cs
rename to src/Apps/KeyAsio/AssemblyInfo.cs
index 4e910653..09b27132 100644
--- a/src/Common/KeyAsio.Shared/AssemblyInfo.cs
+++ b/src/Apps/KeyAsio/AssemblyInfo.cs
@@ -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")]
diff --git a/src/Apps/KeyAsio/Converters/OsuStatusToActiveBoolConverter.cs b/src/Apps/KeyAsio/Converters/OsuStatusToActiveBoolConverter.cs
index eacd98af..5cad29c4 100644
--- a/src/Apps/KeyAsio/Converters/OsuStatusToActiveBoolConverter.cs
+++ b/src/Apps/KeyAsio/Converters/OsuStatusToActiveBoolConverter.cs
@@ -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;
diff --git a/src/Apps/KeyAsio/Converters/StringColorToBrushConverter.cs b/src/Apps/KeyAsio/Converters/StringColorToBrushConverter.cs
index 0f2d5eb6..2845556b 100644
--- a/src/Apps/KeyAsio/Converters/StringColorToBrushConverter.cs
+++ b/src/Apps/KeyAsio/Converters/StringColorToBrushConverter.cs
@@ -1,4 +1,4 @@
-using System.Globalization;
+using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data.Converters;
@@ -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)
{
diff --git a/src/Apps/KeyAsio/DependencyInjectionExtensions.cs b/src/Apps/KeyAsio/DependencyInjectionExtensions.cs
index 142fba27..51f9e031 100644
--- a/src/Apps/KeyAsio/DependencyInjectionExtensions.cs
+++ b/src/Apps/KeyAsio/DependencyInjectionExtensions.cs
@@ -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;
@@ -15,14 +21,35 @@ public static class DependencyInjectionExtensions
{
public static IServiceCollection AddGuiModule(this IServiceCollection services)
{
- services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton(static provider =>
+ provider.GetRequiredService());
+
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton(static provider =>
+ provider.GetRequiredService());
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
+
+ services.AddSingleton();
+ services.AddSingleton(static provider =>
+ provider.GetRequiredService());
+ services.AddSingleton();
+ services.AddSingleton(static provider =>
+ provider.GetRequiredService());
+ services.AddSingleton();
+ services.AddSingleton(static provider =>
+ provider.GetRequiredService());
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
services.AddTransient();
services.AddTransient();
diff --git a/src/Apps/KeyAsio/KeyAsio.csproj b/src/Apps/KeyAsio/KeyAsio.csproj
index 7549f3c8..484de988 100644
--- a/src/Apps/KeyAsio/KeyAsio.csproj
+++ b/src/Apps/KeyAsio/KeyAsio.csproj
@@ -47,35 +47,42 @@
-
-
+
+
+
-
+
-
-
-
-
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
+
+
+
+
+
diff --git a/src/Apps/KeyAsio/Lang/SR.Designer.cs b/src/Apps/KeyAsio/Lang/SR.Designer.cs
index 3cd98418..a46edb96 100644
--- a/src/Apps/KeyAsio/Lang/SR.Designer.cs
+++ b/src/Apps/KeyAsio/Lang/SR.Designer.cs
@@ -1,4 +1,4 @@
-//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
//
// 此代码由工具生成。
// 运行时版本:4.0.30319.42000
@@ -502,20 +502,20 @@ internal static string BalanceMode_CrossMix {
}
///
- /// 查找类似 Mid-Side (Professional) 的本地化字符串。
+ /// 查找类似 Off 的本地化字符串。
///
- internal static string BalanceMode_MidSide {
+ internal static string BalanceMode_Off {
get {
- return ResourceManager.GetString("BalanceMode_MidSide", resourceCulture);
+ return ResourceManager.GetString("BalanceMode_Off", resourceCulture);
}
}
///
- /// 查找类似 Off 的本地化字符串。
+ /// 查找类似 ProMix Focus 的本地化字符串。
///
- internal static string BalanceMode_Off {
+ internal static string BalanceMode_ProMixFocus {
get {
- return ResourceManager.GetString("BalanceMode_Off", resourceCulture);
+ return ResourceManager.GetString("BalanceMode_ProMixFocus", resourceCulture);
}
}
@@ -1340,7 +1340,7 @@ internal static string Sync_BalanceMode {
}
///
- /// 查找类似 Controls stereo balance processing. 'ConstantPower' is standard; 'CrossMix' preserves ambience. Note: 'Mid-Side' mode increases CPU usage. 的本地化字符串。
+ /// 查找类似 Controls stereo balance processing. ProMix Focus preserves the center while smoothly focusing the sound field toward the target side. 的本地化字符串。
///
internal static string Sync_BalanceModeDescription {
get {
diff --git a/src/Apps/KeyAsio/Lang/SR.en.resx b/src/Apps/KeyAsio/Lang/SR.en.resx
index eaf644e5..0ca95fd6 100644
--- a/src/Apps/KeyAsio/Lang/SR.en.resx
+++ b/src/Apps/KeyAsio/Lang/SR.en.resx
@@ -1,4 +1,4 @@
-
+
-
-
-
-
-
-
-
- Used to control if the On_PropertyName_Changed feature is enabled.
-
-
-
-
- Used to control if the Dependent properties feature is enabled.
-
-
-
-
- Used to control if the IsChanged property feature is enabled.
-
-
-
-
- Used to change the name of the method that fires the notify event. This is a string that accepts multiple values in a comma separated form.
-
-
-
-
- Used to control if equality checks should be inserted. If false, equality checking will be disabled for the project.
-
-
-
-
- Used to control if equality checks should use the Equals method resolved from the base class.
-
-
-
-
- Used to control if equality checks should use the static Equals method resolved from the base class.
-
-
-
-
- Used to turn off build warnings from this weaver.
-
-
-
-
- Used to turn off build warnings about mismatched On_PropertyName_Changed methods.
-
-
-
-
-
-
-
- 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
-
-
-
-
- A comma-separated list of error codes that can be safely ignored in assembly verification.
-
-
-
-
- 'false' to turn off automatic generation of the XML Schema file.
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Common/KeyAsio.Shared/KeyAsio.Shared.csproj b/src/Common/KeyAsio.Shared/KeyAsio.Shared.csproj
deleted file mode 100644
index 99262842..00000000
--- a/src/Common/KeyAsio.Shared/KeyAsio.Shared.csproj
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
-
- net10.0
- enable
- enable
- AnyCPU;x64
- true
- true
-
-
-
-
-
-
-
-
-
-
- all
- compile; runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
- all
- compile; runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- osu_memory_rules.json
- PreserveNewest
-
-
-
-
diff --git a/src/Common/KeyAsio.Shared/LogUtils.cs b/src/Common/KeyAsio.Shared/LogUtils.cs
deleted file mode 100644
index de91bf38..00000000
--- a/src/Common/KeyAsio.Shared/LogUtils.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-using System.Runtime.CompilerServices;
-using KeyAsio.Shared.Configuration;
-using Microsoft.Extensions.Logging;
-using Milki.Extensions.Configuration;
-
-namespace KeyAsio.Shared;
-
-public static class LogUtils
-{
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void LogToSentry(LogLevel logLevel, string content, Exception? exception = null,
- Action? configureScope = null)
- {
- var settings = ConfigurationFactory.GetConfiguration(
- ".", "appsettings.yaml", MyYamlConfigurationConverter.Instance);
- if (settings.Logging?.EnableErrorReporting != true) return;
- if (exception != null)
- {
- SentrySdk.CaptureException(exception, k =>
- {
- k.SetTag("message", content);
- ConfigureScope(k);
- });
- }
- else
- {
- var sentryLevel = logLevel switch
- {
- LogLevel.Trace => SentryLevel.Debug,
- LogLevel.Debug => SentryLevel.Debug,
- LogLevel.Information => SentryLevel.Info,
- LogLevel.Warning => SentryLevel.Warning,
- LogLevel.Error => SentryLevel.Error,
- LogLevel.Critical => SentryLevel.Fatal,
- LogLevel.None => SentryLevel.Info,
- _ => throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null)
- };
- if (exception != null)
- {
- sentryLevel = SentryLevel.Error;
- content += $"\r\n{exception}";
- }
-
- SentrySdk.CaptureMessage(content, ConfigureScope, sentryLevel);
- }
-
- void ConfigureScope(Scope scope)
- {
- //scope.SetTag("osu.filename_real", RealtimeModeManager.Instance.OsuFile?.ToString() ?? "");
- //scope.SetTag("osu.status", RealtimeModeManager.Instance.OsuStatus.ToString());
- //var username = RealtimeModeManager.Instance.Username;
- //scope.SetTag("osu.username", string.IsNullOrEmpty(username)
- // ? EncodeUtils.FromBase64StringEmptyIfError(settings.PlayerBase64, Encoding.ASCII)
- // : username);
-
- // todo: BREAK: realtime dep
- configureScope?.Invoke(scope);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Common/KeyAsio.Shared/Models/SharedViewModel.cs b/src/Common/KeyAsio.Shared/Models/SharedViewModel.cs
deleted file mode 100644
index c04d9788..00000000
--- a/src/Common/KeyAsio.Shared/Models/SharedViewModel.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using KeyAsio.Core.Audio;
-using KeyAsio.Shared.Utils;
-
-namespace KeyAsio.Shared.Models;
-
-public class SharedViewModel : ViewModelBase
-{
- private DeviceDescription? _deviceDescription;
- private int _framesPerBuffer;
- private int _playbackLatency;
- private SkinDescription? _selectedSkin;
-
- public SharedViewModel(AppSettings appSettings)
- {
- AppSettings = appSettings;
- }
- public ObservableRangeCollection Skins { get; } = [SkinDescription.Internal];
-
- public SkinDescription? SelectedSkin
- {
- get => _selectedSkin;
- set => SetField(ref _selectedSkin, value);
- }
-
- public DeviceDescription? DeviceDescription
- {
- get => _deviceDescription;
- set => SetField(ref _deviceDescription, value);
- }
-
- public int FramesPerBuffer
- {
- get => _framesPerBuffer;
- set => SetField(ref _framesPerBuffer, value);
- }
-
- public int PlaybackLatency
- {
- get => _playbackLatency;
- set => SetField(ref _playbackLatency, value);
- }
- public bool AutoMode { get; set; }
-
- public string DefaultFolder { get; } = Path.Combine(Environment.CurrentDirectory, "resources", "default");
- public AppSettings AppSettings { get; }
-}
\ No newline at end of file
diff --git a/src/Common/KeyAsio.Shared/Plugins/AudioEngineWrapper.cs b/src/Common/KeyAsio.Shared/Plugins/AudioEngineWrapper.cs
deleted file mode 100644
index df56dbf2..00000000
--- a/src/Common/KeyAsio.Shared/Plugins/AudioEngineWrapper.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using KeyAsio.Core.Audio;
-using KeyAsio.Plugins.Abstractions;
-using NAudio.Wave;
-
-namespace KeyAsio.Shared.Plugins;
-
-public class AudioEngineWrapper : IAudioEngine
-{
- private readonly IPlaybackEngine _engine;
-
- public AudioEngineWrapper(IPlaybackEngine engine)
- {
- _engine = engine;
- }
-
- public ISampleProvider MainMixer => _engine.RootMixer;
- public ISampleProvider EffectMixer => _engine.EffectMixer;
- public ISampleProvider MusicMixer => _engine.MusicMixer;
- public WaveFormat EngineWaveFormat => _engine.EngineWaveFormat;
-
- public void AddMixerInput(ISampleProvider input)
- {
- _engine.RootMixer.AddMixerInput(input);
- }
-
- public void RemoveMixerInput(ISampleProvider input)
- {
- _engine.RootMixer.RemoveMixerInput(input);
- }
-}
\ No newline at end of file
diff --git a/src/Common/KeyAsio.Shared/Plugins/IUserInterfacePlugin.cs b/src/Common/KeyAsio.Shared/Plugins/IUserInterfacePlugin.cs
deleted file mode 100644
index 0ca9a243..00000000
--- a/src/Common/KeyAsio.Shared/Plugins/IUserInterfacePlugin.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using Avalonia.Controls;
-using KeyAsio.Plugins.Abstractions;
-
-namespace KeyAsio.Shared.Plugins;
-
-///
-/// Interface for plugins that provide a user interface component to be injected into the main application.
-///
-public interface IUserInterfacePlugin : IPlugin
-{
- ///
- /// Gets the UI control provided by the plugin.
- ///
- /// The Avalonia control to be displayed.
- Control GetPluginControl();
-}
\ No newline at end of file
diff --git a/src/Common/KeyAsio.Shared/UiDispatcher.cs b/src/Common/KeyAsio.Shared/UiDispatcher.cs
deleted file mode 100644
index dae366ae..00000000
--- a/src/Common/KeyAsio.Shared/UiDispatcher.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-namespace KeyAsio.Shared;
-
-public static class UiDispatcher
-{
- private static SynchronizationContext? _uiContext;
-
- ///
- /// Should be called on UI thread
- ///
- public static void SetUiSynchronizationContext(SynchronizationContext? synchronizationContext = null)
- {
- if (synchronizationContext != null)
- {
- _uiContext = synchronizationContext;
- return;
- }
-
- _uiContext ??= SynchronizationContext.Current;
-
- if (_uiContext != null) return;
- var assemblies = AppDomain.CurrentDomain.GetAssemblies();
- foreach (var assembly in assemblies)
- {
- var fileName = Path.GetFileName(assembly.Location);
- if (fileName == "System.Windows.Forms.dll")
- {
- var type = assembly.DefinedTypes.First(k => k.Name.StartsWith("WindowsFormsSynchronizationContext"));
- _uiContext = Activator.CreateInstance(type) as SynchronizationContext;
- break;
- }
- else if (fileName == "WindowsBase.dll")
- {
- var type = assembly.DefinedTypes.First(k => k.Name.StartsWith("DispatcherSynchronizationContext"));
- _uiContext = Activator.CreateInstance(type) as SynchronizationContext;
- break;
- }
- }
- }
-
- public static void Invoke(Action action)
- {
- if (action == null) throw new ArgumentNullException(nameof(action));
- if (_uiContext != null)
- {
- _uiContext.Send(obj => { action?.Invoke(); }, null);
- }
- else
- {
- try
- {
- action?.Invoke();
- }
- catch (Exception ex)
- {
- Console.WriteLine("UiContext execute error: " + ex.Message);
- }
- }
- }
-
- public static async Task InvokeAsync(Action action)
- {
- var tcs = new TaskCompletionSource();
-
- _uiContext.Post(_ =>
- {
- action.Invoke();
- tcs.TrySetResult();
- }, null);
- await tcs.Task;
-
- }
-
- public static async Task InvokeAsync(Func action)
- {
- var tcs = new TaskCompletionSource();
- Task? task = null;
- _uiContext.Post(_ =>
- {
- task = action.Invoke();
- tcs.TrySetResult();
- }, null);
-
- await tcs.Task;
- await task!;
- }
-}
\ No newline at end of file
diff --git a/src/Common/KeyAsio.Shared/Utils/AsyncSequentialWorker.cs b/src/Common/KeyAsio.Shared/Utils/AsyncSequentialWorker.cs
deleted file mode 100644
index ca31a54f..00000000
--- a/src/Common/KeyAsio.Shared/Utils/AsyncSequentialWorker.cs
+++ /dev/null
@@ -1,109 +0,0 @@
-using System.Threading.Channels;
-using Microsoft.Extensions.Logging;
-
-namespace KeyAsio.Shared.Utils;
-
-public sealed class AsyncSequentialWorker : IDisposable
-{
- private readonly Channel> _channel;
- private readonly Task _workerLoop;
- private readonly CancellationTokenSource _cts = new();
- private readonly ILogger? _logger;
- private readonly string _name;
-
- public AsyncSequentialWorker(ILogger? logger = null, string name = "AsyncSequentialWorker")
- {
- _logger = logger;
- _name = name;
- _channel = Channel.CreateUnbounded>(new UnboundedChannelOptions
- {
- SingleReader = true,
- SingleWriter = false
- });
- _workerLoop = Task.Factory.StartNew(LoopAsync, TaskCreationOptions.LongRunning).Unwrap();
- }
-
- public void Enqueue(Func workItem)
- {
- _channel.Writer.TryWrite(workItem);
- }
-
- public void Enqueue(Func workItem)
- {
- _channel.Writer.TryWrite(_ => workItem());
- }
-
- public Task EnqueueAsync(Func workItem)
- {
- var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
-
- _channel.Writer.TryWrite(async ct =>
- {
- try
- {
- await workItem(ct).ConfigureAwait(false);
- tcs.TrySetResult();
- }
- catch (OperationCanceledException oce)
- {
- tcs.TrySetCanceled(oce.CancellationToken);
- throw;
- }
- catch (Exception ex)
- {
- tcs.TrySetException(ex);
- throw;
- }
- });
-
- return tcs.Task;
- }
-
- public Task EnqueueAsync(Func workItem)
- {
- return EnqueueAsync(_ => workItem());
- }
-
- private async Task LoopAsync()
- {
- var reader = _channel.Reader;
- try
- {
- while (await reader.WaitToReadAsync(_cts.Token))
- {
- while (reader.TryRead(out var workItem))
- {
- if (_cts.IsCancellationRequested) break;
- try
- {
- await workItem(_cts.Token).ConfigureAwait(false);
- }
- catch (OperationCanceledException)
- {
- // Ignore
- }
- catch (Exception ex)
- {
- _logger?.LogError(ex, "Error processing work item in {Name}", _name);
- }
- }
- }
- }
- catch (OperationCanceledException)
- {
- // Ignore
- }
- catch (Exception ex)
- {
- _logger?.LogError(ex, "Fatal error in {Name} loop", _name);
- }
- }
-
- public void Dispose()
- {
- if (_cts.IsCancellationRequested) return;
- _cts.Cancel();
- _channel.Writer.TryComplete();
- _cts.Dispose();
- }
-}
diff --git a/src/Common/KeyAsio.Shared/Utils/UniqueObservableCollection.cs b/src/Common/KeyAsio.Shared/Utils/UniqueObservableCollection.cs
deleted file mode 100644
index 7e14bf9f..00000000
--- a/src/Common/KeyAsio.Shared/Utils/UniqueObservableCollection.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-namespace KeyAsio.Shared.Utils;
-
-public class UniqueObservableCollection : ObservableRangeCollection
-{
- private readonly HashSet _itemSet;
- private readonly IEqualityComparer _comparer;
-
- public UniqueObservableCollection(IEqualityComparer? comparer = null)
- {
- _comparer = comparer ?? EqualityComparer.Default;
- _itemSet = new HashSet(_comparer);
- }
-
- public UniqueObservableCollection(IEnumerable collection, IEqualityComparer? comparer = null)
- : base(collection.Distinct(comparer ?? EqualityComparer.Default))
- {
- _comparer = comparer ?? EqualityComparer.Default;
- _itemSet = new HashSet(Items, _comparer);
- }
-
- public bool ContainsFast(T item)
- {
- return _itemSet.Contains(item);
- }
-
- protected override void InsertItem(int index, T item)
- {
- if (_itemSet.Add(item))
- {
- base.InsertItem(index, item);
- }
- }
-
- protected override void SetItem(int index, T item)
- {
- var oldItem = Items[index];
-
- if (_comparer.Equals(oldItem, item))
- return;
-
- if (_itemSet.Contains(item))
- return;
-
- _itemSet.Remove(oldItem);
- _itemSet.Add(item);
- base.SetItem(index, item);
- }
-
- protected override void RemoveItem(int index)
- {
- _itemSet.Remove(Items[index]);
- base.RemoveItem(index);
- }
-
- protected override void ClearItems()
- {
- _itemSet.Clear();
- base.ClearItems();
- }
-}
\ No newline at end of file
diff --git a/src/Core/KeyAsio.Core.Audio/EnhancedMixingSampleProviderExtension.cs b/src/Core/KeyAsio.Core.Audio/EnhancedMixingSampleProviderExtension.cs
index 97872e59..b267fddd 100644
--- a/src/Core/KeyAsio.Core.Audio/EnhancedMixingSampleProviderExtension.cs
+++ b/src/Core/KeyAsio.Core.Audio/EnhancedMixingSampleProviderExtension.cs
@@ -1,4 +1,4 @@
-using KeyAsio.Core.Audio.Caching;
+using KeyAsio.Core.Audio.Caching;
using KeyAsio.Core.Audio.SampleProviders;
using KeyAsio.Core.Audio.SampleProviders.BalancePans;
using NAudio.Wave;
@@ -105,12 +105,11 @@ private static EnhancedVolumeSampleProvider AddToAdjustVolume(ISampleProvider in
[Obsolete]
private static ProfessionalBalanceProvider AddToBalanceProvider(ISampleProvider input, float balance)
{
- var balanceProvider = new ProfessionalBalanceProvider(input,
- BalanceMode.MidSide, AntiClipStrategy.None) // 由 MasterLimiterProvider 统一处理防削波
+ var balanceProvider = new ProfessionalBalanceProvider(input, BalanceMode.ProMixFocus)
{
ExcludeFromPool = true,
Balance = balance
};
return balanceProvider;
}
-}
\ No newline at end of file
+}
diff --git a/src/Core/KeyAsio.Core.Audio/KeyAsio.Core.Audio.csproj b/src/Core/KeyAsio.Core.Audio/KeyAsio.Core.Audio.csproj
index 9271b0f6..58f7accb 100644
--- a/src/Core/KeyAsio.Core.Audio/KeyAsio.Core.Audio.csproj
+++ b/src/Core/KeyAsio.Core.Audio/KeyAsio.Core.Audio.csproj
@@ -1,4 +1,4 @@
-
+
net10.0
@@ -9,15 +9,15 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/src/Core/KeyAsio.Core.Audio/LoopProvider.cs b/src/Core/KeyAsio.Core.Audio/LoopProvider.cs
index 13953a6b..d4a187af 100644
--- a/src/Core/KeyAsio.Core.Audio/LoopProvider.cs
+++ b/src/Core/KeyAsio.Core.Audio/LoopProvider.cs
@@ -20,7 +20,7 @@ public LoopProvider(CachedAudio cachedAudio, float initialVolume, float initialB
_loopWrapper = RecyclableSampleProviderFactory.RentLoopProvider(_sourceProvider);
_volumeProvider = RecyclableSampleProviderFactory.RentVolumeProvider(_loopWrapper, initialVolume);
_balanceProvider = RecyclableSampleProviderFactory.RentBalanceProvider(_volumeProvider, initialBalance,
- balanceMode, AntiClipStrategy.None); // 由 MasterLimiterProvider 统一处理防削波
+ balanceMode);
}
public void SetBalance(float balance)
@@ -38,6 +38,16 @@ public void SetVolume(float volume)
_volumeProvider.Volume = volume;
}
+ public void Pause()
+ {
+ _loopWrapper.IsPaused = true;
+ }
+
+ public void Resume()
+ {
+ _loopWrapper.IsPaused = false;
+ }
+
public void AddTo(IMixingSampleProvider? mixer)
{
if (_baseMixer != null) return;
@@ -54,6 +64,7 @@ public void RemoveFrom(IMixingSampleProvider? mixer)
public void Dispose()
{
+ _loopWrapper.IsPaused = false;
_baseMixer = null;
}
-}
\ No newline at end of file
+}
diff --git a/src/Core/KeyAsio.Core.Audio/LoopProviderManager.cs b/src/Core/KeyAsio.Core.Audio/LoopProviderManager.cs
index 7d8d829a..66940c17 100644
--- a/src/Core/KeyAsio.Core.Audio/LoopProviderManager.cs
+++ b/src/Core/KeyAsio.Core.Audio/LoopProviderManager.cs
@@ -89,12 +89,11 @@ public void RemoveAll(IMixingSampleProvider? mixer)
public void PauseAll(IMixingSampleProvider? mixer)
{
+ // Mixer removal owns and recycles the entire provider chain, so pause in place.
foreach (var kvp in _dictionary)
{
- var channel = kvp.Key;
var loopProvider = kvp.Value;
-
- loopProvider.RemoveFrom(mixer);
+ loopProvider.Pause();
}
}
@@ -102,10 +101,8 @@ public void RecoverAll(IMixingSampleProvider? mixer)
{
foreach (var kvp in _dictionary)
{
- var channel = kvp.Key;
var loopProvider = kvp.Value;
-
- loopProvider.AddTo(mixer);
+ loopProvider.Resume();
}
}
@@ -134,4 +131,4 @@ public void Create(int slideChannel,
_dictionary.Add(slideChannel, loopProvider);
loopProvider.AddTo(mixingSampleProvider);
}
-}
\ No newline at end of file
+}
diff --git a/src/Core/KeyAsio.Core.Audio/SampleProviders/BalancePans/AntiClipStrategy.cs b/src/Core/KeyAsio.Core.Audio/SampleProviders/BalancePans/AntiClipStrategy.cs
deleted file mode 100644
index 07414214..00000000
--- a/src/Core/KeyAsio.Core.Audio/SampleProviders/BalancePans/AntiClipStrategy.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-namespace KeyAsio.Core.Audio.SampleProviders.BalancePans;
-
-///
-/// 防削波策略
-///
-public enum AntiClipStrategy
-{
- None,
-
- ///
- /// 预防性衰减 - 降低混合增益(最安全,轻微音量损失)
- ///
- PreventiveAttenuation,
-
- ///
- /// 软削波 - 使用 tanh 限制器(音质最好,轻微失真)
- ///
- SoftClipper,
-
- ///
- /// 硬限制 - 直接裁剪到 ±1.0(最快,但有失真)
- ///
- HardLimit,
-
- ///
- /// 动态增益调整 - 实时检测并降低增益(最复杂,无失真)
- ///
- DynamicGain
-}
\ No newline at end of file
diff --git a/src/Core/KeyAsio.Core.Audio/SampleProviders/BalancePans/BalanceMode.cs b/src/Core/KeyAsio.Core.Audio/SampleProviders/BalancePans/BalanceMode.cs
index 1be1301a..5a96ef21 100644
--- a/src/Core/KeyAsio.Core.Audio/SampleProviders/BalancePans/BalanceMode.cs
+++ b/src/Core/KeyAsio.Core.Audio/SampleProviders/BalancePans/BalanceMode.cs
@@ -1,4 +1,4 @@
-using System.ComponentModel;
+using System.ComponentModel;
namespace KeyAsio.Core.Audio.SampleProviders.BalancePans;
@@ -15,32 +15,25 @@ public enum BalanceMode
Off,
///
- /// 等幂声像 (标准 Pan):
- /// 衰减相反声道,总音量保持恒定。
- /// 极端值 = [L, 0] 或 [0, R]。
+ /// KeyASIO Focus (品牌声场导向):
+ /// 保持 Mid 信号,在收窄原始 Side 的同时将声像平滑导向目标侧。
///
- [Description("BalanceMode_ConstantPower")]
- ConstantPower,
+ [Description("BalanceMode_ProMixFocus")]
+ ProMixFocus,
///
- /// 交叉混合 (保留信息):
- /// 将少量相反声道信号混合到当前声道,用于保留空间感。
+ /// 等功率立体声声像(标准 Pan):
+ /// 以 sin/cos 增益将移出声道的信号转移到目标侧。
+ /// 中央位置保持原始立体声;极端值 = [L+R, 0] 或 [0, L+R]。
///
- [Description("BalanceMode_CrossMix")]
- CrossMix,
-
- ///
- /// Mid-Side 处理 (专业混音):
- /// 调整 Mid (中央) 和 Side (立体声宽度) 信号的平衡。
- ///
- [Description("BalanceMode_MidSide")]
- MidSide,
+ [Description("BalanceMode_ConstantPower")]
+ ConstantPower,
///
- /// 单声道混合声像 (听力辅助):
- /// 将 L+R 混合为单声道,并将其硬平移到左侧或右侧。
- /// 极端值 = [L+R, 0] 或 [0, L+R]。
+ /// 线性立体声声像:
+ /// 以线性增益将移出声道的信号转移到目标侧。
+ /// 中央位置保持原始立体声;极端值 = [L+R, 0] 或 [0, L+R]。
///
- [Description("BalanceMode_BinauralMix")]
- BinauralMix,
-}
\ No newline at end of file
+ [Description("BalanceMode_LinearStereoPan")]
+ LinearStereoPan
+}
diff --git a/src/Core/KeyAsio.Core.Audio/SampleProviders/BalancePans/ProfessionalBalanceProvider.cs b/src/Core/KeyAsio.Core.Audio/SampleProviders/BalancePans/ProfessionalBalanceProvider.cs
index bafb4aa1..43e0ed5a 100644
--- a/src/Core/KeyAsio.Core.Audio/SampleProviders/BalancePans/ProfessionalBalanceProvider.cs
+++ b/src/Core/KeyAsio.Core.Audio/SampleProviders/BalancePans/ProfessionalBalanceProvider.cs
@@ -1,4 +1,4 @@
-using System.Runtime.CompilerServices;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
@@ -8,51 +8,11 @@
namespace KeyAsio.Core.Audio.SampleProviders.BalancePans;
+///
+/// Applies stereo panning and sound-field matrices. Clipping protection belongs to the final mixer limiter.
+///
public sealed class ProfessionalBalanceProvider : IRecyclableProvider, IPoolable
{
- // =============== 使用示例和性能对比 ===============
- /*
- 各策略对比:
-
- ┌─────────────────────────┬──────────┬─────────┬──────────┬──────────┐
- │ 策略 │ 音质 │ 性能 │ 音量损失 │ 削波风险 │
- ├─────────────────────────┼──────────┼─────────┼──────────┼──────────┤
- │ PreventiveAttenuation │ 极好 │ 最快 │ 轻微 │ 0% │
- │ SoftClipper (tanh) │ 很好 │ 较慢 │ 无 │ 0% │
- │ HardLimit │ 中等 │ 快 │ 无 │ 0% │
- │ DynamicGain │ 最佳 │ 最慢 │ 动态 │ 0% │
- └─────────────────────────┴──────────┴─────────┴──────────┴──────────┘
-
- 推荐配置:
-
- 1. 音乐播放器 (平衡质量和性能):
- var balance = new SafeBalanceProvider(source,
- BalanceMode.CrossMix,
- AntiClipStrategy.PreventiveAttenuation); // ⭐ 推荐
-
- 2. 实时游戏 (最快性能):
- var balance = new SafeBalanceProvider(source,
- BalanceMode.ConstantPower,
- AntiClipStrategy.PreventiveAttenuation);
-
- 3. 专业 DAW (最高音质):
- var balance = new SafeBalanceProvider(source,
- BalanceMode.MidSide,
- AntiClipStrategy.SoftClipper);
-
- 4. 母带处理 (零失真):
- var balance = new SafeBalanceProvider(source,
- BalanceMode.MidSide,
- AntiClipStrategy.DynamicGain);
-
- 使用方法:
- balance.Balance = -0.8f; // 向左偏移,保证不削波
- balance.AntiClipStrategy = AntiClipStrategy.SoftClipper; // 运行时切换
- */
-
- //private const float GainAttack = 0.9999f; // 快速降低
- private const float GainRelease = 0.99995f; // 缓慢恢复
- private const float ClipThreshold = 0.95f; // 提前触发阈值
public static bool EnableAvx512 { get; set; } = true;
private static readonly bool s_canUseVectorization =
@@ -62,25 +22,18 @@ public sealed class ProfessionalBalanceProvider : IRecyclableProvider, IPoolable
private static readonly Vector128 s_stereoSwapMask;
private static readonly Vector256 s_swapMask256;
private static readonly Vector512 s_swapMask512;
-
- private static readonly Vector128 s_vOne;
- private static readonly Vector128 s_vNegOne;
private static readonly Vector128 s_vHalf;
static ProfessionalBalanceProvider()
{
if (s_canUseVectorization)
{
- // 用于立体声交换的 Shuffle 掩码 [1, 0, 3, 2] -> 将 [L1, R1, L2, R2] 变为 [R1, L1, R2, L2]
s_stereoSwapMask = Vector128.Create(1, 0, 3, 2);
- s_vOne = Vector128.Create(1.0f);
- s_vNegOne = Vector128.Create(-1.0f);
s_vHalf = Vector128.Create(0.5f);
}
if (Vector256.IsHardwareAccelerated)
{
- // [1, 0, 3, 2, 5, 4, 7, 6] -> 交换相邻的 L/R
s_swapMask256 = Vector256.Create(1, 0, 3, 2, 5, 4, 7, 6);
}
@@ -91,37 +44,26 @@ static ProfessionalBalanceProvider()
}
private float _balanceValue;
- private BalanceMode _mode = BalanceMode.CrossMix;
- private AntiClipStrategy _antiClip = AntiClipStrategy.PreventiveAttenuation;
+ private BalanceMode _mode = BalanceMode.ProMixFocus;
- // 缓存的增益值
private float _leftDirectGain;
private float _rightDirectGain;
private float _leftCrossGain;
private float _rightCrossGain;
- // --- Vector 增益缓存---
- // 一次处理2个立体声对: L1, R1, L2, R2
private Vector128 _vDirectGain;
private Vector128 _vCrossGain;
- // 动态增益调整用
- private float _dynamicGainReduction = 1.0f;
-
public ProfessionalBalanceProvider()
{
- Balance = 0f;
+ UpdateGains();
}
- public ProfessionalBalanceProvider(
- ISampleProvider? sourceProvider,
- BalanceMode mode = BalanceMode.CrossMix,
- AntiClipStrategy antiClip = AntiClipStrategy.PreventiveAttenuation)
+ public ProfessionalBalanceProvider(ISampleProvider? sourceProvider, BalanceMode mode = BalanceMode.ProMixFocus)
{
_mode = mode;
- _antiClip = antiClip;
Source = sourceProvider;
- Balance = 0f;
+ UpdateGains();
}
public ISampleProvider? Source
@@ -162,12 +104,6 @@ public BalanceMode Mode
}
}
- public AntiClipStrategy AntiClipStrategy
- {
- get => _antiClip;
- set => _antiClip = value;
- }
-
public WaveFormat WaveFormat => Source?.WaveFormat ?? throw new InvalidOperationException("Source not ready");
public ISampleProvider? ResetAndGetSource()
@@ -183,21 +119,14 @@ private void UpdateGains()
switch (_mode)
{
case BalanceMode.ConstantPower:
- UpdateSimpleFadeGains();
+ UpdateEqualPowerPanGains();
break;
-
- case BalanceMode.CrossMix:
- UpdateCrossMixGains();
- break;
-
- case BalanceMode.MidSide:
- UpdateMidSideGains();
+ case BalanceMode.ProMixFocus:
+ UpdateProMixFocusGains();
break;
-
- case BalanceMode.BinauralMix:
- UpdateBinauralGains();
+ case BalanceMode.LinearStereoPan:
+ UpdateLinearStereoPanGains();
break;
-
case BalanceMode.Off:
UpdateOffGains();
break;
@@ -211,100 +140,69 @@ private void UpdateGains()
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void UpdateSimpleFadeGains()
- {
- float pan = (_balanceValue + 1f) * 0.5f;
- float angle = pan * MathF.PI * 0.5f;
- _leftDirectGain = MathF.Cos(angle);
- _rightDirectGain = MathF.Sin(angle);
- _leftCrossGain = 0f;
- _rightCrossGain = 0f;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void UpdateCrossMixGains()
+ private void UpdateEqualPowerPanGains()
{
if (_balanceValue < 0)
{
- float amount = -_balanceValue;
-
- // 策略1: 预防性衰减 - 混合时使用更保守的增益
- float safetyFactor = (_antiClip == AntiClipStrategy.PreventiveAttenuation) ? 0.5f : 1.0f;
-
- _leftDirectGain = 1.0f;
- _leftCrossGain = amount * 0.4f * safetyFactor; // 降低交叉增益
- _rightDirectGain = 1.0f - amount;
+ float angle = -_balanceValue * MathF.PI * 0.5f;
+ _leftDirectGain = 1f;
+ _leftCrossGain = MathF.Sin(angle);
+ _rightDirectGain = MathF.Cos(angle);
_rightCrossGain = 0f;
+ return;
}
- else if (_balanceValue > 0)
- {
- float amount = _balanceValue;
- float safetyFactor = (_antiClip == AntiClipStrategy.PreventiveAttenuation) ? 0.5f : 1.0f;
- _leftDirectGain = 1.0f - amount;
- _leftCrossGain = 0f;
- _rightDirectGain = 1.0f;
- _rightCrossGain = amount * 0.4f * safetyFactor;
- }
- else
+ if (_balanceValue > 0)
{
- _leftDirectGain = 1.0f;
- _rightDirectGain = 1.0f;
+ float angle = _balanceValue * MathF.PI * 0.5f;
+ _leftDirectGain = MathF.Cos(angle);
_leftCrossGain = 0f;
- _rightCrossGain = 0f;
+ _rightDirectGain = 1f;
+ _rightCrossGain = MathF.Sin(angle);
+ return;
}
+
+ UpdateOffGains();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void UpdateMidSideGains()
- {
- _leftDirectGain = 1.0f;
- _rightDirectGain = 1.0f;
- _leftCrossGain = 0f;
- _rightCrossGain = 0f;
- }
+ private void UpdateProMixFocusGains() => UpdateOffGains();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void UpdateBinauralGains()
+ private void UpdateLinearStereoPanGains()
{
- // Binaural 模式风险最高,必须使用补偿
- float safetyFactor = (_antiClip == AntiClipStrategy.PreventiveAttenuation) ? 0.5f : 1.0f;
-
if (_balanceValue < 0)
{
float amount = -_balanceValue;
- _leftDirectGain = 1.0f * safetyFactor;
- _leftCrossGain = amount * safetyFactor;
- _rightDirectGain = (1.0f - amount) * safetyFactor;
+ _leftDirectGain = 1f;
+ _leftCrossGain = amount;
+ _rightDirectGain = 1f - amount;
_rightCrossGain = 0f;
+ return;
}
- else if (_balanceValue > 0)
+
+ if (_balanceValue > 0)
{
float amount = _balanceValue;
- _leftDirectGain = (1.0f - amount) * safetyFactor;
+ _leftDirectGain = 1f - amount;
_leftCrossGain = 0f;
- _rightDirectGain = 1.0f * safetyFactor;
- _rightCrossGain = amount * safetyFactor;
- }
- else
- {
- _leftDirectGain = 1.0f;
- _rightDirectGain = 1.0f;
- _leftCrossGain = 0f;
- _rightCrossGain = 0f;
+ _rightDirectGain = 1f;
+ _rightCrossGain = amount;
+ return;
}
+
+ UpdateOffGains();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UpdateOffGains()
{
- _leftDirectGain = 1.0f;
- _rightDirectGain = 1.0f;
+ _leftDirectGain = 1f;
+ _rightDirectGain = 1f;
_leftCrossGain = 0f;
_rightCrossGain = 0f;
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Read(float[] buffer, int offset, int sampleCount)
{
if (Source == null)
@@ -316,335 +214,117 @@ public int Read(float[] buffer, int offset, int sampleCount)
if (sampleCount == 0) return 0;
int samplesRead = Source.Read(buffer, offset, sampleCount);
- if ((_balanceValue == 0 && _mode != BalanceMode.MidSide || _mode == BalanceMode.Off) &&
- _antiClip == AntiClipStrategy.None)
+ if (_balanceValue == 0 || _mode == BalanceMode.Off)
{
return samplesRead;
}
- if (s_canUseVectorization)
+ if (_mode == BalanceMode.ProMixFocus)
{
- Span data = buffer.AsSpan(offset, samplesRead);
- if (_mode == BalanceMode.MidSide)
+ if (s_canUseVectorization)
{
- ProcessMidSideVectorized(data);
+ ProcessProMixFocusVectorized(buffer.AsSpan(offset, samplesRead));
}
else
{
- ProcessStandardVectorized(data);
+ ProcessProMixFocusSafe(buffer, offset, samplesRead);
}
}
+ else if (s_canUseVectorization)
+ {
+ ProcessStandardVectorized(buffer.AsSpan(offset, samplesRead));
+ }
else
{
- if (_mode == BalanceMode.MidSide)
- {
- ProcessMidSideSafe(buffer, offset, samplesRead);
- }
- else
- {
- ProcessStandardSafe(buffer, offset, samplesRead);
- }
+ ProcessStandardSafe(buffer, offset, samplesRead);
}
return samplesRead;
}
-
private void ProcessStandardVectorized(Span data)
{
var vecSpan = MemoryMarshal.Cast>(data);
-
- // HardLimit/None/Preventive 可以完全向量化,SoftClipper/DynamicGain 需要标量处理
- bool canFullyVectorize = _antiClip is AntiClipStrategy.None
- or AntiClipStrategy.PreventiveAttenuation
- or AntiClipStrategy.HardLimit;
-
int i = 0;
- if (canFullyVectorize)
- {
- // 完全向量化路径
- for (; i < vecSpan.Length; i++)
- {
- Vector128 vIn = vecSpan[i];
- Vector128 vSwapped = SwapStereoChannels(vIn);
-
- // 矩阵混音: Out = In * DirectGain + Swapped * CrossGain
- Vector128 vOut = (vIn * _vDirectGain) + (vSwapped * _vCrossGain);
-
- if (_antiClip == AntiClipStrategy.HardLimit)
- {
- vOut = Vector128.Min(Vector128.Max(vOut, s_vNegOne), s_vOne);
- }
-
- vecSpan[i] = vOut;
- }
- }
- else
+ for (; i < vecSpan.Length; i++)
{
- // 混合路径: 向量化混音 + 标量 AntiClip
- for (; i < vecSpan.Length; i++)
- {
- Vector128 vIn = vecSpan[i];
- Vector128 vSwapped = SwapStereoChannels(vIn);
- Vector128 vOut = (vIn * _vDirectGain) + (vSwapped * _vCrossGain);
-
- vecSpan[i] = vOut;
-
- // 标量处理 SoftClipper/DynamicGain
- int baseIdx = i * 4;
- ApplyComplexAntiClip(ref data[baseIdx], ref data[baseIdx + 1]);
- ApplyComplexAntiClip(ref data[baseIdx + 2], ref data[baseIdx + 3]);
- }
+ Vector128 input = vecSpan[i];
+ Vector128 swapped = SwapStereoChannels(input);
+ vecSpan[i] = (input * _vDirectGain) + (swapped * _vCrossGain);
}
- // 处理剩余样本
- int remainingStart = i * 4;
- for (int j = remainingStart; j < data.Length; j += 2)
+ for (int j = i * Vector128.Count; j < data.Length; j += 2)
{
- float l = data[j];
- float r = data[j + 1];
- float outL = l * _leftDirectGain + r * _leftCrossGain;
- float outR = r * _rightDirectGain + l * _rightCrossGain;
-
- if (canFullyVectorize)
- {
- if (_antiClip == AntiClipStrategy.HardLimit)
- {
- outL = Math.Clamp(outL, -1f, 1f);
- outR = Math.Clamp(outR, -1f, 1f);
- }
-
- data[j] = outL;
- data[j + 1] = outR;
- }
- else
- {
- ApplyComplexAntiClip(ref outL, ref outR);
- data[j] = outL;
- data[j + 1] = outR;
- }
+ float left = data[j];
+ float right = data[j + 1];
+ data[j] = left * _leftDirectGain + right * _leftCrossGain;
+ data[j + 1] = right * _rightDirectGain + left * _rightCrossGain;
}
}
- private void ProcessMidSideVectorized(Span data)
+ private void ProcessProMixFocusVectorized(Span data)
{
ref float dataRef = ref MemoryMarshal.GetReference(data);
- int len = data.Length;
-
- float sideGainVal = 1.0f - Math.Abs(_balanceValue);
- float midBalance = _balanceValue;
- float midGainL, midGainR;
-
- if (midBalance < 0)
- {
- float amount = -midBalance;
- midGainL = 1.0f + amount * 0.5f;
- midGainR = 1.0f - amount * 0.5f;
- }
- else if (midBalance > 0)
- {
- float amount = midBalance;
- midGainL = 1.0f - amount * 0.5f;
- midGainR = 1.0f + amount * 0.5f;
- }
- else
- {
- midGainL = midGainR = 1.0f;
- }
-
- // 2. 准备不同宽度的向量常量
- // 注意:Create 方法会自动将这4个值平铺填满整个向量
- // 例如 Vector256 会填入 [L, R, L, R, L, R, L, R]
- var vMidMixGain128 = Vector128.Create(midGainL, midGainR, midGainL, midGainR);
- var vSideGain128 = Vector128.Create(sideGainVal);
- var vHalf128 = Vector128.Create(0.5f);
- var vOne128 = Vector128.Create(1.0f);
- var vNegOne128 = Vector128.Create(-1.0f);
-
- // 判断是否只做简单处理 (HardLimit 支持向量化)
- bool canFullyVectorize = _antiClip is AntiClipStrategy.None
- or AntiClipStrategy.PreventiveAttenuation
- or AntiClipStrategy.HardLimit;
-
+ int length = data.Length;
+ float sideGain = 1f - Math.Abs(_balanceValue);
+ float midGainL = 1f - _balanceValue * 0.5f;
+ float midGainR = 1f + _balanceValue * 0.5f;
int i = 0;
- // ---------------------------------------------------------
- // PATH A: AVX-512 (512-bit, 16 floats / 8 stereo pairs)
- // ---------------------------------------------------------
- if (Vector512.IsHardwareAccelerated && canFullyVectorize && EnableAvx512)
- {
- var vMidMixGain = Vector512.Create(midGainL, midGainR, midGainL, midGainR, midGainL, midGainR, midGainL,
- midGainR,
- midGainL, midGainR, midGainL, midGainR, midGainL, midGainR, midGainL, midGainR);
- var vSideGain = Vector512.Create(sideGainVal);
- var vHalf = Vector512.Create(0.5f);
- var vOne = Vector512.Create(1.0f);
- var vNegOne = Vector512.Create(-1.0f);
-
- for (; i <= len - Vector512.Count; i += Vector512.Count)
- {
- // Load
- Vector512 vIn = Vector512.LoadUnsafe(ref dataRef, (nuint)i);
-
- // Swap L/R: [L, R...] -> [R, L...]
- Vector512 vSwapped = Vector512.Shuffle(vIn, s_swapMask512);
-
- // Math
- Vector512 vMid = (vIn + vSwapped) * vHalf;
- Vector512 vRawSide = (vIn - vSwapped) * vHalf; // S = (L-R)/2
- Vector512 vOut = (vMid * vMidMixGain) + (vRawSide * vSideGain);
-
- // Hard Limit
- if (_antiClip == AntiClipStrategy.HardLimit)
- {
- vOut = Vector512.Min(Vector512.Max(vOut, vNegOne), vOne);
- }
-
- // Store
- vOut.StoreUnsafe(ref dataRef, (nuint)i);
- }
- }
- // ---------------------------------------------------------
- // PATH B: AVX2 (256-bit, 8 floats / 4 stereo pairs)
- // ---------------------------------------------------------
- else if (Vector256.IsHardwareAccelerated && canFullyVectorize)
+ if (Vector512.IsHardwareAccelerated && EnableAvx512)
{
- var vMidMixGain = Vector256.Create(midGainL, midGainR, midGainL, midGainR, midGainL, midGainR, midGainL,
- midGainR);
- var vSideGain = Vector256.Create(sideGainVal);
- var vHalf = Vector256.Create(0.5f);
- var vOne = Vector256.Create(1.0f);
- var vNegOne = Vector256.Create(-1.0f);
+ var midGain = Vector512.Create(midGainL, midGainR, midGainL, midGainR, midGainL, midGainR, midGainL,
+ midGainR, midGainL, midGainR, midGainL, midGainR, midGainL, midGainR, midGainL, midGainR);
+ var sideGainVector = Vector512.Create(sideGain);
+ var half = Vector512.Create(0.5f);
- for (; i <= len - Vector256.Count; i += Vector256.Count)
+ for (; i <= length - Vector512.Count; i += Vector512.Count)
{
- Vector256 vIn = Vector256.LoadUnsafe(ref dataRef, (nuint)i);
-
- // .NET 8+ Shuffle 能够生成高效的 vpermps / vpermilps 指令
- Vector256 vSwapped = Vector256.Shuffle(vIn, s_swapMask256);
-
- Vector256 vMid = (vIn + vSwapped) * vHalf;
- Vector256 vRawSide = (vIn - vSwapped) * vHalf;
- Vector256 vOut = (vMid * vMidMixGain) + (vRawSide * vSideGain);
-
- if (_antiClip == AntiClipStrategy.HardLimit)
- {
- vOut = Vector256.Min(Vector256.Max(vOut, vNegOne), vOne);
- }
-
- vOut.StoreUnsafe(ref dataRef, (nuint)i);
+ Vector512 input = Vector512.LoadUnsafe(ref dataRef, (nuint)i);
+ Vector512 swapped = Vector512.Shuffle(input, s_swapMask512);
+ Vector512 mid = (input + swapped) * half;
+ Vector512 side = (input - swapped) * half;
+ ((mid * midGain) + (side * sideGainVector)).StoreUnsafe(ref dataRef, (nuint)i);
}
}
-
- // ---------------------------------------------------------
- // PATH C: SSE/Neon (128-bit, 4 floats / 2 stereo pairs)
- // ---------------------------------------------------------
- // 这里的逻辑和你原有的类似,但是稍微清理了 unsafe 写法,直接用 Vector API
- for (; i <= len - Vector128.Count; i += Vector128.Count)
+ else if (Vector256.IsHardwareAccelerated)
{
- Vector128 vIn = Vector128.LoadUnsafe(ref dataRef, (nuint)i);
-
- // 使用原有的 SwapStereoChannels (假设它已针对 128位 优化)
- Vector128 vSwapped = SwapStereoChannels(vIn);
-
- Vector128 vMid = (vIn + vSwapped) * vHalf128;
- Vector128 vRawSide = (vIn - vSwapped) * vHalf128;
- Vector128 vOut = (vMid * vMidMixGain128) + (vRawSide * vSideGain128);
-
- if (canFullyVectorize && _antiClip == AntiClipStrategy.HardLimit)
- {
- vOut = Vector128.Min(Vector128.Max(vOut, vNegOne128), vOne128);
- }
-
- vOut.StoreUnsafe(ref dataRef, (nuint)i);
+ var midGain = Vector256.Create(midGainL, midGainR, midGainL, midGainR, midGainL, midGainR, midGainL,
+ midGainR);
+ var sideGainVector = Vector256.Create(sideGain);
+ var half = Vector256.Create(0.5f);
- // 处理复杂的 AntiClip (不能向量化的部分)
- if (!canFullyVectorize)
+ for (; i <= length - Vector256.Count; i += Vector256.Count)
{
- int baseIdx = i;
- // Vector128 存回去之后,再取出来做非线性处理 (Tanh/Dynamic)
- // 注意:这里有个性能陷阱。如果你在这里做 SoftClip,
- // 之前的向量化计算可能被 Store-Load Forwarding 延迟拖慢。
- // 但考虑到 SoftClip 本身全是数学函数,这点开销可以接受。
- ApplyComplexAntiClip(ref Unsafe.Add(ref dataRef, baseIdx), ref Unsafe.Add(ref dataRef, baseIdx + 1));
- ApplyComplexAntiClip(ref Unsafe.Add(ref dataRef, baseIdx + 2),
- ref Unsafe.Add(ref dataRef, baseIdx + 3));
+ Vector256 input = Vector256.LoadUnsafe(ref dataRef, (nuint)i);
+ Vector256 swapped = Vector256.Shuffle(input, s_swapMask256);
+ Vector256 mid = (input + swapped) * half;
+ Vector256 side = (input - swapped) * half;
+ ((mid * midGain) + (side * sideGainVector)).StoreUnsafe(ref dataRef, (nuint)i);
}
}
- // 3. 处理剩余尾部 (Scalar Loop)
- for (; i < len; i += 2)
+ var midGain128 = Vector128.Create(midGainL, midGainR, midGainL, midGainR);
+ var sideGain128 = Vector128.Create(sideGain);
+ for (; i <= length - Vector128.Count; i += Vector128.Count)
{
- // 保持原有的 Scalar 处理逻辑 ...
- // (此处代码省略,与原版一致)
- float l = Unsafe.Add(ref dataRef, i);
- float r = Unsafe.Add(ref dataRef, i + 1);
-
- float mid = (l + r) * 0.5f;
- float side = (l - r) * 0.5f * sideGainVal;
-
- float outL = mid * midGainL + side;
- float outR = mid * midGainR - side;
-
- if (canFullyVectorize)
- {
- if (_antiClip == AntiClipStrategy.HardLimit)
- {
- outL = Math.Clamp(outL, -1f, 1f);
- outR = Math.Clamp(outR, -1f, 1f);
- }
- }
- else
- {
- ApplyComplexAntiClip(ref outL, ref outR);
- }
-
- Unsafe.Add(ref dataRef, i) = outL;
- Unsafe.Add(ref dataRef, i + 1) = outR;
+ Vector128 input = Vector128.LoadUnsafe(ref dataRef, (nuint)i);
+ Vector128 swapped = SwapStereoChannels(input);
+ Vector128 mid = (input + swapped) * s_vHalf;
+ Vector128 side = (input - swapped) * s_vHalf;
+ ((mid * midGain128) + (side * sideGain128)).StoreUnsafe(ref dataRef, (nuint)i);
}
- }
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void ApplyComplexAntiClip(ref float left, ref float right)
- {
- if (_antiClip == AntiClipStrategy.SoftClipper)
+ for (; i < length; i += 2)
{
- left = MathF.Tanh(left * 0.9f);
- right = MathF.Tanh(right * 0.9f);
- }
- else if (_antiClip == AntiClipStrategy.DynamicGain)
- {
- ApplyDynamicGainReduction(ref left, ref right);
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void ApplyDynamicGainReduction(ref float left, ref float right)
- {
- // 计算当前峰值
- float peak = Math.Max(Math.Abs(left), Math.Abs(right));
-
- if (peak > ClipThreshold)
- {
- // 需要削减增益
- float requiredReduction = ClipThreshold / peak;
+ float left = Unsafe.Add(ref dataRef, i);
+ float right = Unsafe.Add(ref dataRef, i + 1);
+ float mid = (left + right) * 0.5f;
+ float side = (left - right) * 0.5f * sideGain;
- // 快速降低增益
- if (requiredReduction < _dynamicGainReduction)
- {
- _dynamicGainReduction = requiredReduction;
- }
- }
- else
- {
- // 缓慢恢复增益
- _dynamicGainReduction += (1.0f - _dynamicGainReduction) * (1.0f - GainRelease);
- _dynamicGainReduction = Math.Min(_dynamicGainReduction, 1.0f);
+ Unsafe.Add(ref dataRef, i) = mid * midGainL + side;
+ Unsafe.Add(ref dataRef, i + 1) = mid * midGainR - side;
}
-
- // 应用动态增益
- left *= _dynamicGainReduction;
- right *= _dynamicGainReduction;
}
private void ProcessStandardSafe(float[] buffer, int offset, int count)
@@ -654,104 +334,45 @@ private void ProcessStandardSafe(float[] buffer, int offset, int count)
{
float left = buffer[i];
float right = buffer[i + 1];
-
- // 计算输出
- float outLeft = left * _leftDirectGain + right * _leftCrossGain;
- float outRight = right * _rightDirectGain + left * _rightCrossGain;
-
- // 应用防削波策略
- ApplyAntiClip(ref outLeft, ref outRight);
- buffer[i] = outLeft;
- buffer[i + 1] = outRight;
+ buffer[i] = left * _leftDirectGain + right * _leftCrossGain;
+ buffer[i + 1] = right * _rightDirectGain + left * _rightCrossGain;
}
}
- private void ProcessMidSideSafe(float[] buffer, int offset, int count)
+ private void ProcessProMixFocusSafe(float[] buffer, int offset, int count)
{
int endIndex = offset + count;
- float sideGain = 1.0f - Math.Abs(_balanceValue);
- float midBalance = _balanceValue;
+ float sideGain = 1f - Math.Abs(_balanceValue);
+ float midGainL = 1f - _balanceValue * 0.5f;
+ float midGainR = 1f + _balanceValue * 0.5f;
+
for (int i = offset; i < endIndex; i += 2)
{
float left = buffer[i];
float right = buffer[i + 1];
-
- // Mid-Side 变换
float mid = (left + right) * 0.5f;
- float side = (left - right) * 0.5f;
- side *= sideGain;
- float outLeft, outRight;
- if (midBalance < 0)
- {
- float amount = -midBalance;
- outLeft = mid * (1.0f + amount * 0.5f) + side;
- outRight = mid * (1.0f - amount * 0.5f) - side;
- }
- else if (midBalance > 0)
- {
- float amount = midBalance;
- outLeft = mid * (1.0f - amount * 0.5f) + side;
- outRight = mid * (1.0f + amount * 0.5f) - side;
- }
- else
- {
- outLeft = mid + side;
- outRight = mid - side;
- }
-
- // 应用防削波
- ApplyAntiClip(ref outLeft, ref outRight);
- buffer[i] = outLeft;
- buffer[i + 1] = outRight;
- }
- }
-
- private void ApplyAntiClip(ref float left, ref float right)
- {
- switch (_antiClip)
- {
- case AntiClipStrategy.None:
- break;
- case AntiClipStrategy.PreventiveAttenuation:
- // 已在增益计算中处理,这里无需额外操作
- break;
- case AntiClipStrategy.SoftClipper:
- // 使用 tanh 作为软限制器 (音质最好)
- left = MathF.Tanh(left * 0.9f);
- right = MathF.Tanh(right * 0.9f);
- break;
- case AntiClipStrategy.HardLimit:
- // 硬限制 (最快但有失真)
- left = Math.Clamp(left, -1.0f, 1.0f);
- right = Math.Clamp(right, -1.0f, 1.0f);
- break;
- case AntiClipStrategy.DynamicGain:
- // 动态增益调整
- ApplyDynamicGainReduction(ref left, ref right);
- break;
+ float side = (left - right) * 0.5f * sideGain;
+ buffer[i] = mid * midGainL + side;
+ buffer[i + 1] = mid * midGainR - side;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Vector128 SwapStereoChannels(Vector128 v)
+ private static Vector128 SwapStereoChannels(Vector128 value)
{
if (Ssse3.IsSupported)
{
- // x86: Shuffle (1 指令)
- // 交换左右声道 [L, R] -> [R, L]
- return Vector128.Shuffle(v, s_stereoSwapMask);
+ return Vector128.Shuffle(value, s_stereoSwapMask);
}
if (AdvSimd.Arm64.IsSupported)
{
- // ARM: 使用 Zip/Unzip 指令 (2-3 指令)
- // [L1, R1, L2, R2] -> [R1, L1, R2, L2]
- var odds = AdvSimd.Arm64.UnzipOdd(v, v); // [R1, R2, ?, ?]
- var evens = AdvSimd.Arm64.UnzipEven(v, v); // [L1, L2, ?, ?]
- return AdvSimd.Arm64.ZipLow(odds, evens); // [R1, L1, R2, L2]
+ var odds = AdvSimd.Arm64.UnzipOdd(value, value);
+ var evens = AdvSimd.Arm64.UnzipEven(value, value);
+ return AdvSimd.Arm64.ZipLow(odds, evens);
}
- return Vector128.Create(v[1], v[0], v[3], v[2]);
+ return Vector128.Create(value[1], value[0], value[3], value[2]);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -759,9 +380,8 @@ public void Reset()
{
Source = null;
Balance = 0f;
- Mode = BalanceMode.CrossMix;
- AntiClipStrategy = AntiClipStrategy.PreventiveAttenuation;
+ Mode = BalanceMode.ProMixFocus;
}
public bool ExcludeFromPool { get; init; }
-}
\ No newline at end of file
+}
diff --git a/src/Core/KeyAsio.Core.Audio/SampleProviders/LoopSampleProvider.cs b/src/Core/KeyAsio.Core.Audio/SampleProviders/LoopSampleProvider.cs
index dd8ea4e3..caa3011e 100644
--- a/src/Core/KeyAsio.Core.Audio/SampleProviders/LoopSampleProvider.cs
+++ b/src/Core/KeyAsio.Core.Audio/SampleProviders/LoopSampleProvider.cs
@@ -5,6 +5,8 @@ namespace KeyAsio.Core.Audio.SampleProviders;
public sealed class LoopSampleProvider : IRecyclableProvider, IPoolable
{
+ private volatile bool _isPaused;
+
public LoopSampleProvider()
{
}
@@ -18,6 +20,12 @@ public LoopSampleProvider(CachedAudioProvider source)
public bool EnableLooping { get; set; } = true;
+ public bool IsPaused
+ {
+ get => _isPaused;
+ set => _isPaused = value;
+ }
+
public WaveFormat WaveFormat => Source?.WaveFormat ?? throw new InvalidOperationException("Source not initialized");
public ISampleProvider? ResetAndGetSource()
@@ -29,6 +37,14 @@ public LoopSampleProvider(CachedAudioProvider source)
public int Read(float[] buffer, int offset, int count)
{
+ if (count == 0) return 0;
+
+ if (IsPaused)
+ {
+ Array.Clear(buffer, offset, count);
+ return QueueMixingSampleProvider.SignalKeepAlive;
+ }
+
if (Source == null)
{
Array.Clear(buffer, offset, count);
@@ -75,7 +91,8 @@ public void Reset()
{
Source = null;
EnableLooping = true;
+ IsPaused = false;
}
public bool ExcludeFromPool { get; init; }
-}
\ No newline at end of file
+}
diff --git a/src/Core/KeyAsio.Core.Audio/Utils/RecyclableSampleProviderFactory.cs b/src/Core/KeyAsio.Core.Audio/Utils/RecyclableSampleProviderFactory.cs
index 66ad4474..3cf28c97 100644
--- a/src/Core/KeyAsio.Core.Audio/Utils/RecyclableSampleProviderFactory.cs
+++ b/src/Core/KeyAsio.Core.Audio/Utils/RecyclableSampleProviderFactory.cs
@@ -15,14 +15,12 @@ public static EnhancedVolumeSampleProvider RentVolumeProvider(ISampleProvider so
return provider;
}
- public static ProfessionalBalanceProvider RentBalanceProvider(ISampleProvider source, float balance, BalanceMode mode,
- AntiClipStrategy antiClipStrategy)
+ public static ProfessionalBalanceProvider RentBalanceProvider(ISampleProvider source, float balance, BalanceMode mode)
{
var provider = SharedPool.Rent();
provider.Source = source;
- provider.Balance = balance;
provider.Mode = mode;
- provider.AntiClipStrategy = antiClipStrategy;
+ provider.Balance = balance;
return provider;
}
@@ -59,4 +57,4 @@ public static void Return(CachedAudioProvider provider)
{
SharedPool.Return(provider);
}
-}
\ No newline at end of file
+}
diff --git a/src/Core/KeyAsio.Core.Memory/KeyAsio.Core.Memory.csproj b/src/Core/KeyAsio.Core.Memory/KeyAsio.Core.Memory.csproj
index 99a38b47..0e8a2f82 100644
--- a/src/Core/KeyAsio.Core.Memory/KeyAsio.Core.Memory.csproj
+++ b/src/Core/KeyAsio.Core.Memory/KeyAsio.Core.Memory.csproj
@@ -1,4 +1,4 @@
-
+
net10.0
@@ -10,13 +10,13 @@
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/src/Core/KeyAsio.Core.OsuAudio/KeyAsio.Core.OsuAudio.csproj b/src/Core/KeyAsio.Core.OsuAudio/KeyAsio.Core.OsuAudio.csproj
index 2e1bda74..016347eb 100644
--- a/src/Core/KeyAsio.Core.OsuAudio/KeyAsio.Core.OsuAudio.csproj
+++ b/src/Core/KeyAsio.Core.OsuAudio/KeyAsio.Core.OsuAudio.csproj
@@ -8,6 +8,6 @@
-
+
diff --git a/src/Core/KeyAsio.Sync/Abstractions/IGameplayAudioCache.cs b/src/Core/KeyAsio.Sync/Abstractions/IGameplayAudioCache.cs
new file mode 100644
index 00000000..f78eed3c
--- /dev/null
+++ b/src/Core/KeyAsio.Sync/Abstractions/IGameplayAudioCache.cs
@@ -0,0 +1,6 @@
+namespace KeyAsio.Sync.Abstractions;
+
+public interface IGameplayAudioCache
+{
+ void ClearCaches();
+}
diff --git a/src/Core/KeyAsio.Sync/Abstractions/IPlaybackRuntimeState.cs b/src/Core/KeyAsio.Sync/Abstractions/IPlaybackRuntimeState.cs
new file mode 100644
index 00000000..fdb53b96
--- /dev/null
+++ b/src/Core/KeyAsio.Sync/Abstractions/IPlaybackRuntimeState.cs
@@ -0,0 +1,10 @@
+namespace KeyAsio.Sync.Abstractions;
+
+public interface IPlaybackRuntimeState
+{
+ bool AutoMode { get; }
+
+ string SelectedSkinFolder { get; }
+
+ event Action? SelectedSkinChanged;
+}
diff --git a/src/Core/KeyAsio.Sync/Abstractions/ISkinResourceProvider.cs b/src/Core/KeyAsio.Sync/Abstractions/ISkinResourceProvider.cs
new file mode 100644
index 00000000..cc8d61a7
--- /dev/null
+++ b/src/Core/KeyAsio.Sync/Abstractions/ISkinResourceProvider.cs
@@ -0,0 +1,12 @@
+using KeyAsio.Core.OsuAudio.Hitsounds;
+
+namespace KeyAsio.Sync.Abstractions;
+
+public interface ISkinResourceProvider
+{
+ bool TryGetSkinCatalog(string skinFolder, out IBeatmapResourceCatalog catalog);
+
+ bool TryGetLazerResource(string skinFolder, string key, out byte[] data);
+
+ bool TryGetStableResource(string key, out byte[] data);
+}
diff --git a/src/Core/KeyAsio.Sync/AssemblyInfo.cs b/src/Core/KeyAsio.Sync/AssemblyInfo.cs
new file mode 100644
index 00000000..df63a5c0
--- /dev/null
+++ b/src/Core/KeyAsio.Sync/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("ColumnTest")]
diff --git a/src/Common/KeyAsio.Shared/Sync/AudioProviders/CatchHitsoundSequencer.cs b/src/Core/KeyAsio.Sync/AudioProviders/CatchHitsoundSequencer.cs
similarity index 97%
rename from src/Common/KeyAsio.Shared/Sync/AudioProviders/CatchHitsoundSequencer.cs
rename to src/Core/KeyAsio.Sync/AudioProviders/CatchHitsoundSequencer.cs
index c6a1ad84..f480256d 100644
--- a/src/Common/KeyAsio.Shared/Sync/AudioProviders/CatchHitsoundSequencer.cs
+++ b/src/Core/KeyAsio.Sync/AudioProviders/CatchHitsoundSequencer.cs
@@ -1,11 +1,13 @@
using System.Runtime.CompilerServices;
+using KeyAsio.Configuration;
+using KeyAsio.Configuration.Models;
using KeyAsio.Core.Audio;
using KeyAsio.Core.OsuAudio.Hitsounds.Playback;
-using KeyAsio.Shared.Models;
-using KeyAsio.Shared.Sync.Services;
+using KeyAsio.Sync.Models;
+using KeyAsio.Sync.Services;
using Microsoft.Extensions.Logging;
-namespace KeyAsio.Shared.Sync.AudioProviders;
+namespace KeyAsio.Sync.AudioProviders;
public class CatchHitsoundSequencer : IHitsoundSequencer
{
@@ -182,4 +184,4 @@ private bool IsEngineReady()
return true;
}
-}
\ No newline at end of file
+}
diff --git a/src/Common/KeyAsio.Shared/Sync/AudioProviders/ManiaHitsoundSequencer.cs b/src/Core/KeyAsio.Sync/AudioProviders/ManiaHitsoundSequencer.cs
similarity index 98%
rename from src/Common/KeyAsio.Shared/Sync/AudioProviders/ManiaHitsoundSequencer.cs
rename to src/Core/KeyAsio.Sync/AudioProviders/ManiaHitsoundSequencer.cs
index 65dbf3ba..7f68f5d2 100644
--- a/src/Common/KeyAsio.Shared/Sync/AudioProviders/ManiaHitsoundSequencer.cs
+++ b/src/Core/KeyAsio.Sync/AudioProviders/ManiaHitsoundSequencer.cs
@@ -1,11 +1,12 @@
+using KeyAsio.Common;
+using KeyAsio.Configuration;
using KeyAsio.Core.Audio;
using KeyAsio.Core.OsuAudio.Hitsounds.Playback;
-using KeyAsio.Shared.Models;
-using KeyAsio.Shared.Sync.Services;
-using KeyAsio.Shared.Utils;
+using KeyAsio.Sync.Models;
+using KeyAsio.Sync.Services;
using Microsoft.Extensions.Logging;
-namespace KeyAsio.Shared.Sync.AudioProviders;
+namespace KeyAsio.Sync.AudioProviders;
public class ManiaHitsoundSequencer : IHitsoundSequencer
{
@@ -238,4 +239,4 @@ private void FillNextPlaybackAudio(List buffer, PlaybackEvent? fir
_firstAutoNode = firstNode;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Common/KeyAsio.Shared/Sync/AudioProviders/StandardHitsoundSequencer.cs b/src/Core/KeyAsio.Sync/AudioProviders/StandardHitsoundSequencer.cs
similarity index 98%
rename from src/Common/KeyAsio.Shared/Sync/AudioProviders/StandardHitsoundSequencer.cs
rename to src/Core/KeyAsio.Sync/AudioProviders/StandardHitsoundSequencer.cs
index 635657b4..d7a22961 100644
--- a/src/Common/KeyAsio.Shared/Sync/AudioProviders/StandardHitsoundSequencer.cs
+++ b/src/Core/KeyAsio.Sync/AudioProviders/StandardHitsoundSequencer.cs
@@ -1,11 +1,13 @@
using System.Runtime.CompilerServices;
+using KeyAsio.Configuration;
+using KeyAsio.Configuration.Models;
using KeyAsio.Core.Audio;
using KeyAsio.Core.OsuAudio.Hitsounds.Playback;
-using KeyAsio.Shared.Models;
-using KeyAsio.Shared.Sync.Services;
+using KeyAsio.Sync.Models;
+using KeyAsio.Sync.Services;
using Microsoft.Extensions.Logging;
-namespace KeyAsio.Shared.Sync.AudioProviders;
+namespace KeyAsio.Sync.AudioProviders;
public class StandardHitsoundSequencer : IHitsoundSequencer
{
@@ -266,4 +268,4 @@ private void DequeueAndPlay(List buffer, Queue queue) where
buffer.Add(new PlaybackInfo(cachedSound, node));
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Common/KeyAsio.Shared/Sync/AudioProviders/TaikoHitsoundSequencer.cs b/src/Core/KeyAsio.Sync/AudioProviders/TaikoHitsoundSequencer.cs
similarity index 98%
rename from src/Common/KeyAsio.Shared/Sync/AudioProviders/TaikoHitsoundSequencer.cs
rename to src/Core/KeyAsio.Sync/AudioProviders/TaikoHitsoundSequencer.cs
index db391455..4a337fde 100644
--- a/src/Common/KeyAsio.Shared/Sync/AudioProviders/TaikoHitsoundSequencer.cs
+++ b/src/Core/KeyAsio.Sync/AudioProviders/TaikoHitsoundSequencer.cs
@@ -1,13 +1,15 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
+using KeyAsio.Configuration;
+using KeyAsio.Configuration.Models;
using KeyAsio.Core.Audio;
using KeyAsio.Core.Audio.Caching;
using KeyAsio.Core.OsuAudio.Hitsounds.Playback;
-using KeyAsio.Shared.Models;
-using KeyAsio.Shared.Sync.Services;
+using KeyAsio.Sync.Models;
+using KeyAsio.Sync.Services;
using Microsoft.Extensions.Logging;
-namespace KeyAsio.Shared.Sync.AudioProviders;
+namespace KeyAsio.Sync.AudioProviders;
public class TaikoHitsoundSequencer : IHitsoundSequencer
{
@@ -395,4 +397,4 @@ private void DequeueAndPlay(List buffer, Queue queue) where
buffer.Add(new PlaybackInfo(cachedSound, node));
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Common/KeyAsio.Shared/Sync/ConfigManager.cs b/src/Core/KeyAsio.Sync/ConfigManager.cs
similarity index 95%
rename from src/Common/KeyAsio.Shared/Sync/ConfigManager.cs
rename to src/Core/KeyAsio.Sync/ConfigManager.cs
index 32827a4c..b2462534 100644
--- a/src/Common/KeyAsio.Shared/Sync/ConfigManager.cs
+++ b/src/Core/KeyAsio.Sync/ConfigManager.cs
@@ -1,7 +1,8 @@
-using Coosu.Shared.IO;
+using Coosu.Shared.IO;
+using KeyAsio.Configuration;
using Milki.Extensions.MouseKeyHook;
-namespace KeyAsio.Shared.Sync;
+namespace KeyAsio.Sync;
internal class ConfigManager
{
@@ -51,4 +52,4 @@ private static string GetUserConfigFilename(string username)
{
return $"osu!.{PathUtils.EscapeFileName(username)}.cfg";
}
-}
\ No newline at end of file
+}
diff --git a/src/Common/KeyAsio.Shared/Sync/DependencyInjectionExtensions.cs b/src/Core/KeyAsio.Sync/DependencyInjectionExtensions.cs
similarity index 75%
rename from src/Common/KeyAsio.Shared/Sync/DependencyInjectionExtensions.cs
rename to src/Core/KeyAsio.Sync/DependencyInjectionExtensions.cs
index b92c1464..783bd7cf 100644
--- a/src/Common/KeyAsio.Shared/Sync/DependencyInjectionExtensions.cs
+++ b/src/Core/KeyAsio.Sync/DependencyInjectionExtensions.cs
@@ -1,18 +1,16 @@
-using KeyAsio.Plugins.Abstractions;
-using KeyAsio.Shared.OsuMemory;
-using KeyAsio.Shared.Plugins;
-using KeyAsio.Shared.Services;
-using KeyAsio.Shared.Sync.Services;
+using KeyAsio.Sync.Abstractions;
+using KeyAsio.Sync.Services;
+using KeyAsio.Sync.Sources;
using Microsoft.Extensions.DependencyInjection;
-namespace KeyAsio.Shared.Sync;
+namespace KeyAsio.Sync;
public static class DependencyInjectionExtensions
{
public static IServiceCollection AddSyncModule(this IServiceCollection services)
{
- services.AddSingleton();
-
+ services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
@@ -22,6 +20,8 @@ public static IServiceCollection AddSyncModule(this IServiceCollection services)
serviceProvider.GetRequiredService());
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton(static provider =>
+ provider.GetRequiredService());
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
@@ -29,7 +29,6 @@ public static IServiceCollection AddSyncModule(this IServiceCollection services)
services.AddSingleton();
services.AddSingleton();
- services.AddSingleton();
return services;
}
}
diff --git a/src/Common/KeyAsio.Shared/Events/EventDelegates.cs b/src/Core/KeyAsio.Sync/Events/EventDelegates.cs
similarity index 95%
rename from src/Common/KeyAsio.Shared/Events/EventDelegates.cs
rename to src/Core/KeyAsio.Sync/Events/EventDelegates.cs
index 4a883456..46e377eb 100644
--- a/src/Common/KeyAsio.Shared/Events/EventDelegates.cs
+++ b/src/Core/KeyAsio.Sync/Events/EventDelegates.cs
@@ -1,4 +1,4 @@
-namespace KeyAsio.Shared.Events;
+namespace KeyAsio.Sync.Events;
///
/// Represents a method that handles a signal event (no data).
@@ -26,4 +26,4 @@
/// The old value.
/// The new value.
/// A task that represents the asynchronous operation.
-public delegate Task ValueChangedAsyncEventHandler(T oldValue, T newValue);
\ No newline at end of file
+public delegate Task ValueChangedAsyncEventHandler(T oldValue, T newValue);
diff --git a/src/Common/KeyAsio.Shared/Sync/GameStateMachine.cs b/src/Core/KeyAsio.Sync/GameStateMachine.cs
similarity index 94%
rename from src/Common/KeyAsio.Shared/Sync/GameStateMachine.cs
rename to src/Core/KeyAsio.Sync/GameStateMachine.cs
index c6977713..9f858fcc 100644
--- a/src/Common/KeyAsio.Shared/Sync/GameStateMachine.cs
+++ b/src/Core/KeyAsio.Sync/GameStateMachine.cs
@@ -1,7 +1,7 @@
-using KeyAsio.Plugins.Abstractions.OsuMemory;
-using KeyAsio.Shared.Sync.States;
+using KeyAsio.Plugins.Contracts.Sync;
+using KeyAsio.Sync.States;
-namespace KeyAsio.Shared.Sync;
+namespace KeyAsio.Sync;
public class GameStateMachine
{
@@ -65,4 +65,4 @@ public async Task EnterFromAsync(SyncSessionContext ctx, OsuMemoryStatus from, O
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Common/KeyAsio.Shared/Sync/IHitsoundSequencer.cs b/src/Core/KeyAsio.Sync/IHitsoundSequencer.cs
similarity index 74%
rename from src/Common/KeyAsio.Shared/Sync/IHitsoundSequencer.cs
rename to src/Core/KeyAsio.Sync/IHitsoundSequencer.cs
index 772cb97f..5c482cb8 100644
--- a/src/Common/KeyAsio.Shared/Sync/IHitsoundSequencer.cs
+++ b/src/Core/KeyAsio.Sync/IHitsoundSequencer.cs
@@ -1,13 +1,15 @@
using KeyAsio.Core.OsuAudio.Hitsounds.Playback;
-using KeyAsio.Shared.Models;
+using KeyAsio.Sync.Models;
-namespace KeyAsio.Shared.Sync;
+namespace KeyAsio.Sync;
public interface IHitsoundSequencer
{
void ProcessAutoPlay(List buffer, bool processHitQueueAsAuto);
void ProcessInteraction(List buffer, int keyIndex, int keyTotal);
- void FillAudioList(IReadOnlyList nodeList, List keyList, List playbackList);
+ void FillAudioList(IReadOnlyList nodeList, List keyList,
+ List playbackList);
+
void SeekTo(int playTime);
-}
\ No newline at end of file
+}
diff --git a/src/Core/KeyAsio.Sync/KeyAsio.Sync.csproj b/src/Core/KeyAsio.Sync/KeyAsio.Sync.csproj
new file mode 100644
index 00000000..4b848d1e
--- /dev/null
+++ b/src/Core/KeyAsio.Sync/KeyAsio.Sync.csproj
@@ -0,0 +1,31 @@
+
+
+ net10.0
+ enable
+ enable
+ AnyCPU;x64
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ osu_memory_rules.json
+ PreserveNewest
+
+
+
diff --git a/src/Common/KeyAsio.Shared/Models/PlaybackInfo.cs b/src/Core/KeyAsio.Sync/Models/PlaybackInfo.cs
similarity index 84%
rename from src/Common/KeyAsio.Shared/Models/PlaybackInfo.cs
rename to src/Core/KeyAsio.Sync/Models/PlaybackInfo.cs
index 5742514a..757c5a5d 100644
--- a/src/Common/KeyAsio.Shared/Models/PlaybackInfo.cs
+++ b/src/Core/KeyAsio.Sync/Models/PlaybackInfo.cs
@@ -1,6 +1,6 @@
using KeyAsio.Core.Audio.Caching;
using KeyAsio.Core.OsuAudio.Hitsounds.Playback;
-namespace KeyAsio.Shared.Models;
+namespace KeyAsio.Sync.Models;
public readonly record struct PlaybackInfo(CachedAudio? CachedAudio, PlaybackEvent PlaybackEvent);
diff --git a/src/Common/KeyAsio.Shared/Sync/Services/BeatmapHitsoundLoader.cs b/src/Core/KeyAsio.Sync/Services/BeatmapHitsoundLoader.cs
similarity index 96%
rename from src/Common/KeyAsio.Shared/Sync/Services/BeatmapHitsoundLoader.cs
rename to src/Core/KeyAsio.Sync/Services/BeatmapHitsoundLoader.cs
index b6d74ab2..f0098851 100644
--- a/src/Common/KeyAsio.Shared/Sync/Services/BeatmapHitsoundLoader.cs
+++ b/src/Core/KeyAsio.Sync/Services/BeatmapHitsoundLoader.cs
@@ -1,11 +1,12 @@
using Coosu.Beatmap;
+using KeyAsio.Common;
+using KeyAsio.Configuration;
using KeyAsio.Core.OsuAudio.Hitsounds;
using KeyAsio.Core.OsuAudio.Hitsounds.Playback;
-using KeyAsio.Plugins.Abstractions.OsuMemory;
-using KeyAsio.Shared.Utils;
+using KeyAsio.Plugins.Contracts.Sync;
using Microsoft.Extensions.Logging;
-namespace KeyAsio.Shared.Sync.Services;
+namespace KeyAsio.Sync.Services;
public class BeatmapHitsoundLoader
{
diff --git a/src/Common/KeyAsio.Shared/Sync/Services/GameplayAudioService.cs b/src/Core/KeyAsio.Sync/Services/GameplayAudioService.cs
similarity index 91%
rename from src/Common/KeyAsio.Shared/Sync/Services/GameplayAudioService.cs
rename to src/Core/KeyAsio.Sync/Services/GameplayAudioService.cs
index aee7d230..426207f2 100644
--- a/src/Common/KeyAsio.Shared/Sync/Services/GameplayAudioService.cs
+++ b/src/Core/KeyAsio.Sync/Services/GameplayAudioService.cs
@@ -1,20 +1,20 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
+using KeyAsio.Common;
+using KeyAsio.Configuration;
using KeyAsio.Core.Audio;
using KeyAsio.Core.Audio.Caching;
using KeyAsio.Core.OsuAudio.Hitsounds;
using KeyAsio.Core.OsuAudio.Hitsounds.Playback;
using KeyAsio.Core.OsuAudio.Utils;
-using KeyAsio.Shared.Models;
-using KeyAsio.Shared.Services;
-using KeyAsio.Shared.Utils;
+using KeyAsio.Sync.Abstractions;
using Microsoft.Extensions.Logging;
using NAudio.Wave;
-namespace KeyAsio.Shared.Sync.Services;
+namespace KeyAsio.Sync.Services;
-public class GameplayAudioService : IDisposable
+public class GameplayAudioService : IGameplayAudioCache, IDisposable
{
private const string BeatmapCacheIdentifier = "default";
private const string UserCacheIdentifier = "internal";
@@ -30,8 +30,8 @@ public class GameplayAudioService : IDisposable
private readonly AppSettings _appSettings;
private readonly IPlaybackEngine _playbackEngine;
private readonly AudioCacheManager _audioCacheManager;
- private readonly SharedViewModel _sharedViewModel;
- private readonly SkinManager _skinManager;
+ private readonly IPlaybackRuntimeState _runtimeState;
+ private readonly ISkinResourceProvider _skinResources;
private string? _beatmapFolder;
private string? _audioFilename;
private IBeatmapResourceCatalog? _beatmapResourceCatalog;
@@ -43,28 +43,25 @@ public GameplayAudioService(ILogger logger,
AppSettings appSettings,
IPlaybackEngine playbackEngine,
AudioCacheManager audioCacheManager,
- SharedViewModel sharedViewModel,
- SkinManager skinManager)
+ IPlaybackRuntimeState runtimeState,
+ ISkinResourceProvider skinResources)
{
_logger = logger;
_syncSessionContext = syncSessionContext;
_appSettings = appSettings;
_playbackEngine = playbackEngine;
_audioCacheManager = audioCacheManager;
- _sharedViewModel = sharedViewModel;
- _skinManager = skinManager;
+ _runtimeState = runtimeState;
+ _skinResources = skinResources;
_cachingWorker = new AsyncSequentialWorker(_logger, "GameplayAudioServiceWorker");
- _sharedViewModel.PropertyChanged += SharedViewModel_PropertyChanged;
+ _runtimeState.SelectedSkinChanged += OnSelectedSkinChanged;
}
- private void SharedViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
+ private void OnSelectedSkinChanged()
{
- if (e.PropertyName == nameof(SharedViewModel.SelectedSkin))
- {
- _logger.LogInformation("Skin changed, clearing gameplay audio service caches.");
- ClearCaches();
- }
+ _logger.LogInformation("Skin changed, clearing gameplay audio service caches.");
+ ClearCaches();
}
public void SetContext(string? beatmapFolder, string? audioFilename,
@@ -110,7 +107,7 @@ public void PrecacheMusicAndSkinInBackground()
var folder = _beatmapFolder;
var waveFormat = _playbackEngine.EngineWaveFormat;
- var skinFolder = _sharedViewModel.SelectedSkin?.Folder ?? "";
+ var skinFolder = _runtimeState.SelectedSkinFolder;
_cachingWorker.Enqueue(async token =>
{
@@ -177,7 +174,7 @@ public void PrecacheHitsoundsRangeInBackground(
var folder = _beatmapFolder;
var waveFormat = _playbackEngine.SourceWaveFormat;
- var skinFolder = _sharedViewModel.SelectedSkin?.Folder ?? "";
+ var skinFolder = _runtimeState.SelectedSkinFolder;
_cachingWorker.Enqueue(async token =>
{
@@ -323,7 +320,7 @@ private async Task ResolveAndLoadSkinAudioAsync(string filenameKey,
// Lazer user skins: resolve via the skin's resource catalog (file-to-path mappings
// received from the lazer IPC). The skinFolder is a virtual key, not a real directory.
- if (_skinManager.TryGetSkinCatalog(skinFolder, out var skinCatalog) &&
+ if (_skinResources.TryGetSkinCatalog(skinFolder, out var skinCatalog) &&
skinCatalog.TryResolveAudio(filenameKey, out var skinResource))
{
return await LoadAndCacheAudioAsync(skinResource.Path, category, waveFormat);
@@ -334,7 +331,7 @@ private async Task ResolveAndLoadSkinAudioAsync(string filenameKey,
var lazerSkinFolder = skinFolder.StartsWith("{lazer-", StringComparison.OrdinalIgnoreCase)
? skinFolder
: "{lazer-classic}";
- if (_skinManager.TryGetLazerResource(lazerSkinFolder, filenameKey, out var lazerBytes))
+ if (_skinResources.TryGetLazerResource(lazerSkinFolder, filenameKey, out var lazerBytes))
{
var key = $"lazer://{lazerSkinFolder}/{filenameKey}";
using var stream = new MemoryStream(lazerBytes);
@@ -342,7 +339,7 @@ private async Task ResolveAndLoadSkinAudioAsync(string filenameKey,
}
// Stable fallback (osu!gameplay.dll resources)
- if (_skinManager.TryGetStableResource(filenameKey, out var bytes))
+ if (_skinResources.TryGetStableResource(filenameKey, out var bytes))
{
var key = $"internal://{filenameKey}";
using var stream = new MemoryStream(bytes);
@@ -396,7 +393,7 @@ private async Task LoadAndCacheAudioAsync(string path, string categ
public void Dispose()
{
- _sharedViewModel.PropertyChanged -= SharedViewModel_PropertyChanged;
+ _runtimeState.SelectedSkinChanged -= OnSelectedSkinChanged;
_cachingWorker.Dispose();
}
}
diff --git a/src/Common/KeyAsio.Shared/Sync/Services/GameplaySessionManager.cs b/src/Core/KeyAsio.Sync/Services/GameplaySessionManager.cs
similarity index 95%
rename from src/Common/KeyAsio.Shared/Sync/Services/GameplaySessionManager.cs
rename to src/Core/KeyAsio.Sync/Services/GameplaySessionManager.cs
index 7ffb0d29..4a8e723f 100644
--- a/src/Common/KeyAsio.Shared/Sync/Services/GameplaySessionManager.cs
+++ b/src/Core/KeyAsio.Sync/Services/GameplaySessionManager.cs
@@ -2,14 +2,14 @@
using System.Runtime.CompilerServices;
using Coosu.Beatmap;
using Coosu.Beatmap.Sections.GamePlay;
+using KeyAsio.Common;
using KeyAsio.Core.Audio;
using KeyAsio.Core.OsuAudio.Hitsounds;
using KeyAsio.Core.OsuAudio.Hitsounds.Playback;
-using KeyAsio.Shared.Events;
-using KeyAsio.Shared.Utils;
+using KeyAsio.Sync.Events;
using Microsoft.Extensions.Logging;
-namespace KeyAsio.Shared.Sync.Services;
+namespace KeyAsio.Sync.Services;
public class GameplaySessionManager
{
@@ -146,11 +146,6 @@ public async Task StartAsync(string? beatmapFilenameFull, string? beatmapFilenam
_syncSessionContext.IsStarted = false;
_logger.LogError(ex, "Error while starting a beatmap. Filename: {BeatmapFilename}. FilenameReal: {OsuFile}",
beatmapFilename, OsuFile);
- LogUtils.LogToSentry(LogLevel.Error, "Error while starting a beatmap", ex, k =>
- {
- k.SetTag("osu.filename", beatmapFilename);
- k.SetTag("osu.filename_real", OsuFile?.ToString() ?? "");
- });
}
}
diff --git a/src/Common/KeyAsio.Shared/Sync/Services/SfxPlaybackService.cs b/src/Core/KeyAsio.Sync/Services/SfxPlaybackService.cs
similarity index 96%
rename from src/Common/KeyAsio.Shared/Sync/Services/SfxPlaybackService.cs
rename to src/Core/KeyAsio.Sync/Services/SfxPlaybackService.cs
index f8ff1231..257e6cf4 100644
--- a/src/Common/KeyAsio.Shared/Sync/Services/SfxPlaybackService.cs
+++ b/src/Core/KeyAsio.Sync/Services/SfxPlaybackService.cs
@@ -1,14 +1,15 @@
using System.ComponentModel;
+using KeyAsio.Configuration;
using KeyAsio.Core.Audio;
using KeyAsio.Core.Audio.Caching;
using KeyAsio.Core.Audio.SampleProviders;
using KeyAsio.Core.Audio.SampleProviders.BalancePans;
using KeyAsio.Core.Audio.Utils;
using KeyAsio.Core.OsuAudio.Hitsounds.Playback;
-using KeyAsio.Shared.Models;
+using KeyAsio.Sync.Models;
using Microsoft.Extensions.Logging;
-namespace KeyAsio.Shared.Sync.Services;
+namespace KeyAsio.Sync.Services;
public class SfxPlaybackService
{
@@ -17,7 +18,8 @@ public class SfxPlaybackService
private readonly IPlaybackEngine _playbackEngine;
private readonly AppSettings _appSettings;
- public SfxPlaybackService(ILogger logger, IPlaybackEngine playbackEngine, AppSettings appSettings)
+ public SfxPlaybackService(ILogger logger, IPlaybackEngine playbackEngine,
+ AppSettings appSettings)
{
_logger = logger;
_playbackEngine = playbackEngine;
@@ -116,7 +118,7 @@ public void PlayEffectsAudio(CachedAudio? cachedAudio, float volume, float balan
var volumeProvider = RecyclableSampleProviderFactory.RentVolumeProvider(provider, volume);
var balanceProvider = RecyclableSampleProviderFactory.RentBalanceProvider(volumeProvider, balance,
- _appSettings.Sync.Playback.BalanceMode, AntiClipStrategy.None);
+ _appSettings.Sync.Playback.BalanceMode);
_playbackEngine.EffectMixer.AddMixerInput(balanceProvider);
_logger.LogTrace("Play Dynamic: {Key} (Freq: {Freq:F1})", cachedAudio.SourceHash,
@@ -142,7 +144,7 @@ public void PlayEffectsAudio(CachedAudio? cachedAudio, float volume, float balan
var cachedAudioProvider = RecyclableSampleProviderFactory.RentCacheProvider(cachedAudio);
var volumeProvider = RecyclableSampleProviderFactory.RentVolumeProvider(cachedAudioProvider, volume);
var balanceProvider = RecyclableSampleProviderFactory.RentBalanceProvider(volumeProvider, balance,
- _appSettings.Sync.Playback.BalanceMode, AntiClipStrategy.None); // 削波处理交给MasterLimiterProvider
+ _appSettings.Sync.Playback.BalanceMode);
_playbackEngine.EffectMixer.AddMixerInput(balanceProvider);
}
diff --git a/src/Common/KeyAsio.Shared/Sync/SnareDrumSampleProvider.cs b/src/Core/KeyAsio.Sync/SnareDrumSampleProvider.cs
similarity index 99%
rename from src/Common/KeyAsio.Shared/Sync/SnareDrumSampleProvider.cs
rename to src/Core/KeyAsio.Sync/SnareDrumSampleProvider.cs
index f238f90a..f28a907a 100644
--- a/src/Common/KeyAsio.Shared/Sync/SnareDrumSampleProvider.cs
+++ b/src/Core/KeyAsio.Sync/SnareDrumSampleProvider.cs
@@ -1,8 +1,8 @@
-using System.Runtime.CompilerServices;
+using System.Runtime.CompilerServices;
using KeyAsio.Core.Audio.SampleProviders;
using NAudio.Wave;
-namespace KeyAsio.Shared.Sync;
+namespace KeyAsio.Sync;
///
/// 军鼓生成器
@@ -315,4 +315,4 @@ private float FastNextFloat()
_rngState = x;
return (int)x * 4.6566128752458e-10f;
}
-}
\ No newline at end of file
+}
diff --git a/src/Common/KeyAsio.Shared/OsuMemory/BeatmapIdentifier.cs b/src/Core/KeyAsio.Sync/Sources/BeatmapIdentifier.cs
similarity index 94%
rename from src/Common/KeyAsio.Shared/OsuMemory/BeatmapIdentifier.cs
rename to src/Core/KeyAsio.Sync/Sources/BeatmapIdentifier.cs
index e3c15166..828b766c 100644
--- a/src/Common/KeyAsio.Shared/OsuMemory/BeatmapIdentifier.cs
+++ b/src/Core/KeyAsio.Sync/Sources/BeatmapIdentifier.cs
@@ -1,4 +1,4 @@
-namespace KeyAsio.Shared.OsuMemory;
+namespace KeyAsio.Sync.Sources;
public readonly record struct BeatmapIdentifier
{
@@ -27,4 +27,4 @@ public BeatmapIdentifier(string? folder, string? filename)
public string? Filename { get; }
public static BeatmapIdentifier Default = new();
-}
\ No newline at end of file
+}
diff --git a/src/Common/KeyAsio.Shared/OsuMemory/GameSyncSnapshot.cs b/src/Core/KeyAsio.Sync/Sources/GameSyncSnapshot.cs
similarity index 95%
rename from src/Common/KeyAsio.Shared/OsuMemory/GameSyncSnapshot.cs
rename to src/Core/KeyAsio.Sync/Sources/GameSyncSnapshot.cs
index 49517006..aa627a0f 100644
--- a/src/Common/KeyAsio.Shared/OsuMemory/GameSyncSnapshot.cs
+++ b/src/Core/KeyAsio.Sync/Sources/GameSyncSnapshot.cs
@@ -1,9 +1,9 @@
+using KeyAsio.Configuration.Models;
using KeyAsio.Core.OsuAudio.Hitsounds;
-using KeyAsio.Plugins.Abstractions;
-using KeyAsio.Plugins.Abstractions.OsuMemory;
-using KeyAsio.Shared.Sync;
+using KeyAsio.Plugins.Contracts;
+using KeyAsio.Plugins.Contracts.Sync;
-namespace KeyAsio.Shared.OsuMemory;
+namespace KeyAsio.Sync.Sources;
///
/// Mutable game sync snapshot. A single instance is reused per sync source to
diff --git a/src/Common/KeyAsio.Shared/OsuMemory/GameSyncSourceCoordinator.cs b/src/Core/KeyAsio.Sync/Sources/GameSyncSourceCoordinator.cs
similarity index 97%
rename from src/Common/KeyAsio.Shared/OsuMemory/GameSyncSourceCoordinator.cs
rename to src/Core/KeyAsio.Sync/Sources/GameSyncSourceCoordinator.cs
index 81a87c13..5efe57e1 100644
--- a/src/Common/KeyAsio.Shared/OsuMemory/GameSyncSourceCoordinator.cs
+++ b/src/Core/KeyAsio.Sync/Sources/GameSyncSourceCoordinator.cs
@@ -1,8 +1,7 @@
-using KeyAsio.Plugins.Abstractions.OsuMemory;
-using KeyAsio.Shared.Sync;
+using KeyAsio.Configuration.Models;
using Microsoft.Extensions.Logging;
-namespace KeyAsio.Shared.OsuMemory;
+namespace KeyAsio.Sync.Sources;
public sealed class GameSyncSourceCoordinator
{
@@ -125,6 +124,7 @@ private void ApplySnapshot(GameSyncSnapshot snapshot)
_lastClientType = snapshot.ClientType;
ClientTypeChanged?.Invoke(snapshot.ClientType);
}
+
_syncSessionContext.ProcessId = snapshot.ProcessId;
_syncSessionContext.Username = snapshot.Username;
_syncSessionContext.PlayMods = snapshot.PlayMods;
diff --git a/src/Common/KeyAsio.Shared/OsuMemory/IGameSyncSource.cs b/src/Core/KeyAsio.Sync/Sources/IGameSyncSource.cs
similarity index 84%
rename from src/Common/KeyAsio.Shared/OsuMemory/IGameSyncSource.cs
rename to src/Core/KeyAsio.Sync/Sources/IGameSyncSource.cs
index dd02c7ee..bac797c6 100644
--- a/src/Common/KeyAsio.Shared/OsuMemory/IGameSyncSource.cs
+++ b/src/Core/KeyAsio.Sync/Sources/IGameSyncSource.cs
@@ -1,6 +1,6 @@
-using KeyAsio.Shared.Sync;
+using KeyAsio.Configuration.Models;
-namespace KeyAsio.Shared.OsuMemory;
+namespace KeyAsio.Sync.Sources;
public interface IGameSyncSource
{
diff --git a/src/Common/KeyAsio.Shared/OsuMemory/LazerIpcBridge.cs b/src/Core/KeyAsio.Sync/Sources/LazerIpcBridge.cs
similarity index 99%
rename from src/Common/KeyAsio.Shared/OsuMemory/LazerIpcBridge.cs
rename to src/Core/KeyAsio.Sync/Sources/LazerIpcBridge.cs
index c0d91603..c1932c00 100644
--- a/src/Common/KeyAsio.Shared/OsuMemory/LazerIpcBridge.cs
+++ b/src/Core/KeyAsio.Sync/Sources/LazerIpcBridge.cs
@@ -5,13 +5,15 @@
using KeyAsio.LazerProtocol;
using Microsoft.Extensions.Logging;
-namespace KeyAsio.Shared.OsuMemory;
+namespace KeyAsio.Sync.Sources;
public sealed class LazerIpcBridge : IDisposable
{
public const string PipeName = LazerProtocolConstants.TimingPipeName;
public const string EventPipeName = LazerProtocolConstants.EventPipeName;
+
public const int ProtocolVersion = LazerProtocolConstants.ProtocolVersion;
+
// Event frames may include lazer skin and file metadata; large skin libraries can exceed several MB.
private const int MaxFrameLength = 64 * 1024 * 1024;
diff --git a/src/Common/KeyAsio.Shared/OsuMemory/LazerIpcFrame.cs b/src/Core/KeyAsio.Sync/Sources/LazerIpcFrame.cs
similarity index 98%
rename from src/Common/KeyAsio.Shared/OsuMemory/LazerIpcFrame.cs
rename to src/Core/KeyAsio.Sync/Sources/LazerIpcFrame.cs
index 90021ec4..124a2095 100644
--- a/src/Common/KeyAsio.Shared/OsuMemory/LazerIpcFrame.cs
+++ b/src/Core/KeyAsio.Sync/Sources/LazerIpcFrame.cs
@@ -1,7 +1,7 @@
using KeyAsio.LazerProtocol;
-using KeyAsio.Plugins.Abstractions;
+using KeyAsio.Plugins.Contracts;
-namespace KeyAsio.Shared.OsuMemory;
+namespace KeyAsio.Sync.Sources;
public sealed class LazerIpcFrame
{
@@ -134,4 +134,4 @@ internal static class LazerStatisticsExtensions
{
public static SyncStatistics ToSyncStatistics(this LazerStatistics statistics)
=> new(statistics.Perfect, statistics.Great, statistics.Good, statistics.Ok, statistics.Meh, statistics.Miss);
-}
\ No newline at end of file
+}
diff --git a/src/Common/KeyAsio.Shared/OsuMemory/LazerIpcGameSyncSource.cs b/src/Core/KeyAsio.Sync/Sources/LazerIpcGameSyncSource.cs
similarity index 98%
rename from src/Common/KeyAsio.Shared/OsuMemory/LazerIpcGameSyncSource.cs
rename to src/Core/KeyAsio.Sync/Sources/LazerIpcGameSyncSource.cs
index 22a18323..322f65c9 100644
--- a/src/Common/KeyAsio.Shared/OsuMemory/LazerIpcGameSyncSource.cs
+++ b/src/Core/KeyAsio.Sync/Sources/LazerIpcGameSyncSource.cs
@@ -1,10 +1,10 @@
+using KeyAsio.Configuration.Models;
using KeyAsio.Core.OsuAudio.Hitsounds;
using KeyAsio.LazerProtocol;
-using KeyAsio.Plugins.Abstractions;
-using KeyAsio.Plugins.Abstractions.OsuMemory;
-using KeyAsio.Shared.Sync;
+using KeyAsio.Plugins.Contracts;
+using KeyAsio.Plugins.Contracts.Sync;
-namespace KeyAsio.Shared.OsuMemory;
+namespace KeyAsio.Sync.Sources;
public sealed class LazerIpcGameSyncSource : IGameSyncSource
{
diff --git a/src/Common/KeyAsio.Shared/OsuMemory/MemoryReadObject.cs b/src/Core/KeyAsio.Sync/Sources/MemoryReadObject.cs
similarity index 96%
rename from src/Common/KeyAsio.Shared/OsuMemory/MemoryReadObject.cs
rename to src/Core/KeyAsio.Sync/Sources/MemoryReadObject.cs
index 5eb7767d..292e9eec 100644
--- a/src/Common/KeyAsio.Shared/OsuMemory/MemoryReadObject.cs
+++ b/src/Core/KeyAsio.Sync/Sources/MemoryReadObject.cs
@@ -1,9 +1,9 @@
-using System.Runtime.CompilerServices;
-using KeyAsio.Plugins.Abstractions;
-using KeyAsio.Plugins.Abstractions.OsuMemory;
-using KeyAsio.Shared.Events;
+using System.Runtime.CompilerServices;
+using KeyAsio.Plugins.Contracts;
+using KeyAsio.Plugins.Contracts.Sync;
+using KeyAsio.Sync.Events;
-namespace KeyAsio.Shared.OsuMemory;
+namespace KeyAsio.Sync.Sources;
public class MemoryReadObject
{
diff --git a/src/Common/KeyAsio.Shared/OsuMemory/MemoryScan.cs b/src/Core/KeyAsio.Sync/Sources/MemoryScan.cs
similarity index 99%
rename from src/Common/KeyAsio.Shared/OsuMemory/MemoryScan.cs
rename to src/Core/KeyAsio.Sync/Sources/MemoryScan.cs
index 7b4b13cb..e2ec00eb 100644
--- a/src/Common/KeyAsio.Shared/OsuMemory/MemoryScan.cs
+++ b/src/Core/KeyAsio.Sync/Sources/MemoryScan.cs
@@ -1,14 +1,14 @@
-using System.Diagnostics;
+using System.Diagnostics;
using System.Runtime.CompilerServices;
using Coosu.Shared.IO;
using KeyAsio.Core.Memory;
using KeyAsio.Core.Memory.Configuration;
using KeyAsio.Core.Memory.Utils;
-using KeyAsio.Plugins.Abstractions;
-using KeyAsio.Plugins.Abstractions.OsuMemory;
+using KeyAsio.Plugins.Contracts;
+using KeyAsio.Plugins.Contracts.Sync;
using Microsoft.Extensions.Logging;
-namespace KeyAsio.Shared.OsuMemory;
+namespace KeyAsio.Sync.Sources;
public class MemoryScan
{
diff --git a/src/Common/KeyAsio.Shared/OsuMemory/MemorySyncBridge.cs b/src/Core/KeyAsio.Sync/Sources/MemorySyncBridge.cs
similarity index 97%
rename from src/Common/KeyAsio.Shared/OsuMemory/MemorySyncBridge.cs
rename to src/Core/KeyAsio.Sync/Sources/MemorySyncBridge.cs
index 004962af..019dc0a2 100644
--- a/src/Common/KeyAsio.Shared/OsuMemory/MemorySyncBridge.cs
+++ b/src/Core/KeyAsio.Sync/Sources/MemorySyncBridge.cs
@@ -1,10 +1,10 @@
using System.ComponentModel;
using System.Text;
-using KeyAsio.Shared.Sync;
-using KeyAsio.Shared.Utils;
+using KeyAsio.Common;
+using KeyAsio.Configuration;
using Microsoft.Extensions.Logging;
-namespace KeyAsio.Shared.OsuMemory;
+namespace KeyAsio.Sync.Sources;
public class MemorySyncBridge
{
diff --git a/src/Common/KeyAsio.Shared/OsuMemory/OsuMemoryData.cs b/src/Core/KeyAsio.Sync/Sources/OsuMemoryData.cs
similarity index 96%
rename from src/Common/KeyAsio.Shared/OsuMemory/OsuMemoryData.cs
rename to src/Core/KeyAsio.Sync/Sources/OsuMemoryData.cs
index 215366d3..67a355d4 100644
--- a/src/Common/KeyAsio.Shared/OsuMemory/OsuMemoryData.cs
+++ b/src/Core/KeyAsio.Sync/Sources/OsuMemoryData.cs
@@ -1,4 +1,4 @@
-namespace KeyAsio.Shared.OsuMemory;
+namespace KeyAsio.Sync.Sources;
public record OsuMemoryData
{
@@ -7,6 +7,7 @@ public record OsuMemoryData
//public int SetId { get; set; }
//public string MapString { get; set; } = string.Empty;
public string FolderName { get; set; } = string.Empty;
+
public string OsuFileName { get; set; } = string.Empty;
//public string MD5 { get; set; } = string.Empty;
//public float AR { get; set; }
@@ -17,6 +18,7 @@ public record OsuMemoryData
// General Data
public int RawStatus { get; set; } // OsuStatus
+
//public int GameMode { get; set; }
//public int Retries { get; set; }
//public double TotalAudioTime { get; set; }
@@ -28,6 +30,7 @@ public record OsuMemoryData
// Additional Data
//public bool IsLoggedIn { get; set; }
public string Username { get; set; } = string.Empty;
+
//public string SkinFolder { get; set; } = string.Empty;
public bool IsReplay { get; set; }
public int Score { get; set; }
diff --git a/src/Common/KeyAsio.Shared/OsuMemory/StableMemoryGameSyncSource.cs b/src/Core/KeyAsio.Sync/Sources/StableMemoryGameSyncSource.cs
similarity index 98%
rename from src/Common/KeyAsio.Shared/OsuMemory/StableMemoryGameSyncSource.cs
rename to src/Core/KeyAsio.Sync/Sources/StableMemoryGameSyncSource.cs
index 8f79f669..c3e05cc9 100644
--- a/src/Common/KeyAsio.Shared/OsuMemory/StableMemoryGameSyncSource.cs
+++ b/src/Core/KeyAsio.Sync/Sources/StableMemoryGameSyncSource.cs
@@ -1,6 +1,6 @@
-using KeyAsio.Shared.Sync;
+using KeyAsio.Configuration.Models;
-namespace KeyAsio.Shared.OsuMemory;
+namespace KeyAsio.Sync.Sources;
public sealed class StableMemoryGameSyncSource : IGameSyncSource
{
diff --git a/src/Common/KeyAsio.Shared/Sync/States/BrowsingState.cs b/src/Core/KeyAsio.Sync/States/BrowsingState.cs
similarity index 85%
rename from src/Common/KeyAsio.Shared/Sync/States/BrowsingState.cs
rename to src/Core/KeyAsio.Sync/States/BrowsingState.cs
index 121e11a2..7d310085 100644
--- a/src/Common/KeyAsio.Shared/Sync/States/BrowsingState.cs
+++ b/src/Core/KeyAsio.Sync/States/BrowsingState.cs
@@ -1,8 +1,8 @@
-using KeyAsio.Plugins.Abstractions.OsuMemory;
-using KeyAsio.Shared.OsuMemory;
-using KeyAsio.Shared.Sync.Services;
+using KeyAsio.Plugins.Contracts.Sync;
+using KeyAsio.Sync.Services;
+using KeyAsio.Sync.Sources;
-namespace KeyAsio.Shared.Sync.States;
+namespace KeyAsio.Sync.States;
public class BrowsingState : IGameState
{
@@ -38,4 +38,4 @@ public void OnBeatmapChanged(SyncSessionContext ctx, BeatmapIdentifier beatmap)
public void OnModsChanged(SyncSessionContext ctx, Mods oldMods, Mods newMods)
{
}
-}
\ No newline at end of file
+}
diff --git a/src/Common/KeyAsio.Shared/Sync/States/IGameState.cs b/src/Core/KeyAsio.Sync/States/IGameState.cs
similarity index 80%
rename from src/Common/KeyAsio.Shared/Sync/States/IGameState.cs
rename to src/Core/KeyAsio.Sync/States/IGameState.cs
index 636d3bfd..dccedf44 100644
--- a/src/Common/KeyAsio.Shared/Sync/States/IGameState.cs
+++ b/src/Core/KeyAsio.Sync/States/IGameState.cs
@@ -1,7 +1,7 @@
-using KeyAsio.Plugins.Abstractions.OsuMemory;
-using KeyAsio.Shared.OsuMemory;
+using KeyAsio.Plugins.Contracts.Sync;
+using KeyAsio.Sync.Sources;
-namespace KeyAsio.Shared.Sync.States;
+namespace KeyAsio.Sync.States;
public interface IGameState
{
@@ -13,4 +13,4 @@ public interface IGameState
void OnComboChanged(SyncSessionContext ctx, int oldCombo, int newCombo);
void OnBeatmapChanged(SyncSessionContext ctx, BeatmapIdentifier beatmap);
void OnModsChanged(SyncSessionContext ctx, Mods oldMods, Mods newMods);
-}
\ No newline at end of file
+}
diff --git a/src/Common/KeyAsio.Shared/Sync/States/NotRunningState.cs b/src/Core/KeyAsio.Sync/States/NotRunningState.cs
similarity index 85%
rename from src/Common/KeyAsio.Shared/Sync/States/NotRunningState.cs
rename to src/Core/KeyAsio.Sync/States/NotRunningState.cs
index eef48366..2f042cf5 100644
--- a/src/Common/KeyAsio.Shared/Sync/States/NotRunningState.cs
+++ b/src/Core/KeyAsio.Sync/States/NotRunningState.cs
@@ -1,7 +1,7 @@
-using KeyAsio.Plugins.Abstractions.OsuMemory;
-using KeyAsio.Shared.OsuMemory;
+using KeyAsio.Plugins.Contracts.Sync;
+using KeyAsio.Sync.Sources;
-namespace KeyAsio.Shared.Sync.States;
+namespace KeyAsio.Sync.States;
public class NotRunningState : IGameState
{
@@ -33,4 +33,4 @@ public void OnBeatmapChanged(SyncSessionContext ctx, BeatmapIdentifier beatmap)
public void OnModsChanged(SyncSessionContext ctx, Mods oldMods, Mods newMods)
{
}
-}
\ No newline at end of file
+}
diff --git a/src/Common/KeyAsio.Shared/Sync/States/PlayingState.cs b/src/Core/KeyAsio.Sync/States/PlayingState.cs
similarity index 93%
rename from src/Common/KeyAsio.Shared/Sync/States/PlayingState.cs
rename to src/Core/KeyAsio.Sync/States/PlayingState.cs
index 19c2ac22..28a3e3ad 100644
--- a/src/Common/KeyAsio.Shared/Sync/States/PlayingState.cs
+++ b/src/Core/KeyAsio.Sync/States/PlayingState.cs
@@ -1,14 +1,17 @@
using System.Diagnostics;
using Coosu.Beatmap.Sections.GamePlay;
+using KeyAsio.Configuration;
+using KeyAsio.Configuration.Models;
using KeyAsio.Core.Audio;
using KeyAsio.Core.OsuAudio.Hitsounds.Playback;
-using KeyAsio.Plugins.Abstractions.OsuMemory;
-using KeyAsio.Shared.Models;
-using KeyAsio.Shared.OsuMemory;
-using KeyAsio.Shared.Sync.Services;
+using KeyAsio.Plugins.Contracts.Sync;
+using KeyAsio.Sync.Abstractions;
+using KeyAsio.Sync.Models;
+using KeyAsio.Sync.Services;
+using KeyAsio.Sync.Sources;
using Microsoft.Extensions.Logging;
-namespace KeyAsio.Shared.Sync.States;
+namespace KeyAsio.Sync.States;
public class PlayingState : IGameState
{
@@ -18,7 +21,7 @@ public class PlayingState : IGameState
private readonly IPlaybackEngine _playbackEngine;
private readonly BeatmapHitsoundLoader _beatmapHitsoundLoader;
private readonly SfxPlaybackService _sfxPlaybackService;
- private readonly SharedViewModel _sharedViewModel;
+ private readonly IPlaybackRuntimeState _runtimeState;
private readonly GameplaySessionManager _gameplaySessionManager;
private readonly GameplayAudioService _gameplayAudioService;
private readonly List _playbackBuffer = new(64);
@@ -35,7 +38,7 @@ public PlayingState(
IPlaybackEngine playbackEngine,
BeatmapHitsoundLoader beatmapHitsoundLoader,
SfxPlaybackService sfxPlaybackService,
- SharedViewModel sharedViewModel,
+ IPlaybackRuntimeState runtimeState,
GameplaySessionManager gameplaySessionManager,
GameplayAudioService gameplayAudioService)
{
@@ -43,7 +46,7 @@ public PlayingState(
_playbackEngine = playbackEngine;
_beatmapHitsoundLoader = beatmapHitsoundLoader;
_sfxPlaybackService = sfxPlaybackService;
- _sharedViewModel = sharedViewModel;
+ _runtimeState = runtimeState;
_gameplaySessionManager = gameplaySessionManager;
_gameplayAudioService = gameplayAudioService;
@@ -185,7 +188,7 @@ private void SyncHitsounds(SyncSessionContext ctx, int newMs)
private void PlayAutoPlaybackIfNeeded(SyncSessionContext ctx)
{
- if (!_sharedViewModel.AutoMode &&
+ if (!_runtimeState.AutoMode &&
(ctx.PlayMods & Mods.Autoplay) == 0 &&
(ctx.PlayMods & Mods.Relax) == 0 &&
!ctx.IsReplay) return;
@@ -213,7 +216,8 @@ private void PlayManualPlaybackIfNeeded(SyncSessionContext ctx)
if (_gameplaySessionManager.OsuFile.General.Mode == GameMode.Mania &&
playbackObject.PlaybackEvent is SampleEvent { Layer: SampleLayer.Sampling })
{
- _sfxPlaybackService.DispatchPlayback(playbackObject, playbackObject.PlaybackEvent.Volume * 0.866666666f);
+ _sfxPlaybackService.DispatchPlayback(playbackObject,
+ playbackObject.PlaybackEvent.Volume * 0.866666666f);
continue;
}
diff --git a/src/Common/KeyAsio.Shared/Sync/States/ResultsState.cs b/src/Core/KeyAsio.Sync/States/ResultsState.cs
similarity index 85%
rename from src/Common/KeyAsio.Shared/Sync/States/ResultsState.cs
rename to src/Core/KeyAsio.Sync/States/ResultsState.cs
index 6f1f9ca1..094a241c 100644
--- a/src/Common/KeyAsio.Shared/Sync/States/ResultsState.cs
+++ b/src/Core/KeyAsio.Sync/States/ResultsState.cs
@@ -1,7 +1,7 @@
-using KeyAsio.Plugins.Abstractions.OsuMemory;
-using KeyAsio.Shared.OsuMemory;
+using KeyAsio.Plugins.Contracts.Sync;
+using KeyAsio.Sync.Sources;
-namespace KeyAsio.Shared.Sync.States;
+namespace KeyAsio.Sync.States;
public class ResultsState : IGameState
{
@@ -33,4 +33,4 @@ public void OnBeatmapChanged(SyncSessionContext ctx, BeatmapIdentifier beatmap)
public void OnModsChanged(SyncSessionContext ctx, Mods oldMods, Mods newMods)
{
}
-}
\ No newline at end of file
+}
diff --git a/src/Common/KeyAsio.Shared/Sync/SyncContextWrapper.cs b/src/Core/KeyAsio.Sync/SyncContextWrapper.cs
similarity index 92%
rename from src/Common/KeyAsio.Shared/Sync/SyncContextWrapper.cs
rename to src/Core/KeyAsio.Sync/SyncContextWrapper.cs
index aa9cbfe8..53a87baa 100644
--- a/src/Common/KeyAsio.Shared/Sync/SyncContextWrapper.cs
+++ b/src/Core/KeyAsio.Sync/SyncContextWrapper.cs
@@ -1,7 +1,7 @@
-using KeyAsio.Plugins.Abstractions;
-using KeyAsio.Shared.OsuMemory;
+using KeyAsio.Plugins.Contracts;
+using KeyAsio.Sync.Sources;
-namespace KeyAsio.Shared.Sync;
+namespace KeyAsio.Sync;
public class SyncContextWrapper : ISyncContext
{
diff --git a/src/Common/KeyAsio.Shared/Sync/SyncController.cs b/src/Core/KeyAsio.Sync/SyncController.cs
similarity index 91%
rename from src/Common/KeyAsio.Shared/Sync/SyncController.cs
rename to src/Core/KeyAsio.Sync/SyncController.cs
index 355f1ffe..71edb3f6 100644
--- a/src/Common/KeyAsio.Shared/Sync/SyncController.cs
+++ b/src/Core/KeyAsio.Sync/SyncController.cs
@@ -1,17 +1,17 @@
using System.Diagnostics;
+using KeyAsio.Configuration;
using KeyAsio.Core.Audio;
using KeyAsio.Core.Memory.Utils;
-using KeyAsio.Plugins.Abstractions;
-using KeyAsio.Plugins.Abstractions.OsuMemory;
-using KeyAsio.Shared.Models;
-using KeyAsio.Shared.OsuMemory;
-using KeyAsio.Shared.Sync.AudioProviders;
-using KeyAsio.Shared.Sync.Services;
-using KeyAsio.Shared.Sync.States;
-using Microsoft.Extensions.DependencyInjection;
+using KeyAsio.Plugins.Contracts;
+using KeyAsio.Plugins.Contracts.Sync;
+using KeyAsio.Sync.Abstractions;
+using KeyAsio.Sync.AudioProviders;
+using KeyAsio.Sync.Services;
+using KeyAsio.Sync.Sources;
+using KeyAsio.Sync.States;
using Microsoft.Extensions.Logging;
-namespace KeyAsio.Shared.Sync;
+namespace KeyAsio.Sync;
public class SyncController : IDisposable
{
@@ -22,11 +22,14 @@ public class SyncController : IDisposable
private readonly List _activeSyncPlugins;
public SyncController(ILogger playingStateLogger,
+ ILogger standardSequencerLogger,
+ ILogger taikoSequencerLogger,
+ ILogger maniaSequencerLogger,
+ ILogger catchSequencerLogger,
ILogger logger,
- IServiceProvider serviceProvider,
AppSettings appSettings,
IPlaybackEngine playbackEngine,
- SharedViewModel sharedViewModel,
+ IPlaybackRuntimeState runtimeState,
GameplayAudioService gameplayAudioService,
BeatmapHitsoundLoader beatmapHitsoundLoader,
SfxPlaybackService sfxPlaybackService,
@@ -46,24 +49,25 @@ public SyncController(ILogger playingStateLogger,
_syncSessionContext.OnPlayModsChanged = OnPlayModsChanged;
var standardAudioProvider = new StandardHitsoundSequencer(
- serviceProvider.GetRequiredService>(),
+ standardSequencerLogger,
appSettings, syncSessionContext, playbackEngine, gameplayAudioService, gameplaySessionManager);
var taikoAudioProvider = new TaikoHitsoundSequencer(
- serviceProvider.GetRequiredService>(),
+ taikoSequencerLogger,
appSettings, syncSessionContext, playbackEngine, gameplayAudioService, gameplaySessionManager);
var maniaAudioProvider = new ManiaHitsoundSequencer(
- serviceProvider.GetRequiredService>(),
+ maniaSequencerLogger,
appSettings, syncSessionContext, playbackEngine, gameplayAudioService, gameplaySessionManager);
var catchAudioProvider = new CatchHitsoundSequencer(
- serviceProvider.GetRequiredService>(),
+ catchSequencerLogger,
appSettings, syncSessionContext, playbackEngine, gameplayAudioService, gameplaySessionManager);
- gameplaySessionManager.InitializeProviders(standardAudioProvider, taikoAudioProvider, catchAudioProvider, maniaAudioProvider);
+ gameplaySessionManager.InitializeProviders(standardAudioProvider, taikoAudioProvider, catchAudioProvider,
+ maniaAudioProvider);
// Initialize realtime state machine with scene mappings
_stateMachine = new GameStateMachine(new Dictionary
{
[OsuMemoryStatus.Playing] = new PlayingState(playingStateLogger, appSettings, playbackEngine,
- beatmapHitsoundLoader, sfxPlaybackService, sharedViewModel, gameplaySessionManager,
+ beatmapHitsoundLoader, sfxPlaybackService, runtimeState, gameplaySessionManager,
gameplayAudioService),
[OsuMemoryStatus.ResultsScreen] = new ResultsState(),
[OsuMemoryStatus.NotRunning] = new NotRunningState(),
@@ -340,4 +344,4 @@ public void Dispose()
{
Stop();
}
-}
\ No newline at end of file
+}
diff --git a/src/Common/KeyAsio.Shared/Sync/SyncSessionContext.cs b/src/Core/KeyAsio.Sync/SyncSessionContext.cs
similarity index 96%
rename from src/Common/KeyAsio.Shared/Sync/SyncSessionContext.cs
rename to src/Core/KeyAsio.Sync/SyncSessionContext.cs
index c2b369ab..300fc185 100644
--- a/src/Common/KeyAsio.Shared/Sync/SyncSessionContext.cs
+++ b/src/Core/KeyAsio.Sync/SyncSessionContext.cs
@@ -1,20 +1,16 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
+using KeyAsio.Common;
+using KeyAsio.Configuration;
+using KeyAsio.Configuration.Models;
using KeyAsio.Core.OsuAudio.Hitsounds;
-using KeyAsio.Plugins.Abstractions;
-using KeyAsio.Plugins.Abstractions.OsuMemory;
-using KeyAsio.Shared.Events;
-using KeyAsio.Shared.OsuMemory;
-using KeyAsio.Shared.Utils;
+using KeyAsio.Plugins.Contracts;
+using KeyAsio.Plugins.Contracts.Sync;
+using KeyAsio.Sync.Events;
+using KeyAsio.Sync.Sources;
-namespace KeyAsio.Shared.Sync;
-
-public enum GameClientType
-{
- Stable, // osu!stable: 仅在 Playing 状态应用倍率
- Lazer // osu!lazer: 任何时候都应用倍率
-}
+namespace KeyAsio.Sync;
public class SyncSessionContext
{
@@ -54,6 +50,7 @@ public SyncSessionContext(AppSettings appSettings)
/// 由 PlayTime 的单调性保护逻辑自动维护:当预测时间持续微小倒退时为 true,恢复正常前进时为 false。
///
public bool IsAudioPaused => _isFrozen;
+
public int ProcessId { get; set; } = -1;
public string? Username
diff --git a/tests/AudioTests/AudioTests.csproj b/tests/AudioTests/AudioTests.csproj
index ef067ae9..c2282ce0 100644
--- a/tests/AudioTests/AudioTests.csproj
+++ b/tests/AudioTests/AudioTests.csproj
@@ -1,4 +1,4 @@
-
+
net10.0
@@ -11,13 +11,13 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/tests/AudioTests/LoopProviderTests.cs b/tests/AudioTests/LoopProviderTests.cs
new file mode 100644
index 00000000..f2664e0c
--- /dev/null
+++ b/tests/AudioTests/LoopProviderTests.cs
@@ -0,0 +1,70 @@
+using KeyAsio.Core.Audio;
+using KeyAsio.Core.Audio.Caching;
+using KeyAsio.Core.Audio.SampleProviders;
+using KeyAsio.Core.Audio.SampleProviders.BalancePans;
+using Microsoft.Extensions.Logging.Abstractions;
+using NAudio.Wave;
+
+namespace AudioTests;
+
+public sealed class LoopProviderTests
+{
+ [Fact]
+ public async Task LoopSampleProvider_PauseKeepsSourcePosition()
+ {
+ var cachedAudio = await LoadCachedAudioAsync();
+ var source = new CachedAudioProvider(cachedAudio);
+ var loop = new LoopSampleProvider(source);
+ var buffer = new float[256];
+
+ Assert.Equal(buffer.Length, loop.Read(buffer, 0, buffer.Length));
+ var positionBeforePause = source.PlayTime;
+
+ Array.Fill(buffer, 1f);
+ loop.IsPaused = true;
+
+ Assert.Equal(QueueMixingSampleProvider.SignalKeepAlive, loop.Read(buffer, 0, buffer.Length));
+ Assert.Equal(positionBeforePause, source.PlayTime);
+ Assert.All(buffer, sample => Assert.Equal(0f, sample));
+
+ loop.IsPaused = false;
+
+ Assert.Equal(buffer.Length, loop.Read(buffer, 0, buffer.Length));
+ Assert.True(source.PlayTime > positionBeforePause);
+ }
+
+ [Fact]
+ public async Task LoopProviderManager_PauseAndResumeKeepsMixerInputAlive()
+ {
+ var cachedAudio = await LoadCachedAudioAsync();
+ var waveFormat = WaveFormat.CreateIeeeFloatWaveFormat(
+ cachedAudio.WaveFormat.SampleRate,
+ cachedAudio.WaveFormat.Channels);
+ var mixer = new QueueMixingSampleProvider(waveFormat);
+ var manager = new LoopProviderManager();
+ var buffer = new float[256];
+
+ manager.Create(1, cachedAudio, mixer, 1f, 0f, BalanceMode.Off);
+ Assert.Equal(buffer.Length, mixer.Read(buffer, 0, buffer.Length));
+
+ manager.PauseAll(mixer);
+ Array.Fill(buffer, 1f);
+
+ Assert.Equal(buffer.Length, mixer.Read(buffer, 0, buffer.Length));
+ Assert.All(buffer, sample => Assert.Equal(0f, sample));
+
+ manager.RecoverAll(mixer);
+ Array.Clear(buffer);
+
+ Assert.Equal(buffer.Length, mixer.Read(buffer, 0, buffer.Length));
+ Assert.Contains(buffer, sample => sample != 0f);
+ }
+
+ private static async Task LoadCachedAudioAsync()
+ {
+ var audioCacheManager = new AudioCacheManager(NullLogger.Instance);
+ var filePath = Path.Combine(AppContext.BaseDirectory, "files", "normal-hitnormal.wav");
+ var result = await audioCacheManager.GetOrCreateOrEmptyFromFileAsync(filePath, new WaveFormat(48000, 2));
+ return Assert.IsType(result.CachedAudio);
+ }
+}
diff --git a/tests/ColumnTest/ColumnTest.csproj b/tests/ColumnTest/ColumnTest.csproj
index 089894e7..403f92a5 100644
--- a/tests/ColumnTest/ColumnTest.csproj
+++ b/tests/ColumnTest/ColumnTest.csproj
@@ -1,4 +1,4 @@
-
+
Exe
@@ -9,7 +9,9 @@
-
+
+
+
diff --git a/tests/ColumnTest/Program.cs b/tests/ColumnTest/Program.cs
index c56f9e78..fdd0bd99 100644
--- a/tests/ColumnTest/Program.cs
+++ b/tests/ColumnTest/Program.cs
@@ -3,20 +3,20 @@
using KeyAsio.Core.Audio;
using KeyAsio.Core.OsuAudio.Hitsounds;
using KeyAsio.Core.OsuAudio.Hitsounds.Playback;
-using KeyAsio.Plugins.Abstractions;
-using KeyAsio.Shared;
-using KeyAsio.Shared.Models;
-using KeyAsio.Shared.Plugins;
-using KeyAsio.Shared.Sync;
-using KeyAsio.Shared.Sync.AudioProviders;
-using KeyAsio.Shared.Sync.Services;
+using KeyAsio.Plugins.Contracts;
+using KeyAsio.Configuration;
+using KeyAsio.Application.Models;
+using KeyAsio.Application.Plugins;
+using KeyAsio.Sync;
+using KeyAsio.Sync.AudioProviders;
+using KeyAsio.Sync.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
var services = new ServiceCollection();
var appSettings = new AppSettings();
services.AddSingleton(appSettings);
-services.AddSingleton();
+services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
diff --git a/tests/KeyAsio.UnitTests/AsyncSequentialWorkerTests.cs b/tests/KeyAsio.UnitTests/AsyncSequentialWorkerTests.cs
new file mode 100644
index 00000000..94cc80a2
--- /dev/null
+++ b/tests/KeyAsio.UnitTests/AsyncSequentialWorkerTests.cs
@@ -0,0 +1,72 @@
+using KeyAsio.Common;
+
+namespace KeyAsio.UnitTests;
+
+public sealed class AsyncSequentialWorkerTests
+{
+ [Fact]
+ public async Task EnqueueAsync_ExecutesWorkInSubmissionOrder()
+ {
+ await using var worker = new AsyncSequentialWorker();
+ var firstStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var releaseFirst = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ List order = [];
+
+ var first = worker.EnqueueAsync(async () =>
+ {
+ order.Add(1);
+ firstStarted.SetResult();
+ await releaseFirst.Task;
+ order.Add(2);
+ });
+ await firstStarted.Task;
+
+ var second = worker.EnqueueAsync(() =>
+ {
+ order.Add(3);
+ return Task.CompletedTask;
+ });
+ releaseFirst.SetResult();
+
+ await Task.WhenAll(first, second);
+ Assert.Equal([1, 2, 3], order);
+ }
+
+ [Fact]
+ public async Task BoundedWorker_RejectsSynchronousWriteWhenQueueIsFull()
+ {
+ await using var worker = new AsyncSequentialWorker(capacity: 1);
+ var firstStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var releaseFirst = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ worker.Enqueue(async () =>
+ {
+ firstStarted.SetResult();
+ await releaseFirst.Task;
+ });
+ await firstStarted.Task;
+ worker.Enqueue(() => Task.CompletedTask);
+
+ Assert.Throws(() => worker.Enqueue(() => Task.CompletedTask));
+ releaseFirst.SetResult();
+ }
+
+ [Fact]
+ public async Task DisposeAsync_CancelsRunningAndPendingWork()
+ {
+ var worker = new AsyncSequentialWorker(capacity: 1);
+ var firstStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var first = worker.EnqueueAsync(async cancellationToken =>
+ {
+ firstStarted.SetResult();
+ await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
+ });
+ await firstStarted.Task;
+ var pending = worker.EnqueueAsync(_ => Task.CompletedTask);
+
+ await worker.DisposeAsync();
+
+ await Assert.ThrowsAnyAsync(() => first);
+ await Assert.ThrowsAnyAsync(() => pending);
+ }
+}
diff --git a/tests/KeyAsio.UnitTests/AudioDeviceOperationCoordinatorTests.cs b/tests/KeyAsio.UnitTests/AudioDeviceOperationCoordinatorTests.cs
new file mode 100644
index 00000000..e044a1f3
--- /dev/null
+++ b/tests/KeyAsio.UnitTests/AudioDeviceOperationCoordinatorTests.cs
@@ -0,0 +1,196 @@
+using KeyAsio.Configuration;
+using KeyAsio.Core.Audio;
+using KeyAsio.Services;
+using KeyAsio.Sync.Abstractions;
+using Microsoft.Extensions.Logging;
+using Moq;
+using NAudio.Wave;
+
+namespace KeyAsio.UnitTests;
+
+public sealed class AudioDeviceOperationCoordinatorTests
+{
+ [Fact]
+ public async Task ApplyAsync_StartsBeforeCommittingSettings()
+ {
+ var previous = Device("previous");
+ var requested = Device("requested");
+ var settings = Settings(previous, 44100);
+ var engine = new PlaybackEngineHarness(previous, 44100);
+ var persistence = new Mock();
+ var audioCache = new Mock();
+ var events = engine.Events;
+ persistence.Setup(x => x.Save()).Callback(() => events.Add("save"));
+ audioCache.Setup(x => x.ClearCaches()).Callback(() => events.Add("clear"));
+
+ using var coordinator = CreateCoordinator(settings, persistence, engine, audioCache);
+ var result = await coordinator.ApplyAsync(requested, 96000, TestContext.Current.CancellationToken);
+
+ Assert.True(result.IsSuccess);
+ Assert.Equal(requested, settings.Audio.PlaybackDevice);
+ Assert.Equal(96000, settings.Audio.SampleRate);
+ Assert.Equal(["stop:previous", "start:requested", "save", "clear"], events);
+ Assert.Equal(requested, result.ActiveDevice);
+ }
+
+ [Fact]
+ public async Task ApplyAsync_WhenStartFails_RestoresDeviceAndSettings()
+ {
+ var previous = Device("previous");
+ var requested = Device("requested");
+ var settings = Settings(previous, 44100);
+ var engine = new PlaybackEngineHarness(previous, 44100)
+ {
+ StartFailure = device => device == requested ? new InvalidOperationException("cannot start") : null
+ };
+ var persistence = new Mock();
+ var audioCache = new Mock();
+
+ using var coordinator = CreateCoordinator(settings, persistence, engine, audioCache);
+ var result = await coordinator.ApplyAsync(requested, 96000, TestContext.Current.CancellationToken);
+
+ Assert.False(result.IsSuccess);
+ Assert.True(result.WasRolledBack);
+ Assert.Equal(previous, settings.Audio.PlaybackDevice);
+ Assert.Equal(44100, settings.Audio.SampleRate);
+ Assert.Equal(previous, result.ActiveDevice);
+ Assert.Equal(["stop:previous", "start:requested", "start:previous"], engine.Events);
+ persistence.Verify(x => x.Save(), Times.Never);
+ audioCache.Verify(x => x.ClearCaches(), Times.Once);
+ }
+
+ [Fact]
+ public async Task ApplyAsync_WhenCommitFails_RestoresPersistedAndRuntimeState()
+ {
+ var previous = Device("previous");
+ var requested = Device("requested");
+ var settings = Settings(previous, 48000);
+ var engine = new PlaybackEngineHarness(previous, 48000);
+ var persistence = new Mock();
+ var saveAttempt = 0;
+ persistence.Setup(x => x.Save()).Callback(() =>
+ {
+ if (Interlocked.Increment(ref saveAttempt) == 1)
+ {
+ throw new IOException("write failed");
+ }
+ });
+ var audioCache = new Mock();
+
+ using var coordinator = CreateCoordinator(settings, persistence, engine, audioCache);
+ var result = await coordinator.ApplyAsync(requested, 96000, TestContext.Current.CancellationToken);
+
+ Assert.False(result.IsSuccess);
+ Assert.True(result.WasRolledBack);
+ Assert.Equal(previous, settings.Audio.PlaybackDevice);
+ Assert.Equal(48000, settings.Audio.SampleRate);
+ Assert.Equal(previous, result.ActiveDevice);
+ persistence.Verify(x => x.Save(), Times.Exactly(2));
+ }
+
+ [Fact]
+ public async Task ApplyAsync_WhenStoppingFailsTransiently_RetriesThenApplies()
+ {
+ var previous = Device("previous");
+ var requested = Device("requested");
+ var settings = Settings(previous, 44100);
+ var engine = new PlaybackEngineHarness(previous, 44100)
+ {
+ StopFailuresRemaining = 2
+ };
+ var persistence = new Mock();
+ var audioCache = new Mock();
+
+ using var coordinator = CreateCoordinator(settings, persistence, engine, audioCache);
+ var result = await coordinator.ApplyAsync(requested, 48000, TestContext.Current.CancellationToken);
+
+ Assert.True(result.IsSuccess);
+ Assert.Equal(requested, result.ActiveDevice);
+ Assert.Equal(
+ ["stop:previous", "stop:previous", "stop:previous", "start:requested"],
+ engine.Events);
+ persistence.Verify(x => x.Save(), Times.Once);
+ audioCache.Verify(x => x.ClearCaches(), Times.Once);
+ }
+
+ private static AudioDeviceOperationCoordinator CreateCoordinator(
+ AppSettings settings,
+ Mock persistence,
+ PlaybackEngineHarness engine,
+ Mock audioCache) =>
+ new(
+ settings,
+ persistence.Object,
+ engine.Engine.Object,
+ audioCache.Object,
+ Mock.Of>());
+
+ private static AppSettings Settings(DeviceDescription device, int sampleRate) => new()
+ {
+ Audio = new AppSettingsAudio
+ {
+ PlaybackDevice = device,
+ SampleRate = sampleRate
+ }
+ };
+
+ private static DeviceDescription Device(string name) => new()
+ {
+ WavePlayerType = WavePlayerType.WASAPI,
+ DeviceId = name,
+ FriendlyName = name
+ };
+
+ private sealed class PlaybackEngineHarness
+ {
+ private readonly IWavePlayer _device = Mock.Of();
+ private IWavePlayer? _currentDevice;
+ private DeviceDescription? _description;
+ private WaveFormat _sourceFormat;
+
+ public PlaybackEngineHarness(DeviceDescription? initialDevice, int initialSampleRate)
+ {
+ _currentDevice = initialDevice is null ? null : _device;
+ _description = initialDevice;
+ _sourceFormat = new WaveFormat(initialSampleRate, 2);
+
+ Engine.SetupGet(x => x.CurrentDevice).Returns(() => _currentDevice);
+ Engine.SetupGet(x => x.CurrentDeviceDescription).Returns(() => _description);
+ Engine.SetupGet(x => x.SourceWaveFormat).Returns(() => _sourceFormat);
+ Engine.Setup(x => x.StopDevice()).Callback(() =>
+ {
+ Events.Add($"stop:{_description?.DeviceId}");
+ if (StopFailuresRemaining > 0)
+ {
+ StopFailuresRemaining--;
+ throw new IOException("transient stop failure");
+ }
+
+ _currentDevice = null;
+ _description = null;
+ });
+ Engine.Setup(x => x.StartDevice(It.IsAny(), It.IsAny()))
+ .Callback((DeviceDescription? device, WaveFormat? format) =>
+ {
+ Events.Add($"start:{device?.DeviceId}");
+ var failure = StartFailure?.Invoke(device);
+ if (failure is not null)
+ {
+ throw failure;
+ }
+
+ _currentDevice = device is null ? null : _device;
+ _description = device;
+ if (format is not null)
+ {
+ _sourceFormat = format;
+ }
+ });
+ }
+
+ public Mock Engine { get; } = new();
+ public List Events { get; } = [];
+ public Func? StartFailure { get; init; }
+ public int StopFailuresRemaining { get; set; }
+ }
+}
diff --git a/tests/KeyAsio.UnitTests/GameSyncSourceCoordinatorTests.cs b/tests/KeyAsio.UnitTests/GameSyncSourceCoordinatorTests.cs
index 95769234..b49f21e6 100644
--- a/tests/KeyAsio.UnitTests/GameSyncSourceCoordinatorTests.cs
+++ b/tests/KeyAsio.UnitTests/GameSyncSourceCoordinatorTests.cs
@@ -1,8 +1,9 @@
-using KeyAsio.Plugins.Abstractions;
-using KeyAsio.Plugins.Abstractions.OsuMemory;
-using KeyAsio.Shared;
-using KeyAsio.Shared.OsuMemory;
-using KeyAsio.Shared.Sync;
+using KeyAsio.Plugins.Contracts;
+using KeyAsio.Plugins.Contracts.Sync;
+using KeyAsio.Configuration;
+using KeyAsio.Configuration.Models;
+using KeyAsio.Sync.Sources;
+using KeyAsio.Sync;
using Microsoft.Extensions.Logging.Abstractions;
namespace KeyAsio.UnitTests;
diff --git a/tests/KeyAsio.UnitTests/KeyAsio.UnitTests.csproj b/tests/KeyAsio.UnitTests/KeyAsio.UnitTests.csproj
index f884dd41..832cdfa6 100644
--- a/tests/KeyAsio.UnitTests/KeyAsio.UnitTests.csproj
+++ b/tests/KeyAsio.UnitTests/KeyAsio.UnitTests.csproj
@@ -9,15 +9,15 @@
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -25,6 +25,7 @@
+
diff --git a/tests/KeyAsio.UnitTests/TestAppBuilder.cs b/tests/KeyAsio.UnitTests/TestAppBuilder.cs
index dda5c4c7..f9027b6e 100644
--- a/tests/KeyAsio.UnitTests/TestAppBuilder.cs
+++ b/tests/KeyAsio.UnitTests/TestAppBuilder.cs
@@ -6,7 +6,7 @@
namespace KeyAsio.UnitTests;
-public class TestApp : Application
+public class TestApp : Avalonia.Application
{
}
@@ -14,4 +14,4 @@ public class TestAppBuilder
{
public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure()
.UseHeadless(new AvaloniaHeadlessPlatformOptions());
-}
\ No newline at end of file
+}
diff --git a/tests/KeyAsio.UnitTests/WizardAudioConfigViewModelTests.cs b/tests/KeyAsio.UnitTests/WizardAudioConfigViewModelTests.cs
index 685e9c90..779d3b52 100644
--- a/tests/KeyAsio.UnitTests/WizardAudioConfigViewModelTests.cs
+++ b/tests/KeyAsio.UnitTests/WizardAudioConfigViewModelTests.cs
@@ -1,10 +1,10 @@
-using Avalonia.Headless.XUnit;
+using Avalonia.Headless.XUnit;
using Avalonia.Threading;
using KeyAsio.Core.Audio;
-using KeyAsio.Shared;
+using KeyAsio.Configuration;
+using KeyAsio.Services;
using KeyAsio.ViewModels;
using Moq;
-using NAudio.Wave;
using SukiUI.Toasts;
namespace KeyAsio.UnitTests;
@@ -12,27 +12,34 @@ namespace KeyAsio.UnitTests;
public class WizardAudioConfigViewModelTests
{
private readonly Mock _mockDeviceManager;
- private readonly Mock _mockPlaybackEngine;
+ private readonly Mock _mockDeviceOperations;
private readonly Mock _mockToastManager;
private readonly AppSettings _appSettings;
public WizardAudioConfigViewModelTests()
{
_mockDeviceManager = new Mock();
- _mockPlaybackEngine = new Mock();
+ _mockDeviceOperations = new Mock();
_mockToastManager = new Mock();
_appSettings = new AppSettings();
// Default setup for device manager
_mockDeviceManager.Setup(m => m.GetCachedAvailableDevicesAsync())
.ReturnsAsync(new List());
+ _mockDeviceOperations
+ .Setup(x => x.ApplyAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .Returns((DeviceDescription? device, int sampleRate, CancellationToken cancellationToken) =>
+ Task.FromResult(new AudioDeviceOperationResult(true, device)));
}
private WizardAudioConfigViewModel CreateViewModel()
{
return new WizardAudioConfigViewModel(
_mockDeviceManager.Object,
- _mockPlaybackEngine.Object,
+ _mockDeviceOperations.Object,
_mockToastManager.Object,
_appSettings);
}
@@ -173,7 +180,7 @@ public void LoadDevices_FiltersByDriverType()
}
[AvaloniaFact]
- public void TryGoForward_Configuration_Success()
+ public async Task TryGoForward_Configuration_Success()
{
// Arrange
var vm = CreateViewModel();
@@ -183,19 +190,20 @@ public void TryGoForward_Configuration_Success()
vm.SelectedAudioDevice = device;
// Act
- bool result = vm.TryGoForward();
+ bool result = await vm.TryGoForwardAsync();
// Assert
Assert.True(result);
Assert.Equal(AudioSubStep.Validation, vm.CurrentAudioSubStep);
Assert.True(vm.ValidationSuccess);
- Assert.Equal(device, _appSettings.Audio.PlaybackDevice);
- _mockPlaybackEngine.Verify(e => e.StartDevice(device,
- It.Is(f => f.SampleRate == _appSettings.Audio.SampleRate && f.Channels == 2)), Times.Once);
+ _mockDeviceOperations.Verify(x => x.ApplyAsync(
+ device,
+ _appSettings.Audio.SampleRate,
+ It.IsAny()), Times.Once);
}
[AvaloniaFact]
- public void TryGoForward_Configuration_Failure()
+ public async Task TryGoForward_Configuration_Failure()
{
// Arrange
var vm = CreateViewModel();
@@ -203,11 +211,15 @@ public void TryGoForward_Configuration_Failure()
var device = new DeviceDescription { WavePlayerType = WavePlayerType.ASIO };
vm.SelectedAudioDevice = device;
- _mockPlaybackEngine.Setup(e => e.StartDevice(It.IsAny(), It.IsAny()))
- .Throws(new Exception("Fail"));
+ _mockDeviceOperations
+ .Setup(x => x.ApplyAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .ReturnsAsync(new AudioDeviceOperationResult(false, null, new Exception("Fail")));
// Act
- bool result = vm.TryGoForward();
+ bool result = await vm.TryGoForwardAsync();
// Assert
Assert.True(result);
@@ -238,7 +250,7 @@ public void VirtualDriver_Detection_Success()
}
[AvaloniaFact]
- public void Integration_HardwareFlow_Full()
+ public async Task Integration_HardwareFlow_Full()
{
// Arrange
var asioDevice = new DeviceDescription { WavePlayerType = WavePlayerType.ASIO, FriendlyName = "Real ASIO" };
@@ -258,17 +270,17 @@ public void Integration_HardwareFlow_Full()
Assert.True(vm.CanGoForward);
// 3. Go Forward (Validation)
- vm.TryGoForward();
+ await vm.TryGoForwardAsync();
Assert.Equal(AudioSubStep.Validation, vm.CurrentAudioSubStep);
Assert.True(vm.ValidationSuccess);
// 4. Go Forward (Finish)
- bool result = vm.TryGoForward();
+ bool result = await vm.TryGoForwardAsync();
Assert.False(result); // Should return false to indicate proceeding to next main wizard step
}
[AvaloniaFact]
- public void Integration_ProMixSoftwareFlow_Full()
+ public async Task Integration_ProMixSoftwareFlow_Full()
{
// Arrange
var cableDevice = new DeviceDescription
@@ -296,12 +308,12 @@ public void Integration_ProMixSoftwareFlow_Full()
Assert.True(vm.CanGoForward);
// 5. Go Forward (Validation)
- vm.TryGoForward();
+ await vm.TryGoForwardAsync();
Assert.Equal(AudioSubStep.Validation, vm.CurrentAudioSubStep);
Assert.True(vm.ValidationSuccess);
// 6. Go Forward (Finish)
- bool result = vm.TryGoForward();
+ bool result = await vm.TryGoForwardAsync();
Assert.False(result);
}
}
diff --git a/tests/MemoryReadingTest/App.axaml.cs b/tests/MemoryReadingTest/App.axaml.cs
index 8eca3329..aa6433d8 100644
--- a/tests/MemoryReadingTest/App.axaml.cs
+++ b/tests/MemoryReadingTest/App.axaml.cs
@@ -1,11 +1,11 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
-using KeyAsio.Shared;
-using KeyAsio.Shared.Models;
-using KeyAsio.Shared.OsuMemory;
-using KeyAsio.Shared.Sync;
-using KeyAsio.Shared.Sync.Services;
+using KeyAsio.Configuration;
+using KeyAsio.Application.Models;
+using KeyAsio.Sync.Sources;
+using KeyAsio.Sync;
+using KeyAsio.Sync.Services;
using Microsoft.Extensions.DependencyInjection;
using NLog.Extensions.Logging;
diff --git a/tests/MemoryReadingTest/AppBootstrapper.cs b/tests/MemoryReadingTest/AppBootstrapper.cs
index 43b9ecb9..6a0f2be8 100644
--- a/tests/MemoryReadingTest/AppBootstrapper.cs
+++ b/tests/MemoryReadingTest/AppBootstrapper.cs
@@ -1,10 +1,10 @@
-using KeyAsio.Shared;
-using KeyAsio.Shared.Models;
-using KeyAsio.Shared.OsuMemory;
-using KeyAsio.Shared.Sync;
-using KeyAsio.Shared.Sync.Services;
-using KeyAsio.Plugins.Abstractions;
-using KeyAsio.Shared.Plugins;
+using KeyAsio.Configuration;
+using KeyAsio.Application.Models;
+using KeyAsio.Sync.Sources;
+using KeyAsio.Sync;
+using KeyAsio.Sync.Services;
+using KeyAsio.Plugins.Contracts;
+using KeyAsio.Application.Plugins;
using Microsoft.Extensions.DependencyInjection;
using NLog.Extensions.Logging;
using System;
@@ -18,7 +18,7 @@ public static IServiceProvider InitServices()
var services = new ServiceCollection();
services.AddNLog();
services.AddSingleton(new AppSettings());
- services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
diff --git a/tests/MemoryReadingTest/Charts/PlayTimeChart.axaml.cs b/tests/MemoryReadingTest/Charts/PlayTimeChart.axaml.cs
index 28ee72ba..3171bc95 100644
--- a/tests/MemoryReadingTest/Charts/PlayTimeChart.axaml.cs
+++ b/tests/MemoryReadingTest/Charts/PlayTimeChart.axaml.cs
@@ -1,8 +1,8 @@
-using System.Collections.ObjectModel;
+using System.Collections.ObjectModel;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Threading;
-using KeyAsio.Shared.Models;
+using KeyAsio.Configuration.Models;
using LiveChartsCore;
using LiveChartsCore.Defaults;
using LiveChartsCore.SkiaSharpView;
diff --git a/tests/MemoryReadingTest/MemoryReadingTest.csproj b/tests/MemoryReadingTest/MemoryReadingTest.csproj
index b8296e0e..e67b3af3 100644
--- a/tests/MemoryReadingTest/MemoryReadingTest.csproj
+++ b/tests/MemoryReadingTest/MemoryReadingTest.csproj
@@ -14,17 +14,20 @@
-
-
-
-
+
+
+
+
-
-
+
+
+
-
+
+
+
diff --git a/tests/MemoryReadingTest/Program.cs b/tests/MemoryReadingTest/Program.cs
index 838d3236..14a1022f 100644
--- a/tests/MemoryReadingTest/Program.cs
+++ b/tests/MemoryReadingTest/Program.cs
@@ -1,5 +1,5 @@
using Avalonia;
-using KeyAsio.Shared.OsuMemory;
+using KeyAsio.Sync.Sources;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
diff --git a/tests/PlayingTests/PlayingTests.csproj b/tests/PlayingTests/PlayingTests.csproj
index c2c1c0c5..1fdd654f 100644
--- a/tests/PlayingTests/PlayingTests.csproj
+++ b/tests/PlayingTests/PlayingTests.csproj
@@ -1,4 +1,4 @@
-
+
Exe
@@ -9,11 +9,13 @@
-
+
-
+
+
+
diff --git a/tests/PlayingTests/Program.cs b/tests/PlayingTests/Program.cs
index f56bdd71..b3fa4063 100644
--- a/tests/PlayingTests/Program.cs
+++ b/tests/PlayingTests/Program.cs
@@ -1,4 +1,4 @@
-// See https://aka.ms/new-console-template for more information
+// See https://aka.ms/new-console-template for more information
using System;
using System.IO;
@@ -8,14 +8,14 @@
using KeyAsio.Core.Audio;
using KeyAsio.Core.Audio.Caching;
using KeyAsio.Core.Audio.Utils;
-using KeyAsio.Plugins.Abstractions;
-using KeyAsio.Plugins.Abstractions.OsuMemory;
-using KeyAsio.Shared;
-using KeyAsio.Shared.Models;
-using KeyAsio.Shared.OsuMemory;
-using KeyAsio.Shared.Plugins;
-using KeyAsio.Shared.Sync;
-using KeyAsio.Shared.Sync.Services;
+using KeyAsio.Plugins.Contracts;
+using KeyAsio.Plugins.Contracts.Sync;
+using KeyAsio.Configuration;
+using KeyAsio.Application.Models;
+using KeyAsio.Sync.Sources;
+using KeyAsio.Application.Plugins;
+using KeyAsio.Sync;
+using KeyAsio.Sync.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Milki.Extensions.Configuration;
@@ -41,14 +41,14 @@ public static async Task Main(string[] args)
var services = new ServiceCollection();
services.AddSingleton(appSettings);
services.AddSingleton();
- services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
var provider = services.BuildServiceProvider();
- var sharedViewModel = provider.GetRequiredService();
+ var sharedViewModel = provider.GetRequiredService();
sharedViewModel.AutoMode = true;
var audioEngine = provider.GetRequiredService();
diff --git a/tests/StressTest/Program.cs b/tests/StressTest/Program.cs
index 4cae344c..88b3c981 100644
--- a/tests/StressTest/Program.cs
+++ b/tests/StressTest/Program.cs
@@ -5,14 +5,15 @@
using KeyAsio.Core.Audio.Caching;
using KeyAsio.Core.Audio.SampleProviders;
using KeyAsio.Core.OsuAudio.Hitsounds.Playback;
-using KeyAsio.Plugins.Abstractions;
-using KeyAsio.Shared;
-using KeyAsio.Shared.Models;
-using KeyAsio.Shared.Plugins;
-using KeyAsio.Shared.Services;
-using KeyAsio.Shared.Sync;
-using KeyAsio.Shared.Sync.Services;
-using KeyAsio.Shared.Sync.States;
+using KeyAsio.Plugins.Contracts;
+using KeyAsio.Configuration;
+using KeyAsio.Application.Models;
+using KeyAsio.Application.Plugins;
+using KeyAsio.Application.Services;
+using KeyAsio.Sync;
+using KeyAsio.Sync.Services;
+using KeyAsio.Sync.Models;
+using KeyAsio.Sync.States;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NAudio.Wave;
@@ -37,7 +38,7 @@ public static async Task Main(string[] args)
services.AddSingleton(); // We will manipulate this instance later
// Register Sync Module Services
- services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
@@ -65,7 +66,7 @@ public static async Task Main(string[] args)
audioEngine,
provider.GetRequiredService(),
provider.GetRequiredService(),
- provider.GetRequiredService(),
+ provider.GetRequiredService(),
gameplaySessionManager,
gameplayAudioService
);
@@ -93,32 +94,32 @@ public static async Task Main(string[] args)
// Let's manually create them.
var loggerFactory = provider.GetRequiredService();
- var maniaSequencer = new KeyAsio.Shared.Sync.AudioProviders.ManiaHitsoundSequencer(
- loggerFactory.CreateLogger(),
+ var maniaSequencer = new KeyAsio.Sync.AudioProviders.ManiaHitsoundSequencer(
+ loggerFactory.CreateLogger(),
provider.GetRequiredService(),
ctx,
audioEngine,
gameplayAudioService,
gameplaySessionManager
);
- var stdSequencer = new KeyAsio.Shared.Sync.AudioProviders.StandardHitsoundSequencer(
- loggerFactory.CreateLogger(),
+ var stdSequencer = new KeyAsio.Sync.AudioProviders.StandardHitsoundSequencer(
+ loggerFactory.CreateLogger(),
provider.GetRequiredService(),
ctx,
audioEngine,
gameplayAudioService,
gameplaySessionManager
);
- var taikoSequencer = new KeyAsio.Shared.Sync.AudioProviders.TaikoHitsoundSequencer(
- loggerFactory.CreateLogger(),
+ var taikoSequencer = new KeyAsio.Sync.AudioProviders.TaikoHitsoundSequencer(
+ loggerFactory.CreateLogger(),
provider.GetRequiredService(),
ctx,
audioEngine,
gameplayAudioService,
gameplaySessionManager
);
- var catchSequencer = new KeyAsio.Shared.Sync.AudioProviders.CatchHitsoundSequencer(
- loggerFactory.CreateLogger(),
+ var catchSequencer = new KeyAsio.Sync.AudioProviders.CatchHitsoundSequencer(
+ loggerFactory.CreateLogger(),
provider.GetRequiredService(),
ctx,
audioEngine,
@@ -370,4 +371,4 @@ public void Dispose()
{
Stop();
}
-}
\ No newline at end of file
+}
diff --git a/tests/StressTest/StressTest.csproj b/tests/StressTest/StressTest.csproj
index ad80e2e3..b9966e8f 100644
--- a/tests/StressTest/StressTest.csproj
+++ b/tests/StressTest/StressTest.csproj
@@ -9,11 +9,13 @@
-
+
+
+
-
+