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 @@ - + + + - +