diff --git a/Dependences/GLWpfControl/GLWpfControl.csproj b/Dependences/GLWpfControl/GLWpfControl.csproj index 2eab979e..efcb02ce 100644 --- a/Dependences/GLWpfControl/GLWpfControl.csproj +++ b/Dependences/GLWpfControl/GLWpfControl.csproj @@ -11,7 +11,7 @@ - + diff --git a/Dependences/gemini b/Dependences/gemini index d8933b77..1339d4ff 160000 --- a/Dependences/gemini +++ b/Dependences/gemini @@ -1 +1 @@ -Subproject commit d8933b771fe4b7e68b6187d092d50ebbb1207252 +Subproject commit 1339d4ffb4e30d613ae274aea2f695196bcda959 diff --git a/OngekiFumenEditor/App.config b/OngekiFumenEditor/App.config index 168cc600..38e652d1 100644 --- a/OngekiFumenEditor/App.config +++ b/OngekiFumenEditor/App.config @@ -51,6 +51,9 @@ 1 + + True + @@ -88,6 +91,18 @@ True + + + + + True + + + True + + + False + diff --git a/OngekiFumenEditor/App.xaml b/OngekiFumenEditor/App.xaml index 3cee5a38..8efd065a 100644 --- a/OngekiFumenEditor/App.xaml +++ b/OngekiFumenEditor/App.xaml @@ -23,8 +23,15 @@ + + + + + + + diff --git a/OngekiFumenEditor/App.xaml.cs b/OngekiFumenEditor/App.xaml.cs index a037296e..3733928b 100644 --- a/OngekiFumenEditor/App.xaml.cs +++ b/OngekiFumenEditor/App.xaml.cs @@ -18,11 +18,14 @@ namespace OngekiFumenEditor /// public partial class App : Application { - public App() + public bool IsGUIMode { get; } + + public App(bool isGUIMode = true) { AppDomain.CurrentDomain.AssemblyResolve += OnSatelliteAssemblyResolve; // 设置工作目录为执行文件所在的目录 Directory.SetCurrentDirectory(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)); + IsGUIMode = isGUIMode; } private Assembly OnSatelliteAssemblyResolve(object sender, ResolveEventArgs args) diff --git a/OngekiFumenEditor/AppBootstrapper.cs b/OngekiFumenEditor/AppBootstrapper.cs index 098e3009..10ff7d7f 100644 --- a/OngekiFumenEditor/AppBootstrapper.cs +++ b/OngekiFumenEditor/AppBootstrapper.cs @@ -9,6 +9,7 @@ using System.Security.Principal; using System.Text; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; @@ -19,7 +20,9 @@ using Gemini.Modules.Output; using OngekiFumenEditor.Kernel.ArgProcesser; using OngekiFumenEditor.Kernel.Audio; +using OngekiFumenEditor.Kernel.CommandExecutor; using OngekiFumenEditor.Kernel.EditorLayout; +using OngekiFumenEditor.Kernel.ProgramUpdater; using OngekiFumenEditor.Kernel.Scheduler; using OngekiFumenEditor.Modules.AudioPlayerToolViewer; using OngekiFumenEditor.Modules.FumenVisualEditor.Base; @@ -29,6 +32,7 @@ using OngekiFumenEditor.Utils; using OngekiFumenEditor.Utils.DeadHandler; using OngekiFumenEditor.Utils.Logs.DefaultImpls; +using SevenZip.Compression.LZ; #if !DEBUG using System.Runtime.InteropServices; using System.Threading; @@ -52,6 +56,13 @@ public AppBootstrapper(bool useApplication = true) : base(useApplication) { } + private bool? isGUIMode = null; + public bool IsGUIMode + { + get => isGUIMode ?? ((App.Current as App)?.IsGUIMode ?? false); + set => isGUIMode = value; + } + protected override void BindServices(CompositionBatch batch) { base.BindServices(batch); @@ -158,17 +169,63 @@ private bool CheckIfAdminPermission() return principal.IsInRole(WindowsBuiltInRole.Administrator); } - protected override async void OnStartup(object sender, StartupEventArgs e) + protected override void OnStartup(object sender, StartupEventArgs e) + { + var isGUIMode = (App.Current as App)?.IsGUIMode ?? false; + + if (isGUIMode) + { + OnStartupForGUI(sender, e); + } + else + { + OnStartupForCMD(sender, e); + } + } + + public async void OnStartupForCMD(object sender, StartupEventArgs e) { + IsGUIMode = false; + Log.Instance.RemoveOutput(); + + await IoC.Get().Init(); + + var executor = IoC.Get(); + + try + { + Application.Current.Shutdown(await executor.Execute(e.Args)); + } + catch (Exception ex) + { + Log.LogError($"Unhandled exception processing arguments:\n{ex.Message}"); + Application.Current.Shutdown(1); + } + } + + public async void OnStartupForGUI(object sender, StartupEventArgs e) + { + IsGUIMode = true; + +#if DEBUG + ConsoleWindowHelper.SetConsoleWindowVisible(true); +#else + ConsoleWindowHelper.SetConsoleWindowVisible(ProgramSetting.Default.ShowConsoleWindowInGUIMode); +#endif + InitExceptionCatcher(); LogBaseInfos(); InitIPCServer(); await IoC.Get().Init(); - try { + try + { + //process command args await IoC.Get().ProcessArgs(e.Args); - } catch (Exception ex) { + } + catch (Exception ex) + { await Console.Error.WriteLineAsync($"Unhandled exception processing arguments:\n{ex.Message}"); Application.Current.Shutdown(-1); return; @@ -186,11 +243,6 @@ protected override async void OnStartup(object sender, StartupEventArgs e) curProc.PriorityBoostEnabled = true; } - ShowStartupGUI(); - } - - private void OnStartupForGUI() - { //overwrite ViewLocator var locateForModel = ViewLocator.LocateForModel; ViewLocator.LocateForModel = (model, hostControl, ctx) => @@ -205,12 +257,10 @@ private void OnStartupForGUI() if (CheckIfAdminPermission()) { Log.LogWarn("Program is within admin permission."); - IoC.Get().TitleContent = "(以管理员权限运行)"; - } - else - { - IoC.Get().TitleContent = ""; + var prevSuffix = IoC.Get().TitleSuffix; + IoC.Get().TitleSuffix = prevSuffix + "(以管理员权限运行)"; } + IoC.Get().UpdateWindowTitle(); IoC.Get().ToolBars.Visible = true; @@ -222,18 +272,26 @@ private void OnStartupForGUI() Log.LogInfo(IoC.Get().MainContentViewModel.Message = "Application is Ready."); + await DisplayRootViewForAsync(); + if (Application.MainWindow is Window window) { window.AllowDrop = true; window.Drop += MainWindow_Drop; - } - } - public async void ShowStartupGUI() - { - OnStartupForGUI(); + //program will forget position/size when it has been called as commandline. + //so we have to remember and restore windows' position/size manually. + window.Closed += MainWindow_Closed; + if (!string.IsNullOrWhiteSpace(ProgramSetting.Default.WindowSizePositionLastTime)) + { + var arr = ProgramSetting.Default.WindowSizePositionLastTime.Split(",").Select(x => double.Parse(x.Trim())).ToArray(); + window.Left = arr[0]; + window.Top = arr[1]; + window.Width = arr[2]; + window.Height = arr[3]; + } + } - await DisplayRootViewForAsync(); var showSplashWindow = IoC.Get().Documents.IsEmpty() && !ProgramSetting.Default.DisableShowSplashScreenAfterBoot; if (showSplashWindow) @@ -251,6 +309,25 @@ public async void ShowStartupGUI() ProgramSetting.Default.IsFirstTimeOpenEditor = false; ProgramSetting.Default.Save(); } + + IoC.Get().CheckUpdatable().NoWait(); + } + + private void MainWindow_Closed(object sender, EventArgs e) + { + if (sender is not Window mainWindow) + return; + + ProgramSetting.Default.WindowSizePositionLastTime = string.Join(", ", new[] { + mainWindow.Left, + mainWindow.Top, + mainWindow.Width, + mainWindow.Height + }); + ProgramSetting.Default.Save(); + Log.LogInfo($"WindowSizePositionLastTime = {ProgramSetting.Default.WindowSizePositionLastTime}"); + + App.Current.Shutdown(); } private void InitIPCServer() @@ -275,15 +352,15 @@ private void InitIPCServer() try { - var line = (await IPCHelper.ReadLineAsync(cancelToken))?.Trim(); + var line = IPCHelper.ReadLine(cancelToken)?.Trim(); if (string.IsNullOrWhiteSpace(line)) continue; Log.LogDebug($"Recv line by IPC:{line}"); if (line.StartsWith("CMD:")) { var args = JsonSerializer.Deserialize(line[4..]).Args; - await Application.Current.Dispatcher.InvokeAsync(() => - IoC.Get().ProcessArgs(args)); + Application.Current.Dispatcher.Invoke(() => + IoC.Get().ProcessArgs(args)).NoWait(); } } catch (Exception e) diff --git a/OngekiFumenEditor/Base/EditorObjects/Svg/SvgPrefabBase.cs b/OngekiFumenEditor/Base/EditorObjects/Svg/SvgPrefabBase.cs index 550730f0..3ea01343 100644 --- a/OngekiFumenEditor/Base/EditorObjects/Svg/SvgPrefabBase.cs +++ b/OngekiFumenEditor/Base/EditorObjects/Svg/SvgPrefabBase.cs @@ -28,7 +28,7 @@ public bool IsForceColorful set => Set(ref isForceColorful, value); } - private ColorId colorfulLaneColor = ColorIdConst.LaneGreen; + private ColorId colorfulLaneColor = ColorIdConst.Yuzu; public ColorId ColorfulLaneColor { get => colorfulLaneColor; diff --git a/OngekiFumenEditor/Base/OngekiObjects/ColorIdConst.cs b/OngekiFumenEditor/Base/OngekiObjects/ColorIdConst.cs index 6b70aefa..8908f720 100644 --- a/OngekiFumenEditor/Base/OngekiObjects/ColorIdConst.cs +++ b/OngekiFumenEditor/Base/OngekiObjects/ColorIdConst.cs @@ -145,28 +145,6 @@ public static class ColorIdConst Color = Color.FromArgb(255, 71, 145, 255) }; - public static ColorId LaneRed { get; } = new ColorId() - { - Id = 1020, - Name = nameof(LaneRed), - Color = Color.FromArgb(255, 255, 0, 0) - }; - - public static ColorId LaneGreen { get; } = new ColorId() - { - Id = 1021, - Name = nameof(LaneGreen), - Color = Color.FromArgb(255, 0, 255, 0) - }; - - public static ColorId LaneBlue { get; } = new ColorId() - { - Id = 1022, - Name = nameof(LaneBlue), - Color = Color.FromArgb(255, 0, 0, 255) - }; - - public static IEnumerable AllColors { get; } = new[] { Akari, @@ -188,9 +166,6 @@ public static class ColorIdConst Black, Akane, Aoi, - LaneRed, - LaneGreen,LaneG, - LaneBlue, }; } } diff --git a/OngekiFumenEditor/Kernel/ArgProcesser/DefaultArgProcessManager.cs b/OngekiFumenEditor/Kernel/ArgProcesser/DefaultArgProcessManager.cs new file mode 100644 index 00000000..36949282 --- /dev/null +++ b/OngekiFumenEditor/Kernel/ArgProcesser/DefaultArgProcessManager.cs @@ -0,0 +1,72 @@ +using Caliburn.Micro; +using OngekiFumenEditor.Base; +using OngekiFumenEditor.Kernel.Audio; +using OngekiFumenEditor.Modules.FumenVisualEditor; +using OngekiFumenEditor.Modules.PreviewSvgGenerator; +using OngekiFumenEditor.Parser; +using OngekiFumenEditor.Properties; +using OngekiFumenEditor.Utils; +using OngekiFumenEditor.Utils.Logs.DefaultImpls; +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Parsing; +using System.ComponentModel.Composition; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using System.Windows; +using OngekiFumenEditor.Modules.FumenConverter; +using OngekiFumenEditor.Modules.FumenConverter.Kernel; +using OngekiFumenEditor.Modules.OptionGeneratorTools.Base; +using OngekiFumenEditor.Modules.OptionGeneratorTools.Kernel; +using OngekiFumenEditor.Modules.OptionGeneratorTools.Models; +using Expression = System.Linq.Expressions.Expression; +using OngekiFumenEditor.Kernel.CommandExecutor.Attributes; +using OngekiFumenEditor.UI.Dialogs; + +namespace OngekiFumenEditor.Kernel.ArgProcesser +{ + [Export(typeof(IProgramArgProcessManager))] + internal class DefaultArgProcessManager : IProgramArgProcessManager + { + public async Task ProcessArgs(string[] args) + { + if (args.Length == 0) + return; + + //if args[0] is openable file likes .ogkr/.nyagekiProj/.nyageki ... + if (args.IsOnlyOne(out var filePath)) + { + if (File.Exists(filePath)) + { + Log.LogInfo($"arg.filePath: {filePath}"); + + _ = Application.Current.Dispatcher.Invoke(async () => + { + if (await DocumentOpenHelper.TryOpenAsDocument(filePath)) + Application.Current?.MainWindow?.Focus(); + }); + } + } + + if (args.Contains("--notifySucess", StringComparer.InvariantCultureIgnoreCase)) + { + Version sourceVersion = default; + for (int i = 0; i < args.Length; i++) + { + if ("--sourceVersion".Equals(args[i], StringComparison.InvariantCultureIgnoreCase) && Version.TryParse(args.ElementAtOrDefault(i + 1), out var sv)) + sourceVersion = sv; + } + Application.Current.Dispatcher.Invoke(async () => + { + //wait for styles/resources have been loaded. + while (Application.Current.MainWindow is not Window mainWindow) + await Task.Delay(100); + new AboutWindow(true, sourceVersion).Show(); + }).NoWait(); + } + } + } +} diff --git a/OngekiFumenEditor/Kernel/ArgProcesser/IProgramArgProcessManager.cs b/OngekiFumenEditor/Kernel/ArgProcesser/IProgramArgProcessManager.cs index d77d2425..25c68606 100644 --- a/OngekiFumenEditor/Kernel/ArgProcesser/IProgramArgProcessManager.cs +++ b/OngekiFumenEditor/Kernel/ArgProcesser/IProgramArgProcessManager.cs @@ -5,5 +5,5 @@ namespace OngekiFumenEditor.Kernel.ArgProcesser public interface IProgramArgProcessManager { Task ProcessArgs(string[] args); - } + } } diff --git a/OngekiFumenEditor/Kernel/Audio/DefaultCommonImpl/Sound/DefaultFumenSoundPlayer.cs b/OngekiFumenEditor/Kernel/Audio/DefaultCommonImpl/Sound/DefaultFumenSoundPlayer.cs index 56287ecd..d3092e87 100644 --- a/OngekiFumenEditor/Kernel/Audio/DefaultCommonImpl/Sound/DefaultFumenSoundPlayer.cs +++ b/OngekiFumenEditor/Kernel/Audio/DefaultCommonImpl/Sound/DefaultFumenSoundPlayer.cs @@ -1,43 +1,44 @@ -using Caliburn.Micro; +using Caliburn.Micro; using IntervalTree; -using OngekiFumenEditor.Base; -using OngekiFumenEditor.Base.OngekiObjects; +using OngekiFumenEditor.Base; +using OngekiFumenEditor.Base.OngekiObjects; using OngekiFumenEditor.Base.OngekiObjects.Beam; using OngekiFumenEditor.Kernel.Audio.NAudioImpl.Sound; -using OngekiFumenEditor.Modules.FumenVisualEditor; -using OngekiFumenEditor.Modules.FumenVisualEditor.ViewModels; -using OngekiFumenEditor.Properties; -using OngekiFumenEditor.Utils; -using OngekiFumenEditor.Utils.ObjectPool; -using System; -using System.Collections.Generic; -using System.ComponentModel.Composition; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using System.Windows; - -namespace OngekiFumenEditor.Kernel.Audio.DefaultCommonImpl.Sound +using OngekiFumenEditor.Modules.FumenVisualEditor; +using OngekiFumenEditor.Modules.FumenVisualEditor.ViewModels; +using OngekiFumenEditor.Properties; +using OngekiFumenEditor.Utils; +using OngekiFumenEditor.Utils.ObjectPool; +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; + +namespace OngekiFumenEditor.Kernel.Audio.DefaultCommonImpl.Sound { - [Export(typeof(IFumenSoundPlayer))] - public partial class DefaultFumenSoundPlayer : PropertyChangedBase, IFumenSoundPlayer, IDisposable - { - private record MeterAction(TimeSpan Time, TimeSpan BeatInterval, int BeatCount, bool isSkip); + [Export(typeof(IFumenSoundPlayer))] + public partial class DefaultFumenSoundPlayer : PropertyChangedBase, IFumenSoundPlayer, IDisposable + { + private record MeterAction(TimeSpan Time, TimeSpan BeatInterval, int BeatCount, bool isSkip); - private IntervalTree durationEvents = new(); - private HashSet currentPlayingDurationEvents = new(); - private object locker = new object(); + private IntervalTree durationEvents = new(); + private HashSet currentPlayingDurationEvents = new(); + private object locker = new object(); - private LinkedList events = new(); - private LinkedListNode itor; + private LinkedList events = new(); + private LinkedListNode itor; - private LinkedList meterActions = new(); - private LinkedListNode meterActionsItor; - private int currentMeterHitCount = 0; + private LinkedList meterActions = new(); + private LinkedListNode meterActionsItor; + private int currentMeterHitCount = 0; - private AbortableThread thread; + private AbortableThread thread; private IAudioPlayer player; private FumenVisualEditorViewModel editor; @@ -46,101 +47,102 @@ private record MeterAction(TimeSpan Time, TimeSpan BeatInterval, int BeatCount, private static int loopIdGen = 0; private Stopwatch stopwatch = new(); - public SoundControl SoundControl { get; set; } = SoundControl.All; + public SoundControl SoundControl { get; set; } = SoundControl.All; - private float volume = 1; - public float Volume - { - get => volume; - set - { - Set(ref volume, value); - } - } - - private Dictionary cacheSounds = new(); - private Task loadTask; - - public DefaultFumenSoundPlayer() - { - InitSounds(); - } - - private async void InitSounds() - { - var source = new TaskCompletionSource(); - loadTask = source.Task; - var audioManager = IoC.Get(); - - var soundFolderPath = AudioSetting.Default.SoundFolderPath; - if (!Directory.Exists(soundFolderPath)) - { - var msg = Resources.ErrorSoundFolderNotFound; - MessageBox.Show(msg); - Log.LogError(msg); - source.SetResult(false); - return; - } - else - Log.LogInfo($"SoundFolderPath : {soundFolderPath} , fullpath : {Path.GetFullPath(soundFolderPath)}"); - - bool noError = true; - - async Task load(SoundControl sound, string fileName) - { - var fixFilePath = Path.Combine(soundFolderPath, fileName); + private float volume = 1; + public float Volume + { + get => volume; + set + { + Set(ref volume, value); + } + } + + private Dictionary cacheSounds = new(); + private Task loadTask; + + public DefaultFumenSoundPlayer() + { + InitSounds(); + } + + private async void InitSounds() + { + var source = new TaskCompletionSource(); + loadTask = source.Task; + var audioManager = IoC.Get(); + + var soundFolderPath = AudioSetting.Default.SoundFolderPath; + if (!Directory.Exists(soundFolderPath)) + { + var msg = Resources.ErrorSoundFolderNotFound; + MessageBox.Show(msg); + Log.LogError(msg); + source.SetResult(false); + return; + } + else + Log.LogInfo($"SoundFolderPath : {soundFolderPath} , fullpath : {Path.GetFullPath(soundFolderPath)}"); - try - { - cacheSounds[sound] = await audioManager.LoadSoundAsync(fixFilePath); - } - catch (Exception e) - { - Log.LogError($"Can't load {sound} sound file : {fixFilePath} , reason : {e.Message}"); - noError = false; - } - } + bool noError = true; - cacheSounds.Clear(); - await load(SoundControl.Tap, "tap.wav"); - await load(SoundControl.Bell, "bell.wav"); - await load(SoundControl.CriticalTap, "extap.wav"); - await load(SoundControl.WallTap, "wall.wav"); - await load(SoundControl.CriticalWallTap, "exwall.wav"); - await load(SoundControl.Flick, "flick.wav"); - await load(SoundControl.Bullet, "bullet.wav"); - await load(SoundControl.CriticalFlick, "exflick.wav"); - await load(SoundControl.HoldEnd, "holdend.wav"); - await load(SoundControl.ClickSE, "clickse.wav"); - await load(SoundControl.HoldTick, "holdtick.wav"); - await load(SoundControl.BeamPrepare, "beamprepare.wav"); - await load(SoundControl.BeamLoop, "beamlooping.wav"); - await load(SoundControl.BeamEnd, "beamend.wav"); - await load(SoundControl.MetronomeStrongBeat, "metronomeStrongBeat.wav"); - await load(SoundControl.MetronomeWeakBeat, "metronomeWeakBeat.wav"); - - if (!noError) - { - MessageBox.Show(Resources.WarnSomeSoundsNotLoad); - source.SetResult(false); - return; - } + async Task load(SoundControl sound, string fileName) + { + var fixFilePath = Path.Combine(soundFolderPath, fileName); + + try + { + cacheSounds[sound] = await audioManager.LoadSoundAsync(fixFilePath); + } + catch (Exception e) + { + Log.LogError($"Can't load {sound} sound file : {fixFilePath} , reason : {e.Message}"); + noError = false; + } + } + + cacheSounds.Clear(); + await load(SoundControl.Tap, "tap.wav"); + await load(SoundControl.Bell, "bell.wav"); + await load(SoundControl.CriticalTap, "extap.wav"); + await load(SoundControl.WallTap, "wall.wav"); + await load(SoundControl.CriticalWallTap, "exwall.wav"); + await load(SoundControl.Flick, "flick.wav"); + await load(SoundControl.Bullet, "bullet.wav"); + await load(SoundControl.CriticalFlick, "exflick.wav"); + await load(SoundControl.HoldEnd, "holdend.wav"); + await load(SoundControl.ClickSE, "clickse.wav"); + await load(SoundControl.HoldTick, "holdtick.wav"); + await load(SoundControl.BeamPrepare, "beamprepare.wav"); + await load(SoundControl.BeamLoop, "beamlooping.wav"); + await load(SoundControl.BeamEnd, "beamend.wav"); + await load(SoundControl.MetronomeStrongBeat, "metronomeStrongBeat.wav"); + await load(SoundControl.MetronomeWeakBeat, "metronomeWeakBeat.wav"); + await load(SoundControl.BossWave, "bossWave.wav"); + + if (!noError) + { + MessageBox.Show(Resources.WarnSomeSoundsNotLoad); + source.SetResult(false); + return; + } - source.SetResult(true); - } + source.SetResult(true); + } - public async Task Prepare(FumenVisualEditorViewModel editor, IAudioPlayer player) - { - await loadTask; + public async Task Prepare(FumenVisualEditorViewModel editor, IAudioPlayer player) + { + await loadTask; - if (thread is not null) - { - thread.Abort(); - thread = null; - } + if (thread is not null) + { + thread.Abort(); + thread = null; + } - this.player = player; - this.editor = editor; + this.player = player; + this.editor = editor; RebuildEvents(); stopwatch.Restart(); @@ -150,287 +152,288 @@ public async Task Prepare(FumenVisualEditorViewModel editor, IAudioPlayer player thread.Start(); } - private static IEnumerable CalculateHoldTicks(Hold x, OngekiFumen fumen) - { - int? CalcHoldTickStepSizeA() - { - //calculate stepGrid - var met = fumen.MeterChanges.GetMeter(x.TGrid); - var bpm = fumen.BpmList.GetBpm(x.TGrid); - var resT = bpm.TGrid.ResT; - var beatCount = met.Bunbo; - if (beatCount == 0) - return null; - return (int)(resT / beatCount); - } - - if (CalcHoldTickStepSizeA() is not int lengthPerBeat) - yield break; - var stepGrid = new GridOffset(0, lengthPerBeat); - - var curTGrid = x.TGrid + stepGrid; - if (x.HoldEnd is null) - yield break; - while (curTGrid < x.HoldEnd.TGrid) - { - yield return curTGrid; - curTGrid = curTGrid + stepGrid; - } - } - - private static IEnumerable CalculateDefaultClickSEs(OngekiFumen fumen) - { - var tGrid = TGrid.Zero; - var endTGrid = new TGrid(1, 0); - //calculate stepGrid - var met = fumen.MeterChanges.GetMeter(tGrid); - var bpm = fumen.BpmList.GetBpm(tGrid); - var resT = bpm.TGrid.ResT; - var beatCount = met.Bunbo * 1; - if (beatCount != 0) - { - var lengthPerBeat = (int)(resT / beatCount); - - var stepGrid = new GridOffset(0, lengthPerBeat); - - var curTGrid = tGrid + stepGrid; - while (curTGrid < endTGrid) - { - yield return curTGrid; - curTGrid = curTGrid + stepGrid; - } - } - } - - private void RebuildEvents() - { - StopAllLoop(); - events.ForEach(ObjectPool.Return); - durationEvents.Select(x => x.Value).ForEach(ObjectPool.Return); - events.Clear(); - durationEvents.Clear(); - currentPlayingDurationEvents.Clear(); - - var list = new HashSet(); - var durationList = new HashSet(); - - void AddSound(SoundControl sound, TGrid tGrid) - { - var evt = ObjectPool.Get(); - - evt.Sounds = sound; - evt.Time = TGridCalculator.ConvertTGridToAudioTime(tGrid, editor); - //evt.TGrid = tGrid; - - list.Add(evt); - } - - void AddDurationSound(SoundControl sound, TGrid tGrid, TGrid endTGrid, int loopId = 0) - { - var evt = ObjectPool.Get(); - - evt.Sounds = sound; - evt.LoopId = loopId; - evt.Time = TGridCalculator.ConvertTGridToAudioTime(tGrid, editor); - evt.EndTime = TGridCalculator.ConvertTGridToAudioTime(endTGrid, editor); - //evt.TGrid = tGrid; - - durationList.Add(evt); - } - - var fumen = editor.Fumen; - - var soundObjects = fumen.GetAllDisplayableObjects().OfType(); - - //add default clickse objects. - if (!fumen.ClickSEs.Any(x => x.TGrid.TotalUnit <= 1)) - { - foreach (var tGrid in CalculateDefaultClickSEs(fumen)) - AddSound(SoundControl.ClickSE, tGrid); - } - - using var _d = ObjectPool>.GetWithUsingDisposable(out var typeSet, out _); + private static IEnumerable CalculateHoldTicks(Hold x, OngekiFumen fumen) + { + int? CalcHoldTickStepSizeA() + { + //calculate stepGrid + var met = fumen.MeterChanges.GetMeter(x.TGrid); + var bpm = fumen.BpmList.GetBpm(x.TGrid); + var resT = bpm.TGrid.ResT; + var beatCount = met.Bunbo; + if (beatCount == 0) + return null; + return (int)(resT / beatCount); + } + + if (CalcHoldTickStepSizeA() is not int lengthPerBeat) + yield break; + var stepGrid = new GridOffset(0, lengthPerBeat); + + var curTGrid = x.TGrid + stepGrid; + if (x.HoldEnd is null) + yield break; + while (curTGrid < x.HoldEnd.TGrid) + { + yield return curTGrid; + curTGrid = curTGrid + stepGrid; + } + } + + private static IEnumerable CalculateDefaultClickSEs(OngekiFumen fumen) + { + var tGrid = TGrid.Zero; + var endTGrid = new TGrid(1, 0); + //calculate stepGrid + var met = fumen.MeterChanges.GetMeter(tGrid); + var bpm = fumen.BpmList.GetBpm(tGrid); + var resT = bpm.TGrid.ResT; + var beatCount = met.Bunbo * 1; + if (beatCount != 0) + { + var lengthPerBeat = (int)(resT / beatCount); + + var stepGrid = new GridOffset(0, lengthPerBeat); + + var curTGrid = tGrid + stepGrid; + while (curTGrid < endTGrid) + { + yield return curTGrid; + curTGrid = curTGrid + stepGrid; + } + } + } + + private void RebuildEvents() + { + StopAllLoop(); + events.ForEach(ObjectPool.Return); + durationEvents.Select(x => x.Value).ForEach(ObjectPool.Return); + events.Clear(); + durationEvents.Clear(); + currentPlayingDurationEvents.Clear(); + + var list = new HashSet(); + var durationList = new HashSet(); + + void AddSound(SoundControl sound, TGrid tGrid) + { + var evt = ObjectPool.Get(); - foreach (var group in soundObjects.GroupBy(x => x.TGrid)) - { - var sounds = (SoundControl)0; - typeSet.Clear(); + evt.Sounds = sound; + evt.Time = TGridCalculator.ConvertTGridToAudioTime(tGrid, editor); + //evt.TGrid = tGrid; - foreach (var obj in group.Where(x => - { - if (x is Tap) - return true; - return typeSet.Add(x.GetType()); - })) - { - sounds = sounds | obj switch - { - Tap { ReferenceLaneStart: { IsWallLane: true }, IsCritical: false } or Hold { ReferenceLaneStart: { IsWallLane: true }, IsCritical: false } => SoundControl.WallTap, - Tap { ReferenceLaneStart: { IsWallLane: true }, IsCritical: true } or Hold { ReferenceLaneStart: { IsWallLane: true }, IsCritical: true } => SoundControl.CriticalWallTap, - Tap { ReferenceLaneStart: { IsWallLane: false }, IsCritical: false } or Hold { ReferenceLaneStart: { IsWallLane: false }, IsCritical: false } => SoundControl.Tap, - Tap { ReferenceLaneStart: { IsWallLane: false }, IsCritical: true } or Hold { ReferenceLaneStart: { IsWallLane: false }, IsCritical: true } => SoundControl.CriticalTap, - Tap { ReferenceLaneStart: null, IsCritical: false } or Hold { ReferenceLaneStart: null, IsCritical: false } => SoundControl.Tap, - Tap { ReferenceLaneStart: null, IsCritical: true } or Hold { ReferenceLaneStart: null, IsCritical: true } => SoundControl.CriticalTap, - Bell => SoundControl.Bell, - Bullet => SoundControl.Bullet, - Flick { IsCritical: false } => SoundControl.Flick, - Flick { IsCritical: true } => SoundControl.CriticalFlick, - HoldEnd => SoundControl.HoldEnd, - ClickSE => SoundControl.ClickSE, - _ => default - }; - - if (obj is Hold hold) - { - //add hold ticks - foreach (var tickTGrid in CalculateHoldTicks(hold, fumen)) - { - AddSound(SoundControl.HoldTick, tickTGrid); - } - } - - if (obj is BeamStart beam) - { - var loopId = ++loopIdGen; - - //generate stop - AddSound(SoundControl.BeamEnd, beam.MaxTGrid); - AddDurationSound(SoundControl.BeamLoop, beam.TGrid, beam.MaxTGrid, loopId); - var leadBodyInTGrid = TGridCalculator.ConvertAudioTimeToTGrid(TGridCalculator.ConvertTGridToAudioTime(beam.TGrid, editor) - TGridCalculator.ConvertFrameToAudioTime(BeamStart.LEAD_IN_DURATION_FRAME), editor); - if (leadBodyInTGrid is null) - leadBodyInTGrid = TGrid.Zero; - AddSound(SoundControl.BeamPrepare, leadBodyInTGrid); - } - } - if (sounds != 0) - AddSound(sounds, group.Key); - } - events = new LinkedList(list.OrderBy(x => x.Time)); - foreach (var durationEvent in durationList) - durationEvents.Add(durationEvent.Time, durationEvent.EndTime, durationEvent); - itor = null; + list.Add(evt); + } - meterActions.Clear(); - if (EditorGlobalSetting.Default.LoopPlayTiming) - { - var oneTGrid = new TGrid(1, 0); + void AddDurationSound(SoundControl sound, TGrid tGrid, TGrid endTGrid, int loopId = 0) + { + var evt = ObjectPool.Get(); - var timeSignatureList = fumen.MeterChanges.GetCachedAllTimeSignatureUniformPositionList(fumen.BpmList); - foreach (var timeSignature in timeSignatureList) - { - var beatCount = timeSignature.meter.Bunbo; - var isSkip = beatCount == 0; - var beatInterval = isSkip ? default : - TimeSpan.FromMilliseconds(MathUtils.CalculateBPMLength(TGrid.Zero, oneTGrid, timeSignature.bpm.BPM)) - / beatCount; - - var action = new MeterAction(timeSignature.audioTime, beatInterval, beatCount, isSkip); - meterActions.AddLast(action); - } - } - meterActionsItor = default; - currentMeterHitCount = 0; - } + evt.Sounds = sound; + evt.LoopId = loopId; + evt.Time = TGridCalculator.ConvertTGridToAudioTime(tGrid, editor); + evt.EndTime = TGridCalculator.ConvertTGridToAudioTime(endTGrid, editor); + //evt.TGrid = tGrid; - private void UpdateInternal(CancellationToken token) - { - if ((itor is null && meterActionsItor is null) || player is null || token.IsCancellationRequested) - return; - if (!IsPlaying) - { - //stop all looping - StopAllLoop(); - return; - } + durationList.Add(evt); + } - var currentTime = player.CurrentTime; + var fumen = editor.Fumen; - //播放物件音效 - while (itor is not null) - { - var nextBeatTime = itor.Value.Time.TotalMilliseconds; - var ct = currentTime.TotalMilliseconds - nextBeatTime; - if (ct >= 0) - { - //Debug.WriteLine($"diff:{ct:F2}ms, target:{itor.Value}, currentTime:{currentTime}"); - PlaySoundsOnce(itor.Value.Sounds); - itor = itor.Next; - } - else - break; - } + var soundObjects = fumen.GetAllDisplayableObjects().OfType(); - //播放节拍器 - while (meterActionsItor is not null) - { - var nextActionItor = meterActionsItor.Next; - - //检查当前是否有效 - if (meterActionsItor.Value.isSkip) - { - meterActionsItor = nextActionItor; - currentMeterHitCount = 0; - continue; - } - - var nextBeatTime = meterActionsItor.Value.Time + - meterActionsItor.Value.BeatInterval * currentMeterHitCount; + //add default clickse objects. + if (!fumen.ClickSEs.Any(x => x.TGrid.TotalUnit <= 1)) + { + foreach (var tGrid in CalculateDefaultClickSEs(fumen)) + AddSound(SoundControl.ClickSE, tGrid); + } - //检查是否超过下一个 - if (nextActionItor != null) - { - if (nextBeatTime > nextActionItor.Value.Time) - { - meterActionsItor = nextActionItor; - currentMeterHitCount = 0; - continue; - } - } + using var _d = ObjectPool>.GetWithUsingDisposable(out var typeSet, out _); - //没超过就检查了 - var ct = currentTime.TotalMilliseconds - nextBeatTime.TotalMilliseconds; - if (ct >= 0) - { - //Log.LogDebug($"currentMeterHitCount:{currentMeterHitCount}, nextBeatTime:{nextBeatTime}, diff:{ct:F2}ms, meterActionsItor:{meterActionsItor.Value}"); - var beatIdx = currentMeterHitCount % meterActionsItor.Value.BeatCount; - var sound = beatIdx == 0 ? SoundControl.MetronomeStrongBeat : SoundControl.MetronomeWeakBeat; - PlaySoundsOnce(sound); - currentMeterHitCount++; - } - else - break; - } + foreach (var group in soundObjects.GroupBy(x => x.TGrid)) + { + var sounds = (SoundControl)0; + typeSet.Clear(); + + foreach (var obj in group.Where(x => + { + if (x is Tap) + return true; + return typeSet.Add(x.GetType()); + })) + { + sounds = sounds | obj switch + { + Tap { ReferenceLaneStart: { IsWallLane: true }, IsCritical: false } or Hold { ReferenceLaneStart: { IsWallLane: true }, IsCritical: false } => SoundControl.WallTap, + Tap { ReferenceLaneStart: { IsWallLane: true }, IsCritical: true } or Hold { ReferenceLaneStart: { IsWallLane: true }, IsCritical: true } => SoundControl.CriticalWallTap, + Tap { ReferenceLaneStart: { IsWallLane: false }, IsCritical: false } or Hold { ReferenceLaneStart: { IsWallLane: false }, IsCritical: false } => SoundControl.Tap, + Tap { ReferenceLaneStart: { IsWallLane: false }, IsCritical: true } or Hold { ReferenceLaneStart: { IsWallLane: false }, IsCritical: true } => SoundControl.CriticalTap, + Tap { ReferenceLaneStart: null, IsCritical: false } or Hold { ReferenceLaneStart: null, IsCritical: false } => SoundControl.Tap, + Tap { ReferenceLaneStart: null, IsCritical: true } or Hold { ReferenceLaneStart: null, IsCritical: true } => SoundControl.CriticalTap, + Bell => SoundControl.Bell, + Bullet => SoundControl.Bullet, + Flick { IsCritical: false } => SoundControl.Flick, + Flick { IsCritical: true } => SoundControl.CriticalFlick, + HoldEnd => SoundControl.HoldEnd, + ClickSE => SoundControl.ClickSE, + EnemySet { TagTblValue: EnemySet.WaveChangeConst.Boss } => SoundControl.BossWave, + _ => default + }; + + if (obj is Hold hold) + { + //add hold ticks + foreach (var tickTGrid in CalculateHoldTicks(hold, fumen)) + { + AddSound(SoundControl.HoldTick, tickTGrid); + } + } + + if (obj is BeamStart beam) + { + var loopId = ++loopIdGen; + + //generate stop + AddSound(SoundControl.BeamEnd, beam.MaxTGrid); + AddDurationSound(SoundControl.BeamLoop, beam.TGrid, beam.MaxTGrid, loopId); + var leadBodyInTGrid = TGridCalculator.ConvertAudioTimeToTGrid(TGridCalculator.ConvertTGridToAudioTime(beam.TGrid, editor) - TGridCalculator.ConvertFrameToAudioTime(BeamStart.LEAD_IN_DURATION_FRAME), editor); + if (leadBodyInTGrid is null) + leadBodyInTGrid = TGrid.Zero; + AddSound(SoundControl.BeamPrepare, leadBodyInTGrid); + } + } + if (sounds != 0) + AddSound(sounds, group.Key); + } + events = new LinkedList(list.OrderBy(x => x.Time)); + foreach (var durationEvent in durationList) + durationEvents.Add(durationEvent.Time, durationEvent.EndTime, durationEvent); + itor = null; + + meterActions.Clear(); + if (EditorGlobalSetting.Default.LoopPlayTiming) + { + var oneTGrid = new TGrid(1, 0); + + var timeSignatureList = fumen.MeterChanges.GetCachedAllTimeSignatureUniformPositionList(fumen.BpmList); + foreach (var timeSignature in timeSignatureList) + { + var beatCount = timeSignature.meter.Bunbo; + var isSkip = beatCount == 0; + var beatInterval = isSkip ? default : + TimeSpan.FromMilliseconds(MathUtils.CalculateBPMLength(TGrid.Zero, oneTGrid, timeSignature.bpm.BPM)) + / beatCount; + + var action = new MeterAction(timeSignature.audioTime, beatInterval, beatCount, isSkip); + meterActions.AddLast(action); + } + } + meterActionsItor = default; + currentMeterHitCount = 0; + } + + private void UpdateInternal(CancellationToken token) + { + if ((itor is null && meterActionsItor is null) || player is null || token.IsCancellationRequested) + return; + if (!IsPlaying) + { + //stop all looping + StopAllLoop(); + return; + } - //检查循环音效 - lock (locker) - { - var queryDurationEvents = durationEvents.Query(currentTime); - foreach (var durationEvent in queryDurationEvents) - { - //检查是否正在播放了 - if (!currentPlayingDurationEvents.Contains(durationEvent)) - { - if (SoundControl.HasFlag(durationEvent.Sounds) && cacheSounds.TryGetValue(durationEvent.Sounds, out var soundPlayer)) - { - var initPlayTime = currentTime - durationEvent.Time; - soundPlayer.PlayLoop(durationEvent.LoopId, initPlayTime); - - currentPlayingDurationEvents.Add(durationEvent); - } - } - } - //检查是否已经播放完成 - foreach (var durationEvent in currentPlayingDurationEvents.Where(x => currentTime < x.Time || currentTime > x.EndTime).ToArray()) - { - if (cacheSounds.TryGetValue(durationEvent.Sounds, out var soundPlayer)) - { - soundPlayer.StopLoop(durationEvent.LoopId); - currentPlayingDurationEvents.Remove(durationEvent); - } - } - } + var currentTime = player.CurrentTime; - /* + //播放物件音效 + while (itor is not null) + { + var nextBeatTime = itor.Value.Time.TotalMilliseconds; + var ct = currentTime.TotalMilliseconds - nextBeatTime; + if (ct >= 0) + { + //Debug.WriteLine($"diff:{ct:F2}ms, target:{itor.Value}, currentTime:{currentTime}"); + PlaySoundsOnce(itor.Value.Sounds); + itor = itor.Next; + } + else + break; + } + + //播放节拍器 + while (meterActionsItor is not null) + { + var nextActionItor = meterActionsItor.Next; + + //检查当前是否有效 + if (meterActionsItor.Value.isSkip) + { + meterActionsItor = nextActionItor; + currentMeterHitCount = 0; + continue; + } + + var nextBeatTime = meterActionsItor.Value.Time + + meterActionsItor.Value.BeatInterval * currentMeterHitCount; + + //检查是否超过下一个 + if (nextActionItor != null) + { + if (nextBeatTime > nextActionItor.Value.Time) + { + meterActionsItor = nextActionItor; + currentMeterHitCount = 0; + continue; + } + } + + //没超过就检查了 + var ct = currentTime.TotalMilliseconds - nextBeatTime.TotalMilliseconds; + if (ct >= 0) + { + //Log.LogDebug($"currentMeterHitCount:{currentMeterHitCount}, nextBeatTime:{nextBeatTime}, diff:{ct:F2}ms, meterActionsItor:{meterActionsItor.Value}"); + var beatIdx = currentMeterHitCount % meterActionsItor.Value.BeatCount; + var sound = beatIdx == 0 ? SoundControl.MetronomeStrongBeat : SoundControl.MetronomeWeakBeat; + PlaySoundsOnce(sound); + currentMeterHitCount++; + } + else + break; + } + + //检查循环音效 + lock (locker) + { + var queryDurationEvents = durationEvents.Query(currentTime); + foreach (var durationEvent in queryDurationEvents) + { + //检查是否正在播放了 + if (!currentPlayingDurationEvents.Contains(durationEvent)) + { + if (SoundControl.HasFlag(durationEvent.Sounds) && cacheSounds.TryGetValue(durationEvent.Sounds, out var soundPlayer)) + { + var initPlayTime = currentTime - durationEvent.Time; + soundPlayer.PlayLoop(durationEvent.LoopId, initPlayTime); + + currentPlayingDurationEvents.Add(durationEvent); + } + } + } + //检查是否已经播放完成 + foreach (var durationEvent in currentPlayingDurationEvents.Where(x => currentTime < x.Time || currentTime > x.EndTime).ToArray()) + { + if (cacheSounds.TryGetValue(durationEvent.Sounds, out var soundPlayer)) + { + soundPlayer.StopLoop(durationEvent.LoopId); + currentPlayingDurationEvents.Remove(durationEvent); + } + } + } + + /* else { var sleepTime = Math.Min(1000, (int)((Math.Abs(ct) - 2) * player.Speed)); @@ -438,7 +441,7 @@ private void UpdateInternal(CancellationToken token) Thread.Sleep(sleepTime); break; }*/ - } + } private void OnUpdate(CancellationToken cancel) { @@ -457,147 +460,149 @@ private void OnUpdate(CancellationToken cancel) } } - private void PlaySoundsOnce(SoundControl sounds) - { - void checkPlay(SoundControl subFlag) - { - if (sounds.HasFlag(subFlag) && SoundControl.HasFlag(subFlag) && cacheSounds.TryGetValue(subFlag, out var sound)) - sound.PlayOnce(); - } - - checkPlay(SoundControl.Tap); - checkPlay(SoundControl.CriticalTap); - checkPlay(SoundControl.Bell); - checkPlay(SoundControl.WallTap); - checkPlay(SoundControl.CriticalWallTap); - checkPlay(SoundControl.Bullet); - checkPlay(SoundControl.Flick); - checkPlay(SoundControl.CriticalFlick); - checkPlay(SoundControl.HoldEnd); - checkPlay(SoundControl.HoldTick); - checkPlay(SoundControl.ClickSE); - checkPlay(SoundControl.BeamPrepare); - checkPlay(SoundControl.BeamEnd); - checkPlay(SoundControl.MetronomeStrongBeat); - checkPlay(SoundControl.MetronomeWeakBeat); - } - - public void Seek(TimeSpan msec, bool pause) - { - Pause(); - itor = events.Find(events.FirstOrDefault(x => msec < x.Time)); - meterActionsItor = meterActions.Find(meterActions.LastOrDefault(x => msec >= x.Time)); - if (meterActionsItor is null) - currentMeterHitCount = 0; - else - { - if (meterActionsItor.Value.isSkip) - currentMeterHitCount = 0; - else - currentMeterHitCount = (int)((msec - meterActionsItor.Value.Time) / meterActionsItor.Value.BeatInterval); - } - - if (!pause) - PlayInternal(); - } - - private void StopAllLoop() - { - lock (locker) - { - foreach (var durationEvent in currentPlayingDurationEvents.ToArray()) - { - if (durationEvent is null) - continue; - if (cacheSounds.TryGetValue(durationEvent.Sounds, out var soundPlayer)) - { - soundPlayer.StopLoop(durationEvent.LoopId); - currentPlayingDurationEvents.Remove(durationEvent); - } - } - } - } - - public void Stop() - { - thread?.Abort(); - StopAllLoop(); - isPlaying = false; - } - - public void PlayInternal() - { - if (player is null) - return; - isPlaying = true; - } - - public void Play() - { - if (player is null) - return; - itor = itor ?? events.First; - meterActionsItor = meterActionsItor ?? meterActions.First; - currentMeterHitCount = 0; - - PlayInternal(); - } - - public void Pause() - { - isPlaying = false; - StopAllLoop(); - } - - public void Dispose() - { - thread?.Abort(); - foreach (var sound in cacheSounds.Values) - sound.Dispose(); - } - - public Task Clean() - { - Stop(); - - thread = null; - - player = null; - editor = null; - - events.Clear(); - - return Task.CompletedTask; - } - - public float? GetVolume(SoundControl sound) - { - foreach (var item in cacheSounds) - { - if (item.Key == sound) - { - return item.Value.Volume; - } - } - - return null; - } - - public void SetVolume(SoundControl sound, float volume) - { - foreach (var item in cacheSounds) - { - if (item.Key == sound) - { - item.Value.Volume = volume; - } - } - } - - public async Task ReloadSoundFiles() - { - InitSounds(); - return await loadTask; - } - } -} + private void PlaySoundsOnce(SoundControl sounds) + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void checkPlay(SoundControl subFlag) + { + if (sounds.HasFlag(subFlag) && SoundControl.HasFlag(subFlag) && cacheSounds.TryGetValue(subFlag, out var sound)) + sound.PlayOnce(); + } + + checkPlay(SoundControl.Tap); + checkPlay(SoundControl.CriticalTap); + checkPlay(SoundControl.Bell); + checkPlay(SoundControl.WallTap); + checkPlay(SoundControl.CriticalWallTap); + checkPlay(SoundControl.Bullet); + checkPlay(SoundControl.Flick); + checkPlay(SoundControl.CriticalFlick); + checkPlay(SoundControl.HoldEnd); + checkPlay(SoundControl.HoldTick); + checkPlay(SoundControl.ClickSE); + checkPlay(SoundControl.BeamPrepare); + checkPlay(SoundControl.BeamEnd); + checkPlay(SoundControl.MetronomeStrongBeat); + checkPlay(SoundControl.MetronomeWeakBeat); + checkPlay(SoundControl.BossWave); + } + + public void Seek(TimeSpan msec, bool pause) + { + Pause(); + itor = events.Find(events.FirstOrDefault(x => msec < x.Time)); + meterActionsItor = meterActions.Find(meterActions.LastOrDefault(x => msec >= x.Time)); + if (meterActionsItor is null) + currentMeterHitCount = 0; + else + { + if (meterActionsItor.Value.isSkip) + currentMeterHitCount = 0; + else + currentMeterHitCount = (int)((msec - meterActionsItor.Value.Time) / meterActionsItor.Value.BeatInterval); + } + + if (!pause) + PlayInternal(); + } + + private void StopAllLoop() + { + lock (locker) + { + foreach (var durationEvent in currentPlayingDurationEvents.ToArray()) + { + if (durationEvent is null) + continue; + if (cacheSounds.TryGetValue(durationEvent.Sounds, out var soundPlayer)) + { + soundPlayer.StopLoop(durationEvent.LoopId); + currentPlayingDurationEvents.Remove(durationEvent); + } + } + } + } + + public void Stop() + { + thread?.Abort(); + StopAllLoop(); + isPlaying = false; + } + + public void PlayInternal() + { + if (player is null) + return; + isPlaying = true; + } + + public void Play() + { + if (player is null) + return; + itor = itor ?? events.First; + meterActionsItor = meterActionsItor ?? meterActions.First; + currentMeterHitCount = 0; + + PlayInternal(); + } + + public void Pause() + { + isPlaying = false; + StopAllLoop(); + } + + public void Dispose() + { + thread?.Abort(); + foreach (var sound in cacheSounds.Values) + sound.Dispose(); + } + + public Task Clean() + { + Stop(); + + thread = null; + + player = null; + editor = null; + + events.Clear(); + + return Task.CompletedTask; + } + + public float? GetVolume(SoundControl sound) + { + foreach (var item in cacheSounds) + { + if (item.Key == sound) + { + return item.Value.Volume; + } + } + + return null; + } + + public void SetVolume(SoundControl sound, float volume) + { + foreach (var item in cacheSounds) + { + if (item.Key == sound) + { + item.Value.Volume = volume; + } + } + } + + public async Task ReloadSoundFiles() + { + InitSounds(); + return await loadTask; + } + } +} diff --git a/OngekiFumenEditor/Kernel/Audio/NAudioImpl/NAudioManager.cs b/OngekiFumenEditor/Kernel/Audio/NAudioImpl/NAudioManager.cs index 4d1f59c5..0e383322 100644 --- a/OngekiFumenEditor/Kernel/Audio/NAudioImpl/NAudioManager.cs +++ b/OngekiFumenEditor/Kernel/Audio/NAudioImpl/NAudioManager.cs @@ -1,4 +1,5 @@ using Caliburn.Micro; +using ControlzEx.Standard; using NAudio.CoreAudioApi; using NAudio.Wave; using NAudio.Wave.SampleProviders; @@ -21,6 +22,7 @@ namespace OngekiFumenEditor.Kernel.Audio.NAudioImpl public class NAudioManager : PropertyChangedBase, IAudioManager { private HashSet> ownAudioPlayerRefs = new(); + private bool enableSoundMultiPlay; private int targetSampleRate; private readonly IWavePlayer audioOutputDevice; @@ -28,6 +30,9 @@ public class NAudioManager : PropertyChangedBase, IAudioManager private readonly MixingSampleProvider soundMixer; private readonly MixingSampleProvider musicMixer; + private readonly Dictionary cs2providerMap = new(); + private readonly Dictionary provider2csMap = new(); + private readonly VolumeSampleProvider soundVolumeWrapper; private readonly VolumeSampleProvider musicVolumeWrapper; @@ -65,10 +70,13 @@ public float MusicVolume public NAudioManager() { - var audioOutputType = (AudioOutputType)Properties.AudioSetting.Default.AudioOutputType; - targetSampleRate = Properties.AudioSetting.Default.AudioSampleRate; + var audioOutputType = (AudioOutputType)AudioSetting.Default.AudioOutputType; + enableSoundMultiPlay = AudioSetting.Default.EnableSoundMultiPlay; + targetSampleRate = AudioSetting.Default.AudioSampleRate; + Log.LogDebug($"targetSampleRate: {targetSampleRate}"); Log.LogDebug($"audioOutputType: {audioOutputType}"); + Log.LogDebug($"enableSoundMultiPlay: {enableSoundMultiPlay}"); try { @@ -96,6 +104,7 @@ public NAudioManager() //setup sound soundMixer = new MixingSampleProvider(format); soundMixer.ReadFully = true; + soundMixer.MixerInputEnded += SoundMixer_MixerInputEnded; soundVolumeWrapper = new VolumeSampleProvider(soundMixer); audioMixer.AddMixerInput(soundVolumeWrapper); SoundVolume = AudioSetting.Default.SoundVolume; @@ -110,8 +119,20 @@ public NAudioManager() Log.LogInfo($"Audio implement will use {GetType()}"); } + private void SoundMixer_MixerInputEnded(object sender, SampleProviderEventArgs e) + { + RemoveSoundMixerInput(e.SampleProvider, false); + } + public void PlaySound(CachedSound sound, float volume, TimeSpan init) { + if (!enableSoundMultiPlay) + { + //stop previous + if (cs2providerMap.TryGetValue(sound, out var prevProvider)) + RemoveSoundMixerInput(prevProvider, true); + } + ISampleProvider provider = new VolumeSampleProvider(new CachedSoundSampleProvider(sound)) { Volume = volume @@ -124,17 +145,36 @@ public void PlaySound(CachedSound sound, float volume, TimeSpan init) }; } - AddSoundMixerInput(provider); + AddSoundMixerInput(provider, sound); } - public void AddSoundMixerInput(ISampleProvider input) + public void AddSoundMixerInput(ISampleProvider input, CachedSound cachedSound) { + if (!enableSoundMultiPlay) + { + cs2providerMap[cachedSound] = input; + provider2csMap[input] = cachedSound; + } + soundMixer.AddMixerInput(input); } - public void RemoveSoundMixerInput(ISampleProvider input) + /// + /// + /// + /// + /// mixer是否需要调用RemoveMixerInput() + public void RemoveSoundMixerInput(ISampleProvider input, bool mixerRemove) { - soundMixer.RemoveMixerInput(input); + if (mixerRemove) + soundMixer.RemoveMixerInput(input); + + if (!enableSoundMultiPlay) + { + if (provider2csMap.TryGetValue(input, out var cachedSound)) + cs2providerMap.Remove(cachedSound); + provider2csMap.Remove(input); + } } public async Task LoadAudioAsync(string filePath) @@ -179,6 +219,11 @@ public void Dispose() public ILoopHandle PlayLoopSound(CachedSound sound, float volume, TimeSpan init) { + if (!enableSoundMultiPlay) + { + + } + ISampleProvider provider = new LoopableProvider(new CachedSoundSampleProvider(sound)); if (init.TotalMilliseconds != 0) @@ -193,7 +238,7 @@ public ILoopHandle PlayLoopSound(CachedSound sound, float volume, TimeSpan init) handle.Volume = volume; //add to mixer - AddSoundMixerInput(handle.Provider); + AddSoundMixerInput(handle.Provider, sound); //Log.LogDebug($"handle hashcode = {handle.GetHashCode()}"); return handle; @@ -205,7 +250,7 @@ public void StopLoopSound(ILoopHandle h) return; //Log.LogDebug($"handle hashcode = {handle.GetHashCode()}"); - RemoveSoundMixerInput(handle.Provider); + RemoveSoundMixerInput(handle.Provider, true); } } } diff --git a/OngekiFumenEditor/Kernel/Audio/NAudioImpl/Sound/CachedSound.cs b/OngekiFumenEditor/Kernel/Audio/NAudioImpl/Sound/CachedSound.cs index 7d6f52ec..d06f2ce9 100644 --- a/OngekiFumenEditor/Kernel/Audio/NAudioImpl/Sound/CachedSound.cs +++ b/OngekiFumenEditor/Kernel/Audio/NAudioImpl/Sound/CachedSound.cs @@ -14,11 +14,6 @@ public class CachedSound public WaveFormat WaveFormat { get; init; } public TimeSpan Duration { get; init; } - private CachedSound() - { - //no way - } - public CachedSound(ISampleProvider copySourceProvider) { AudioData = copySourceProvider.ToArray(); diff --git a/OngekiFumenEditor/Kernel/Audio/SoundControl.cs b/OngekiFumenEditor/Kernel/Audio/SoundControl.cs index 18fa4836..3da8877a 100644 --- a/OngekiFumenEditor/Kernel/Audio/SoundControl.cs +++ b/OngekiFumenEditor/Kernel/Audio/SoundControl.cs @@ -2,31 +2,32 @@ namespace OngekiFumenEditor.Kernel.Audio { - [Flags] - public enum SoundControl - { - Tap = 1, - CriticalTap = 2, - Hold = 4, - CriticalHold = 8, - WallTap = 16, - CriticalWallTap = 32, - WallHold = 64, - CriticalWallHold = 128, - Flick = 256, - CriticalFlick = 512, - Bullet = 1024, - Beam = 2048, - Bell = 4096, - ClickSE = 8192, - HoldTick = 16384, - HoldEnd = 32768, - BeamPrepare = HoldEnd * 2, - BeamLoop = BeamPrepare * 2, - BeamEnd = BeamLoop * 2, - MetronomeStrongBeat = BeamEnd * 2, - MetronomeWeakBeat = MetronomeStrongBeat * 2, + [Flags] + public enum SoundControl + { + Tap = 1, + CriticalTap = 2, + Hold = 4, + CriticalHold = 8, + WallTap = 16, + CriticalWallTap = 32, + WallHold = 64, + CriticalWallHold = 128, + Flick = 256, + CriticalFlick = 512, + Bullet = 1024, + Beam = 2048, + Bell = 4096, + ClickSE = 8192, + HoldTick = 16384, + HoldEnd = 32768, + BeamPrepare = HoldEnd * 2, + BeamLoop = BeamPrepare * 2, + BeamEnd = BeamLoop * 2, + MetronomeStrongBeat = BeamEnd * 2, + MetronomeWeakBeat = MetronomeStrongBeat * 2, + BossWave = MetronomeWeakBeat * 2, - All = MetronomeStrongBeat | MetronomeWeakBeat | BeamPrepare | BeamLoop | BeamEnd | HoldEnd | HoldTick | ClickSE | Bell | Beam | Bullet | CriticalFlick | Flick | CriticalWallHold | WallHold | CriticalWallTap | WallTap | CriticalHold | Hold | CriticalTap | Tap - } + All = BossWave | MetronomeStrongBeat | MetronomeWeakBeat | BeamPrepare | BeamLoop | BeamEnd | HoldEnd | HoldTick | ClickSE | Bell | Beam | Bullet | CriticalFlick | Flick | CriticalWallHold | WallHold | CriticalWallTap | WallTap | CriticalHold | Hold | CriticalTap | Tap + } } diff --git a/OngekiFumenEditor/Kernel/ArgProcesser/Attributes/OptionBindingAttrbute.cs b/OngekiFumenEditor/Kernel/CommandExecutor/Attributes/OptionBindingAttrbute.cs similarity index 93% rename from OngekiFumenEditor/Kernel/ArgProcesser/Attributes/OptionBindingAttrbute.cs rename to OngekiFumenEditor/Kernel/CommandExecutor/Attributes/OptionBindingAttrbute.cs index e90e3e2f..807bf4bb 100644 --- a/OngekiFumenEditor/Kernel/ArgProcesser/Attributes/OptionBindingAttrbute.cs +++ b/OngekiFumenEditor/Kernel/CommandExecutor/Attributes/OptionBindingAttrbute.cs @@ -1,52 +1,52 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Resources; -using System.Text; -using System.Threading.Tasks; -using OngekiFumenEditor.Properties; -using OngekiFumenEditor.Utils; - -namespace OngekiFumenEditor.Kernel.ArgProcesser.Attributes -{ - public abstract class OptionBindingAttrbuteBase : Attribute - { - public OptionBindingAttrbuteBase(string name, string description, object defaultValue, Type type) - { - Name = name; - Description = description; - DefaultValue = defaultValue; - Type = type; - } - - public string Name { get; set; } - public string Description { get; set; } - public object DefaultValue { get; set; } - public Type Type { get; } - public bool Require { get; set; } - } - - [AttributeUsage(AttributeTargets.Property)] - public class OptionBindingAttrbute : OptionBindingAttrbuteBase - { - public OptionBindingAttrbute(string name, string description, T defaultValue) : base(name, description, defaultValue, typeof(T)) - { - - } - } - - [AttributeUsage(AttributeTargets.Property)] - public class LocalizableOptionBindingAttribute : OptionBindingAttrbute - { - public LocalizableOptionBindingAttribute(string name, string resourceKey, T defaultValue, bool require = false) - : base(name, Resources.ResourceManager.GetString(resourceKey) ?? string.Empty, defaultValue) - { - Require = require; -#if DEBUG - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - if (string.IsNullOrWhiteSpace(Description)) - Log.LogDebug($"Invalid resource key '{resourceKey}' for option '{name}'"); -#endif - } - } +using System; +using System.Collections.Generic; +using System.Linq; +using System.Resources; +using System.Text; +using System.Threading.Tasks; +using OngekiFumenEditor.Properties; +using OngekiFumenEditor.Utils; + +namespace OngekiFumenEditor.Kernel.CommandExecutor.Attributes +{ + public abstract class OptionBindingAttrbuteBase : Attribute + { + public OptionBindingAttrbuteBase(string name, string description, object defaultValue, Type type) + { + Name = name; + Description = description; + DefaultValue = defaultValue; + Type = type; + } + + public string Name { get; set; } + public string Description { get; set; } + public object DefaultValue { get; set; } + public Type Type { get; } + public bool Require { get; set; } + } + + [AttributeUsage(AttributeTargets.Property)] + public class OptionBindingAttrbute : OptionBindingAttrbuteBase + { + public OptionBindingAttrbute(string name, string description, T defaultValue) : base(name, description, defaultValue, typeof(T)) + { + + } + } + + [AttributeUsage(AttributeTargets.Property)] + public class LocalizableOptionBindingAttribute : OptionBindingAttrbute + { + public LocalizableOptionBindingAttribute(string name, string resourceKey, T defaultValue, bool require = false) + : base(name, Resources.ResourceManager.GetString(resourceKey) ?? string.Empty, defaultValue) + { + Require = require; +#if DEBUG + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (string.IsNullOrWhiteSpace(Description)) + Log.LogDebug($"Invalid resource key '{resourceKey}' for option '{name}'"); +#endif + } + } } \ No newline at end of file diff --git a/OngekiFumenEditor/Kernel/ArgProcesser/DefaultImp/DefaultArgProcessManager.cs b/OngekiFumenEditor/Kernel/CommandExecutor/DefaultCommandExecutor.cs similarity index 66% rename from OngekiFumenEditor/Kernel/ArgProcesser/DefaultImp/DefaultArgProcessManager.cs rename to OngekiFumenEditor/Kernel/CommandExecutor/DefaultCommandExecutor.cs index 03566018..bd8c0a6b 100644 --- a/OngekiFumenEditor/Kernel/ArgProcesser/DefaultImp/DefaultArgProcessManager.cs +++ b/OngekiFumenEditor/Kernel/CommandExecutor/DefaultCommandExecutor.cs @@ -1,264 +1,240 @@ -using Caliburn.Micro; -using OngekiFumenEditor.Base; -using OngekiFumenEditor.Kernel.ArgProcesser.Attributes; -using OngekiFumenEditor.Kernel.Audio; -using OngekiFumenEditor.Modules.FumenVisualEditor; -using OngekiFumenEditor.Modules.PreviewSvgGenerator; -using OngekiFumenEditor.Parser; -using OngekiFumenEditor.Properties; -using OngekiFumenEditor.Utils; -using OngekiFumenEditor.Utils.Logs.DefaultImpls; -using System; -using System.Collections.Generic; -using System.CommandLine; -using System.CommandLine.Parsing; -using System.ComponentModel.Composition; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using System.Windows; -using OngekiFumenEditor.Modules.FumenConverter; -using OngekiFumenEditor.Modules.FumenConverter.Kernel; -using OngekiFumenEditor.Modules.OptionGeneratorTools.Base; -using OngekiFumenEditor.Modules.OptionGeneratorTools.Kernel; -using OngekiFumenEditor.Modules.OptionGeneratorTools.Models; -using Expression = System.Linq.Expressions.Expression; - -namespace OngekiFumenEditor.Kernel.ArgProcesser.DefaultImp -{ - [Export(typeof(IProgramArgProcessManager))] - internal class DefaultArgProcessManager : IProgramArgProcessManager - { - void Exit(int code = 0) => ErrorExit(string.Empty, true, code); - - void ErrorExit(string message, bool noDialog, int code = 0) - { - if (!string.IsNullOrWhiteSpace(message)) - { - if (noDialog) - { - var prevColor = Console.ForegroundColor; - Console.ForegroundColor = ConsoleColor.Red; - Console.Error.WriteLine($"{Resources.CliInputErrorHeader}: {message}"); - Console.ForegroundColor = prevColor; - } - else { - MessageBox.Show(message, Resources.Error, MessageBoxButton.OK, MessageBoxImage.Stop); - } - } - - Application.Current.Shutdown(code); - } - - public async Task ProcessArgs(string[] args) - { - if (args.Length == 0) - return; - - if (args.Length == 1) - { - var filePath = args[0]; - - if (File.Exists(filePath)) - { - Log.LogInfo($"arg.filePath: {filePath}"); - - _ = Application.Current.Dispatcher.Invoke(async () => - { - if (await DocumentOpenHelper.TryOpenAsDocument(filePath)) - Application.Current?.MainWindow?.Focus(); - }); - - return; - } - } - - var rootCommand = new RootCommand("CommandLine for OngekiFumenEditor"); - rootCommand.AddCommand(GenerateVerbCommands("svg", Resources.ProgramCommandDescriptionSvg, ProcessSvgCommand)); - rootCommand.AddCommand(GenerateVerbCommands("convert", Resources.ProgramCommandConvert, ProcessConvertCommand)); - rootCommand.AddCommand(GenerateVerbCommands("jacket", Resources.ProgramCommandJacket, ProcessJacketCommand)); - rootCommand.AddCommand(GenerateVerbCommands("acb", Resources.ProgramCommandAcb, ProcessAcbCommand)); - - var verbosityOption = new Option(new[] {"--verbose", "-v"}, Resources.ProgramOptionDescriptionVerbose); - verbosityOption.AddValidator(res => - { - if (res.GetValueOrDefault()) - Log.Instance.AddOutputIfNotExist(); - }); - rootCommand.AddGlobalOption(verbosityOption); - - await rootCommand.InvokeAsync(args); - - Exit(); - } - - private Command GenerateVerbCommands(string verb, string description, Func callbackFunc) where T : new() - { - var command = new Command(verb, description); - foreach (var option in GenerateOptionsByAttributes()) - command.AddOption(option); - - command.SetHandler(async ctx => - { - var opt = Generate(command, ctx.ParseResult); - await callbackFunc(opt); - }); - return command; - } - - private async Task ProcessSvgCommand(GenerateOption opt) - { - if (CheckRelativePaths(opt.AudioFilePath, opt.InputFumenFilePath, opt.OutputFilePath)) - return; - - try - { - using var fumenFileStream = File.OpenRead(opt.InputFumenFilePath); - var fumenDeserializer = IoC.Get().GetDeserializer(opt.InputFumenFilePath); - if (fumenDeserializer is null) - throw new NotSupportedException($"{Resources.DeserializeFumenFileFail}{opt.InputFumenFilePath}"); - var fumen = await fumenDeserializer.DeserializeAsync(fumenFileStream); - - //calculate duration - if (File.Exists(opt.AudioFilePath)) - { - var audioPlayer = await IoC.Get().LoadAudioAsync(opt.AudioFilePath); - opt.Duration = audioPlayer.Duration; - } - else - { - //只能通过谱面来计算 - var maxTGrid = fumen.GetAllDisplayableObjects().OfType().Max(x => x.TGrid); - maxTGrid += new GridOffset(5, 0); - var duration = TGridCalculator.ConvertTGridToAudioTime(maxTGrid, fumen.BpmList); - opt.Duration = duration; - } - - _ = await IoC.Get().GenerateSvgAsync(fumen, opt); - Log.LogInfo(Resources.GenerateSvgSuccess); - } - catch (Exception e) - { - Log.LogError(Resources.CallGenerateSvgAsyncFail, e); - Exit(1); - } - - Exit(); - } - - private async Task ProcessConvertCommand(FumenConvertOption opt) - { - if (CheckRelativePaths(opt.InputFumenFilePath, opt.OutputFumenFilePath)) - return; - - var result = await FumenConverterWrapper.Generate(opt); - if (!result.IsSuccess) { - await Console.Error.WriteLineAsync($"{Resources.ConvertFail} {result.Message}"); - Exit(1); - return; - } - - Exit(); - } - - private async Task ProcessJacketCommand(JacketGenerateOption arg) - { - if (CheckRelativePaths(arg.InputImageFilePath, arg.OutputAssetbundleFolderPath)) - return; - - GenerateResult result; - try { - result = await JacketGenerateWrapper.Generate(arg); - } - catch (Exception e) { - result = new(false, e.Message); - } - - if (!result.IsSuccess) { - await Console.Error.WriteLineAsync($"{Resources.GenerateJacketFileFail} {result.Message}"); - Exit(1); - return; - } - - Exit(); - } - - private async Task ProcessAcbCommand(AcbGenerateOption arg) - { - if (CheckRelativePaths(arg.InputAudioFilePath, arg.OutputFolderPath)) - return; - - GenerateResult result; - try { - result = await AcbGeneratorFuckWrapper.Generate(arg); - } - catch (Exception e) { - result = new(false, e.Message); - } - - if (!result.IsSuccess) { - await Console.Error.WriteLineAsync($"{Resources.GenerateAudioFileFail} {result.Message}"); - Exit(1); - return; - } - - Exit(); +using Caliburn.Micro; +using OngekiFumenEditor.Base; +using OngekiFumenEditor.Kernel.Audio; +using OngekiFumenEditor.Kernel.CommandExecutor.Attributes; +using OngekiFumenEditor.Kernel.ProgramUpdater; +using OngekiFumenEditor.Modules.FumenConverter; +using OngekiFumenEditor.Modules.FumenConverter.Kernel; +using OngekiFumenEditor.Modules.FumenVisualEditor; +using OngekiFumenEditor.Modules.OptionGeneratorTools.Base; +using OngekiFumenEditor.Modules.OptionGeneratorTools.Kernel; +using OngekiFumenEditor.Modules.OptionGeneratorTools.Models; +using OngekiFumenEditor.Modules.PreviewSvgGenerator; +using OngekiFumenEditor.Parser; +using OngekiFumenEditor.Properties; +using OngekiFumenEditor.Utils; +using OngekiFumenEditor.Utils.Logs.DefaultImpls; +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Parsing; +using System.ComponentModel.Composition; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using static OngekiFumenEditor.Modules.FumenVisualEditor.Base.EditorProjectDataUtils; + +namespace OngekiFumenEditor.Kernel.CommandExecutor +{ + [Export(typeof(ICommandExecutor))] + internal class DefaultCommandExecutor : ICommandExecutor + { + private readonly RootCommand rootCommand; + + public DefaultCommandExecutor() + { + rootCommand = new RootCommand("CommandLine for OngekiFumenEditor"); + rootCommand.AddCommand(GenerateVerbCommands("svg", Resources.ProgramCommandDescriptionSvg, ProcessSvgCommand)); + rootCommand.AddCommand(GenerateVerbCommands("convert", Resources.ProgramCommandConvert, ProcessConvertCommand)); + rootCommand.AddCommand(GenerateVerbCommands("jacket", Resources.ProgramCommandJacket, ProcessJacketCommand)); + rootCommand.AddCommand(GenerateVerbCommands("acb", Resources.ProgramCommandAcb, ProcessAcbCommand)); + rootCommand.AddCommand(GenerateVerbCommands("updater", string.Empty, ProcessUpdaterCommand)); + + var verbosityOption = new Option(new[] { "--verbose", "-v" }, Resources.ProgramOptionDescriptionVerbose); + verbosityOption.AddValidator(res => + { + if (res.GetValueOrDefault()) + Log.Instance.AddOutputIfNotExist(); + }); + rootCommand.AddGlobalOption(verbosityOption); } - private bool CheckRelativePaths(params string[] paths) + private async Task ProcessUpdaterCommand(UpdaterOption option) { - if (paths.Any(path => !Path.IsPathRooted(path))) { - ErrorExit(Resources.CliArgumentNotAbsolutePath, true, 2); - return true; - } - - return false; - } - - #region Option generation - IEnumerable