diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..20bb1f2
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,125 @@
+name: .NET Build + Publish
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ build-windows:
+ runs-on: windows-2022
+
+ permissions:
+ contents: write
+
+ outputs:
+ version-number: ${{ steps.version.outputs.number}}
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 9.0.x
+ - name: Restore dependencies
+ run: dotnet restore
+ - name: Build
+ run: dotnet build --no-restore -p:PostBuildEvent=
+ - name: Publish Windows 64bit
+ if: ${{ github.event_name != 'pull_request' }}
+ run: dotnet publish --os win --arch x64 -c Release --self-contained false MSUScripter/MSUScripter.csproj
+ - name: Publish Windows 32bit
+ if: ${{ github.event_name != 'pull_request' }}
+ run: dotnet publish --os win --arch x86 -c Release --self-contained false MSUScripter/MSUScripter.csproj
+ - name: Get version number
+ if: ${{ github.event_name != 'pull_request' }}
+ id: version
+ run: |
+ $version = (Get-Item "MSUScripter\bin\Release\net9.0\win-x86\publish\MSUScripter.exe").VersionInfo.ProductVersion
+ $version = $version.Split("+")[0]
+ Write-Host $version
+ Write-Output "number=$version" >> $env:GITHUB_OUTPUT
+ shell: pwsh
+ - name: Building the Windows installer
+ if: ${{ github.event_name != 'pull_request' }}
+ run: '"%programfiles(x86)%/Inno Setup 6/iscc.exe" "Setup/MSUScripter.iss"'
+ shell: cmd
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ if: ${{ github.event_name != 'pull_request' }}
+ with:
+ path: "setup/Output/*"
+ name: MSUScripterWindows
+
+ build-linux:
+ runs-on: ubuntu-22.04
+ if: ${{ github.event_name != 'pull_request' }}
+ needs: [build-windows]
+
+ permissions:
+ contents: write
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 9.0.x
+ - name: Update VersionOverride in source file
+ run: |
+ pwd
+ VERSION="${{ needs.build-windows.outputs.version-number }}"
+ BASE_VERSION="${VERSION%%-*}"
+ FILE="MSUScripter/App.axaml.cs"
+ sed -i -E "s|^[[:space:]]*private static readonly string\?[[:space:]]+VersionOverride[[:space:]]*=[[:space:]]*null;|private static readonly string? VersionOverride = \"${VERSION}\";|" "$FILE"
+ sed -i "s/^AppVersionRelease *= *.*/AppVersionRelease = ${BASE_VERSION}/" Setup/AppImage.pupnet.conf
+ echo "Updated VersionOverride to: ${VERSION}"
+ - name: Install PupNet
+ run: dotnet tool install -g KuiperZone.PupNet
+ - name: Download AppImageTool
+ run: |
+ wget -P "$HOME/.local/bin" "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
+ chmod +x "$HOME/.local/bin/appimagetool-x86_64.AppImage"
+ appimagetool-x86_64.AppImage --version
+ - name: Run PupNet
+ run: pupnet Setup/AppImage.pupnet.conf --kind appimage -y
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ if: ${{ github.event_name != 'pull_request' }}
+ with:
+ path: "Setup/Output/MSUScripter*"
+ name: MSUScripterLinux
+
+ package:
+ runs-on: ubuntu-22.04
+ needs: [build-windows, build-linux]
+ if: ${{ github.event_name != 'pull_request' }}
+
+ permissions:
+ contents: write
+
+ steps:
+ - uses: actions/download-artifact@v5
+ with:
+ name: MSUScripterWindows
+ path: out
+ - uses: actions/download-artifact@v5
+ with:
+ name: MSUScripterLinux
+ path: out
+ - name: Extract some files
+ run: |
+ ls -alR
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ if: ${{ github.event_name != 'pull_request' }}
+ with:
+ path: "out/*"
+ name: MSUScripter_${{ needs.build-windows.outputs.version-number }}
+ - name: Delete old artifacts
+ uses: geekyeggo/delete-artifact@v5
+ with:
+ name: |
+ MSUScripterWindows
+ MSUScripterLinux
diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
deleted file mode 100644
index 2ecfcb5..0000000
--- a/.github/workflows/dotnet.yml
+++ /dev/null
@@ -1,58 +0,0 @@
-name: .NET Build + Publish
-
-on:
- push:
- branches: [main]
- pull_request:
- branches: [main]
-
-jobs:
- build:
- runs-on: windows-latest
-
- permissions:
- contents: write
-
- steps:
- - uses: actions/checkout@v4
- - name: Setup .NET
- uses: actions/setup-dotnet@v4
- with:
- dotnet-version: 9.0.x
- - name: Restore dependencies
- run: dotnet restore
- - name: Build
- run: dotnet build --no-restore -p:PostBuildEvent=
- - name: Publish Windows 64bit
- if: ${{ github.event_name != 'pull_request' }}
- run: dotnet publish --os win --arch x64 -c Release --self-contained false MSUScripter/MSUScripter.csproj
- - name: Publish Windows 32bit
- if: ${{ github.event_name != 'pull_request' }}
- run: dotnet publish --os win --arch x86 -c Release --self-contained false MSUScripter/MSUScripter.csproj
- - name: Publish Linux 64bit
- if: ${{ github.event_name != 'pull_request' }}
- run: dotnet publish --os linux --arch x64 -c Release --self-contained false MSUScripter/MSUScripter.csproj
- - name: Get version number
- if: ${{ github.event_name != 'pull_request' }}
- id: version
- run: |
- $version = (Get-Item "MSUScripter\bin\Release\net9.0\win-x86\publish\MSUScripter.exe").VersionInfo.ProductVersion
- $version = $version.Split("+")[0]
- Write-Host $version
- Write-Output "number=$version" >> $env:GITHUB_OUTPUT
- shell: pwsh
- - name: Building the Windows installer
- if: ${{ github.event_name != 'pull_request' }}
- run: '"%programfiles(x86)%/Inno Setup 6/iscc.exe" "setup/MSUScripter.iss"'
- shell: cmd
- - name: Building the Linux 64bit package
- if: ${{ github.event_name != 'pull_request' }}
- working-directory: setup
- run: "./LinuxBuildZipper.ps1"
- shell: pwsh
- - name: Upload artifact
- uses: actions/upload-artifact@v4
- if: ${{ github.event_name != 'pull_request' }}
- with:
- path: "setup/Output/*"
- name: MSUScripter_${{ steps.version.outputs.number }}
diff --git a/.gitignore b/.gitignore
index 78fdf50..9f5fb0d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,6 +31,7 @@ bld/
[Oo]bj/
[Ll]og/
[Ll]ogs/
+[Dd]eploy/
# Visual Studio 2015/2017 cache/options directory
.vs/
@@ -396,5 +397,6 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
+.idea/**/workspace.xml
-Setup/output/*
\ No newline at end of file
+[Ss]etup/[Oo]utput/*
\ No newline at end of file
diff --git a/.idea/.idea.MSUScripter/.idea/.gitignore b/.idea/.idea.MSUScripter/.idea/.gitignore
index 6a40d01..44cf8ba 100644
--- a/.idea/.idea.MSUScripter/.idea/.gitignore
+++ b/.idea/.idea.MSUScripter/.idea/.gitignore
@@ -2,10 +2,10 @@
/shelf/
/workspace.xml
# Rider ignored files
-/projectSettingsUpdater.xml
/modules.xml
-/.idea.MSUScripter.iml
/contentModel.xml
+/projectSettingsUpdater.xml
+/.idea.MSUScripter.iml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
diff --git a/.idea/.idea.MSUScripter/.idea/avalonia.xml b/.idea/.idea.MSUScripter/.idea/avalonia.xml
index 662d442..b0eb439 100644
--- a/.idea/.idea.MSUScripter/.idea/avalonia.xml
+++ b/.idea/.idea.MSUScripter/.idea/avalonia.xml
@@ -4,62 +4,35 @@
diff --git a/.idea/.idea.MSUScripter/.idea/projectSettingsUpdater.xml b/.idea/.idea.MSUScripter/.idea/projectSettingsUpdater.xml
new file mode 100644
index 0000000..ef20cb0
--- /dev/null
+++ b/.idea/.idea.MSUScripter/.idea/projectSettingsUpdater.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.MSUScripter/.idea/riderPublish.xml b/.idea/.idea.MSUScripter/.idea/riderPublish.xml
deleted file mode 100644
index df325f2..0000000
--- a/.idea/.idea.MSUScripter/.idea/riderPublish.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Docs/changelog.md b/Docs/changelog.md
new file mode 100644
index 0000000..26df81d
--- /dev/null
+++ b/Docs/changelog.md
@@ -0,0 +1,67 @@
+# MSU Scripter 5.0.0
+
+Release 5.0.0 introduces some major overhauls to the MSU Scripter in order to make the experience better for users.
+
+## Updated UI
+
+The UI has been completely redesigned to allow you to make things easier and more streamlined.
+
+### Main Starting Window
+
+
+
+The main starting window has been redone to have tabs for the creating new projects, opening projects, and changing settings.
+The first time launching, it will default to the new project tab, which now allows you to populate additional fields before creating the MSU project than before.
+When launching the MSU Scripter after creating a project, it will default to the open project tab.
+
+### MSU Project Window
+
+
+
+After opening a project, the list of all tracks and songs is now displayed on the left.
+Using this left panel, you can select the track you want to view, add songs to tracks, and even move songs around between different tracks.
+You can easily search for specific tracks as well as customize the view to show additional icons to easily see the status of the project.
+
+### Basic vs Advanced Song Views
+
+
+
+When editing the details of a song, you can choose to either use the basic view with integrated PyMusicLooper that will only show the most commonly edited fields,
+or you can use the advanced view which has all MsuPcm++ fields accessible. The advanced view has a panel similar to the track panel that allows you to add, copy,
+and move sub tracks and sub channels easily. By default the MSU Scripter will ask each time which view you want to use, but you can select a default view if desired.
+
+The buttons for playing the song and the audio controls are now always accessible at the bottom when viewing a song as well.
+
+## Dependency Installation
+
+
+
+A common issue people have ran into has been getting some of the dependencies installed such as PyMusicLooper and the YouTube video creation application.
+In order to help alleviate that, the MSU Scripter when first launching will check for dependencies and offer to install them for you. This will install
+portable versions of Python and ffmpeg. If you'd like to avoid the extra space, you can still install the dependencies manually by following the
+install documentation.
+
+## Better Linux Support
+
+Previously the Linux version of the MSU Scripter was limited in functionality. You were unable to jump to to specific parts of songs while playing them,
+it did not provide any warnings regarding the sample rate, and you had to manually install dotnet to get it to run. Going forward the Linux version is
+now being released as an AppImage file, so dotnet will no longer be a required pre-requisite to run the MSU Scripter.
+
+The AppImage file has been tested onto Linux Mint 21 (based on Ubuntu 22.04), Linux Mint Debian Edition 6 (based on Debian Bookworm), EndeavourOS
+(based on Arch), and Fedora. When first starting, the application will offer to create a Desktop file to add it to your desktop environment's menu.
+
+## Miscellaneous Changes and Fixes
+
+- Pressing space bar after clicking the button to play a song will now pause playing songs.
+- An additional track list format has been added. You can select "album - song (artist)", "song by artist (album)", and the "table" formats.
+- Fixed an issue where packaging MSUs into a zip file was adding in files that were no longer selected to be added.
+- Dither has been added as a per track option. If this is enabled, you will no longer be able to generate a tracks.json to send to other people to generate the MSU.
+- Fixed an issue where the file inputs would allow you to type into the them.
+- For non-looped tracks, the audio player will add a small pause before replaying from the beginning.
+- Fixed an issue where clicking prev in the PyMusicLooper panel would prevent you from clicking next again.
+- Fixed a crash that would occur when running PyMusicLooper and the starting samples would filter out all results.
+- Fixed an issue where pausing, moving the play tracker location, and resuming play would play a few incorrect samples before playing from the correct location.
+- Lowered the memory footprint that used to occur when changing tracks/songs (fixed by UI rewrite)
+- Fixed an issue where sometimes you would scroll accidentally down the page after entering values (fixed by UI redesign)
+- The MSU scripter sould now auto set itself as the default application for .msup (MSU Scripter Project) files
+- The pending changes window when closing a project now allows you to save
diff --git a/Docs/install.md b/Docs/install.md
new file mode 100644
index 0000000..e564cae
--- /dev/null
+++ b/Docs/install.md
@@ -0,0 +1,39 @@
+# Installation Instructions
+
+## Basic Installation
+
+### Windows
+
+1. Download the MSUScripterSetupWin file from the [latest release](https://github.com/MattEqualsCoder/MSUScripter/releases)
+2. Run the executable to install
+3. Install all dependencies in the dependency installer window
+
+### Linux
+
+1. Download the MSUScripter AppImage file from the [latest release](https://github.com/MattEqualsCoder/MSUScripter/releases)
+2. Place the AppImage file where you'd like it to exist on your computer and make the file executable
+3. Install all dependencies in the dependency installer window
+
+## Manual Dependency Installation
+
+With the exception of msupcm++, you can install the dependencies manually. This can be useful to preserve space if you are going to use FFmpeg or Python for other things.
+
+### FFmpeg
+
+FFmpeg is used by the Python Companion App to create YouTube videos for testing for copyright strikes.
+
+1. Download [FFmpeg](https://www.ffmpeg.org/download.html)
+2. Make sure FFmpeg is available in your PATH
+3. To verify you should be able to open up a new terminal/command prompt window and simply type in `ffmpeg -version` to get a response
+
+### Python Companion App
+
+The Python Companion App combines both PyMusicLooper and the copyright test video creator. It is not needed for basic MsuPcm++ usage.
+
+1. Download and install [Python](https://www.python.org/downloads/) (not needed on Linux)
+ - Make sure you select the option to add it to the path.
+2. Download and install [pipx](https://pipx.pypa.io/stable/installation/)
+ - Make sure to run `pipx ensurepath`
+3. Run the command `pipx install py-msu-scripter-app`
+ - If preferred, you can also install pip and install via `python -m pip install py-msu-scripter-app` or `pip install py-msu-scripter-app` based on your environment
+4. Verify it's installed by running either `py-msu-scripter-app --version` or `python -m py-msu-scripter-app --version`
diff --git a/LICENSE b/LICENSE
index edd5502..6503052 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2023 MattEqualsCoder
+Copyright (c) 2023-2025 MattEqualsCoder
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/MSUScripter/App.axaml b/MSUScripter/App.axaml
index 39674d6..d3e93ab 100644
--- a/MSUScripter/App.axaml
+++ b/MSUScripter/App.axaml
@@ -21,6 +21,7 @@
+
\ No newline at end of file
diff --git a/MSUScripter/App.axaml.cs b/MSUScripter/App.axaml.cs
index 5e4afe9..73dc681 100644
--- a/MSUScripter/App.axaml.cs
+++ b/MSUScripter/App.axaml.cs
@@ -1,5 +1,7 @@
using System.Diagnostics;
using System.Reflection;
+using System.Runtime.Versioning;
+using AppImageManager;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
@@ -7,20 +9,25 @@
using AvaloniaControls.Controls;
using Microsoft.Extensions.DependencyInjection;
using MSUScripter.Configs;
+using MSUScripter.Models;
using MSUScripter.Views;
namespace MSUScripter;
-public partial class App : Application
+public class App : Application
{
public static MainWindow MainWindow = null!;
+ public const string AppId = "org.mattequalscoder.msuscripter";
+ public const string AppName = "MSU Scripter";
+
+ private static readonly string? VersionOverride = null;
public static string Version
{
get
{
var version = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly()!.Location);
- return (version.ProductVersion ?? "").Split("+")[0];
+ return VersionOverride ?? (version.ProductVersion ?? "").Split("+")[0];
}
}
@@ -43,4 +50,13 @@ public override void OnFrameworkInitializationCompleted()
base.OnFrameworkInitializationCompleted();
}
+
+ [SupportedOSPlatform("linux")]
+ internal static CreateDesktopFileResponse BuildLinuxDesktopFile()
+ {
+ return new DesktopFileBuilder(AppId, AppName)
+ .AddUninstallAction(Directories.BaseFolder)
+ .WithMimeType("application/x-msu-scripter-project", "MSU Scripter Project", "*.msup", true)
+ .Build();
+ }
}
\ No newline at end of file
diff --git a/MSUScripter/empty.pcm b/MSUScripter/Assets/empty.pcm
similarity index 100%
rename from MSUScripter/empty.pcm
rename to MSUScripter/Assets/empty.pcm
diff --git a/MSUScripter/msu-randomizer-settings.yaml b/MSUScripter/Assets/msu-randomizer-settings.yaml
similarity index 100%
rename from MSUScripter/msu-randomizer-settings.yaml
rename to MSUScripter/Assets/msu-randomizer-settings.yaml
diff --git a/MSUScripter/Configs/DitherType.cs b/MSUScripter/Configs/DitherType.cs
new file mode 100644
index 0000000..f984a41
--- /dev/null
+++ b/MSUScripter/Configs/DitherType.cs
@@ -0,0 +1,21 @@
+using System.ComponentModel;
+
+namespace MSUScripter.Configs;
+
+public enum DitherType
+{
+ [Description("")]
+ Default,
+
+ [Description("All Tracks")]
+ All,
+
+ [Description("No Tracks")]
+ None,
+
+ [Description("Per Track (Default On)")]
+ DefaultOn,
+
+ [Description("Per Track (Default Off)")]
+ DefaultOff,
+}
\ No newline at end of file
diff --git a/MSUScripter/Configs/MsuBasicInfo.cs b/MSUScripter/Configs/MsuBasicInfo.cs
index bb0fc15..1db3fa6 100644
--- a/MSUScripter/Configs/MsuBasicInfo.cs
+++ b/MSUScripter/Configs/MsuBasicInfo.cs
@@ -14,13 +14,17 @@ public class MsuBasicInfo
public string? Url { get; set; }
public double? Normalization { get; set; }
public bool? Dither { get; set; }
+ public DitherType DitherType { get; set; } = DitherType.Default;
+ public bool HasSeenDitherWarning { get; set; }
public bool IsMsuPcmProject { get; set; } = true;
public bool CreateAltSwapperScript { get; set; } = true;
public bool CreateSplitSmz3Script { get; set; }
- public string TrackList { get; set; } = TrackListType.List;
+ public string TrackList { get; } = TrackListTypeDeprecated.List;
+ public TrackList TrackListType { get; set; } = Configs.TrackList.ListAlbumFirst;
public bool WriteYamlFile { get; set; } = true;
public string? ZeldaMsuPath { get; set; }
public string? MetroidMsuPath { get; set; }
public bool IsSmz3Project { get; set; }
+ public bool? IncludeJson { get; set; }
public DateTime LastModifiedDate { get; set; }
}
\ No newline at end of file
diff --git a/MSUScripter/Configs/MsuProject.cs b/MSUScripter/Configs/MsuProject.cs
index b2c57cb..7ae358d 100644
--- a/MSUScripter/Configs/MsuProject.cs
+++ b/MSUScripter/Configs/MsuProject.cs
@@ -1,26 +1,97 @@
using System;
using System.Collections.Generic;
+using System.IO;
using MSURandomizerLibrary.Configs;
using MSUScripter.Models;
-using MSUScripter.Tools;
using YamlDotNet.Serialization;
namespace MSUScripter.Configs;
public class MsuProject
{
+ public string Id { get; set; } = "";
public string ProjectFilePath { get; set; } = "";
public string BackupFilePath { get; set; } = "";
public string MsuPath { get; set; } = "";
public string MsuTypeName { get; set; } = "";
public DateTime LastSaveTime { get; set; }
- public List IgnoreWarnings { get; set; } = new();
+ public List IgnoreWarnings { get; set; } = [];
[YamlIgnore, SkipConvert]
public MsuType MsuType { get; set; } = null!;
[SkipConvert]
- public MsuBasicInfo BasicInfo { get; set; } = new();
+ public MsuBasicInfo BasicInfo { get; init; } = new();
[SkipConvert]
- public List Tracks { get; set; } = new();
+ public List Tracks { get; set; } = [];
+ public Dictionary SampleRates { get; set; } = [];
[YamlIgnore, SkipConvert]
public bool IsNewProject { get; set; }
+
+ public string GetMsuGenerationCacheFilePath()
+ {
+ return Path.Combine(Directories.CacheFolder, "Generation", $"{Id}.yml");
+ }
+
+ public string GetMsuGenerationTempFilePath(MsuSongInfo? song = null)
+ {
+ return song == null
+ ? Path.Combine(Directories.TempFolder, "Generation", Id, Guid.NewGuid().ToString("N"))
+ : Path.Combine(Directories.TempFolder, "Generation", Id, song.Id);
+ }
+
+ public string GetYamlPath()
+ {
+ return Path.ChangeExtension(MsuPath, ".yml");
+ }
+
+ public string GetMetroidMsuPath()
+ {
+ return BasicInfo.MetroidMsuPath ?? "";
+ }
+
+ public string GetZeldaMsuPath()
+ {
+ return BasicInfo.ZeldaMsuPath ?? "";
+ }
+
+ public string GetMetroidMsuYamlPath()
+ {
+ return Path.ChangeExtension(GetMetroidMsuPath(), ".yml");
+ }
+
+ public string GetZeldaMsuYamlPath()
+ {
+ return Path.ChangeExtension(GetZeldaMsuPath(), ".yml");
+ }
+
+ public string GetTracksJsonPath()
+ {
+ var fileInfo = new FileInfo(MsuPath);
+ return Path.Combine(fileInfo.DirectoryName ?? "", "tracks.json");
+ }
+
+ public string GetTracksTextPath()
+ {
+ var msuFileInfo = new FileInfo(MsuPath);
+ return Path.Combine(msuFileInfo.DirectoryName!, "Track List.txt");
+ }
+
+ public string GetAltSwapperPath()
+ {
+ var fileInfo = new FileInfo(MsuPath);
+ return Path.Combine(fileInfo.DirectoryName ?? "", "!Swap_Alt_Tracks.bat");
+ }
+
+ public string GetSmz3SwapperPath()
+ {
+ var fileInfo = new FileInfo(MsuPath);
+ return Path.Combine(fileInfo.DirectoryName ?? "", "!Split_Or_Combine_SMZ3_ALttP_SM_MSUs.bat");
+ }
+
+ [YamlIgnore, SkipConvert] public MsuProjectGenerationCache GenerationCache { get; set; } = new();
+}
+
+public class FileSampleInfo
+{
+ public long FileLength { get; init; }
+ public int SampleRate { get; init; }
}
\ No newline at end of file
diff --git a/MSUScripter/Configs/MsuSongInfo.cs b/MSUScripter/Configs/MsuSongInfo.cs
index 4ac614c..5fd0184 100644
--- a/MSUScripter/Configs/MsuSongInfo.cs
+++ b/MSUScripter/Configs/MsuSongInfo.cs
@@ -7,8 +7,9 @@ namespace MSUScripter.Configs;
[Description("Details about a song for YAML and PCM generation")]
public class MsuSongInfo
{
- [JsonSchemaIgnore]
- public int TrackNumber { get; set; }
+ [JsonSchemaIgnore] public string Id { get; set; } = "";
+
+ [JsonSchemaIgnore] public int TrackNumber { get; set; }
[JsonSchemaIgnore]
public string? TrackName { get; set; }
@@ -42,15 +43,20 @@ public class MsuSongInfo
[JsonSchemaIgnore]
public bool IsComplete { get; set; }
-
- [JsonSchemaIgnore]
- public bool ShowPanel { get; set; } = true;
-
+
[JsonSchemaIgnore]
public DateTime LastModifiedDate { get; set; }
[JsonSchemaIgnore]
public DateTime LastGeneratedDate { get; set; }
-
+ [JsonSchemaIgnore]
+ public bool? DisplayAdvancedMode { get; set; }
+
+ public bool HasAudioFiles() => MsuPcmInfo.HasFiles();
+
+ public bool HasChangesSince(DateTime time)
+ {
+ return MsuPcmInfo.HasChangesSince(time) || LastModifiedDate > time;
+ }
}
\ No newline at end of file
diff --git a/MSUScripter/Configs/MsuSongMsuPcmInfo.cs b/MSUScripter/Configs/MsuSongMsuPcmInfo.cs
index b27b3d6..bdfbdd0 100644
--- a/MSUScripter/Configs/MsuSongMsuPcmInfo.cs
+++ b/MSUScripter/Configs/MsuSongMsuPcmInfo.cs
@@ -52,6 +52,9 @@ public class MsuSongMsuPcmInfo
[Description("The file to be used as the input for this track/sub-track/sub-channel")]
public string? File { get; set; }
+ [Description("Whether or not to apply audio dither to the final output.")]
+ public bool? Dither { get; set; }
+
[JsonSchemaIgnore]
public bool ShowPanel { get; set; } = true;
@@ -71,8 +74,18 @@ public void ClearFieldsForYaml()
}
}
+ public bool AreFilesValid()
+ {
+ return GetFiles().All(System.IO.File.Exists);
+ }
+
public List GetFiles()
{
+ if (!string.IsNullOrEmpty(Output))
+ {
+ var outputFile = Output;
+ }
+
List files = new List();
if (!string.IsNullOrEmpty(File))
@@ -96,4 +109,79 @@ public bool HasBothSubTracksAndSubChannels
SubTracks.Any(x => x.HasBothSubTracksAndSubChannels);
}
}
+
+ [YamlIgnore]
+ public bool HasValidSubChannelCount
+ {
+ get
+ {
+ return SubChannels.Count != 1 && SubChannels.All(x => x.HasValidSubChannelCount) &&
+ SubTracks.All(x => x.HasValidSubChannelCount);
+ }
+ }
+
+ [YamlIgnore]
+ public bool HasValidChildTypes
+ {
+ get
+ {
+ return SubChannels.All(x => x.SubChannels.Count == 0 && x.HasValidChildTypes) &&
+ SubTracks.All(x => x.SubTracks.Count == 0 && x.HasValidChildTypes);
+ }
+ }
+
+ public bool HasData()
+ {
+ return Loop > 0 || TrimStart > 0 || TrimEnd > 0 || FadeIn > 0 || FadeOut > 0 || CrossFade > 0 || PadStart > 0 ||
+ PadEnd > 0 || (Tempo.HasValue && Tempo != 0) || (Normalization.HasValue && Normalization != 0) ||
+ !string.IsNullOrEmpty(File) || SubChannels.Count > 0 || SubTracks.Count > 0;
+ }
+
+ public bool HasAdvancedData()
+ {
+ return FadeIn > 0 || FadeOut > 0 || CrossFade > 0 || PadStart > 0 || PadEnd > 0 ||
+ (Tempo.HasValue && Tempo != 0) || SubChannels.Count > 0 || SubTracks.Count > 0;
+ }
+
+ public bool HasFiles()
+ {
+ return GetFiles().Count > 0;
+ }
+
+ public int MoveSubInfo(MsuSongMsuPcmInfo info, bool toSubTrack, int index, MsuSongMsuPcmInfo? previousParent)
+ {
+ var destination = toSubTrack ? SubTracks : SubChannels;
+
+ if (destination.Contains(info))
+ {
+ var currentIndex = destination.IndexOf(info);
+ if (index > currentIndex)
+ {
+ index--;
+ }
+ }
+
+ previousParent?.SubTracks.Remove(info);
+ previousParent?.SubChannels.Remove(info);
+
+ if (index > destination.Count)
+ {
+ destination.Add(info);
+ }
+ else
+ {
+ destination.Insert(index, info);
+ }
+
+ return index;
+ }
+
+ public bool HasChangesSince(DateTime time)
+ {
+ if (SubTracks.Any(x => x.HasChangesSince(time)))
+ return true;
+ if (SubChannels.Any(x => x.HasChangesSince(time)))
+ return true;
+ return LastModifiedDate > time;
+ }
}
\ No newline at end of file
diff --git a/MSUScripter/Configs/MsuTrackInfo.cs b/MSUScripter/Configs/MsuTrackInfo.cs
index 6374060..8c9e901 100644
--- a/MSUScripter/Configs/MsuTrackInfo.cs
+++ b/MSUScripter/Configs/MsuTrackInfo.cs
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
+using System.IO;
+using System.Linq;
using MSUScripter.Models;
namespace MSUScripter.Configs;
@@ -12,5 +14,107 @@ public class MsuTrackInfo
public bool IsScratchPad { get; set; }
[SkipConvert]
- public List Songs { get; set; } = new List();
+ public List Songs { get; set; } = [];
+
+ public MsuSongInfo AddSong(MsuProject project, int index = 0, bool advancedMode = false)
+ {
+ var msu = new FileInfo(project.MsuPath);
+
+ var newSong = new MsuSongInfo
+ {
+ Id = Guid.NewGuid().ToString("N"),
+ TrackNumber = TrackNumber,
+ TrackName = TrackName,
+ DisplayAdvancedMode = advancedMode
+ };
+
+ UpdateSongPath(project, newSong, index);
+
+ Songs.Insert(index, newSong);
+
+ for (var i = index + 1; i < Songs.Count; i++)
+ {
+ var oldIndex = i - 1;
+ var oldIsAlt = oldIndex > 0;
+ string oldDefaultOutputPath;
+
+ if (!oldIsAlt)
+ {
+ oldDefaultOutputPath = msu.FullName.Replace(msu.Extension, $"-{TrackNumber}.pcm");
+ }
+ else
+ {
+ var altSuffix = oldIndex == 1 ? "alt" : $"alt{oldIndex}";
+ oldDefaultOutputPath = msu.FullName.Replace(msu.Extension, $"-{TrackNumber}_{altSuffix}.pcm");
+ }
+
+ if (Songs[i].OutputPath == oldDefaultOutputPath)
+ {
+ var altSuffix = i == 1 ? "alt" : $"alt{i}";
+ var newOutputPath = msu.FullName.Replace(msu.Extension, $"-{TrackNumber}_{altSuffix}.pcm");
+ Songs[i].OutputPath = newOutputPath;
+ Songs[i].MsuPcmInfo.Output = newOutputPath;
+ }
+ }
+
+ return newSong;
+ }
+
+ public void RemoveSong(MsuSongInfo song)
+ {
+ var index = Songs.IndexOf(song);
+
+ for (var i = Songs.Count - 1; i >= index + 1; i--)
+ {
+ Songs[i].OutputPath = Songs[i - 1].OutputPath;
+ Songs[i].MsuPcmInfo.Output = Songs[i - 1].MsuPcmInfo.Output;
+ Songs[i].IsAlt = Songs[i - 1].IsAlt;
+ }
+
+ Songs.Remove(song);
+ }
+
+ public void MoveSong(MsuProject project, MsuSongInfo song, int index)
+ {
+ var oldTrack = project.Tracks.First(x => x.TrackNumber == song.TrackNumber);
+
+ oldTrack.Songs.Remove(song);
+ for (var i = 0; i < oldTrack.Songs.Count; i++)
+ {
+ UpdateSongPath(project, oldTrack.Songs[i], i);
+ }
+
+ Songs.Insert(index, song);
+ for (var i = 0; i < Songs.Count; i++)
+ {
+ UpdateSongPath(project, Songs[i], i);
+ }
+
+ song.TrackName = TrackName;
+ song.TrackNumber = TrackNumber;
+ }
+
+ private void UpdateSongPath(MsuProject project, MsuSongInfo song, int? index = null)
+ {
+ index ??= Songs.IndexOf(song);
+
+ var msu = new FileInfo(project.MsuPath);
+ song.IsAlt = index > 0;
+
+ if (song.TrackNumber >= 9999)
+ {
+ song.OutputPath = Path.Combine(Directories.TempFolder, project.Id, song.Id, "temp.pcm");
+ }
+ else if (!song.IsAlt)
+ {
+ song.OutputPath = msu.FullName.Replace(msu.Extension, $"-{TrackNumber}.pcm");
+ }
+ else
+ {
+ var altSuffix = index == 1 ? "alt" : $"alt{index}";
+ song.OutputPath = msu.FullName.Replace(msu.Extension, $"-{TrackNumber}_{altSuffix}.pcm");
+ }
+
+ song.MsuPcmInfo.Output = song.OutputPath;
+ }
}
\ No newline at end of file
diff --git a/MSUScripter/Configs/Settings.cs b/MSUScripter/Configs/Settings.cs
index 780b29e..48db5b7 100644
--- a/MSUScripter/Configs/Settings.cs
+++ b/MSUScripter/Configs/Settings.cs
@@ -1,11 +1,12 @@
using System.Collections.Generic;
+using MSUScripter.ViewModels;
namespace MSUScripter.Configs;
public class Settings
{
public string? MsuPcmPath { get; set; }
- public bool PromptOnUpdate { get; set; } = true;
+ public bool CheckForUpdates { get; set; } = true;
public bool PromptOnPreRelease { get; set; }
public bool DarkTheme { get; set; } = true;
public int LoopDuration { get; set; } = 5;
@@ -13,9 +14,20 @@ public class Settings
public ICollection RecentProjects { get; set; } = new List();
public double Volume { get; set; } = 1;
public string? PreviousPath { get; set; }
+ public string? PreviousVideoPath { get; set; }
public bool HideSubTracksSubChannelsWarning { get; set; }
public bool AutomaticallyRunPyMusicLooper { get; set; } = true;
public bool RunMsuPcmWithKeepTemps { get; set; }
public bool HasDoneFirstTimeSetup { get; set; }
- public string? PyMusicLooperPath { get; set; }
+ public bool IgnoreMissingDependencies { get; set; }
+ public bool ProjectTreeFilterOnlyTracksMissingSongs { get; set; }
+ public bool ProjectTreeFilterOnlyIncomplete { get; set; }
+ public bool ProjectTreeFilterOnlyMissingAudio { get; set; }
+ public bool ProjectTreeFilterOnlyCopyrightUntested { get; set; }
+ public bool ProjectTreeDisplayIsCompleteIcon { get; set; }
+ public bool ProjectTreeDisplayHasSongIcon { get; set; }
+ public bool ProjectTreeDisplayCheckCopyrightIcon { get; set; }
+ public bool ProjectTreeDisplayCopyrightSafeIcon { get; set; }
+ public bool SkipDesktopFile { get; set; }
+ public DefaultSongPanel DefaultSongPanel { get; set; } = DefaultSongPanel.Prompt;
}
\ No newline at end of file
diff --git a/MSUScripter/Configs/TrackListType.cs b/MSUScripter/Configs/TrackListType.cs
index 7ff4628..bfb29d6 100644
--- a/MSUScripter/Configs/TrackListType.cs
+++ b/MSUScripter/Configs/TrackListType.cs
@@ -1,6 +1,8 @@
-namespace MSUScripter.Configs;
+using System.ComponentModel;
-public static class TrackListType
+namespace MSUScripter.Configs;
+
+public static class TrackListTypeDeprecated
{
public const string List = "List";
public const string Table = "Table";
@@ -12,4 +14,14 @@ public static class TrackListType
Table,
Disabled
];
+}
+
+public enum TrackList
+{
+ [Description("List: album - song (artist)")]
+ ListAlbumFirst,
+ [Description("List: song by artist (album)")]
+ ListSongFirst,
+ Table,
+ Disabled
}
\ No newline at end of file
diff --git a/MSUScripter/MSUScripter.csproj b/MSUScripter/MSUScripter.csproj
index 7c0ff0a..991179c 100644
--- a/MSUScripter/MSUScripter.csproj
+++ b/MSUScripter/MSUScripter.csproj
@@ -1,66 +1,71 @@
-
- WinExe
- net9.0
- enable
- true
- app.manifest
- true
- MSUScripterIcon.ico
- MSUScripterIcon.ico
- 4.2.1-beta.1
- 9.0.0
- false
- 12
-
+
+ WinExe
+ net9.0
+ enable
+ true
+ app.manifest
+ true
+ MSUScripterIcon.ico
+ MSUScripterIcon.ico
+ 5.0.0
+ 9.0.0
+ false
+ 12
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
- MainWindow.axaml
- Code
-
-
-
-
-
-
-
+
+
+ MainWindow.axaml
+ Code
+
+
+
+
+
+
-
-
- C:\Users\matte\.nuget\packages\avalonia.themes.simple\11.0.0\lib\net6.0\Avalonia.Themes.Simple.dll
-
-
+
+
+
+ C:\Users\matte\.nuget\packages\avalonia.themes.simple\11.0.0\lib\net6.0\Avalonia.Themes.Simple.dll
+
+
-
-
-
-
-
+
+
+
diff --git a/MSUScripter/Models/AnalysisDataOutput.cs b/MSUScripter/Models/AnalysisDataOutput.cs
index f305e1f..aa49532 100644
--- a/MSUScripter/Models/AnalysisDataOutput.cs
+++ b/MSUScripter/Models/AnalysisDataOutput.cs
@@ -2,6 +2,6 @@ namespace MSUScripter.Models;
public class AnalysisDataOutput
{
- public double? AvgDecibels { get; set; }
- public double? MaxDecibels { get; set; }
+ public double? AvgDecibels { get; init; }
+ public double? MaxDecibels { get; init; }
}
\ No newline at end of file
diff --git a/MSUScripter/Models/BasicEventArgs.cs b/MSUScripter/Models/BasicEventArgs.cs
deleted file mode 100644
index 3d4df16..0000000
--- a/MSUScripter/Models/BasicEventArgs.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-namespace MSUScripter.Models;
-
-public class BasicEventArgs
-{
- public string? Data { get; set; }
-
- public BasicEventArgs(string? data)
- {
- Data = data;
- }
-}
\ No newline at end of file
diff --git a/MSUScripter/Models/Directories.cs b/MSUScripter/Models/Directories.cs
index 34fa0ab..ec827b3 100644
--- a/MSUScripter/Models/Directories.cs
+++ b/MSUScripter/Models/Directories.cs
@@ -4,12 +4,14 @@
namespace MSUScripter.Models;
-public class Directories
+public static class Directories
{
public static string BaseFolder => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MSUScripter");
public static string LogFolder => Path.Combine(BaseFolder, "logs");
+ public static string Dependencies => Path.Combine(BaseFolder, "dependencies");
+
public static string CacheFolder
{
get
@@ -35,33 +37,4 @@ public static string TempFolder
return path;
}
}
-
- public static bool OpenDirectory(string path, bool isFile = false)
- {
- if (isFile)
- {
- path = new FileInfo(path).DirectoryName ?? "";
- }
-
- if (!Directory.Exists(path))
- {
- return false;
- }
-
- try
- {
- Process.Start(new ProcessStartInfo()
- {
- FileName = path,
- UseShellExecute = true,
- Verb = "open"
- });
- }
- catch (Exception)
- {
- return false;
- }
-
- return true;
- }
}
\ No newline at end of file
diff --git a/MSUScripter/Models/FileInputControlType.cs b/MSUScripter/Models/FileInputControlType.cs
deleted file mode 100644
index f837ba8..0000000
--- a/MSUScripter/Models/FileInputControlType.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace MSUScripter.Models;
-
-public enum FileInputControlType
-{
- OpenFile,
- SaveFile,
- Folder
-}
\ No newline at end of file
diff --git a/MSUScripter/Models/MessageWindowResult.cs b/MSUScripter/Models/MessageWindowResult.cs
deleted file mode 100644
index 20786ee..0000000
--- a/MSUScripter/Models/MessageWindowResult.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace MSUScripter.Models;
-
-public enum MessageWindowResult
-{
- Ok,
- Cancel,
- Yes,
- No,
- DontShow
-}
\ No newline at end of file
diff --git a/MSUScripter/Models/MessageWindowType.cs b/MSUScripter/Models/MessageWindowType.cs
deleted file mode 100644
index d40ff34..0000000
--- a/MSUScripter/Models/MessageWindowType.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace MSUScripter.Models;
-
-public enum MessageWindowType
-{
- Basic,
- Warning,
- Error,
- Info,
- YesNo,
- PcmWarning,
- DoNotShowAgain
-}
\ No newline at end of file
diff --git a/MSUScripter/Models/MsuProjectGenerationCache.cs b/MSUScripter/Models/MsuProjectGenerationCache.cs
new file mode 100644
index 0000000..a62abd5
--- /dev/null
+++ b/MSUScripter/Models/MsuProjectGenerationCache.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Concurrent;
+
+namespace MSUScripter.Models;
+
+public class MsuProjectGenerationCache
+{
+ public ConcurrentDictionary Songs { get; set; } = [];
+}
+
+public class MsuProjectSongCache
+{
+ public ulong JsonHash { get; init; }
+ public int JsonLength { get; init; }
+ public DateTime FileGenerationTime { get; init; }
+ public long FileLength { get; init; }
+
+ public static bool IsValid(MsuProjectSongCache? a, MsuProjectSongCache? b)
+ {
+ if (a is null || b is null) return false;
+ return a.JsonHash == b.JsonHash && a.JsonLength == b.JsonLength &&
+ a.FileGenerationTime == b.FileGenerationTime & a.FileLength == b.FileLength;
+ }
+}
\ No newline at end of file
diff --git a/MSUScripter/Models/PcmEventArgs.cs b/MSUScripter/Models/PcmEventArgs.cs
deleted file mode 100644
index 838c2ec..0000000
--- a/MSUScripter/Models/PcmEventArgs.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System;
-using MSUScripter.Tools;
-using MSUScripter.ViewModels;
-
-namespace MSUScripter.Models;
-
-public class PcmEventArgs : EventArgs
-{
- public MsuSongInfoViewModel Song { get; set; }
- public PcmEventType Type { get; set; }
- public MsuSongMsuPcmInfoViewModel? PcmInfo { get; set; }
-
- public PcmEventArgs(MsuSongInfoViewModel song, PcmEventType type, MsuSongMsuPcmInfoViewModel? ppmInfo = null)
- {
- Song = song;
- Type = type;
- PcmInfo = ppmInfo;
- }
-}
\ No newline at end of file
diff --git a/MSUScripter/Models/PcmEventType.cs b/MSUScripter/Models/PcmEventType.cs
deleted file mode 100644
index e7293fe..0000000
--- a/MSUScripter/Models/PcmEventType.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace MSUScripter.Models;
-
-public enum PcmEventType
-{
- Play,
- PlayLoop,
- Generate,
- GenerateAsPrimary,
- LoopWindow,
- StopMusic,
- GenerateEmpty,
- StartingSamples,
- AddedSubChannelOrSubTrack
-}
\ No newline at end of file
diff --git a/MSUScripter/Models/PythonCompanionServiceModels.cs b/MSUScripter/Models/PythonCompanionServiceModels.cs
new file mode 100644
index 0000000..dcd0b5d
--- /dev/null
+++ b/MSUScripter/Models/PythonCompanionServiceModels.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Text;
+// ReSharper disable UnusedAutoPropertyAccessor.Global
+// ReSharper disable CollectionNeverUpdated.Global
+// ReSharper disable UnusedMember.Global
+// ReSharper disable ClassNeverInstantiated.Global
+
+namespace MSUScripter.Models;
+
+public class GetSampleRateRequest
+{
+ public string Mode => "samples";
+ public required string File { get; init; }
+}
+
+public class GetSampleRateResponse : PythonCompanionModeResponse
+{
+ public double Duration { get; set; }
+ public int SampleRate { get; init; } = 44100;
+ public int Channels { get; init; }
+ public int BitsPerSample { get; init; }
+ public bool IsBlankSuccess { get; init; }
+}
+
+public class RunPyMusicLooperRequest
+{
+ public string Mode => "py_music_looper";
+ public required string File { get; init; }
+ public double? MinDurationMultiplier { get; init; } = 0.25f;
+ public double? MinLoopDuration { get; init; }
+ public double? MaxLoopDuration { get; init; }
+ public double? ApproxLoopStart { get; init; }
+ public double? ApproxLoopEnd { get; init; }
+
+}
+
+public class RunPyMusicLooperResponse : PythonCompanionModeResponse
+{
+ public List Pairs { get; init; } = [];
+}
+
+public class PythonCompanionModeResponse
+{
+ public bool Successful { get; set; }
+ public string Error { get; set; } = string.Empty;
+}
+
+public class CreateVideoRequest
+{
+ public string Mode => "create_video";
+ public required string OutputVideo { get; set; }
+ public string? ProgressFile { get; set; }
+ public required List Files { get; set; }
+}
+
+public class CreateVideoResponse : PythonCompanionModeResponse;
+
+public class RunPyResult
+{
+ public bool Success { get; set; }
+ public string Result { get; init; } = string.Empty;
+ public string Error { get; init; } = string.Empty;
+ public bool IsBlankSuccess => Success && string.IsNullOrEmpty(Result) && string.IsNullOrEmpty(Error);
+}
+public class PyMusicLooperPair
+{
+ public int LoopStart { get; set; }
+ public int LoopEnd { get; set; }
+ public decimal LoudnessDifference { get; set; }
+ public decimal NoteDistance { get; set; }
+ public decimal Score { get; set; }
+}
+
+public class PyMusicLooperCacheKey
+{
+ private string File { get; }
+ private DateTime AudioFileModifiedDate { get; }
+ private long AudioFileLength { get; }
+ private string MinDurationMultiplier { get; }
+ private string MinLoopDuration { get; }
+ private string MaxLoopDuration { get; }
+ private string ApproxLoopStart { get; }
+ private string ApproxLoopEnd { get; }
+
+ public PyMusicLooperCacheKey(RunPyMusicLooperRequest request)
+ {
+ File = request.File;
+
+ var fileInfo = new FileInfo(request.File);
+ AudioFileModifiedDate = fileInfo.LastWriteTimeUtc;
+ AudioFileLength = fileInfo.Length;
+
+ MinDurationMultiplier = Math.Round(request.MinDurationMultiplier ?? -1, 4).ToString(CultureInfo.InvariantCulture);
+ MinLoopDuration = Math.Round(request.MinLoopDuration ?? -1, 4).ToString(CultureInfo.InvariantCulture);
+ MaxLoopDuration = Math.Round(request.MaxLoopDuration ?? -1, 4).ToString(CultureInfo.InvariantCulture);
+ ApproxLoopStart = Math.Round(request.ApproxLoopStart ?? -1, 4).ToString(CultureInfo.InvariantCulture);
+ ApproxLoopEnd = Math.Round(request.ApproxLoopEnd ?? -1, 4).ToString(CultureInfo.InvariantCulture);
+ }
+
+ public override string ToString()
+ {
+ var inputBytes = Encoding.UTF8.GetBytes(File);
+ var hashBytes = System.Security.Cryptography.MD5.HashData(inputBytes);
+ var fileHash = Convert.ToHexString(hashBytes);
+
+ var start =
+ $"{File}|{AudioFileModifiedDate}|{AudioFileLength}|{MinDurationMultiplier}|{MinLoopDuration}|{MaxLoopDuration}|{ApproxLoopStart}|{ApproxLoopEnd}";
+ inputBytes = Encoding.UTF8.GetBytes(start);
+ hashBytes = System.Security.Cryptography.MD5.HashData(inputBytes);
+ var keyHash = Convert.ToHexString(hashBytes);
+
+ return $"{fileHash}_{keyHash}";
+ }
+}
diff --git a/MSUScripter/Models/RunMethod.cs b/MSUScripter/Models/RunMethod.cs
index 23ce4e5..b936545 100644
--- a/MSUScripter/Models/RunMethod.cs
+++ b/MSUScripter/Models/RunMethod.cs
@@ -3,6 +3,7 @@ namespace MSUScripter.Models;
public enum RunMethod
{
Unknown,
+ Installed,
Direct,
Py,
Python3
diff --git a/MSUScripter/Models/SkipConvertAttribute.cs b/MSUScripter/Models/SkipConvertAttribute.cs
index c96cea5..dc38bfb 100644
--- a/MSUScripter/Models/SkipConvertAttribute.cs
+++ b/MSUScripter/Models/SkipConvertAttribute.cs
@@ -1,7 +1,4 @@
namespace MSUScripter.Models;
-[System.AttributeUsage(System.AttributeTargets.Property)
-]
-public class SkipConvertAttribute : System.Attribute
-{
-}
\ No newline at end of file
+[System.AttributeUsage(System.AttributeTargets.Property) ]
+public class SkipConvertAttribute : System.Attribute;
\ No newline at end of file
diff --git a/MSUScripter/Models/SkipLastModifiedAttribute.cs b/MSUScripter/Models/SkipLastModifiedAttribute.cs
new file mode 100644
index 0000000..550434f
--- /dev/null
+++ b/MSUScripter/Models/SkipLastModifiedAttribute.cs
@@ -0,0 +1,4 @@
+namespace MSUScripter.Models;
+
+[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Class)]
+public class SkipLastModifiedAttribute : System.Attribute;
\ No newline at end of file
diff --git a/MSUScripter/Models/SongFileEventArgs.cs b/MSUScripter/Models/SongFileEventArgs.cs
deleted file mode 100644
index 8d2a687..0000000
--- a/MSUScripter/Models/SongFileEventArgs.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System;
-using MSUScripter.ViewModels;
-
-namespace MSUScripter.Models;
-
-public class SongFileEventArgs : EventArgs
-{
- public SongFileEventArgs(MsuSongInfoViewModel songViewModel, string filePath, bool force)
- {
- SongViewModel = songViewModel;
- FilePath = filePath;
- Force = force;
- }
-
- public MsuSongInfoViewModel SongViewModel { get; set; }
- public string FilePath { get; set; }
- public bool Force { get; set; }
-
-}
\ No newline at end of file
diff --git a/MSUScripter/Models/TrackEventArgs.cs b/MSUScripter/Models/TrackEventArgs.cs
deleted file mode 100644
index e9919a4..0000000
--- a/MSUScripter/Models/TrackEventArgs.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using System;
-
-namespace MSUScripter.Models;
-
-public class TrackEventArgs : EventArgs
-{
- public int TrackNumber { get; set; }
-
- public TrackEventArgs(int trackNumber)
- {
- TrackNumber = trackNumber;
- }
-}
\ No newline at end of file
diff --git a/MSUScripter/Models/VideoCreatorServiceEventArgs.cs b/MSUScripter/Models/VideoCreatorServiceEventArgs.cs
deleted file mode 100644
index 883f60c..0000000
--- a/MSUScripter/Models/VideoCreatorServiceEventArgs.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System;
-
-namespace MSUScripter.Models;
-
-public class VideoCreatorServiceEventArgs: EventArgs
-{
- public bool Successful { get; set; }
-
- public string? Message { get; set; }
-
- public VideoCreatorServiceEventArgs(bool successful, string? message)
- {
- Successful = successful;
- Message = message;
- }
-}
\ No newline at end of file
diff --git a/MSUScripter/Models/WindowRestoreDetails.cs b/MSUScripter/Models/WindowRestoreDetails.cs
deleted file mode 100644
index 1b3ada1..0000000
--- a/MSUScripter/Models/WindowRestoreDetails.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using Avalonia;
-
-namespace MSUScripter.Models;
-
-public class WindowRestoreDetails
-{
- public int X { get; set; }
- public int Y { get; set; }
- public double Width { get; set; }
- public double Height { get; set; }
- public bool IsMaximized { get; set; }
-
- public PixelPoint GetPosition() => new(X, Y);
-}
\ No newline at end of file
diff --git a/MSUScripter/Program.cs b/MSUScripter/Program.cs
index 52e8dd7..a9f91bf 100644
--- a/MSUScripter/Program.cs
+++ b/MSUScripter/Program.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+// ReSharper disable once RedundantUsingDirective
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -15,8 +16,6 @@
using MSURandomizerLibrary;
using MSUScripter.Models;
using MSUScripter.Services;
-using MSUScripter.Services.ControlServices;
-using MSUScripter.Views;
using Serilog;
using Win32RenderingMode = Avalonia.Win32RenderingMode;
@@ -62,7 +61,7 @@ public static void Main(string[] args)
{
StartingProject = args[0];
}
-
+
MainHost = Host.CreateDefaultBuilder(args)
.UseSerilog()
.ConfigureLogging(logging =>
@@ -75,7 +74,7 @@ public static void Main(string[] args)
})
.Build();
- InitializeServices(args);
+ InitializeServices();
ExceptionWindow.GitHubUrl = "https://github.com/MattEqualsCoder/MSUScripter/issues";
ExceptionWindow.LogPath = Directories.LogFolder;
@@ -89,14 +88,14 @@ public static void Main(string[] args)
}
catch (Exception e)
{
- ShowExceptionPopup(e).ContinueWith(t => source.Cancel(), TaskScheduler.FromCurrentSynchronizationContext());
+ ShowExceptionPopup(e).ContinueWith(_ => source.Cancel(), TaskScheduler.FromCurrentSynchronizationContext());
Dispatcher.UIThread.MainLoop(source.Token);
}
}
// Avalonia configuration, don't remove; also used by visual designer.
- public static AppBuilder BuildAvaloniaApp()
+ private static AppBuilder BuildAvaloniaApp()
{
return AppBuilder.Configure()
.UsePlatformDetect()
@@ -118,28 +117,26 @@ private static IServiceCollection ConfigureServices(IServiceCollection collectio
.AddSingleton()
.AddSingleton()
.AddSingleton()
- .AddSingleton()
.AddSingleton()
.AddSingleton()
- .AddTransient()
- .AddTransient()
- .AddTransient()
+ .AddSingleton()
+ .AddSingleton()
.AddAvaloniaControlServices()
.AddTransient();
if (OperatingSystem.IsWindows())
{
- collection.AddSingleton();
+ collection.AddSingleton();
}
else
{
- collection.AddSingleton();
+ collection.AddSingleton();
}
return collection;
}
- private static void InitializeServices(string[] args)
+ private static void InitializeServices()
{
var services = MainHost.Services;
services.GetRequiredService();
@@ -147,7 +144,7 @@ private static void InitializeServices(string[] args)
services.GetRequiredService();
services.GetRequiredService();
services.GetRequiredService();
- services.GetRequiredService().Initialize(args);
+ services.GetRequiredService().Initialize();
}
private static async Task ShowExceptionPopup(Exception e)
diff --git a/MSUScripter/Services/ApplicationInitializationService.cs b/MSUScripter/Services/ApplicationInitializationService.cs
index a364932..6ef38e0 100644
--- a/MSUScripter/Services/ApplicationInitializationService.cs
+++ b/MSUScripter/Services/ApplicationInitializationService.cs
@@ -11,14 +11,14 @@ namespace MSUScripter.Services;
public class ApplicationInitializationService(ILogger logger)
{
- public void Initialize(string[] args)
+ public void Initialize()
{
logger.LogInformation("Assembly Location: {Location}", Assembly.GetExecutingAssembly().Location);
- logger.LogInformation("Starting MSU Scripter {Version}", GetAppVersion());
+ logger.LogInformation("Starting MSU Scripter {Version}", App.Version);
var msuInitializationRequest = new MsuRandomizerInitializationRequest
{
- MsuAppSettingsStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MSUScripter.msu-randomizer-settings.yaml"),
+ MsuAppSettingsStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MSUScripter.Assets.msu-randomizer-settings.yaml"),
UserOptionsPath = Path.Combine(Directories.BaseFolder, "msu-user-settings.yml")
};
@@ -29,10 +29,4 @@ public void Initialize(string[] args)
Program.MainHost.Services.GetRequiredService().Initialize(msuInitializationRequest);
}
-
- private static string GetAppVersion()
- {
- var version = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly()!.Location);
- return (version.ProductVersion ?? "").Split("+")[0];
- }
}
\ No newline at end of file
diff --git a/MSUScripter/Services/AudioAnalysisService.cs b/MSUScripter/Services/AudioAnalysisService.cs
index d27a67f..1aafcbc 100644
--- a/MSUScripter/Services/AudioAnalysisService.cs
+++ b/MSUScripter/Services/AudioAnalysisService.cs
@@ -9,34 +9,49 @@
using MSUScripter.Models;
using MSUScripter.ViewModels;
using NAudio.Wave;
+using SoundFlow.Interfaces;
+using SoundFlow.Providers;
using File = System.IO.File;
namespace MSUScripter.Services;
+public class AudioSampleRateResponse
+{
+ public int SampleRate { get; set; } = 44100;
+ public bool Successful { get; set; }
+}
+
public class AudioAnalysisService(
IAudioPlayerService audioPlayerService,
MsuPcmService msuPcmService,
StatusBarService statusBarService,
- ConverterService converterService,
+ PythonCompanionService pythonCompanionService,
ILogger logger)
{
public async Task AnalyzePcmFiles(AudioAnalysisViewModel audioAnalysis, CancellationToken ct = new())
{
- var project = audioAnalysis.Project == null ? null : converterService.ConvertProject(audioAnalysis.Project);
+ var project = audioAnalysis.Project;
await Parallel.ForEachAsync(audioAnalysis.Rows,
new ParallelOptions { MaxDegreeOfParallelism = 10, CancellationToken = ct },
- async (song, token) =>
+ async (song, _) =>
{
- await AnalyzePcmFile(project, song);
- audioAnalysis.SongsCompleted++;
- });
- }
+ try
+ {
+ await AnalyzePcmFile(project, song);
+ }
+ catch (Exception e)
+ {
+ logger.LogError(e, "Error analyzing pcm file");
+ }
- public async Task AnalyzePcmFile(MsuProjectViewModel projectViewModel, AudioAnalysisSongViewModel song)
- {
- var project = converterService.ConvertProject(projectViewModel);
- await AnalyzePcmFile(project, song);
+ audioAnalysis.UpdateSongsCompleted();
+ });
+
+ if (project?.BasicInfo.IsMsuPcmProject == true)
+ {
+ msuPcmService.SaveGenerationCache(project);
+ }
}
public async Task AnalyzePcmFile(MsuProject? project, AudioAnalysisSongViewModel song)
@@ -51,19 +66,21 @@ public async Task AnalyzePcmFile(MsuProject? project, AudioAnalysisSongViewModel
song.WarningMessage = "PCM file missing";
return;
}
- else if (project?.BasicInfo.IsMsuPcmProject == true && song.OriginalViewModel?.HasFiles() != true && !File.Exists(song.Path))
+ else if (project?.BasicInfo.IsMsuPcmProject == true && song.MsuSongInfo == null)
{
- song.WarningMessage = "No input files specified for PCM file";
+ song.WarningMessage = "Song information missing";
return;
}
// Regenerate the pcm file if it has updates that have been made to it
- if (project?.BasicInfo.IsMsuPcmProject == true && song.OriginalViewModel != null && song.OriginalViewModel.HasFiles() && (song.OriginalViewModel.HasChangesSince(song.OriginalViewModel.LastGeneratedDate) || !File.Exists(song.Path)))
+ if (project?.BasicInfo.IsMsuPcmProject == true && song.MsuSongInfo != null)
{
- logger.LogInformation("PCM file {File} out of date, regenerating", song.Path);
- if (!await GeneratePcmFile(project, song.OriginalViewModel))
+ var response = await msuPcmService.CreatePcm(project, song.MsuSongInfo, false, true, true);
+ if (!response.Successful)
{
- song.WarningMessage = "Could not generate new PCM file";
+ song.WarningMessage = File.Exists(song.Path)
+ ? "Could not generate new PCM file"
+ : "PCM file missing and could not be generated";
}
}
@@ -71,76 +88,92 @@ public async Task AnalyzePcmFile(MsuProject? project, AudioAnalysisSongViewModel
song.ApplyAudioAnalysis(data);
logger.LogInformation("Analysis for pcm file {File} complete", song.Path);
}
-
- private async Task GeneratePcmFile(MsuProject project, MsuSongInfoViewModel songModel)
- {
- var song = new MsuSongInfo();
- converterService.ConvertViewModel(songModel, song);
- converterService.ConvertViewModel(songModel.MsuPcmInfo, song.MsuPcmInfo);
- var response = await msuPcmService.CreatePcm(false, project, song);
- if (!response.GeneratedPcmFile)
- {
- logger.LogInformation("PCM file {File} failed to regenerate: {Error}", song.OutputPath, response.Message);
- }
- else
- {
- songModel.LastGeneratedDate = DateTime.Now;
- logger.LogInformation("PCM file {File} regenerated successfully", song.OutputPath);
- }
- return response.GeneratedPcmFile;
- }
-
- public int GetAudioSampleRate(string? path)
+ public async Task GetAudioSampleRateAsync(string? path)
{
- if (!OperatingSystem.IsWindows() || string.IsNullOrEmpty(path) || !File.Exists(path))
+ if (string.IsNullOrEmpty(path) || !File.Exists(path))
{
- return 44100;
+ return new AudioSampleRateResponse
+ {
+ Successful = false,
+ };
}
List incompatibleFileTypes = [".ogg"];
if (incompatibleFileTypes.Contains(new FileInfo(path).Extension.ToLower()))
{
- logger.LogInformation("AudioSampleRate Incompatible file {File}", path);
- return 44100;
+ logger.LogInformation("AudioSampleRate Incompatible file {File}. Assuming 44100.", path);
+ return new AudioSampleRateResponse
+ {
+ Successful = false,
+ };
+ }
+
+ if (OperatingSystem.IsLinux())
+ {
+ var response = await pythonCompanionService.GetSampleRateAsync(new GetSampleRateRequest()
+ {
+ File = path
+ });
+
+ if (response.Successful)
+ {
+ logger.LogInformation("Successfully retrieved sample rate of {Rate} for {File}", response.SampleRate, path);
+ }
+ else
+ {
+ logger.LogError("Failed to retrieve sample rate for {File}. Assuming 44100.", path);
+ }
+
+ return new AudioSampleRateResponse
+ {
+ Successful = response.Successful,
+ SampleRate = response.Successful ? response.SampleRate : 44100,
+ };
}
try
{
var mp3 = new AudioFileReader(path);
- return mp3.WaveFormat.SampleRate;
+ logger.LogInformation("Successfully retrieved sample rate of {Rate} for {File}", mp3.WaveFormat.SampleRate, path);
+ return new AudioSampleRateResponse
+ {
+ Successful = true,
+ SampleRate = mp3.WaveFormat.SampleRate
+ };
}
catch (Exception e)
{
- logger.LogError(e, "Unable to retrieve audio sample rate");
- return 44100;
+ logger.LogError(e, "Failed to retrieve sample rate for {File}. Assuming 44100.", path);
+ return new AudioSampleRateResponse
+ {
+ Successful = false,
+ };
}
}
public int GetAudioStartingSample(string path)
{
- if (!OperatingSystem.IsWindows())
- {
- throw new InvalidOperationException("This is only supported on Windows");
- }
-
logger.LogInformation("GetAudioStartingSample");
try
{
+ using var reader = new AudioReader(path);
+
var totalSampleCount = 0;
- var samples = 0;
+ int samples;
var readBuffer = new float[10000];
var quit = false;
- var mp3 = new AudioFileReader(path);
+
+ var threshold = .0003;
do
{
- samples = mp3.Read(readBuffer, 0, readBuffer.Length);
+ samples = reader.Read(readBuffer);
for (var i = 0; i < samples; i++)
{
- if (Math.Abs(readBuffer[i]) > .0003)
+ if (Math.Abs(readBuffer[i]) > threshold)
{
totalSampleCount += i;
quit = true;
@@ -156,53 +189,49 @@ public int GetAudioStartingSample(string path)
} while (!quit && samples == readBuffer.Length);
statusBarService.UpdateStatusBar("Retrieved Starting Samples");
- return totalSampleCount / mp3.WaveFormat.Channels;
+ return totalSampleCount / reader.Divider;
}
catch (Exception e)
{
logger.LogError(e, "Unable to get audio samples for file");
- throw;
+ return -1;
}
-
}
public int GetAudioEndingSample(string path)
{
- if (!OperatingSystem.IsWindows())
- {
- throw new InvalidOperationException("This is only supported on Windows");
- }
-
logger.LogInformation("GetAudioEndingSample");
try
{
- var samples = 0;
+ using var reader = new AudioReader(path);
+
+ int samples;
var readBuffer = new float[10000];
- var mp3 = new AudioFileReader(path);
- int lastLoudSample = 0;
+ var lastLoudSample = 0;
+
+ var threshold = .0003;
do
{
- samples = mp3.Read(readBuffer, 0, readBuffer.Length);
+ samples = reader.Read(readBuffer);
for (var i = 0; i < samples; i++)
{
- if (Math.Abs(readBuffer[i]) > .0003)
+ if (Math.Abs(readBuffer[i]) > threshold)
{
lastLoudSample++;
}
}
-
+
} while (samples == readBuffer.Length);
statusBarService.UpdateStatusBar("Retrieved Ending Samples");
- return lastLoudSample / mp3.WaveFormat.Channels;
+ return lastLoudSample / reader.Divider;
}
catch (Exception e)
{
logger.LogError(e, "Unable to get audio samples for file");
- throw;
+ return -1;
}
-
}
public async Task AnalyzeAudio(string path)
@@ -237,7 +266,7 @@ public async Task AnalyzeAudio(string path)
double sum = 0;
var totalSampleCount = 0;
- var samples = 0;
+ int samples;
do
{
samples = sampleProvider.Read(readBuffer, 0, readBuffer.Length);
@@ -255,13 +284,58 @@ public async Task AnalyzeAudio(string path)
};
}
- public double ConvertToDecibel(float value)
+ private double ConvertToDecibel(float value)
{
return Math.Round(20 * Math.Log10(Math.Abs(value)), 4);
}
-
- public double ConvertToDecibel(double value)
+
+ private double ConvertToDecibel(double value)
{
return Math.Round(20 * Math.Log10(Math.Abs(value)), 4);
}
+}
+
+public class AudioReader : IDisposable
+{
+ private readonly AudioFileReader? _windowsReader;
+ private readonly ISoundDataProvider? _linuxReader;
+
+ public AudioReader(string file)
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ _windowsReader = new AudioFileReader(file);
+ Channels = _windowsReader.WaveFormat.Channels;
+ BytesPerSample = _windowsReader.WaveFormat.BitsPerSample / 8;
+ Divider = Channels;
+ }
+ else
+ {
+ _linuxReader = new StreamDataProvider(File.OpenRead(file));
+ Divider = 1;
+ }
+ }
+
+ public int Read(float[] buffer)
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ return _windowsReader!.Read(buffer, 0, buffer.Length);
+ }
+ else
+ {
+ return _linuxReader!.ReadBytes(buffer);
+ }
+ }
+
+ public int Channels { get; init; }
+ public int BytesPerSample { get; init; }
+ public int BytesPerFrame => BytesPerSample * Channels;
+ public int Divider { get; init; }
+
+ public void Dispose()
+ {
+ _windowsReader?.Dispose();
+ _linuxReader?.Dispose();
+ }
}
\ No newline at end of file
diff --git a/MSUScripter/Services/AudioMetadataService.cs b/MSUScripter/Services/AudioMetadataService.cs
index 2311396..163a70c 100644
--- a/MSUScripter/Services/AudioMetadataService.cs
+++ b/MSUScripter/Services/AudioMetadataService.cs
@@ -9,15 +9,8 @@
namespace MSUScripter.Services;
-public class AudioMetadataService
+public class AudioMetadataService(ILogger logger)
{
- private readonly ILogger _logger;
-
- public AudioMetadataService(ILogger logger)
- {
- _logger = logger;
- }
-
public AudioMetadata GetAudioMetadata(string file)
{
if (!File.Exists(file))
@@ -27,7 +20,7 @@ public AudioMetadata GetAudioMetadata(string file)
try
{
- var toReturn = new AudioMetadata()
+ var toReturn = new AudioMetadata
{
SongName = Path.GetFileNameWithoutExtension(file),
Artist = "",
@@ -84,7 +77,7 @@ public AudioMetadata GetAudioMetadata(string file)
}
catch (Exception e)
{
- _logger.LogError(e, "Unable to retrieve metadata for {File}", file);
+ logger.LogError(e, "Unable to retrieve metadata for {File}", file);
return new AudioMetadata();
}
}
diff --git a/MSUScripter/Services/AudioPlayerServiceLinux.cs b/MSUScripter/Services/AudioPlayerServiceLinux.cs
deleted file mode 100644
index 178ad25..0000000
--- a/MSUScripter/Services/AudioPlayerServiceLinux.cs
+++ /dev/null
@@ -1,198 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Linq;
-using System.Text.RegularExpressions;
-using System.Threading.Tasks;
-using AvaloniaControls.Services;
-using Microsoft.Extensions.Logging;
-using MSUScripter.Configs;
-using NAudio.Wave;
-
-namespace MSUScripter.Services;
-
-public class AudioPlayerServiceLinux : IAudioPlayerService
-{
- private readonly ILogger _logger;
- private readonly Settings _settings;
- private readonly PythonCommandRunnerService _python;
- private Process? _process;
- private bool _isPlaying;
- private bool _isTestingLoop;
- private bool _canSetLoopValue;
- private const string MinVersionSetLoop = "0.3.0";
- private static readonly Regex digitsOnly = new(@"[^\d.]");
-
- public AudioPlayerServiceLinux(ILogger logger, Settings settings, PythonCommandRunnerService python)
- {
- _logger = logger;
- _settings = settings;
- _python = python;
- if (_python.SetBaseCommand("pcm_player", "--version", out var result, out var error) &&
- result.StartsWith("pcm_player "))
- {
- logger.LogInformation("{Version} found", result);
- var version = digitsOnly.Replace(result, "").Split(".").Select(int.Parse).ToList();
- var versionValue = ConvertVersionNumber(version[0], version[1], version[2]);
- var minVersionValue = GetMinVersionNumberForSetLoop();
- _canSetLoopValue = versionValue >= minVersionValue;
- CanPlayMusic = true;
- IAudioPlayerService.CanPlaySongs = true;
- }
- }
-
- public string CurrentPlayingFile { get; set; } = "";
-
- public bool IsPlaying => _isPlaying;
-
- public bool IsPaused => false;
-
- public bool IsStopped => !_isPlaying;
-
- public void Pause()
- {
- if (_process?.HasExited == false)
- {
- _isPlaying = false;
- _process.Exited -= ProcessOnExited;
- _process.Kill();
- _process = null;
- PlayStopped?.Invoke(this, EventArgs.Empty);
- }
- }
-
- public void PlayPause()
- {
- if (_isPlaying)
- {
- Pause();
- }
- else if (!string.IsNullOrEmpty(CurrentPlayingFile))
- {
- PlaySongAsync(CurrentPlayingFile, _isTestingLoop);
- }
- }
-
- public void Play()
- {
- // Do nothing
- return;
- }
-
- public double? GetCurrentPosition()
- {
- return null;
- }
-
- public double GetLengthSeconds()
- {
- // Do nothing
- return 0;
- }
-
- public double GetCurrentPositionSeconds()
- {
- // Do nothing
- return 0;
- }
-
- public void SetPosition(double value)
- {
- // Do nothing
- }
-
- public void JumpToTime(double seconds)
- {
- // Do nothing
- }
-
- public void SetVolume(double volume)
- {
- // Do nothing
- }
-
- public Task PlaySongAsync(string path, bool fromEnd)
- {
- Pause();
- _isTestingLoop = fromEnd;
- CurrentPlayingFile = path;
-
- if (_canSetLoopValue)
- {
- if (fromEnd)
- {
- var duration = _settings.LoopDuration;
- _process = _python.RunCommandAsync($"-l -s {duration} \"{path}\"");
- }
- else
- {
- _process = _python.RunCommandAsync($"\"{path}\"");
- }
- }
- else
- {
- if (fromEnd)
- {
- _process = _python.RunCommandAsync($"-f \"{path}\" -l");
- }
- else
- {
- _process = _python.RunCommandAsync($"-f \"{path}\"");
- }
- }
-
-
- if (_process != null)
- {
- _isPlaying = true;
- _process.Exited += ProcessOnExited;
- _process.Disposed += ProcessOnExited;
- PlayStarted?.Invoke(this, EventArgs.Empty);
-
- ITaskService.Run(() =>
- {
- var processToWaitFor = _process;
- processToWaitFor.WaitForExit();
- if (_process == processToWaitFor)
- {
- Pause();
- }
- });
- }
-
- return Task.FromResult(_process != null);
- }
-
- private void ProcessOnExited(object? sender, EventArgs e)
- {
- _isPlaying = false;
- PlayStopped?.Invoke(this, EventArgs.Empty);
- }
-
- private int GetMinVersionNumberForSetLoop()
- {
- var version = MinVersionSetLoop.Split(".").Select(int.Parse).ToList();
- return ConvertVersionNumber(version[0], version[1], version[2]);
- }
-
- private int ConvertVersionNumber(int a, int b, int c)
- {
- return a * 10000 + b * 100 + c;
- }
-
- public Task StopSongAsync(string? newSongPath = null, bool waitForFile = false)
- {
- Pause();
- return Task.FromResult(true);
- }
-
- public EventHandler? PlayStarted { get; set; }
- public EventHandler? PlayPaused { get; set; }
- public EventHandler? PlayStopped { get; set; }
-
- public bool CanPlayMusic { get; set; } = false;
-
- public bool CanSetMusicPosition { get; set; } = false;
-
- public bool CanChangeVolume { get; set; } = false;
- public bool CanPauseMusic { get; set; } = false;
-}
\ No newline at end of file
diff --git a/MSUScripter/Services/AudioPlayerServiceWindows.cs b/MSUScripter/Services/AudioPlayerServiceNAudio.cs
similarity index 56%
rename from MSUScripter/Services/AudioPlayerServiceWindows.cs
rename to MSUScripter/Services/AudioPlayerServiceNAudio.cs
index 113c644..7b28a4c 100644
--- a/MSUScripter/Services/AudioPlayerServiceWindows.cs
+++ b/MSUScripter/Services/AudioPlayerServiceNAudio.cs
@@ -9,19 +9,10 @@
namespace MSUScripter.Services;
-public class AudioPlayerServiceWindows : IAudioPlayerService
+public class AudioPlayerServiceNAudio(ILogger logger, Settings settings) : IAudioPlayerService
{
private WaveOutEvent? _waveOutEvent;
- private LoopStream? _loopStream;
- private readonly ILogger _logger;
- private readonly Settings _settings;
-
- public AudioPlayerServiceWindows(ILogger logger, Settings settings)
- {
- _logger = logger;
- _settings = settings;
- IAudioPlayerService.CanPlaySongs = true;
- }
+ private WaveStream? _loopStream;
public string CurrentPlayingFile { get; set; } = "";
@@ -81,6 +72,7 @@ public void SetPosition(double value)
{
if (_waveOutEvent == null || _loopStream == null) return;
value = Math.Clamp(value, 0.0, 1.0);
+
_loopStream.Position = (long)(_loopStream.Length * value + 8.0);
}
@@ -91,17 +83,21 @@ public void JumpToTime(double seconds)
public void SetVolume(double volume)
{
- if (_waveOutEvent == null || _loopStream == null) return;
+ if (_waveOutEvent == null) return;
volume = Math.Clamp(volume, 0.0, 1.0);
_waveOutEvent.Volume = (float)volume;
}
-
+
+ private WaveStream? _stopStream;
+
public async Task StopSongAsync(string? newSongPath = null, bool waitForFile = false)
{
- if (_waveOutEvent?.PlaybackState == PlaybackState.Playing || _waveOutEvent?.PlaybackState == PlaybackState.Paused)
+ if (_waveOutEvent?.PlaybackState is PlaybackState.Playing or PlaybackState.Paused)
{
_waveOutEvent?.Stop();
}
+
+ _stopStream = _loopStream;
if (waitForFile && !string.IsNullOrEmpty(CurrentPlayingFile))
{
@@ -130,7 +126,7 @@ public async Task StopSongAsync(string? newSongPath = null, bool waitForFi
}
catch
{
- _logger.LogInformation("Song not accessible");
+ logger.LogInformation("Song not accessible");
return false;
}
}
@@ -167,18 +163,18 @@ public async Task StopSongAsync(string? newSongPath = null, bool waitForFi
}
catch (Exception e)
{
- _logger.LogError(e, "Error stopping music");
+ logger.LogError(e, "Error stopping music");
}
- _logger.LogInformation("Song stopped playing {Value}", canPlay ? "successfully" : "unsuccessfully");
+ logger.LogInformation("Song stopped playing {Value}", canPlay ? "successfully" : "unsuccessfully");
return canPlay;
}
- _logger.LogInformation("Song stopped playing successfully");
+ logger.LogInformation("Song stopped playing successfully");
return true;
}
- public async Task PlaySongAsync(string path, bool fromEnd)
+ public async Task PlaySongAsync(string path, bool fromEnd, bool isLoopingSong)
{
var canPlay = await StopSongAsync(path);
@@ -188,96 +184,124 @@ public async Task PlaySongAsync(string path, bool fromEnd)
_ = ITaskService.Run(() =>
{
- _logger.LogInformation("Playing song {Path}", path);
+ _ = PlaySongInternal(path, fromEnd, isLoopingSong);
+ });
+
+ return true;
+ }
+
+ private async Task PlaySongInternal(string path, bool fromEnd, bool isLoopingSong)
+ {
+ logger.LogInformation("Playing song {Path}", path);
- var initBytes = new byte[8];
- using (var reader = new BinaryReader(new FileStream(path, FileMode.Open)))
+ var initBytes = new byte[8];
+ using (var reader = new BinaryReader(new FileStream(path, FileMode.Open)))
+ {
+ reader.BaseStream.Seek(0, SeekOrigin.Begin);
+ _ = reader.Read(initBytes, 0, 8);
+ }
+
+ var replay = false;
+
+ logger.LogInformation("Audio file read");
+
+ var loopPoint = BitConverter.ToInt32(initBytes, 4) * 1.0;
+ var totalBytes = new FileInfo(path).Length - 8.0;
+ var totalSamples = totalBytes / 4.0;
+ var loopBytes = (long)(loopPoint / totalSamples * totalBytes) + 8;
+ var startPosition = 8L;
+ if (fromEnd)
+ {
+ var endSamples = totalSamples - 44100 * settings.LoopDuration;
+ startPosition = (long)(endSamples / totalSamples * totalBytes) + 8;
+ if (startPosition < 8)
{
- reader.BaseStream.Seek(0, SeekOrigin.Begin);
- reader.Read(initBytes, 0, 8);
+ startPosition = 8;
}
+ }
+
+ // Fix bad loops to be at the beginning
+ var enableLoop = loopBytes >= 8 && loopBytes < totalBytes + 8;
+
+ try
+ {
+ await using var fs = File.OpenRead(path);
+ await using var rs = new RawSourceWaveStream(fs, new WaveFormat(44100, 2));
+ await using var loop = new NAudioLoopStream(rs);
+ using var waveOutEvent = new WaveOutEvent();
+
+ _waveOutEvent = waveOutEvent;
+ _waveOutEvent.Volume = (float)settings.Volume;
- _logger.LogInformation("Audio file read");
-
- var loopPoint = BitConverter.ToInt32(initBytes, 4) * 1.0;
- var totalBytes = new FileInfo(path).Length - 8.0;
- var totalSamples = totalBytes / 4.0;
- var loopBytes = (long)(loopPoint / totalSamples * totalBytes) + 8;
- var startPosition = 8L;
- if (fromEnd)
+ if (isLoopingSong)
{
- var endSamples = totalSamples - 44100 * _settings.LoopDuration;
- startPosition = (long)(endSamples / totalSamples * totalBytes) + 8;
- if (startPosition < 8)
- {
- startPosition = 8;
- }
+ loop.EnableLooping = enableLoop;
+ _loopStream = loop;
+ waveOutEvent.Init(loop);
+ _loopStream.Position = startPosition;
+ loop.LoopPosition = loopBytes;
+ }
+ else
+ {
+ _loopStream = rs;
+ _loopStream.Position = startPosition;
+ waveOutEvent.Init(rs);
+ }
+
+ Play();
+ logger.LogInformation("Playing audio file");
+ PlayStarted?.Invoke(this, EventArgs.Empty);
+ Thread.Sleep(200);
+ while (waveOutEvent.PlaybackState != PlaybackState.Stopped)
+ {
+ Thread.Sleep(200);
}
- // Fix bad loops to be at the beginning
- var enableLoop = loopBytes >= 8 && loopBytes < totalBytes + 8;
-
- try
+ if (!isLoopingSong && _loopStream == rs && _stopStream != _loopStream)
{
- using (var fs = File.OpenRead(path))
- using (var rs = new RawSourceWaveStream(fs, new WaveFormat(44100, 2)))
- using (var loop = new LoopStream(rs))
- using (var waveOutEvent = new WaveOutEvent())
- {
- loop.EnableLooping = enableLoop;
- _waveOutEvent = waveOutEvent;
- _waveOutEvent.Volume = (float)_settings.Volume;
- _loopStream = loop;
- waveOutEvent.Init(loop);
- loop.Position = startPosition;
- loop.LoopPosition = loopBytes;
- Play();
- _logger.LogInformation("Playing audio file");
- PlayStarted?.Invoke(this, EventArgs.Empty);
- Thread.Sleep(200);
- while (waveOutEvent.PlaybackState != PlaybackState.Stopped)
- {
- Thread.Sleep(200);
- }
- _waveOutEvent = null;
- _loopStream = null;
- PlayStopped?.Invoke(this, EventArgs.Empty);
- }
+ replay = true;
}
- catch (Exception e)
+
+ _waveOutEvent = null;
+ _loopStream = null;
+ _stopStream = null;
+ PlayStopped?.Invoke(this, EventArgs.Empty);
+ }
+ catch (Exception e)
+ {
+ logger.LogError(e, "Failure playing song");
+ CurrentPlayingFile = "";
+
+ try
{
- _logger.LogError(e, "Failure playing song");
- CurrentPlayingFile = "";
-
- try
+ if (_loopStream != null)
{
- if (_loopStream != null)
- {
- _loopStream.Dispose();
- _loopStream = null;
- }
-
- if (_waveOutEvent != null)
- {
- _waveOutEvent.Dispose();
- _waveOutEvent = null;
- }
-
- canPlay = true;
+ await _loopStream.DisposeAsync();
+ _loopStream = null;
}
- catch (Exception e2)
+
+ if (_waveOutEvent != null)
{
- _logger.LogError(e2, "Error stopping music");
+ _waveOutEvent.Dispose();
+ _waveOutEvent = null;
}
}
-
- });
-
- return true;
+ catch (Exception e2)
+ {
+ logger.LogError(e2, "Error stopping music");
+ }
+ }
+
+ if (replay)
+ {
+ await Task.Delay(TimeSpan.FromSeconds(2));
+ if (_loopStream == null)
+ {
+ _ = PlaySongInternal(path, false, false);
+ }
+ }
}
-
-
public EventHandler? PlayStarted { get; set; }
public EventHandler? PlayPaused { get; set; }
public EventHandler? PlayStopped { get; set; }
@@ -288,4 +312,4 @@ public async Task PlaySongAsync(string path, bool fromEnd)
public bool CanChangeVolume { get; set; } = true;
public bool CanPauseMusic { get; set; } = true;
-}
+}
\ No newline at end of file
diff --git a/MSUScripter/Services/AudioPlayerServiceSoundFlow.cs b/MSUScripter/Services/AudioPlayerServiceSoundFlow.cs
new file mode 100644
index 0000000..b98fea6
--- /dev/null
+++ b/MSUScripter/Services/AudioPlayerServiceSoundFlow.cs
@@ -0,0 +1,235 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using AvaloniaControls.Services;
+using Microsoft.Extensions.Logging;
+using MSUScripter.Configs;
+using SoundFlow.Backends.MiniAudio;
+using SoundFlow.Components;
+using SoundFlow.Enums;
+using SoundFlow.Providers;
+
+namespace MSUScripter.Services;
+
+public class AudioPlayerServiceSoundFlow(ILogger logger, Settings settings)
+ : IAudioPlayerService
+{
+ private SoundPlayer? _soundPlayer;
+
+ // ReSharper disable once NotAccessedField.Local
+ private MiniAudioEngine _audioEngine = new(44100, Capability.Playback);
+
+ public string CurrentPlayingFile { get; set; } = "";
+
+ public bool IsPlaying => _soundPlayer?.State == PlaybackState.Playing;
+
+ public bool IsPaused => _soundPlayer?.State == PlaybackState.Paused;
+
+ public bool IsStopped => !IsPlaying && !IsPaused;
+
+ public void Pause()
+ {
+ if (_soundPlayer == null) return;
+ _soundPlayer?.Pause();
+ PlayPaused?.Invoke(this, EventArgs.Empty);
+ }
+
+ public void PlayPause()
+ {
+ if (_soundPlayer == null) return;
+ if (_soundPlayer.State == PlaybackState.Playing)
+ {
+ Pause();
+ }
+ else if (_soundPlayer.State == PlaybackState.Paused)
+ {
+ Play();
+ }
+ }
+
+ public void Play()
+ {
+ if (_soundPlayer == null) return;
+ _soundPlayer?.Play();
+ PlayStarted?.Invoke(this, EventArgs.Empty);
+ }
+
+ public double? GetCurrentPosition()
+ {
+ if (_soundPlayer == null) return null;
+ return _soundPlayer.Time / _soundPlayer.Duration;
+ }
+
+ public double GetLengthSeconds()
+ {
+ if (_soundPlayer == null) return 0;
+ return _soundPlayer.Duration;
+ }
+
+ public double GetCurrentPositionSeconds()
+ {
+ if (_soundPlayer == null) return 0;
+ return _soundPlayer.Time;
+ }
+
+ public void SetPosition(double value)
+ {
+ if (_soundPlayer == null) return;
+ var percent = (float)Math.Clamp(value, 0.0, 1.0);
+ _soundPlayer.Seek(percent * _soundPlayer.Duration);
+ }
+
+ public void JumpToTime(double seconds)
+ {
+ SetPosition(seconds / GetLengthSeconds());
+ }
+
+ public void SetVolume(double volume)
+ {
+ if (_soundPlayer == null) return;
+ volume = Math.Clamp(volume, 0.0, 1.0) * 1.5f;
+ _soundPlayer.Volume = (float)volume;
+ _soundPlayer.Pan = 0.5f;
+ }
+
+ public async Task StopSongAsync(string? newSongPath = null, bool waitForFile = false)
+ {
+ if (_soundPlayer == null)
+ {
+ return true;
+ }
+
+ if (_soundPlayer.State == PlaybackState.Playing)
+ {
+ _soundPlayer.Stop();
+ }
+
+ // Wait until the previous song has stopped playing
+ if (_soundPlayer.State == PlaybackState.Playing)
+ {
+ for(var i = 0; i < 30; i++)
+ {
+ logger.LogInformation($"{_soundPlayer.State}");
+ await Task.Delay(200);
+ if (_soundPlayer.State != PlaybackState.Playing)
+ {
+ break;
+ }
+ }
+ }
+
+ try
+ {
+ if (_soundPlayer != null)
+ {
+ Mixer.Master.RemoveComponent(_soundPlayer);
+ _soundPlayer = null;
+ }
+
+ logger.LogInformation("Song stopped playing successfully");
+
+ return true;
+ }
+ catch (Exception e)
+ {
+ logger.LogError(e, "Error stopping music");
+
+ return false;
+ }
+ }
+
+ public async Task PlaySongAsync(string path, bool fromEnd, bool isLoopingSong)
+ {
+ var canPlay = await StopSongAsync(path);
+
+ if (!canPlay) return false;
+
+ CurrentPlayingFile = path;
+
+ _ = ITaskService.Run(() =>
+ {
+ logger.LogInformation("Playing song {Path}", path);
+
+ var initBytes = new byte[8];
+ using (var reader = new BinaryReader(new FileStream(path, FileMode.Open)))
+ {
+ reader.BaseStream.Seek(0, SeekOrigin.Begin);
+ if (reader.Read(initBytes, 0, 8) < 8)
+ {
+ logger.LogInformation("Invalid file");
+ return;
+ }
+ }
+
+ logger.LogInformation("Audio file read");
+
+ var loopSamples = BitConverter.ToInt32(initBytes, 4) * 1;
+ var totalBytes = new FileInfo(path).Length - 8;
+ var totalSamples = totalBytes / 4;
+ var startPosition = 0;
+ if (fromEnd)
+ {
+ var endSamples = totalSamples - 44100 * settings.LoopDuration;
+ startPosition = (int)endSamples;
+ if (startPosition < 0)
+ {
+ startPosition = 0;
+ }
+ }
+
+ // Fix bad loops to be at the beginning
+ if (loopSamples > totalSamples)
+ {
+ loopSamples = 0;
+ }
+
+ var bytes = File.ReadAllBytes(path).Skip(8);
+ _soundPlayer = new SoundPlayer(new RawDataProvider(bytes.ToArray(), SampleFormat.S16));
+
+ if (isLoopingSong)
+ {
+ _soundPlayer.IsLooping = true;
+ _soundPlayer.SetLoopPoints(loopSamples * 2);
+ }
+ else
+ {
+ _soundPlayer.PlaybackEnded += SoundPlayerOnPlaybackEnded;
+ }
+
+ // Add the player to the master mixer. This connects the player's output to the audio engine's output.
+ Mixer.Master.AddComponent(_soundPlayer);
+
+ // Start playback.
+ _soundPlayer.Play();
+ _soundPlayer.Seek(startPosition * 2);
+ _soundPlayer.Volume = (float)settings.Volume;
+ _soundPlayer.Pan = 0.5f;
+
+ PlayStarted?.Invoke(this, EventArgs.Empty);
+
+ });
+
+ return true;
+ }
+
+ private void SoundPlayerOnPlaybackEnded(object? sender, EventArgs e)
+ {
+ ITaskService.Run(async () =>
+ {
+ await Task.Delay(TimeSpan.FromSeconds(2));
+ _soundPlayer?.Seek(0);
+ _soundPlayer?.Play();
+ });
+ }
+
+
+ public EventHandler? PlayStarted { get; set; }
+ public EventHandler? PlayPaused { get; set; }
+ public EventHandler? PlayStopped { get; set; }
+
+ public bool CanPlayMusic { get; set; } = true;
+ public bool CanSetMusicPosition { get; set; } = true;
+ public bool CanChangeVolume { get; set; } = true;
+ public bool CanPauseMusic { get; set; } = true;
+}
diff --git a/MSUScripter/Services/ControlServices/AddSongWindowService.cs b/MSUScripter/Services/ControlServices/AddSongWindowService.cs
deleted file mode 100644
index 40f4e70..0000000
--- a/MSUScripter/Services/ControlServices/AddSongWindowService.cs
+++ /dev/null
@@ -1,239 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using AvaloniaControls.Controls;
-using AvaloniaControls.ControlServices;
-using AvaloniaControls.Models;
-using AvaloniaControls.Services;
-using Microsoft.Extensions.Logging;
-using MSUScripter.ViewModels;
-using MSUScripter.Views;
-
-namespace MSUScripter.Services.ControlServices;
-
-public class AddSongWindowService(
- AudioMetadataService audioMetadataService,
- AudioAnalysisService audioAnalysisService,
- MsuPcmService msuPcmService,
- ConverterService converterService,
- IAudioPlayerService audioPlayerService,
- PyMusicLooperService pyMusicLooperService) : ControlService
-{
- private readonly AddSongWindowViewModel _model = new();
-
- public AddSongWindowViewModel InitializeModel(MsuProjectViewModel project, int? trackNumber, string? filePath, bool singleMode)
- {
- _model.MsuProjectViewModel = project;
- _model.MsuProject = converterService.ConvertProject(project);
-
- var addDescriptions = project.Tracks.First().HasDescription;
- _model.SingleMode = singleMode;
- _model.TrackSearchItems = [new ComboBoxAndSearchItem(null, "Track", addDescriptions ? "Default description" : null)];
- _model.TrackSearchItems.AddRange(project.Tracks.OrderBy(x => x.TrackNumber).Select(x =>
- new ComboBoxAndSearchItem(x, x.ToString(), x.Description)));
- _model.SelectedTrack = trackNumber == null ? null : project.Tracks.FirstOrDefault(x => x.TrackNumber == trackNumber);
-
- UpdateFilePath(filePath);
-
- _model.HasBeenModified = false;
-
- // TODO: Run PyMusicLooper on start if file exists
-
- return _model;
- }
-
- public void UpdateFilePath(string? path)
- {
- if (string.IsNullOrEmpty(path))
- {
- return;
- }
-
- var metadata = audioMetadataService.GetAudioMetadata(path);
-
- _model.FilePath = path;
- _model.SongName = metadata.SongName ?? _model.SongName;
- _model.ArtistName = metadata.Artist ?? _model.ArtistName;
- _model.AlbumName = metadata.Album ?? _model.AlbumName;
- _model.DisplayHertzWarning = audioAnalysisService.GetAudioSampleRate(path) != 44100;
- }
-
- public void UpdatePyMusicLooperPanel(PyMusicLooperPanel? panel)
- {
- if (panel == null || !_model.CanEditMainFields) return;
-
- var pcmData = new MsuSongMsuPcmInfoViewModel
- {
- File = _model.FilePath
- };
-
- panel.UpdateModel(_model.MsuProjectViewModel, new MsuSongInfoViewModel
- {
- MsuPcmInfo = pcmData
- }, pcmData);
- }
-
- public async Task PlaySong(bool fromEnd)
- {
- // Stop the song if it is currently playing
- await StopSong();
-
- if (string.IsNullOrEmpty(_model.FilePath) || !File.Exists(_model.FilePath))
- {
- return;
- }
-
- var outputPath = await CreateTempPcm();
-
- if (string.IsNullOrEmpty(outputPath))
- {
- return;
- }
-
- await audioPlayerService.PlaySongAsync(outputPath, fromEnd);
- }
-
- public async Task StopSong(bool wait = true)
- {
- await audioPlayerService.StopSongAsync(null, wait);
- }
-
- public void UpdateFromPyMusicLooper(PyMusicLooperResultViewModel? result)
- {
- if (result == null)
- {
- return;
- }
-
- _model.LoopPoint = result.LoopStart;
- _model.TrimEnd = result.LoopEnd;
- }
-
- private async Task CreateTempPcm()
- {
- var response = await msuPcmService.CreateTempPcm(false, _model.MsuProject, _model.FilePath, _model.LoopPoint,
- _model.TrimEnd, _model.Normalization ?? _model.MsuProjectViewModel.BasicInfo.Normalization,
- _model.TrimStart);
-
- return response.GeneratedPcmFile ? response.OutputPath : null;
- }
-
- public async Task AddSongToProject(AddSongWindow parent)
- {
- if (_model.SelectedTrack == null || string.IsNullOrEmpty(_model.FilePath))
- {
- return null;
- }
-
- var track = _model.SelectedTrack;
-
- var response = await msuPcmService.CreateTempPcm(true, _model.MsuProject, _model.FilePath, _model.LoopPoint,
- _model.TrimEnd, _model.Normalization ?? _model.MsuProjectViewModel.BasicInfo.Normalization,
- _model.TrimStart);
-
- if (!response.GeneratedPcmFile)
- {
- await MessageWindow.ShowErrorDialog(response.Message ?? "Unknown error", "Error", parent);
- return null;
- }
-
- if (!response.Successful)
- {
- if (!await MessageWindow.ShowYesNoDialog($"{response.Message}\r\nDo you want to continue adding this song?",
- "Continue?", parent))
- {
- return null;
- }
- }
-
- var isAlt = track.Songs.Any();
- string outputPath;
- var msu = new FileInfo(_model.MsuProjectViewModel.MsuPath);
-
- if (!isAlt)
- {
- outputPath = msu.FullName.Replace(msu.Extension, $"-{track.TrackNumber}.pcm");
- }
- else
- {
- var altSuffix = track.Songs.Count == 1 ? "alt" : $"alt{track.Songs.Count}";
- outputPath = msu.FullName.Replace(msu.Extension, $"-{track.TrackNumber}_{altSuffix}.pcm");
- }
-
- var song = new MsuSongInfoViewModel
- {
- SongName = _model.SongName,
- Artist = _model.ArtistName,
- Album = _model.AlbumName,
- CheckCopyright = _model.CheckCopyright,
- IsCopyrightSafe = _model.IsCopyrightSafe,
- OutputPath = outputPath,
- MsuPcmInfo = new MsuSongMsuPcmInfoViewModel
- {
- Loop = _model.LoopPoint,
- TrimStart = _model.TrimStart,
- TrimEnd = _model.TrimEnd,
- Normalization = _model.Normalization,
- File = _model.FilePath
- }
- };
-
- song.ApplyCascadingSettings(_model.MsuProjectViewModel, track, isAlt, false, true, true);
-
- track.Songs.Add(song);
-
- _model.AddSongButtonText = "Added Song";
- _ = ITaskService.Run(() =>
- {
- Thread.Sleep(TimeSpan.FromSeconds(3));
- _model.AddSongButtonText = "Add Song";
- });
-
- return song;
- }
-
- public void ClearModel()
- {
- _model.Clear();
- }
-
- public void AnalyzeAudio()
- {
- _model.AverageAudio = "Running";
- _model.PeakAudio = null;
-
- ITaskService.Run(async () =>
- {
- await StopSong(false);
-
- var outputPath = await CreateTempPcm();
-
- if (!string.IsNullOrEmpty(outputPath))
- {
- var output = await audioAnalysisService.AnalyzeAudio(outputPath);
-
- if (output is { AvgDecibels: not null, MaxDecibels: not null })
- {
- _model.AverageAudio = $"Average: {Math.Round(output.AvgDecibels.Value, 2)}db";
- _model.PeakAudio = $"Peak: {Math.Round(output.MaxDecibels.Value, 2)}db";
- }
- else
- {
- _model.AverageAudio = "Error analyzing audio";
- _model.PeakAudio = null;
- }
- }
- else
- {
- _model.AverageAudio = "Error generating PCM";
- _model.PeakAudio = null;
- }
- });
- }
-
- public bool IsPyMusicLooperRunning() => pyMusicLooperService.IsRunning;
-
- public bool HasChanges => !string.IsNullOrEmpty(_model.FilePath);
-}
\ No newline at end of file
diff --git a/MSUScripter/Services/ControlServices/AudioAnalysisWindowService.cs b/MSUScripter/Services/ControlServices/AudioAnalysisWindowService.cs
index 51a5738..b8db9af 100644
--- a/MSUScripter/Services/ControlServices/AudioAnalysisWindowService.cs
+++ b/MSUScripter/Services/ControlServices/AudioAnalysisWindowService.cs
@@ -4,18 +4,31 @@
using System.Threading;
using AvaloniaControls.ControlServices;
using AvaloniaControls.Services;
+using Microsoft.Extensions.Logging;
using MSURandomizerLibrary.Services;
+using MSUScripter.Configs;
using MSUScripter.ViewModels;
namespace MSUScripter.Services.ControlServices;
-public class AudioAnalysisWindowService(AudioAnalysisService audioAnalysisService, IMsuLookupService msuLookupService) : ControlService
+// ReSharper disable once ClassNeverInstantiated.Global
+public class AudioAnalysisWindowService(
+ AudioAnalysisService audioAnalysisService,
+ IMsuLookupService msuLookupService,
+ MsuPcmService msuPcmService,
+ ILogger logger) : ControlService
{
private readonly AudioAnalysisViewModel _model = new();
private readonly CancellationTokenSource _cts = new();
- public AudioAnalysisViewModel InitializeModel(MsuProjectViewModel project)
+ public AudioAnalysisViewModel InitializeModel(MsuProject project)
{
+ if (msuPcmService.IsGeneratingPcm)
+ {
+ _model.LoadError = "Another PCM file is currently being generated";
+ return _model;
+ }
+
_model.Project = project;
var msuDirectory = new FileInfo(project.MsuPath).DirectoryName;
@@ -31,10 +44,10 @@ public AudioAnalysisViewModel InitializeModel(MsuProjectViewModel project)
.Select(x => new AudioAnalysisSongViewModel()
{
SongName = Path.GetRelativePath(msuDirectory, new FileInfo(x.OutputPath!).FullName),
- TrackName = x.TrackName,
+ TrackName = x.TrackName ?? $"Track {x.TrackNumber}",
TrackNumber = x.TrackNumber,
Path = x.OutputPath ?? "",
- OriginalViewModel = x,
+ MsuSongInfo = x,
})
.ToList();
@@ -46,6 +59,12 @@ public AudioAnalysisViewModel InitializeModel(string msuPath)
{
_model.ShowCompareButton = false;
+ if (msuPcmService.IsGeneratingPcm)
+ {
+ _model.LoadError = "Another PCM file is currently being generated";
+ return _model;
+ }
+
var msuDirectory = new FileInfo(msuPath).DirectoryName;
if (string.IsNullOrEmpty(msuDirectory))
{
@@ -63,13 +82,13 @@ public AudioAnalysisViewModel InitializeModel(string msuPath)
var songs = msu.Tracks
.OrderBy(x => x.Number)
- .Select(x => new AudioAnalysisSongViewModel()
+ .Select(x => new AudioAnalysisSongViewModel
{
SongName = Path.GetRelativePath(msuDirectory, new FileInfo(x.Path).FullName),
TrackName = x.TrackName,
TrackNumber = x.Number,
- Path = x.Path ?? "",
- OriginalViewModel = null,
+ Path = x.Path,
+ MsuSongInfo = null,
CanRefresh = false
})
.ToList();
@@ -114,7 +133,7 @@ public void RunSong(AudioAnalysisSongViewModel song)
_ = ITaskService.Run(async () =>
{
- await audioAnalysisService!.AnalyzePcmFile(_model.Project!, song);
+ await audioAnalysisService.AnalyzePcmFile(_model.Project, song);
CheckSongWarnings(song, GetAverageRms(), GetAveragePeak());
UpdateBottomMessage();
}, _cts.Token);
@@ -125,6 +144,11 @@ public void Stop()
_cts.Cancel();
}
+ public void LogError(Exception e, string message)
+ {
+ logger.LogError(e, "{Message}", message);
+ }
+
public event EventHandler? Completed;
private double GetAverageRms() => Math.Round(_model.Rows.Where(x => x.AvgDecibels != null).Average(x => x.AvgDecibels) ?? 0, 4);
diff --git a/MSUScripter/Services/ControlServices/AudioControlService.cs b/MSUScripter/Services/ControlServices/AudioControlService.cs
index 8d75364..4d21d03 100644
--- a/MSUScripter/Services/ControlServices/AudioControlService.cs
+++ b/MSUScripter/Services/ControlServices/AudioControlService.cs
@@ -8,6 +8,7 @@
namespace MSUScripter.Services.ControlServices;
+// ReSharper disable once ClassNeverInstantiated.Global
public class AudioControlService(IAudioPlayerService audioService, SettingsService settingsService) : ControlService
{
private readonly AudioControlViewModel _model = new();
@@ -45,6 +46,8 @@ public AudioControlViewModel InitializeModel()
return _model;
}
+ public EventHandler? OnPlayStarted;
+
public Task StopAsync()
{
return audioService.StopSongAsync();
@@ -107,6 +110,7 @@ private void PlayPaused(object? sender, EventArgs e)
private void PlayStarted(object? sender, EventArgs e)
{
+ OnPlayStarted?.Invoke(this, EventArgs.Empty);
_model.Icon = MaterialIconKind.Pause;
_model.CanPlayPause = true;
StartTimer();
diff --git a/MSUScripter/Services/ControlServices/CopyMoveTrackWindowService.cs b/MSUScripter/Services/ControlServices/CopyMoveTrackWindowService.cs
deleted file mode 100644
index 1424f03..0000000
--- a/MSUScripter/Services/ControlServices/CopyMoveTrackWindowService.cs
+++ /dev/null
@@ -1,107 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using AvaloniaControls.ControlServices;
-using Microsoft.Extensions.Logging;
-using MSUScripter.Configs;
-using MSUScripter.ViewModels;
-
-namespace MSUScripter.Services.ControlServices;
-
-public class CopyMoveTrackWindowService (ConverterService converterService) : ControlService
-{
- private readonly CopyMoveTrackWindowViewModel _model = new();
-
- public CopyMoveTrackWindowViewModel InitializeModel(MsuProjectViewModel msuProjectViewModel, MsuTrackInfoViewModel trackViewModel,
- MsuSongInfoViewModel msuSongInfoViewModel, CopyMoveType type)
- {
- _model.Project = msuProjectViewModel;
- _model.PreviousTrack = trackViewModel;
- _model.PreviousSong = msuSongInfoViewModel;
- _model.Type = type;
- _model.Tracks = msuProjectViewModel.Tracks.OrderBy(x => x.TrackNumber).ToList();
- _model.TargetTrack = _model.PreviousTrack;
- _model.OriginalLocation = trackViewModel.Songs.IndexOf(msuSongInfoViewModel);
-
- return _model;
- }
-
- public void RunCopyMove()
- {
- if (_model.PreviousTrack == null || _model.PreviousSong == null || _model.Project == null)
- {
- return;
- }
-
- var songInfo = _model.PreviousSong;
- var previousTrack = _model.PreviousTrack;
- var destinationTrack = _model.TargetTrack;
-
- if (_model.Type == CopyMoveType.Move)
- {
- var targetLocation = _model.TargetLocation;
- if (previousTrack == destinationTrack && targetLocation > _model.OriginalLocation)
- {
- targetLocation--;
- }
- previousTrack.Songs.Remove(songInfo);
- destinationTrack.Songs.Insert(targetLocation, songInfo);
- }
- else if (_model.Type == CopyMoveType.Copy)
- {
- var msuSongInfo = new MsuSongInfo();
- converterService.ConvertViewModel(_model.PreviousSong, msuSongInfo);
- converterService.ConvertViewModel(_model.PreviousSong.MsuPcmInfo, msuSongInfo.MsuPcmInfo);
-
- var msuSongInfoCloned = new MsuSongInfoViewModel();
- converterService.ConvertViewModel(msuSongInfo, msuSongInfoCloned);
- converterService.ConvertViewModel(msuSongInfo.MsuPcmInfo, msuSongInfoCloned.MsuPcmInfo);
-
- destinationTrack.Songs.Insert(_model.TargetLocation, msuSongInfoCloned);
- }
- else if (_model.Type == CopyMoveType.Swap)
- {
- var originalIndex = previousTrack.Songs.IndexOf(songInfo);
- var swapSong = destinationTrack.Songs[_model.TargetLocation];
- previousTrack.Songs.Remove(songInfo);
- destinationTrack.Songs.Remove(swapSong);
- previousTrack.Songs.Insert(Math.Clamp(originalIndex, 0, previousTrack.Songs.Count), swapSong);
- destinationTrack.Songs.Insert(Math.Clamp(_model.TargetLocation, 0, destinationTrack.Songs.Count), songInfo);
- }
-
- previousTrack.FixTrackSuffixes(songInfo.CanPlaySongs);
- if (previousTrack != destinationTrack)
- {
- destinationTrack.FixTrackSuffixes(songInfo.CanPlaySongs);
- }
- }
-
- public void UpdateTrackLocations()
- {
- List locationOptions = [];
-
- var prefix = _model.Type == CopyMoveType.Swap ? "Song " : "Before song ";
-
- for (var i = 0; i < _model.TargetTrack.Songs.Count; i++)
- {
- if (string.IsNullOrEmpty(_model.TargetTrack.Songs[0].SongName))
- {
- locationOptions.Add($"{prefix}{i+1}");
- }
- else
- {
- locationOptions.Add($"{prefix}{i+1}: {_model.TargetTrack.Songs[i].SongName}");
- }
- }
-
- if (_model.Type != CopyMoveType.Swap)
- {
- locationOptions.Add("At the end of the list");
- }
-
- _model.TargetLocationOptions = locationOptions;
- _model.TargetLocation = locationOptions.Count - 1;
- _model.IsTargetLocationEnabled = locationOptions.Count > 1;
- }
-}
\ No newline at end of file
diff --git a/MSUScripter/Services/ControlServices/CopyProjectWindowService.cs b/MSUScripter/Services/ControlServices/CopyProjectWindowService.cs
index b2bd83c..1eeef03 100644
--- a/MSUScripter/Services/ControlServices/CopyProjectWindowService.cs
+++ b/MSUScripter/Services/ControlServices/CopyProjectWindowService.cs
@@ -4,32 +4,50 @@
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Platform.Storage;
-using AvaloniaControls;
using AvaloniaControls.ControlServices;
+using Microsoft.Extensions.Logging;
using MSUScripter.Configs;
using MSUScripter.ViewModels;
namespace MSUScripter.Services.ControlServices;
-public class CopyProjectWindowService(ConverterService converterService) : ControlService
+// ReSharper disable once ClassNeverInstantiated.Global
+public class CopyProjectWindowService(ConverterService converterService, ILogger logger) : ControlService
{
- private CopyProjectWindowViewModel _model = new();
+ private readonly CopyProjectWindowViewModel _model = new();
public CopyProjectWindowViewModel InitializeModel()
{
return _model;
}
- public void SetProject(MsuProject project)
+ public void SetProject(MsuProject project, bool isCopy)
{
- _model.OriginalProject = project;
- _model.ProjectViewModel = converterService.ConvertProject(project);
+ if (isCopy)
+ {
+ _model.OriginalProject = project;
+ _model.NewProject = converterService.CloneProject(project);
+ }
+ else
+ {
+ _model.OriginalProject = converterService.CloneProject(project);
+ _model.NewProject = project;
+ }
+
+ _model.IsCopy = isCopy;
+ _model.ButtonText = isCopy ? "Copy Project" : "Open Project";
- var paths = new List
+ var title = string.IsNullOrEmpty(project.BasicInfo.PackName) ? "Project" : project.BasicInfo.PackName;
+ _model.Title = isCopy ? $"Copy {title}" : $"Update {title}";
+
+ var paths = new List();
+
+ if (isCopy)
{
- new(project.ProjectFilePath),
- new(project.MsuPath)
- };
+ paths.Add(new CopyProjectViewModel(project.ProjectFilePath));
+ }
+
+ paths.Add(new CopyProjectViewModel(project.MsuPath));
if (!string.IsNullOrEmpty(project.BasicInfo.ZeldaMsuPath))
{
@@ -81,46 +99,53 @@ public async Task UpdatePath(CopyProjectViewModel viewModel, IStorageItem file)
public void ImportProject()
{
- if (_model.ProjectViewModel == null)
+ if (_model.NewProject == null)
{
return;
}
- _model.ProjectViewModel.ProjectFilePath = _model.Paths
- .First(x => x.Extension.Equals(".msup", StringComparison.OrdinalIgnoreCase)).NewPath;
+ if (_model.IsCopy)
+ {
+ _model.NewProject.ProjectFilePath = _model.Paths
+ .First(x => x.Extension.Equals(".msup", StringComparison.OrdinalIgnoreCase)).NewPath;
+ }
foreach (var path in _model.Paths.Where(x => x.Extension.Equals(".msu", StringComparison.OrdinalIgnoreCase)))
{
- if (_model.ProjectViewModel.MsuPath == path.PreviousPath)
+ if (_model.NewProject.MsuPath == path.PreviousPath)
{
- _model.ProjectViewModel.MsuPath = path.NewPath;
+ _model.NewProject.MsuPath = path.NewPath;
}
- else if (_model.ProjectViewModel.BasicInfo.MetroidMsuPath == path.PreviousPath)
+ else if (_model.NewProject.BasicInfo.MetroidMsuPath == path.PreviousPath)
{
- _model.ProjectViewModel.BasicInfo.MetroidMsuPath = path.NewPath;
+ _model.NewProject.BasicInfo.MetroidMsuPath = path.NewPath;
}
- else if (_model.ProjectViewModel.BasicInfo.ZeldaMsuPath == path.PreviousPath)
+ else if (_model.NewProject.BasicInfo.ZeldaMsuPath == path.PreviousPath)
{
- _model.ProjectViewModel.BasicInfo.ZeldaMsuPath = path.NewPath;
+ _model.NewProject.BasicInfo.ZeldaMsuPath = path.NewPath;
}
}
var oldMsuPath = _model.OriginalProject?.MsuPath.Replace(".msu", "", StringComparison.OrdinalIgnoreCase) ?? "";
- var newMsuPath = _model.ProjectViewModel?.MsuPath.Replace(".msu", "", StringComparison.OrdinalIgnoreCase) ?? "";
+ var newMsuPath = _model.NewProject?.MsuPath.Replace(".msu", "", StringComparison.OrdinalIgnoreCase) ?? "";
foreach (var path in _model.Paths.Where(x => !x.Extension.Equals(".msu", StringComparison.OrdinalIgnoreCase) && !x.Extension.Equals(".msup", StringComparison.OrdinalIgnoreCase)))
{
- foreach (var song in _model.ProjectViewModel!.Tracks.SelectMany(x => x.Songs))
+ foreach (var song in _model.NewProject!.Tracks.SelectMany(x => x.Songs))
{
UpdateSongPaths(song, path, oldMsuPath, newMsuPath);
}
}
+
+ _model.SavedProject = _model.NewProject;
+ }
- _model.NewProject = converterService.ConvertProject(_model.ProjectViewModel!);
- return;
+ public void LogError(Exception e, string message)
+ {
+ logger.LogError(e, "{Message}", message);
}
- private void UpdateSongPaths(MsuSongInfoViewModel song, CopyProjectViewModel update, string oldMsuPath, string newMsuPath)
+ private void UpdateSongPaths(MsuSongInfo song, CopyProjectViewModel update, string oldMsuPath, string newMsuPath)
{
song.OutputPath = song.OutputPath?.Replace(oldMsuPath, newMsuPath);
if (song.MsuPcmInfo.HasFiles())
@@ -129,7 +154,7 @@ private void UpdateSongPaths(MsuSongInfoViewModel song, CopyProjectViewModel upd
}
}
- private void UpdateMsuPcmInfo(MsuSongMsuPcmInfoViewModel pcmInfo, CopyProjectViewModel update)
+ private void UpdateMsuPcmInfo(MsuSongMsuPcmInfo pcmInfo, CopyProjectViewModel update)
{
pcmInfo.File = pcmInfo.File?.Replace(update.PreviousPath, update.NewPath);
foreach (var subchannel in pcmInfo.SubChannels)
@@ -159,7 +184,7 @@ private void CheckFiles()
}
}
- _model.IsValid = _model.Paths.All(x => x.IsValid);
+ _model.IsValid = !_model.IsCopy || _model.Paths.All(x => x.IsValid);
}
}
\ No newline at end of file
diff --git a/MSUScripter/Services/ControlServices/EditProjectPanelService.cs b/MSUScripter/Services/ControlServices/EditProjectPanelService.cs
deleted file mode 100644
index 6f2179e..0000000
--- a/MSUScripter/Services/ControlServices/EditProjectPanelService.cs
+++ /dev/null
@@ -1,250 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Timers;
-using AvaloniaControls;
-using AvaloniaControls.ControlServices;
-using AvaloniaControls.Models;
-using MSUScripter.Configs;
-using MSUScripter.ViewModels;
-
-namespace MSUScripter.Services.ControlServices;
-
-public class EditProjectPanelService(
- ProjectService projectService,
- MsuPcmService msuPcmService,
- IAudioPlayerService audioService,
- ConverterService converterService,
- TrackListService trackListService,
- StatusBarService statusBarService,
- AudioAnalysisService audioAnalysisService) : ControlService
-{
- private EditProjectPanelViewModel _model = new();
- private bool _isFirstInit = true;
- private readonly Timer _backupTimer = new(TimeSpan.FromSeconds(60));
-
- public EditProjectPanelViewModel InitializeModel(MsuProject project)
- {
- if (_isFirstInit)
- {
- statusBarService.StatusBarTextUpdated += (sender, args) =>
- {
- _model.StatusBarText = args.Data;
- };
-
- _backupTimer.Elapsed += BackupTimerOnElapsed;
- }
-
- var projectModel = converterService.ConvertProject(project);
-
- foreach (var songViewModel in projectModel.Tracks.SelectMany(x => x.Songs))
- {
- songViewModel.MsuPcmInfo.UpdateHertzWarning(audioAnalysisService.GetAudioSampleRate(songViewModel.MsuPcmInfo.File));
- songViewModel.MsuPcmInfo.UpdateMultiWarning();
- songViewModel.MsuPcmInfo.UpdateSubTrackSubChannelWarning();
- }
-
- _model = new EditProjectPanelViewModel
- {
- MsuProject = project,
- MsuProjectViewModel = projectModel,
- Tracks = projectModel.Tracks.OrderBy(x => x.TrackNumber).ToList(),
- LastAutoSave = projectModel.LastSaveTime
- };
-
- List searchItems =
- [
- new ComboBoxAndSearchItem(0, "MSU Details"),
- new ComboBoxAndSearchItem(1, "Track Overview"),
- ];
- searchItems.AddRange(_model.Tracks.Select((t, i) => new ComboBoxAndSearchItem(i + 2, t.ToString())));
- _model.TrackSearchItems = searchItems;
-
- _backupTimer.Start();
-
- if (project.IsNewProject)
- {
- statusBarService.UpdateStatusBar("Created New Project");
- }
- else
- {
- statusBarService.UpdateStatusBar("Loaded Project");
- }
-
- return _model;
- }
-
- public void IncrementPage(int mod)
- {
- _model.PageNumber = Math.Clamp(_model.PageNumber + mod, 0, _model.Tracks.Count + 1);
- }
-
- public void SetPage(int page)
- {
- _model.PageNumber = Math.Clamp(page, 0, _model.Tracks.Count + 1);
- }
-
- public void SetToTrackPage(int trackNumber)
- {
- _model.PageNumber = _model.Tracks.IndexOf(_model.Tracks.First(x => x.TrackNumber == trackNumber)) + 2;
- }
-
- public void SaveProject()
- {
- if (_model.MsuProjectViewModel == null) return;
- var project = converterService.ConvertProject(_model.MsuProjectViewModel!);
- projectService.SaveMsuProject(project, false);
- _model.MsuProjectViewModel.LastSaveTime = project.LastSaveTime;
- _model.LastAutoSave = project.LastSaveTime;
- }
-
- public string? ExportYaml(MsuProject? project = null)
- {
- if (_model.MsuProjectViewModel?.BasicInfo.WriteYamlFile != true) return null;
- project ??= converterService.ConvertProject(_model.MsuProjectViewModel);
- projectService!.ExportMsuRandomizerYaml(project, out var error);
- return error;
- }
-
- public string? ValidateProject()
- {
- if (_model.MsuProjectViewModel == null) return null;
- projectService.ValidateProject(_model.MsuProjectViewModel, out var message);
- return message;
- }
-
- public void WriteTrackList(MsuProject? project = null)
- {
- if (_model.MsuProjectViewModel == null) return;
- project ??= converterService.ConvertProject(_model.MsuProjectViewModel);
- trackListService.WriteTrackListFile(project);
- }
-
- public void WriteTrackJson()
- {
- if (_model.MsuProjectViewModel == null) return;
- var project = converterService.ConvertProject(_model.MsuProjectViewModel);
- msuPcmService.ExportMsuPcmTracksJson(false, project);
- }
-
- public string? WriteSwapperBatchFiles()
- {
- if (_model.MsuProjectViewModel == null) return null;
- var project = converterService.ConvertProject(_model.MsuProjectViewModel);
-
- var extraProjects = new List();
-
- if (project.BasicInfo.CreateSplitSmz3Script)
- {
- extraProjects = projectService.GetSmz3SplitMsuProjects(project, out _, out var error).ToList();
- if (!string.IsNullOrEmpty(error))
- {
- return error;
- }
- }
-
- return !projectService.CreateAltSwapperFile(project, extraProjects)
- ? "Could not create alt swapper bat file. Project file may be corrupt. Verify output pcm file paths."
- : null;
- }
-
- public string? CreateSmz3SplitBatchFile()
- {
- if (_model.MsuProjectViewModel == null) return null;
- var project = converterService.ConvertProject(_model.MsuProjectViewModel);
- projectService.GetSmz3SplitMsuProjects(project, out var conversions, out var error);
- if (!string.IsNullOrEmpty(error))
- {
- return error;
- }
-
- return !projectService.CreateSmz3SplitScript(project, conversions)
- ? "Insufficient tracks to create the SMZ3 to ALttP and SM MSUs batch file."
- : null;
- }
-
- public string? SetupForMsuGenerationWindow()
- {
- if (_model.MsuProjectViewModel == null) return null;
- var project = converterService.ConvertProject(_model.MsuProjectViewModel);
-
- if (!projectService.CreateMsuFiles(project))
- {
- return "Unable to create MSU files";
- }
-
- var extraProjects = new List();
-
- if (project.BasicInfo.CreateSplitSmz3Script)
- {
- extraProjects = projectService.GetSmz3SplitMsuProjects(project, out var conversions, out var error).ToList();
-
- if (!string.IsNullOrEmpty(error))
- {
- return error;
- }
-
- projectService.CreateSmz3SplitScript(project, conversions);
- }
-
- if (project.BasicInfo.CreateAltSwapperScript)
- {
- if (!projectService.CreateAltSwapperFile(project, extraProjects))
- {
- return "Could not create alt swapper bat file. Project file may be corrupt. Verify output pcm file paths.";
- }
- }
-
- if (project.BasicInfo.TrackList != TrackListType.Disabled)
- {
- WriteTrackList(project);
- }
-
- if (!project.BasicInfo.IsMsuPcmProject)
- {
- return ExportYaml(project);
- }
-
- msuPcmService.ExportMsuPcmTracksJson(false, project);
-
- return null;
- }
-
- public bool OpenFolder()
- {
- return _model.MsuProjectViewModel == null ||
- CrossPlatformTools.OpenDirectory(_model.MsuProjectViewModel.MsuPath, true);
- }
-
- public void UpdateExportMenuOptions()
- {
- _model.DisplayAltSwapperExportButton =
- _model.CreateAltSwapper && _model.MsuProjectViewModel?.Tracks.Any(x => x is { IsScratchPad: false, Songs.Count: > 1 }) == true;
- }
-
- public void Disable()
- {
- _backupTimer.Stop();
- _ = audioService.StopSongAsync();
- }
-
- public bool ArePcmFilesUpToDate()
- {
- return _model.MsuProjectViewModel?.BasicInfo.IsMsuPcmProject == true && _model.MsuProjectViewModel.Tracks
- .SelectMany(x => x.Songs).Any(x => x.HasChangesSince(x.LastGeneratedDate));
- }
-
- public bool HasPendingChanges() => _model.MsuProjectViewModel?.HasPendingChanges() == true;
-
- public MsuProjectViewModel? MsuProjectViewModel => _model.MsuProjectViewModel;
-
- private void BackupTimerOnElapsed(object? sender, ElapsedEventArgs e)
- {
- if (_model.MsuProjectViewModel?.HasChangesSince(_model.LastAutoSave) != true)
- return;
- var backupProject = converterService.ConvertProject(_model.MsuProjectViewModel);
- projectService.SaveMsuProject(backupProject, true);
- _model.LastAutoSave = DateTime.Now;
- _model.StatusBarText = "Created Project Backup";
- }
-}
\ No newline at end of file
diff --git a/MSUScripter/Services/ControlServices/InstallDependenciesWindowService.cs b/MSUScripter/Services/ControlServices/InstallDependenciesWindowService.cs
new file mode 100644
index 0000000..985ec8c
--- /dev/null
+++ b/MSUScripter/Services/ControlServices/InstallDependenciesWindowService.cs
@@ -0,0 +1,146 @@
+using System;
+using System.Threading.Tasks;
+using AvaloniaControls.ControlServices;
+using MSUScripter.ViewModels;
+
+namespace MSUScripter.Services.ControlServices;
+
+// ReSharper disable once ClassNeverInstantiated.Global
+public class InstallDependenciesWindowService (MsuPcmService msuPcmService, PythonCompanionService pythonCompanionService, SettingsService settingsService) : ControlService
+{
+ private readonly InstallDependenciesWindowViewModel _viewModel = new();
+
+ public InstallDependenciesWindowViewModel InitializeModel()
+ {
+ _viewModel.MsuPcmState = msuPcmService.IsValid ? InstallState.Valid : InstallState.CanInstall;
+ _viewModel.FfmpegState = pythonCompanionService.IsFfMpegValid ? InstallState.Valid : InstallState.CanInstall;
+ _viewModel.PyAppState = pythonCompanionService.IsValid ? InstallState.Valid : InstallState.CanInstall;
+ _viewModel.InitialDontRemindMeAgain = settingsService.Settings.IgnoreMissingDependencies;
+ return _viewModel;
+ }
+
+ public async Task InstallMsuPcm()
+ {
+ _viewModel.MsuPcmState = InstallState.InProgress;
+ _viewModel.MsuPcmInstallProgress = "Starting";
+ await Task.Delay(TimeSpan.FromMilliseconds(100));
+ var result = await msuPcmService.InstallAsync(progress =>
+ {
+ _viewModel.MsuPcmInstallProgress = progress;
+ });
+ if (result.Success)
+ {
+ _viewModel.MsuPcmState = InstallState.Valid;
+ }
+ else
+ {
+ _viewModel.MsuPcmState = InstallState.Error;
+ _viewModel.MsuPcmErrorText = result.MissingSharedLibraries ? "Missing Libraries" : "Install Failed";
+ _viewModel.MsuPcmErrorToolTip = result.MissingSharedLibraries
+ ? "MsuPcm++ is missing some libraries that it is dependent on. Please click to view additional installation instructions."
+ : "Failed to be able to download and run MsuPcm++. Please click to view additional installation instructions.";
+ }
+ }
+
+ public async Task RetryMsuPcm()
+ {
+ if ((await msuPcmService.VerifyInstalledAsync()).Successful)
+ {
+ _viewModel.MsuPcmState = InstallState.Valid;
+ return;
+ }
+ await InstallMsuPcm();
+ }
+
+ public async Task RevalidateMsuPcm()
+ {
+ if ((await msuPcmService.VerifyInstalledAsync()).Successful)
+ {
+ _viewModel.MsuPcmState = InstallState.Valid;
+ }
+ }
+
+ public async Task InstallFfmpeg()
+ {
+ _viewModel.FfmpegState = InstallState.InProgress;
+ _viewModel.FfmpegInstallProgress = "Starting";
+ await Task.Delay(TimeSpan.FromMilliseconds(100));
+ var result = await pythonCompanionService.InstallFfmpegAsync(progress =>
+ {
+ _viewModel.FfmpegInstallProgress = progress;
+ });
+ if (result)
+ {
+ _viewModel.FfmpegState = InstallState.Valid;
+ }
+ else
+ {
+ _viewModel.FfmpegState = InstallState.Error;
+ _viewModel.MsuPcmErrorText = "Install Failed";
+ _viewModel.MsuPcmErrorToolTip = "Failed to be able to download and run FFmpeg. Please click to view additional installation instructions.";
+ }
+ }
+
+ public async Task RetryFfmpeg()
+ {
+ if (await pythonCompanionService.VerifyFfMpegAsync())
+ {
+ _viewModel.FfmpegState = InstallState.Valid;
+ return;
+ }
+ await InstallFfmpeg();
+ }
+
+ public async Task RevalidateFfmpeg()
+ {
+ if (await pythonCompanionService.VerifyFfMpegAsync())
+ {
+ _viewModel.FfmpegState = InstallState.Valid;
+ }
+ }
+
+ public async Task InstallPyApp()
+ {
+ _viewModel.PyAppState = InstallState.InProgress;
+ _viewModel.PyAppInstallProgress = "Starting";
+ await Task.Delay(TimeSpan.FromMilliseconds(100));
+ var result = await pythonCompanionService.InstallPyApp(progress =>
+ {
+ _viewModel.PyAppInstallProgress = progress;
+ });
+ if (result)
+ {
+ _viewModel.PyAppState = InstallState.Valid;
+ }
+ else
+ {
+ _viewModel.PyAppState = InstallState.Error;
+ _viewModel.PyAppErrorText = "Install Failed";
+ _viewModel.PyAppErrorToolTip = "Failed to be able to download and run Python Companion App. Please click to view additional installation instructions.";
+ }
+ }
+
+ public async Task RetryPyApp()
+ {
+ if (await pythonCompanionService.VerifyInstalledAsync())
+ {
+ _viewModel.PyAppState = InstallState.Valid;
+ return;
+ }
+ await InstallPyApp();
+ }
+
+ public async Task RevalidatePyApp()
+ {
+ if (await pythonCompanionService.VerifyInstalledAsync())
+ {
+ _viewModel.PyAppState = InstallState.Valid;
+ }
+ }
+
+ public void SaveSettings()
+ {
+ settingsService.Settings.IgnoreMissingDependencies = _viewModel.DontRemindMeAgain;
+ settingsService.TrySaveSettings();
+ }
+}
\ No newline at end of file
diff --git a/MSUScripter/Services/ControlServices/MainWindowService.cs b/MSUScripter/Services/ControlServices/MainWindowService.cs
index 62cc90c..f7ef9cc 100644
--- a/MSUScripter/Services/ControlServices/MainWindowService.cs
+++ b/MSUScripter/Services/ControlServices/MainWindowService.cs
@@ -1,23 +1,37 @@
+using System;
using System.IO;
+using System.Linq;
+using System.Runtime.Versioning;
using System.Threading.Tasks;
-using AvaloniaControls;
using AvaloniaControls.ControlServices;
using AvaloniaControls.Services;
using GitHubReleaseChecker;
+using Microsoft.Extensions.Logging;
+using MSURandomizerLibrary.Services;
using MSUScripter.Configs;
+using MSUScripter.Models;
using MSUScripter.ViewModels;
namespace MSUScripter.Services.ControlServices;
-public class MainWindowService(Settings settings, SettingsService settingsService, MsuPcmService msuPcmService, PyMusicLooperService pyMusicLooperService, ProjectService projectService, IGitHubReleaseCheckerService gitHubReleaseCheckerService) : ControlService
+// ReSharper disable once ClassNeverInstantiated.Global
+public class MainWindowService(
+ Settings settings,
+ SettingsService settingsService,
+ MsuPcmService msuPcmService,
+ ProjectService projectService,
+ IGitHubReleaseCheckerService gitHubReleaseCheckerService,
+ PythonCompanionService pythonCompanionService,
+ IMsuTypeService msuTypeService,
+ ILogger logger) : ControlService
{
private readonly MainWindowViewModel _model = new();
public MainWindowViewModel InitializeModel()
{
- _ = CheckForNewRelease();
_ = CleanUpFolders();
- OpenCommandlineArgsProject();
+
+ _model.InitProject = Program.StartingProject;
_model.AppVersion = $" v{App.Version}";
if (!settings.HasDoneFirstTimeSetup && !string.IsNullOrEmpty(settings.MsuPcmPath))
@@ -26,61 +40,105 @@ public MainWindowViewModel InitializeModel()
settingsService.SaveSettings();
}
- _model.HasDoneFirstTimeSetup = settings.HasDoneFirstTimeSetup;
+ _model.Settings.LoadSettings(settings);
+
+ _model.MsuTypes = msuTypeService.MsuTypes
+ .OrderBy(x => x.DisplayName)
+ .ToList();
+ _model.RecentProjects = settings.RecentProjects.ToList();
+
+ if (_model.RecentProjects.Count != 0)
+ {
+ _model.DisplayNewProjectPage = false;
+ _model.DisplayOpenProjectPage = true;
+ }
+ else
+ {
+ _model.DisplayNewProjectPage = true;
+ _model.DisplayOpenProjectPage = false;
+ }
UpdateTitle();
return _model;
}
-
- public void OpenEditProjectPanel(MsuProject project)
+
+ public (MsuProject? mainProject, MsuProject? backupProject, string? error) LoadProject(string? path = null)
{
- _model.CurrentMsuProject = project;
- UpdateTitle();
+ path ??= _model.SelectedRecentProject?.ProjectPath;
+
+ if (string.IsNullOrEmpty(path))
+ {
+ return (null, null, "Invalid project path");
+ }
+
+ try
+ {
+ var project = projectService.LoadMsuProject(path, false);
+ MsuProject? backupProject = null;
+
+ if (project == null)
+ {
+ return (null, null, "Project not found");
+ }
+
+ if (!string.IsNullOrEmpty(project.BackupFilePath))
+ {
+ var potentialBackupProject = projectService.LoadMsuProject(project.BackupFilePath, true);
+ if (potentialBackupProject != null && potentialBackupProject.LastSaveTime > project.LastSaveTime)
+ {
+ backupProject = potentialBackupProject;
+ }
+ }
+
+ return (project, backupProject, null);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error opening project");
+ return (null, null, "Error opening project. Please contact MattEqualsCoder or post an issue on GitHub");
+ }
}
- public void CloseEditProjectPanel()
+ public bool ValidateProjectPaths(MsuProject project)
{
- _model.CurrentMsuProject = null;
- UpdateTitle();
+ return project.Tracks.SelectMany(x => x.Songs).All(x => x.MsuPcmInfo.AreFilesValid());
}
- public void OpenGitHubReleasePage()
+ public void UpdateLegacySmz3Project(MsuProject project)
{
- if (string.IsNullOrEmpty(_model.GitHubReleaseUrl)) return;
- CrossPlatformTools.OpenUrl(_model.GitHubReleaseUrl);
+ projectService.ConvertLegacySmz3Project(project);
+ projectService.SaveMsuProject(project, false);
}
- public void CloseNewReleaseBanner(bool permanently)
+ public bool IsLegacySmz3Project(MsuProject project)
{
- if (permanently)
- {
- settings.PromptOnUpdate = false;
- settingsService.SaveSettings();
- }
-
- _model.GitHubReleaseUrl = "";
+ return project.MsuType.DisplayName == "SMZ3 Classic (Metroid First)";
}
-
- public bool ValidateMsuPcm(string msupcmPath)
+
+ public void RefreshRecentProjects()
{
- return msuPcmService.ValidateMsuPcmPath(msupcmPath, out _);
+ _model.RecentProjects = settings.RecentProjects.Where(x => File.Exists(x.ProjectPath)).ToList();
+ settingsService.TrySaveSettings();
}
- public void UpdateHasDoneFirstTimeSetup(string? msupcmPath)
+ public async Task ValidateDependencies()
{
- settings.HasDoneFirstTimeSetup = true;
- settings.MsuPcmPath = msupcmPath;
- settingsService.SaveSettings();
+ var isMsuPcmServiceValid = await msuPcmService.VerifyInstalledAsync();
+ var isCompanionServiceValid = await pythonCompanionService.VerifyInstalledAsync();
+ if (settings.IgnoreMissingDependencies)
+ {
+ return true;
+ }
+ return isMsuPcmServiceValid.Successful && isCompanionServiceValid;
}
public void Shutdown()
{
settingsService.SaveSettings();
- msuPcmService.DeleteTempPcms();
- msuPcmService.DeleteTempJsonFiles();
+ CleanDirectory(Directories.TempFolder);
}
- public void UpdateTitle()
+ private void UpdateTitle()
{
if (_model.CurrentMsuProject == null)
{
@@ -94,49 +152,156 @@ public void UpdateTitle()
}
}
- public bool IsEditPanelDisplayed => _model.DisplayEditPage;
-
- private void OpenCommandlineArgsProject()
+ public MsuProject? CreateNewProject()
{
- if (string.IsNullOrEmpty(Program.StartingProject)) return;
+ var name = _model.MsuProjectName;
+ var creator = _model.MsuCreatorName;
+ var msuPath = _model.MsuPath;
+ var projectPath = _model.MsuProjectPath;
+ var msuType = _model.SelectedMsuType;
+ var msuPcmJson = _model.MsuPcmJsonPath;
+ var msuPcmWorkingDir = _model.MsuPcmWorkingPath;
+
+ if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(creator) || string.IsNullOrEmpty(msuPath) ||
+ string.IsNullOrEmpty(projectPath) || msuType == null)
+ {
+ return null;
+ }
- _model.InitProject = projectService.LoadMsuProject(Program.StartingProject, false);
+ try
+ {
+ logger.LogInformation("Creating new MSU Project");
+ return projectService.NewMsuProject(projectPath, msuType, msuPath, msuPcmJson, msuPcmWorkingDir, name, creator);
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ }
- if (_model.InitProject == null)
+ public void SaveSettings()
+ {
+ if (!_model.DisplaySettingsPage)
{
- _model.InitProjectError = true;
return;
}
+
+ _model.Settings.SaveChanges();
+ settingsService.SaveSettings();
+ }
+
+ public void LogError(Exception ex, string message)
+ {
+ logger.LogError(ex, "{Message}", message);
+ }
+
+ private bool CleanDirectory(string path, TimeSpan? timeout = null)
+ {
+ timeout ??= TimeSpan.Zero;
+ var currentDateTime = DateTime.UtcNow;
+ var isEmpty = true;
+ foreach (var file in Directory.EnumerateFiles(path))
+ {
+ var fileInfo = new FileInfo(file);
+ if (currentDateTime - fileInfo.LastWriteTimeUtc > timeout)
+ {
+ try
+ {
+ fileInfo.Delete();
+ }
+ catch
+ {
+ // Do nothing
+ }
+ }
+ else
+ {
+ isEmpty = false;
+ }
+ }
- if (!string.IsNullOrEmpty(_model.InitProject.BackupFilePath))
+ foreach (var folder in Directory.EnumerateDirectories(path))
{
- _model.InitBackupProject = projectService.LoadMsuProject(_model.InitProject.BackupFilePath, true);
+ if (CleanDirectory(folder, timeout))
+ {
+ try
+ {
+ Directory.Delete(folder);
+ }
+ catch
+ {
+ // Do nothing
+ }
+ }
+ else
+ {
+ isEmpty = false;
+ }
}
+
+ return isEmpty;
}
- private async Task CheckForNewRelease()
+ public async Task<(string ReleaseUrl, string? DownloadUrl)?> CheckForNewRelease()
{
- if (settings.PromptOnUpdate == false) return;
+ if (!settings.CheckForUpdates)
+ {
+ return null;
+ }
var newerGitHubRelease = await gitHubReleaseCheckerService.GetGitHubReleaseToUpdateToAsync("MattEqualsCoder",
- "MSUScripter", App.Version, settings.PromptOnPreRelease);
+ "MSUScripter", App.Version, false);
if (newerGitHubRelease != null)
{
- _model.GitHubReleaseUrl = newerGitHubRelease.Url;
+ if (OperatingSystem.IsLinux())
+ {
+ return (newerGitHubRelease.Url,
+ newerGitHubRelease.Asset.FirstOrDefault(x => x.Url.ToLower().EndsWith(".appimage"))?.Url);
+ }
+ return (newerGitHubRelease.Url, null);
}
+
+ return null;
+ }
+
+ [SupportedOSPlatform("linux")]
+ public void CreateDesktopFile()
+ {
+ ITaskService.Run(() =>
+ {
+ var response = App.BuildLinuxDesktopFile();
+
+ if (response.Success)
+ {
+ logger.LogInformation("Created desktop file for AppImage");
+ }
+ else
+ {
+ logger.LogError("Error creating desktop fie for AppImage: {Error}",
+ response.ErrorMessage ?? "Unknown Error");
+ }
+ });
+ }
+
+ public void SkipDesktopFile()
+ {
+ settingsService.Settings.SkipDesktopFile = true;
+ settingsService.TrySaveSettings();
+ }
+
+ public void IgnoreFutureUpdates()
+ {
+ settings.CheckForUpdates = false;
+ settingsService.SaveSettings();
}
private async Task CleanUpFolders()
{
await ITaskService.Run(() =>
{
- msuPcmService.DeleteTempPcms();
- msuPcmService.DeleteTempJsonFiles();
- msuPcmService.ClearCache();
- pyMusicLooperService.ClearCache();
+ CleanDirectory(Directories.CacheFolder, TimeSpan.FromDays(30));
+ CleanDirectory(Directories.TempFolder);
});
}
-
-
}
\ No newline at end of file
diff --git a/MSUScripter/Services/ControlServices/MsuGenerationWindowService.cs b/MSUScripter/Services/ControlServices/MsuGenerationWindowService.cs
new file mode 100644
index 0000000..dce8643
--- /dev/null
+++ b/MSUScripter/Services/ControlServices/MsuGenerationWindowService.cs
@@ -0,0 +1,397 @@
+using System;
+using System.Collections.Concurrent;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AvaloniaControls.ControlServices;
+using AvaloniaControls.Services;
+using Microsoft.Extensions.Logging;
+using MSUScripter.Configs;
+using MSUScripter.Events;
+using MSUScripter.ViewModels;
+
+namespace MSUScripter.Services.ControlServices;
+
+// ReSharper disable once ClassNeverInstantiated.Global
+public class MsuGenerationWindowService(
+ MsuPcmService msuPcmService,
+ ProjectService projectService,
+ StatusBarService statusBarService,
+ TrackListService trackListService,
+ ILogger logger) : ControlService
+{
+ private readonly MsuGenerationViewModel _model = new();
+
+ private readonly CancellationTokenSource _cts = new();
+
+ public event EventHandler>? PcmGenerationComplete;
+
+ public MsuGenerationViewModel InitializeModel(MsuProject project)
+ {
+ _model.MsuProject = project;
+
+ var msuDirectory = new FileInfo(project.MsuPath).DirectoryName;
+ if (string.IsNullOrEmpty(msuDirectory)) return _model;
+
+ var rows = project.Tracks
+ .Where(x => !x.IsScratchPad)
+ .SelectMany(x => x.Songs)
+ .OrderBy(x => x.TrackNumber)
+ .Select(x => new MsuGenerationRowViewModel(x))
+ .ToList();
+
+ rows.Add(new MsuGenerationRowViewModel(MsuGenerationRowType.Msu, project));
+
+ if (project.BasicInfo.WriteYamlFile)
+ {
+ rows.Add(new MsuGenerationRowViewModel(MsuGenerationRowType.Yaml, project));
+ }
+
+ if (project.BasicInfo.TrackListType != TrackList.Disabled)
+ {
+ rows.Add(new MsuGenerationRowViewModel(MsuGenerationRowType.TrackList, project));
+ }
+
+ if (project.BasicInfo.IncludeJson == true)
+ {
+ rows.Add(new MsuGenerationRowViewModel(MsuGenerationRowType.MsuPcmJson, project));
+ }
+
+ if (project.BasicInfo.CreateAltSwapperScript && project.Tracks.Any(x => x is { IsScratchPad: false, Songs.Count: > 1 }))
+ {
+ rows.Add(new MsuGenerationRowViewModel(MsuGenerationRowType.SwapperScript, project));
+ }
+
+ if (project.BasicInfo.IsSmz3Project)
+ {
+ if (!string.IsNullOrEmpty(project.BasicInfo.ZeldaMsuPath))
+ {
+ rows.Add(new MsuGenerationRowViewModel(MsuGenerationRowType.Smz3Zelda, project));
+ }
+
+ if (!string.IsNullOrEmpty(project.BasicInfo.MetroidMsuPath))
+ {
+ rows.Add(new MsuGenerationRowViewModel(MsuGenerationRowType.Smz3Metroid, project));
+ }
+
+ if (project.BasicInfo.CreateSplitSmz3Script)
+ {
+ rows.Add(new MsuGenerationRowViewModel(MsuGenerationRowType.Smz3Script, project));
+ }
+
+ if (project.BasicInfo.WriteYamlFile)
+ {
+ rows.Add(new MsuGenerationRowViewModel(MsuGenerationRowType.Smz3MetroidYaml, project));
+ rows.Add(new MsuGenerationRowViewModel(MsuGenerationRowType.Smz3ZeldaYaml, project));
+ }
+ }
+
+ if (!string.IsNullOrEmpty(_model.ZipPath))
+ {
+ rows.Add(new MsuGenerationRowViewModel(MsuGenerationRowType.Compress, project, _model.ZipPath));
+ }
+
+ _model.Rows = rows;
+ _model.TotalSongs = rows.Count;
+
+ return _model;
+ }
+
+ public void SetZipPath(string path)
+ {
+ _model.ZipPath = path;
+ _model.TotalSongs = _model.TotalSongs * 2 + 1;
+ var rows = _model.Rows.Concat([
+ new MsuGenerationRowViewModel(MsuGenerationRowType.Compress, _model.MsuProject, _model.ZipPath)
+ ]).ToList();
+ _model.Rows = rows;
+ }
+
+ public void RunGeneration()
+ {
+ _ = ITaskService.Run(async () =>
+ {
+
+ var start = DateTime.Now;
+
+ // If the yaml file exists, but the user doesn't want to use it,
+ // then we need to remove it to prevent issues validating the msu
+ if (!_model.MsuProject.BasicInfo.WriteYamlFile)
+ {
+ var yamlPath = Path.ChangeExtension(_model.MsuProject.MsuPath, ".yml");
+ if (File.Exists(yamlPath))
+ {
+ try
+ {
+ File.Delete(yamlPath);
+ }
+ catch (Exception e)
+ {
+ logger.LogError(e, "Failed to delete yaml file");
+ }
+ }
+ }
+
+ ConcurrentBag toRetry = [];
+
+ var generationRows = _model.Rows.Where(x => x.CanParallelize).ToList();
+
+ await Parallel.ForEachAsync(generationRows,
+ new ParallelOptions { MaxDegreeOfParallelism = 10, CancellationToken = _cts.Token }, async (model, _) =>
+ {
+ try
+ {
+ await PerformAction(model, toRetry);
+ }
+ catch (Exception)
+ {
+ // Do nothing
+ }
+ });
+
+ // For retries, try again linearly
+ foreach (var songDetails in toRetry)
+ {
+ logger.LogInformation("Retrying song {File}", songDetails.Path);
+ await ProcessSong(songDetails, true);
+ }
+
+ await Task.Delay(TimeSpan.FromSeconds(1));
+
+ foreach (var details in _model.Rows.Where(x => !x.CanParallelize)
+ .OrderBy(x => x.Type == MsuGenerationRowType.Compress))
+ {
+ await PerformAction(details);
+ }
+
+ if (_model.NumErrors > 0)
+ {
+ var errorString = _model.NumErrors == 1 ? "was 1 error" : $"were {_model.NumErrors} errors";
+ _model.GenerationErrors.Add($" - There were {errorString} when generating the MSU project.");
+ }
+
+ await Task.Delay(TimeSpan.FromSeconds(1));
+
+ msuPcmService.SaveGenerationCache(_model.MsuProject);
+
+ if (!projectService.ValidateProject(_model.MsuProject, out var validationError))
+ {
+ _model.GenerationErrors.Add($" - {validationError}");
+ }
+
+ var end = DateTime.Now;
+ var duration = end - start;
+ _model.GenerationSeconds = Math.Round(duration.TotalSeconds, 2);
+ _model.IsFinished = true;
+ _model.ButtonText = "Close";
+ _model.SongsCompleted = _model.TotalSongs;
+ statusBarService.UpdateStatusBar("MSU Generated");
+ PcmGenerationComplete?.Invoke(this, new ValueEventArgs(_model));
+
+ }, _cts.Token);
+ }
+
+ private async Task PerformAction(MsuGenerationRowViewModel rowDetails, ConcurrentBag? toRetry = null)
+ {
+ try
+ {
+ if (rowDetails.Type == MsuGenerationRowType.Song)
+ {
+ if (!await ProcessSong(rowDetails, false))
+ {
+ toRetry?.Add(rowDetails);
+ }
+ }
+ else if (rowDetails.Type is MsuGenerationRowType.Msu or MsuGenerationRowType.Smz3Metroid
+ or MsuGenerationRowType.Smz3Zelda)
+ {
+ if (!File.Exists(rowDetails.Path))
+ {
+ await using (File.Create(rowDetails.Path))
+ {
+ }
+ }
+
+ rowDetails.Message = "Generated";
+ _model.SongsCompleted++;
+ }
+ else if (rowDetails.Type == MsuGenerationRowType.Yaml)
+ {
+ projectService.ExportMsuRandomizerYaml(_model.MsuProject, out var error);
+ rowDetails.Message = string.IsNullOrEmpty(error) ? "Generated" : error;
+ _model.SongsCompleted++;
+ }
+ else if (rowDetails.Type == MsuGenerationRowType.MsuPcmJson)
+ {
+ msuPcmService.ExportMsuPcmTracksJson(_model.MsuProject);
+ rowDetails.Message =
+ _model.MsuProject.BasicInfo.DitherType is DitherType.DefaultOn or DitherType.DefaultOff
+ ? "Generated with dither value inconsistencies"
+ : "Generated";
+ _model.SongsCompleted++;
+ }
+ else if (rowDetails.Type == MsuGenerationRowType.TrackList)
+ {
+ trackListService.WriteTrackListFile(_model.MsuProject);
+ rowDetails.Message = "Generated";
+ _model.SongsCompleted++;
+ }
+ else if (rowDetails.Type == MsuGenerationRowType.SwapperScript)
+ {
+ rowDetails.Message = !projectService.CreateAltSwapperFile(_model.MsuProject)
+ ? "Could not create alt swapper bat file. Project file may be corrupt. Verify output pcm file paths."
+ : "Generated";
+ _model.SongsCompleted++;
+ }
+ else if (rowDetails.Type == MsuGenerationRowType.Smz3Script)
+ {
+ rowDetails.Message = projectService.CreateSmz3SplitScript(_model.MsuProject)
+ ? "Generated"
+ : "Could not generate SMZ3 Split Script";
+ _model.SongsCompleted++;
+ }
+ else if (rowDetails.Type == MsuGenerationRowType.Smz3MetroidYaml)
+ {
+ rowDetails.Message =
+ projectService.CreateSmz3SplitRandomizerYaml(_model.MsuProject, true, false, out var error)
+ ? "Generated"
+ : error ?? "Could not generate YAML file";
+ _model.SongsCompleted++;
+ }
+ else if (rowDetails.Type == MsuGenerationRowType.Smz3ZeldaYaml)
+ {
+ rowDetails.Message =
+ projectService.CreateSmz3SplitRandomizerYaml(_model.MsuProject, false, true, out var error)
+ ? "Generated"
+ : error ?? "Could not generate YAML file";
+ _model.SongsCompleted++;
+ }
+ else if (rowDetails.Type == MsuGenerationRowType.Compress)
+ {
+ Compress(rowDetails);
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error running action {Type}", rowDetails.Type);
+ }
+ }
+
+ private void Compress(MsuGenerationRowViewModel compressRow)
+ {
+ try
+ {
+ if (File.Exists(_model.ZipPath))
+ {
+ File.Delete(_model.ZipPath);
+ }
+
+ using var zip = ZipFile.Open(_model.ZipPath!, ZipArchiveMode.Create);
+
+ foreach (var row in _model.Rows.Where(row => row.Type != MsuGenerationRowType.Compress).TakeWhile(_ => !_cts.Token.IsCancellationRequested))
+ {
+ if (!File.Exists(row.Path))
+ {
+ _model.SongsCompleted++;
+ continue;
+ }
+
+ try
+ {
+ zip.CreateEntryFromFile(row.Path, Path.GetFileName(row.Path));
+ row.Message = "Compressed";
+ _model.SongsCompleted++;
+ }
+ catch (Exception)
+ {
+ row.Message = "Failed to add to zip file";
+ _model.SongsCompleted++;
+ return;
+ }
+ }
+
+ compressRow.Message = "Success!";
+ _model.SongsCompleted++;
+ }
+ catch (Exception)
+ {
+ compressRow.HasWarning = true;
+ compressRow.Message = "Failed to create zip file";
+ }
+
+
+ }
+
+ public void Cancel()
+ {
+ if (!_model.IsFinished)
+ {
+ _cts.Cancel();
+ }
+ }
+
+ private async Task ProcessSong(MsuGenerationRowViewModel rowDetails, bool isRetry)
+ {
+ if (_cts.IsCancellationRequested)
+ {
+ return true;
+ }
+
+ if (!_model.MsuProject.BasicInfo.IsMsuPcmProject)
+ {
+ if (!File.Exists(rowDetails.Path))
+ {
+ rowDetails.HasWarning = true;
+ rowDetails.Message = "PCM file not found";
+ _model.NumErrors++;
+ }
+ else
+ {
+ rowDetails.Message = "Waiting";
+ }
+
+ _model.SongsCompleted++;
+ return true;
+ }
+
+ var songInfo = rowDetails.SongInfo!;
+ var generationResponse = await msuPcmService.CreatePcm(_model.MsuProject, songInfo, false, true, true);
+
+ if (!generationResponse.Successful)
+ {
+ // If this is an error for the sox temp file for the first run, ignore so it can be retried
+ if (!isRetry && generationResponse.Message?.Contains("__sox_wrapper_temp") == true &&
+ generationResponse.Message.Contains("Permission denied"))
+ {
+ return false;
+ }
+ // Partially ignore empty pcms with no input files
+ else if (generationResponse.Message?.EndsWith("No input files specified") == true && File.Exists(rowDetails.Path) && new FileInfo(rowDetails.Path).Length <= 44500)
+ {
+ rowDetails.HasWarning = true;
+ rowDetails.Message = generationResponse.Message;
+ }
+ else
+ {
+ rowDetails.HasWarning = true;
+ rowDetails.Message = generationResponse.Message ?? "Unknown error";
+ _model.NumErrors++;
+ }
+
+ }
+ else
+ {
+ rowDetails.Message = "Generated";
+ }
+
+ _model.SongsCompleted++;
+ return true;
+ }
+
+ public void LogError(Exception ex, string message)
+ {
+ logger.LogError(ex, "{Message}", message);
+ }
+}
\ No newline at end of file
diff --git a/MSUScripter/Services/ControlServices/MsuPcmGenerationWindowService.cs b/MSUScripter/Services/ControlServices/MsuPcmGenerationWindowService.cs
deleted file mode 100644
index 645c67a..0000000
--- a/MSUScripter/Services/ControlServices/MsuPcmGenerationWindowService.cs
+++ /dev/null
@@ -1,165 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using AvaloniaControls.ControlServices;
-using AvaloniaControls.Services;
-using MSUScripter.Configs;
-using MSUScripter.Events;
-using MSUScripter.ViewModels;
-
-namespace MSUScripter.Services.ControlServices;
-
-public class MsuPcmGenerationWindowService(MsuPcmService msuPcmService, ConverterService converterService, ProjectService projectService, StatusBarService statusBarService) : ControlService
-{
- private MsuPcmGenerationViewModel _model = new();
-
- private readonly CancellationTokenSource _cts = new();
-
- public event EventHandler>? PcmGenerationComplete;
-
- public MsuPcmGenerationViewModel InitializeModel(MsuProjectViewModel project, bool exportYaml)
- {
- _model.MsuProjectViewModel = project;
- _model.MsuProject = converterService.ConvertProject(project);
- _model.ExportYaml = exportYaml;
- _model.SplitSmz3 = project.BasicInfo.CreateSplitSmz3Script;
-
- var msuDirectory = new FileInfo(project.MsuPath).DirectoryName;
- if (string.IsNullOrEmpty(msuDirectory)) return _model;
-
- var songs = project.Tracks
- .Where(x => !x.IsScratchPad)
- .SelectMany(x => x.Songs)
- .OrderBy(x => x.TrackNumber)
- .Select(x => new MsuPcmGenerationSongViewModel()
- {
- SongName = Path.GetRelativePath(msuDirectory, new FileInfo(x.OutputPath!).FullName),
- TrackName = x.TrackName,
- TrackNumber = x.TrackNumber,
- Path = x.OutputPath!,
- OriginalViewModel = x
- })
- .ToList();
-
- _model.Rows = songs;
- _model.TotalSongs = songs.Count;
-
- return _model;
- }
-
- public void RunGeneration()
- {
- _ = ITaskService.Run(async () => {
-
- var start = DateTime.Now;
-
- List toRetry = [];
-
- if (!File.Exists(_model.MsuProjectViewModel.MsuPath))
- {
- using (File.Create(_model.MsuProjectViewModel.MsuPath)) { }
- }
-
- Parallel.ForEach(_model.Rows,
- new ParallelOptions { MaxDegreeOfParallelism = 10, CancellationToken = _cts.Token },
- ParallelAction);
-
- // For retries, try again linearly
- foreach (var songDetails in toRetry)
- {
- await ProcessSong(songDetails, true);
- }
-
- if (_model.ExportYaml)
- {
- projectService.ExportMsuRandomizerYaml(_model.MsuProject, out var error);
-
- if (!string.IsNullOrEmpty(error))
- {
- _model.GenerationErrors.Add($"- YAML file generation failed: {error}");
- }
- }
-
- if (_model.NumErrors > 0)
- {
- var errorString = _model.NumErrors == 1 ? "was 1 error" : $"were {_model.NumErrors} errors";
- _model.GenerationErrors.Add($"- There {errorString} when running MsuPcm++");
- }
-
- var end = DateTime.Now;
- var duration = end - start;
- _model.GenerationSeconds = Math.Round(duration.TotalSeconds, 2);
- _model.IsFinished = true;
- _model.ButtonText = "Close";
- _model.SongsCompleted = _model.Rows.Count;
- statusBarService.UpdateStatusBar("MSU Generated");
- PcmGenerationComplete?.Invoke(this, new ValueEventArgs(_model));
- return;
-
- async void ParallelAction(MsuPcmGenerationSongViewModel songDetails)
- {
- if (!await ProcessSong(songDetails, false))
- {
- toRetry.Add(songDetails);
- }
- }
- }, _cts.Token);
- }
-
- public void Cancel()
- {
- if (!_model.IsFinished)
- {
- _cts.Cancel();
- }
- }
-
- private async Task ProcessSong(MsuPcmGenerationSongViewModel songDetails, bool isRetry)
- {
- if (_cts.IsCancellationRequested)
- {
- return true;
- }
-
- var songViewModel = songDetails.OriginalViewModel;
- var song = new MsuSongInfo();
- converterService.ConvertViewModel(songViewModel, song);
- converterService.ConvertViewModel(songViewModel!.MsuPcmInfo, song.MsuPcmInfo);
-
- var generationResponse = await msuPcmService.CreatePcm(false, _model.MsuProject, song);
-
- if (!generationResponse.Successful)
- {
- // If this is an error for the sox temp file for the first run, ignore so it can be retried
- if (!isRetry && generationResponse.Message?.Contains("__sox_wrapper_temp") == true &&
- generationResponse.Message.Contains("Permission denied"))
- {
- return false;
- }
- // Partially ignore empty pcms with no input files
- else if (generationResponse.Message?.EndsWith("No input files specified") == true && File.Exists(song.OutputPath) && new FileInfo(song.OutputPath).Length <= 44500)
- {
- songDetails.HasWarning = true;
- songDetails.Message = generationResponse.Message;
- }
- else
- {
- songDetails.HasWarning = true;
- songDetails.Message = generationResponse.Message ?? "Unknown error";
- _model.NumErrors++;
- }
-
- }
- else
- {
- songViewModel.LastGeneratedDate = DateTime.Now;
- songDetails.Message = "Success!";
- }
-
- _model.SongsCompleted++;
- return true;
- }
-}
\ No newline at end of file
diff --git a/MSUScripter/Services/ControlServices/MsuProjectWindowService.cs b/MSUScripter/Services/ControlServices/MsuProjectWindowService.cs
new file mode 100644
index 0000000..cc1d761
--- /dev/null
+++ b/MSUScripter/Services/ControlServices/MsuProjectWindowService.cs
@@ -0,0 +1,1190 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Avalonia.Media;
+using Avalonia.Threading;
+using AvaloniaControls.Controls;
+using AvaloniaControls.ControlServices;
+using AvaloniaControls.Services;
+using DynamicData;
+using Material.Icons;
+using Microsoft.Extensions.Logging;
+using MSUScripter.Configs;
+using MSUScripter.Events;
+using MSUScripter.Models;
+using MSUScripter.ViewModels;
+
+namespace MSUScripter.Services.ControlServices;
+
+// ReSharper disable once ClassNeverInstantiated.Global
+public class MsuProjectWindowService(
+ ConverterService converterService,
+ YamlService yamlService,
+ StatusBarService statusBarService,
+ ProjectService projectService,
+ TrackListService trackListService,
+ SettingsService settingsService,
+ IAudioPlayerService audioPlayerService,
+ MsuPcmService msuPcmService,
+ PythonCompanionService pythonCompanionService,
+ ILogger logger) : ControlService
+{
+ private Settings Settings => settingsService.Settings;
+
+ private MsuProjectWindowViewModel _viewModel = null!;
+ private MsuProject _project = null!;
+ private MsuProjectWindowViewModelTreeData? _draggedItem;
+ private MsuProjectWindowViewModelTreeData? _hoveredItem;
+
+ public MsuProjectWindowViewModel InitViewModel(MsuProject project)
+ {
+ _project = project;
+
+ var windowTitle = "MSU Scripter";
+ if (!string.IsNullOrEmpty(project.BasicInfo.PackName))
+ {
+ windowTitle = $"{project.BasicInfo.PackName} - MSU Scripter";
+ }
+ else if(!string.IsNullOrEmpty(project.MsuPath))
+ {
+ var baseName = Path.GetFileName(project.MsuPath);
+ windowTitle = $"{baseName} - MSU Scripter";
+ }
+
+ var sidebarItems = new List
+ {
+ new()
+ {
+ Name = "MSU Details",
+ CollapseIcon = MaterialIconKind.Note,
+ LeftSpacing = 0,
+ ShowCheckbox = false,
+ SortIndex = -10000,
+ MsuDetails = true
+ }
+ };
+
+ var totalTracks = 0;
+ var completedTracks = 0;
+ var totalSongs = 0;
+ var completedSongs = 0;
+
+ foreach (var track in _project.Tracks)
+ {
+ var trackSortIndex = track.IsScratchPad ? -1000 : track.TrackNumber * 1000;
+
+ if (!track.IsScratchPad)
+ {
+ totalTracks++;
+ }
+
+ var trackTreeData = new MsuProjectWindowViewModelTreeData
+ {
+ Name = track.IsScratchPad ? "Scratch Pad" : $"#{track.TrackNumber} {track.TrackName}",
+ CollapseIcon = MaterialIconKind.MusicNote,
+ CollapseIconOpacity = 0.4,
+ TrackInfo = track,
+ LeftSpacing = 0,
+ SortIndex = trackSortIndex,
+ ShowCheckbox = true,
+ Track = true,
+ };
+ sidebarItems.Add(trackTreeData);
+
+ if (track is { IsScratchPad: false, Songs.Count: > 0 })
+ {
+ completedTracks++;
+ }
+
+ for (var i = 0; i < track.Songs.Count; i++)
+ {
+ var song = track.Songs[i];
+
+ totalSongs++;
+
+ if (song.IsComplete)
+ {
+ completedSongs++;
+ }
+
+ if (track.Songs.Count > 1)
+ {
+ var songTreeData = new MsuProjectWindowViewModelTreeData
+ {
+ Name = string.IsNullOrEmpty(song.SongName) ? $"Song {i + 1}" : song.SongName,
+ CollapseIcon = MaterialIconKind.MusicNote,
+ CollapseIconOpacity = 0.4,
+ LeftSpacing = 12,
+ SortIndex = trackSortIndex + 1 + i,
+ ParentIndex = trackSortIndex,
+ ParentTreeData = trackTreeData,
+ ShowCheckbox = true,
+ TrackInfo = track,
+ SongInfo = song,
+ };
+ songTreeData.UpdateCompletedFlag();
+ trackTreeData.CollapseIcon = MaterialIconKind.ChevronDown;
+ trackTreeData.CollapseIconOpacity = 1;
+ trackTreeData.ChildTreeData.Add(songTreeData);
+ sidebarItems.Add(songTreeData);
+ }
+ else
+ {
+ trackTreeData.SongInfo = song;
+ trackTreeData.CollapseIcon = MaterialIconKind.MusicNote;
+ trackTreeData.CollapseIconOpacity = 0.4;
+ }
+ }
+
+ trackTreeData.UpdateCompletedFlag();
+ }
+
+ _viewModel = new MsuProjectWindowViewModel()
+ {
+ MsuProject = project,
+ SongSummary = $"{completedSongs}/{totalSongs} Songs Completed",
+ TrackSummary = $"{completedTracks}/{totalTracks} Tracks With Songs Added",
+ WindowTitle = windowTitle,
+ PreviousVideoPath = settingsService.Settings.PreviousVideoPath
+ };
+
+ _viewModel.BasicInfoViewModel.UpdateModel(project);
+
+ _viewModel.TreeItems.AddRange(sidebarItems.OrderBy(x => x.SortIndex));
+
+ if (Settings.ProjectTreeDisplayIsCompleteIcon)
+ {
+ ToggleCompletedIcons(true);
+ }
+
+ if (Settings.ProjectTreeDisplayCheckCopyrightIcon)
+ {
+ ToggleCheckCopyrightIcons(true);
+ }
+
+ if (Settings.ProjectTreeDisplayCopyrightSafeIcon)
+ {
+ ToggleCopyrightStatusIcons(true);
+ }
+
+ if (Settings.ProjectTreeDisplayHasSongIcon)
+ {
+ ToggleHasAudioIcons(true);
+ }
+
+ _viewModel.FilterOnlyTracksMissingSongs = Settings.ProjectTreeFilterOnlyTracksMissingSongs;
+ _viewModel.FilterOnlyCopyrightUntested = Settings.ProjectTreeFilterOnlyCopyrightUntested;
+ _viewModel.FilterOnlyIncomplete = Settings.ProjectTreeFilterOnlyIncomplete;
+ _viewModel.FilterOnlyMissingAudio = Settings.ProjectTreeFilterOnlyMissingAudio;
+ FilterTree();
+
+ statusBarService.StatusBarTextUpdated += StatusBarServiceOnStatusBarTextUpdated;
+ msuPcmService.GeneratingPcm += MsuPcmServiceOnGeneratingPcm;
+
+ _viewModel.RecentProjects = settingsService.Settings.RecentProjects.Where(x => x.ProjectPath != project.ProjectFilePath)
+ .ToList();
+
+ LoadSettings();
+
+ _viewModel.LastModifiedDate = project.LastSaveTime;
+
+ _viewModel.BasicInfoViewModel.PropertyChanged += (_, args) =>
+ {
+ if (args.PropertyName == "PackName")
+ {
+ if (!string.IsNullOrEmpty(_viewModel.BasicInfoViewModel.PackName))
+ {
+ _viewModel.WindowTitle = $"{_viewModel.BasicInfoViewModel.PackName} - MSU Scripter";
+ }
+ else if(!string.IsNullOrEmpty(project.MsuPath))
+ {
+ var baseName = Path.GetFileName(project.MsuPath);
+ _viewModel.WindowTitle = $"{baseName} - MSU Scripter";
+ }
+ }
+ };
+
+ return _viewModel;
+ }
+
+ private void MsuPcmServiceOnGeneratingPcm(object? sender, bool e)
+ {
+ _viewModel.MsuSongViewModel.IsGeneratingPcmFiles = e;
+ _viewModel.MsuSongViewModel.AdvancedPanelViewModel.IsGeneratingPcmFile = e;
+ }
+
+ private void StatusBarServiceOnStatusBarTextUpdated(object? sender, ValueEventArgs e)
+ {
+ _viewModel.StatusBarText = e.Data;
+ }
+
+ public void LoadSettings()
+ {
+ _viewModel.DefaultSongPanel = settingsService.Settings.DefaultSongPanel;
+ _viewModel.MsuSongViewModel.BasicPanelViewModel.PyMusicLooperEnabled = pythonCompanionService.IsValid;
+ }
+
+ public void UpdateCompletedSummary()
+ {
+ var totalTracks = 0;
+ var completedTracks = 0;
+ var totalSongs = 0;
+ var completedSongs = 0;
+
+ foreach (var track in _project.Tracks)
+ {
+ if (!track.IsScratchPad)
+ {
+ totalTracks++;
+ if (track.Songs.Count > 0) completedTracks++;
+ }
+
+ totalSongs += track.Songs.Count;
+ completedSongs += track.Songs.Count(x => x.IsComplete);
+ }
+
+ _viewModel.SongSummary = $"{completedSongs}/{totalSongs} Songs Completed";
+ _viewModel.TrackSummary = $"{completedTracks}/{totalTracks} Tracks With Songs Added";
+ }
+
+ public void ToggleFilterTracksMissingSongs()
+ {
+ _viewModel.FilterOnlyTracksMissingSongs = !_viewModel.FilterOnlyTracksMissingSongs;
+ FilterTree();
+ settingsService.Settings.ProjectTreeFilterOnlyTracksMissingSongs = _viewModel.FilterOnlyTracksMissingSongs;
+ settingsService.TrySaveSettings();
+ }
+
+ public void ToggleFilterIncomplete()
+ {
+ _viewModel.FilterOnlyIncomplete = !_viewModel.FilterOnlyIncomplete;
+ FilterTree();
+ settingsService.Settings.ProjectTreeFilterOnlyIncomplete = _viewModel.FilterOnlyIncomplete;
+ settingsService.TrySaveSettings();
+ }
+
+ public void ToggleFilterMissingAudio()
+ {
+ _viewModel.FilterOnlyMissingAudio = !_viewModel.FilterOnlyMissingAudio;
+ FilterTree();
+ settingsService.Settings.ProjectTreeFilterOnlyMissingAudio = _viewModel.FilterOnlyMissingAudio;
+ settingsService.TrySaveSettings();
+ }
+
+ public void ToggleFilterCopyrightUntested()
+ {
+ _viewModel.FilterOnlyCopyrightUntested = !_viewModel.FilterOnlyCopyrightUntested;
+ FilterTree();
+ settingsService.Settings.ProjectTreeFilterOnlyCopyrightUntested = _viewModel.FilterOnlyCopyrightUntested;
+ settingsService.TrySaveSettings();
+ }
+
+ public void FilterTree()
+ {
+ var hasFilterToggle = _viewModel.FilterOnlyTracksMissingSongs || _viewModel.FilterOnlyIncomplete ||
+ _viewModel.FilterOnlyMissingAudio || _viewModel.FilterOnlyCopyrightUntested;
+ _viewModel.FilterEyeIcon = hasFilterToggle ? MaterialIconKind.FilterCheck : MaterialIconKind.Filter;
+ List parentTreeItems = [];
+ var filterText = string.IsNullOrEmpty(_viewModel.FilterText) ? null : _viewModel.FilterText.ToLower();
+ foreach (var treeData in _viewModel.TreeItems)
+ {
+ if (treeData.ChildTreeData.Count > 0)
+ {
+ parentTreeItems.Add(treeData);
+ }
+ else
+ {
+ var matches = treeData.MatchesFilter(filterText, _viewModel.FilterOnlyTracksMissingSongs, _viewModel.FilterOnlyIncomplete,
+ _viewModel.FilterOnlyMissingAudio, _viewModel.FilterOnlyCopyrightUntested);
+ treeData.IsFilteredOut = !matches;
+ }
+ }
+
+ foreach (var treeData in parentTreeItems)
+ {
+ if (treeData.MatchesFilter(filterText, _viewModel.FilterOnlyTracksMissingSongs, _viewModel.FilterOnlyIncomplete, _viewModel.FilterOnlyMissingAudio, _viewModel.FilterOnlyCopyrightUntested))
+ {
+ treeData.IsFilteredOut = false;
+ foreach (var childData in treeData.ChildTreeData)
+ {
+ childData.IsFilteredOut = false;
+ }
+ }
+ else
+ {
+ treeData.IsFilteredOut = treeData.ChildTreeData.All(x => x.IsFilteredOut);
+ }
+ }
+ }
+
+ public void SelectedTreeItem(MsuProjectWindowViewModelTreeData treeData, bool isIconClick)
+ {
+ if (treeData.ChildTreeData.Count > 0)
+ {
+ if (isIconClick)
+ {
+ treeData.ToggleAsParent(true, treeData.CollapseIcon == MaterialIconKind.ChevronDown);
+ }
+ else
+ {
+ SaveCurrentPanel();
+ _viewModel.CurrentTreeItem = treeData;
+ _viewModel.BasicInfoViewModel.IsVisible = false;
+ _viewModel.MsuSongViewModel.BasicPanelViewModel.PyMusicLooperEnabled = pythonCompanionService.IsValid;
+ _viewModel.MsuSongViewModel.UpdateViewModel(_project, treeData.TrackInfo!, null, treeData);
+ }
+ }
+ else if (treeData.IsSongOrTrack)
+ {
+ SaveCurrentPanel();
+ _viewModel.CurrentTreeItem = treeData;
+ _viewModel.BasicInfoViewModel.IsVisible = false;
+ _viewModel.MsuSongViewModel.BasicPanelViewModel.PyMusicLooperEnabled = pythonCompanionService.IsValid;
+ _viewModel.MsuSongViewModel.UpdateViewModel(_project, treeData.TrackInfo!, treeData.SongInfo, treeData);
+ }
+ else if (treeData.MsuDetails)
+ {
+ if (_viewModel.MsuSongViewModel.IsEnabled)
+ {
+ _viewModel.MsuSongViewModel.SaveChanges();
+ _viewModel.MsuSongViewModel.IsEnabled = false;
+ }
+ _viewModel.CurrentTreeItem = treeData;
+ _viewModel.BasicInfoViewModel.UpdateModel(_project);
+ _viewModel.BasicInfoViewModel.IsVisible = true;
+ }
+ }
+
+ public bool SaveCurrentPanel()
+ {
+ if (_viewModel.MsuSongViewModel.IsEnabled)
+ {
+ _viewModel.MsuSongViewModel.SaveChanges();
+ if (_viewModel.MsuSongViewModel.LastModifiedDate > _viewModel.LastModifiedDate)
+ {
+ _viewModel.LastModifiedDate = _viewModel.MsuSongViewModel.LastModifiedDate;
+ return true;
+ }
+ }
+ else if (_viewModel.BasicInfoViewModel.IsVisible)
+ {
+ _viewModel.BasicInfoViewModel.SaveChanges();
+ if (_viewModel.BasicInfoViewModel.LastModifiedDate > _viewModel.LastModifiedDate)
+ {
+ _viewModel.LastModifiedDate = _viewModel.BasicInfoViewModel.LastModifiedDate;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public void SaveProject(bool isBackup = false)
+ {
+ SaveCurrentPanel();
+ projectService.SaveMsuProject(_project, isBackup);
+ }
+
+ public bool CreateYamlFile(out string? error)
+ {
+ SaveCurrentPanel();
+ projectService.ExportMsuRandomizerYaml(_project, out error);
+ if (!string.IsNullOrEmpty(error))
+ {
+ return false;
+ }
+
+ return _project.BasicInfo is not { IsSmz3Project: true, CreateSplitSmz3Script: true } ||
+ projectService.CreateSmz3SplitRandomizerYaml(_project, false, false, out error);
+ }
+
+ public string? CreateTracksJsonFile()
+ {
+ SaveCurrentPanel();
+ msuPcmService.ExportMsuPcmTracksJson(_project);
+
+ if (_project.BasicInfo.DitherType is DitherType.DefaultOn or DitherType.DefaultOff)
+ {
+ return "Generated with dither value inconsistencies";
+ }
+
+ return null;
+ }
+
+ public void CreateTrackList()
+ {
+ SaveCurrentPanel();
+ trackListService.WriteTrackListFile(_project);
+ }
+
+ public bool CreateScriptFiles()
+ {
+ SaveCurrentPanel();
+ if (!projectService.CreateAltSwapperFile(_project))
+ {
+ return false;
+ }
+
+ if (_project.BasicInfo is { IsSmz3Project: true, CreateSplitSmz3Script: true })
+ {
+ return projectService.CreateSmz3SplitScript(_project);
+ }
+
+ return true;
+ }
+
+ public void AddNewSong(MsuProjectWindowViewModelTreeData? treeData = null, bool duplicate = false, bool advancedMode = false, bool rememberSetting = false, string? initialFile = null)
+ {
+ if (rememberSetting)
+ {
+ settingsService.Settings.DefaultSongPanel = advancedMode ? DefaultSongPanel.Advanced : DefaultSongPanel.Basic;
+ settingsService.SaveSettings();
+ _viewModel.DefaultSongPanel = settingsService.Settings.DefaultSongPanel;
+ }
+
+ treeData ??= _viewModel.CurrentTreeItem;
+ if (treeData?.TrackInfo == null || _viewModel.MsuProject == null)
+ {
+ throw new InvalidOperationException("Invalid tree data item for adding a song");
+ }
+
+ var trackInfo = treeData.TrackInfo;
+ var index = 0;
+ if (treeData.SongInfo != null)
+ {
+ index = trackInfo.Songs.IndexOf(treeData.SongInfo) + 1;
+ }
+ var newSong = trackInfo.AddSong(_viewModel.MsuProject, index, advancedMode);
+
+ if (duplicate && treeData.SongInfo != null)
+ {
+ var outputPath = newSong.OutputPath ?? newSong.MsuPcmInfo.Output;
+ if (converterService.CloneModel(treeData.SongInfo, newSong) &&
+ converterService.CloneModel(treeData.SongInfo.MsuPcmInfo, newSong.MsuPcmInfo))
+ {
+ newSong.Id = Guid.NewGuid().ToString("N");
+ newSong.OutputPath = outputPath;
+ newSong.MsuPcmInfo.Output = outputPath;
+ }
+ }
+
+ var parentSortIndex = treeData.ParentIndex != 0 ? treeData.ParentIndex : treeData.SortIndex;
+ var parentTreeData = _viewModel.TreeItems.First(x => x.SortIndex == parentSortIndex);
+ var parentIndex = _viewModel.TreeItems.IndexOf(parentTreeData);
+
+ // Added the first song, so just use the same tree data
+ if (trackInfo.Songs.Count == 1)
+ {
+ treeData.SongInfo = newSong;
+ }
+ // Added a song to a node that already has multiple songs
+ else if (parentTreeData.ChildTreeData.Count > 0)
+ {
+ parentTreeData.SongInfo = null;
+
+ var songTreeData = new MsuProjectWindowViewModelTreeData
+ {
+ Name = string.IsNullOrEmpty(newSong.SongName) ? $"Song {trackInfo.Songs.Count}" : newSong.SongName,
+ CollapseIcon = MaterialIconKind.MusicNote,
+ CollapseIconOpacity = 0.4,
+ LeftSpacing = 12,
+ SortIndex = parentSortIndex + 1 + index,
+ ParentIndex = parentSortIndex,
+ ParentTreeData = parentTreeData,
+ ShowCheckbox = true,
+ TrackInfo = trackInfo,
+ SongInfo = newSong,
+ };
+
+ parentTreeData.ToggleAsParent(true, false);
+ parentTreeData.ChildTreeData.Insert(index, songTreeData);
+ _viewModel.TreeItems.Insert(parentIndex + index + 1, songTreeData);
+
+ for (var i = index + 1; i < treeData.TrackInfo.Songs.Count; i++)
+ {
+ parentTreeData.ChildTreeData[i].SortIndex++;
+ }
+
+ songTreeData.UpdateCompletedFlag();
+ songTreeData.DisplayIsCompleteIcon = _viewModel.DisplayIsCompleteIcon;
+ songTreeData.DisplayHasSongIcon = _viewModel.DisplayHasSongIcon;
+ songTreeData.DisplayCopyrightSafeIcon = _viewModel.DisplayCopyrightSafeIcon;
+ songTreeData.DisplayCheckCopyrightIcon = _viewModel.DisplayCheckCopyrightIcon;
+ parentTreeData.UpdateCompletedFlag();
+ treeData = songTreeData;
+ }
+ // Add a second song, meaning we need to switch the track to be a collapsible
+ else
+ {
+ parentTreeData.SongInfo = null;
+
+ for (var i = 0; i < trackInfo.Songs.Count; i++)
+ {
+ var song = trackInfo.Songs[i];
+
+ var songTreeData = new MsuProjectWindowViewModelTreeData
+ {
+ Name = string.IsNullOrEmpty(song.SongName) ? $"Song {i + 1}" : song.SongName,
+ CollapseIcon = MaterialIconKind.MusicNote,
+ CollapseIconOpacity = 0.4,
+ LeftSpacing = 12,
+ SortIndex = parentSortIndex + 1 + i,
+ ParentIndex = parentSortIndex,
+ ParentTreeData = parentTreeData,
+ ShowCheckbox = true,
+ TrackInfo = trackInfo,
+ SongInfo = song,
+ };
+
+ songTreeData.UpdateCompletedFlag();
+ songTreeData.DisplayIsCompleteIcon = _viewModel.DisplayIsCompleteIcon;
+ songTreeData.DisplayHasSongIcon = _viewModel.DisplayHasSongIcon;
+ songTreeData.DisplayCopyrightSafeIcon = _viewModel.DisplayCopyrightSafeIcon;
+ songTreeData.DisplayCheckCopyrightIcon = _viewModel.DisplayCheckCopyrightIcon;
+ treeData = songTreeData;
+
+ parentTreeData.ToggleAsParent(true, false);
+ parentTreeData.ChildTreeData.Add(songTreeData);
+ _viewModel.TreeItems.Insert(parentIndex + i + 1, songTreeData);
+ }
+
+ parentTreeData.UpdateCompletedFlag();
+ }
+
+ _viewModel.MsuSongViewModel.UpdateViewModel(_project, treeData.TrackInfo, newSong, treeData);
+ _viewModel.SelectedTreeItem = treeData;
+ treeData.UpdateCompletedFlag();
+ UpdateCompletedSummary();
+
+ if (_viewModel.MsuSongViewModel.BasicPanelViewModel.IsEnabled && !string.IsNullOrEmpty(initialFile))
+ {
+ _viewModel.MsuSongViewModel.BasicPanelViewModel.DragDropFile(initialFile);
+ }
+ else if (_viewModel.MsuSongViewModel.AdvancedPanelViewModel.IsEnabled && !string.IsNullOrEmpty(initialFile))
+ {
+ _viewModel.MsuSongViewModel.AdvancedPanelViewModel.DragDropFile(initialFile);
+ }
+
+ logger.LogInformation("Successfully added new song");
+
+ _viewModel.LastModifiedDate = DateTime.Now;
+ }
+
+ public void RemoveSong(MsuProjectWindowViewModelTreeData treeData)
+ {
+ if (treeData.SongInfo == null || treeData.TrackInfo == null)
+ {
+ throw new InvalidOperationException("Attempted to delete tree data without song information");
+ }
+
+ var trackInfo = treeData.TrackInfo;
+ var songInfo = treeData.SongInfo;
+ var parentSortIndex = treeData.ParentIndex != 0 ? treeData.ParentIndex : treeData.SortIndex;
+ var parentTreeData = _viewModel.TreeItems.First(x => x.SortIndex == parentSortIndex);
+
+ trackInfo.RemoveSong(songInfo);
+
+ if (trackInfo.Songs.Count <= 1)
+ {
+ var toRemove = _viewModel.TreeItems.Where(x => x.ParentIndex == parentSortIndex).ToList();
+ foreach (var itemToRemove in toRemove)
+ {
+ _viewModel.TreeItems.Remove(itemToRemove);
+ }
+
+ parentTreeData.SongInfo = trackInfo.Songs.Count == 1 ? trackInfo.Songs[0] : null;
+ parentTreeData.ChildTreeData.Clear();
+ parentTreeData.ToggleAsParent(false, false);
+ parentTreeData.UpdateCompletedFlag();
+ _viewModel.MsuSongViewModel.UpdateViewModel(_project, trackInfo, parentTreeData.SongInfo, treeData);
+ _viewModel.SelectedTreeItem = parentTreeData;
+ }
+ else
+ {
+ _viewModel.TreeItems.Remove(treeData);
+ parentTreeData.ChildTreeData.Remove(treeData);
+ parentTreeData.UpdateCompletedFlag();
+ _viewModel.MsuSongViewModel.UpdateViewModel(_project, trackInfo, parentTreeData.ChildTreeData.First().SongInfo, treeData);
+ _viewModel.SelectedTreeItem = parentTreeData.ChildTreeData.First();
+ }
+
+ logger.LogInformation("Removed song");
+ UpdateCompletedSummary();
+ _viewModel.LastModifiedDate = DateTime.Now;
+ }
+
+ public void UpdateDrag(MsuProjectWindowViewModelTreeData? treeData)
+ {
+ if (_hoveredItem != null)
+ {
+ _hoveredItem.GridBackground = Brushes.Transparent;
+ _hoveredItem.BorderColor = Brushes.Transparent;
+ }
+
+ if (treeData == null)
+ {
+ if (_draggedItem != null && _hoveredItem != null && _draggedItem != _hoveredItem && !(_draggedItem.ParentIndex > 0 && _hoveredItem.ParentIndex > 0 &&
+ _draggedItem.ParentIndex == _hoveredItem.ParentIndex &&
+ _draggedItem.SortIndex == _hoveredItem.SortIndex + 1))
+ {
+ HandleDragged(_draggedItem, _hoveredItem);
+ }
+
+ _hoveredItem = null;
+ _draggedItem = null;
+ _viewModel.IsDraggingItem = false;
+ }
+ else if (treeData.SongInfo != null)
+ {
+ _hoveredItem = null;
+ _draggedItem = treeData;
+ _viewModel.IsDraggingItem = true;
+ }
+ }
+
+ public void UpdateCompletedFlag(MsuProjectWindowViewModelTreeData treeData)
+ {
+ if (treeData.SongInfo == null)
+ {
+ return;
+ }
+
+ if (_viewModel.SelectedTreeItem == treeData && _viewModel.MsuSongViewModel.SongInfo == treeData.SongInfo)
+ {
+ _viewModel.MsuSongViewModel.IsComplete = !_viewModel.MsuSongViewModel.IsComplete;
+ }
+ else
+ {
+ treeData.SongInfo.IsComplete = !treeData.SongInfo.IsComplete;
+ treeData.UpdateCompletedFlag();
+ treeData.ParentTreeData?.UpdateCompletedFlag();
+ }
+
+ logger.LogInformation("Updated completed flag");
+ _viewModel.LastModifiedDate = DateTime.Now;
+ }
+
+ public void UpdateCheckCopyright(MsuProjectWindowViewModelTreeData treeData)
+ {
+ if (treeData.SongInfo == null)
+ {
+ return;
+ }
+
+ if (_viewModel.SelectedTreeItem == treeData && _viewModel.MsuSongViewModel.SongInfo == treeData.SongInfo)
+ {
+ if (_viewModel.MsuSongViewModel.BasicPanelViewModel.IsEnabled)
+ {
+ _viewModel.MsuSongViewModel.BasicPanelViewModel.CheckCopyright =
+ _viewModel.MsuSongViewModel.BasicPanelViewModel.CheckCopyright != true;
+ }
+ else if (_viewModel.MsuSongViewModel.AdvancedPanelViewModel.IsEnabled)
+ {
+ _viewModel.MsuSongViewModel.AdvancedPanelViewModel.CheckCopyright =
+ _viewModel.MsuSongViewModel.AdvancedPanelViewModel.CheckCopyright != true;
+ }
+ }
+ else
+ {
+ treeData.SongInfo.CheckCopyright = treeData.SongInfo.CheckCopyright != true;
+ treeData.UpdateCompletedFlag();
+ treeData.ParentTreeData?.UpdateCompletedFlag();
+ }
+
+ _viewModel.LastModifiedDate = DateTime.Now;
+ }
+
+ public void UpdateCopyrightSafe(MsuProjectWindowViewModelTreeData treeData)
+ {
+ if (treeData.SongInfo == null)
+ {
+ return;
+ }
+
+ if (_viewModel.SelectedTreeItem == treeData && _viewModel.MsuSongViewModel.SongInfo == treeData.SongInfo)
+ {
+ if (_viewModel.MsuSongViewModel.BasicPanelViewModel.IsEnabled)
+ {
+ _viewModel.MsuSongViewModel.BasicPanelViewModel.IsCopyrightSafe =
+ _viewModel.MsuSongViewModel.BasicPanelViewModel.IsCopyrightSafe switch
+ {
+ true => null,
+ false => true,
+ null => false
+ };
+ }
+ else if (_viewModel.MsuSongViewModel.AdvancedPanelViewModel.IsEnabled)
+ {
+ _viewModel.MsuSongViewModel.AdvancedPanelViewModel.IsCopyrightSafe =
+ _viewModel.MsuSongViewModel.AdvancedPanelViewModel.IsCopyrightSafe switch
+ {
+ true => null,
+ false => true,
+ null => false
+ };
+ }
+ }
+ else
+ {
+ if (treeData.SongInfo.IsCopyrightSafe == true)
+ {
+ treeData.SongInfo.IsCopyrightSafe = null;
+ }
+ else if (treeData.SongInfo.IsCopyrightSafe == false)
+ {
+ treeData.SongInfo.IsCopyrightSafe = true;
+ }
+ else
+ {
+ treeData.SongInfo.IsCopyrightSafe = false;
+ }
+
+ treeData.UpdateCompletedFlag();
+ treeData.ParentTreeData?.UpdateCompletedFlag();
+ }
+
+ _viewModel.LastModifiedDate = DateTime.Now;
+ }
+
+ public async Task CreateVideo(List songs, string videoPath, MessageWindow progressWindow, CancellationToken cancellationToken)
+ {
+ await ITaskService.Run(async () =>
+ {
+ await Parallel.ForEachAsync(songs,
+ new ParallelOptions { MaxDegreeOfParallelism = 10, CancellationToken = cancellationToken }, async (song, _) =>
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return;
+ }
+
+ try
+ {
+ await msuPcmService.CreatePcm(_viewModel.MsuProject!, song, false, true, true);
+ }
+ catch (Exception)
+ {
+ // Do nothing
+ }
+ });
+
+ var pcmFiles = songs.Select(x => x.OutputPath ?? "").Where(x => !string.IsNullOrEmpty(x) && File.Exists(x)).ToList();
+
+ var response = await pythonCompanionService.CreateVideoAsync(new CreateVideoRequest
+ {
+ Files = pcmFiles,
+ OutputVideo = videoPath
+ }, progress =>
+ {
+ Dispatcher.UIThread.Invoke(() =>
+ {
+ progressWindow.UpdateProgressBar(progress * 100);
+ });
+ }, cancellationToken);
+
+ Dispatcher.UIThread.Invoke(() =>
+ {
+ progressWindow.UpdateMessageText(response.Successful
+ ? "Video generation successful"
+ : "Error generating video");
+ progressWindow.UpdatePrimaryButtonText("Close");
+ });
+
+ settingsService.Settings.PreviousVideoPath = videoPath;
+ settingsService.TrySaveSettings();
+
+ }, cancellationToken);
+ }
+
+ private void HandleDragged(MsuProjectWindowViewModelTreeData from, MsuProjectWindowViewModelTreeData to)
+ {
+ if (_viewModel.MsuProject == null || from.TrackInfo == null || to.TrackInfo == null || from.SongInfo == null)
+ {
+ return;
+ }
+
+ var currentParent = from.ParentIndex != 0
+ ? _viewModel.TreeItems.First(x => x.SortIndex == from.ParentIndex)
+ : from;
+ var destinationParent = to.ParentIndex != 0
+ ? _viewModel.TreeItems.First(x => x.SortIndex == to.ParentIndex)
+ : to;
+
+ if (currentParent.TrackInfo == null || destinationParent.TrackInfo == null)
+ {
+ return;
+ }
+
+ var destinationTrack = to.TrackInfo;
+ int destinationIndex;
+ if (to.ParentIndex == 0)
+ {
+ destinationIndex = to.SongInfo == null ? 0 : 1;
+ }
+ else
+ {
+ destinationIndex = destinationParent.ChildTreeData.IndexOf(to) + 1;
+ }
+
+ logger.LogInformation("Dragged song {Name} to {Destination} #{Index}", from.SongInfo.SongName, to.TrackInfo.TrackName, destinationIndex);
+
+ if (currentParent == destinationParent && destinationIndex > destinationParent.ChildTreeData.IndexOf(from))
+ {
+ destinationIndex--;
+ }
+
+ destinationTrack.MoveSong(_viewModel.MsuProject, from.SongInfo, destinationIndex);
+
+ if (currentParent == destinationParent)
+ {
+ var dictionary = destinationParent.ChildTreeData.ToDictionary(x => x.SongInfo!, x => x);
+ destinationParent.ChildTreeData.Clear();
+ var destinationParentIndex = _viewModel.TreeItems.IndexOf(destinationParent);
+
+ for (var i = 0; i < destinationTrack.Songs.Count; i++)
+ {
+ var song = destinationTrack.Songs[i];
+ var songTreeData = dictionary[song];
+ destinationParent.ChildTreeData.Add(songTreeData);
+ _viewModel.TreeItems.Remove(songTreeData);
+ _viewModel.TreeItems.Insert(destinationParentIndex + i + 1, songTreeData);
+ songTreeData.SortIndex = destinationParent.SortIndex + i + 1;
+ }
+
+ _viewModel.MsuSongViewModel.UpdateViewModel(_project, destinationTrack, from.SongInfo, from);
+ _viewModel.SelectedTreeItem = from;
+ }
+ else
+ {
+ var newTreeData = from;
+ var fromSongInfo = from.SongInfo;
+
+ foreach (var previousTree in currentParent.ChildTreeData.Concat(destinationParent.ChildTreeData))
+ {
+ _viewModel.TreeItems.Remove(previousTree);
+ }
+
+ currentParent.ChildTreeData.Clear();
+ destinationParent.ChildTreeData.Clear();
+
+ if (currentParent.TrackInfo.Songs.Count <= 1)
+ {
+ currentParent.SongInfo = currentParent.TrackInfo.Songs.FirstOrDefault();
+ currentParent.UpdateCompletedFlag();
+ currentParent.ToggleAsParent(false, false);
+ }
+ else
+ {
+ for (var i = 0; i < currentParent.TrackInfo.Songs.Count; i++)
+ {
+ var song = currentParent.TrackInfo.Songs[i];
+
+ var songTreeData = new MsuProjectWindowViewModelTreeData
+ {
+ Name = string.IsNullOrEmpty(song.SongName) ? $"Song {i + 1}" : song.SongName,
+ CollapseIcon = MaterialIconKind.MusicNote,
+ CollapseIconOpacity = 0.4,
+ LeftSpacing = 12,
+ SortIndex = currentParent.SortIndex + 1 + i,
+ ParentIndex = currentParent.SortIndex,
+ ParentTreeData = currentParent,
+ ShowCheckbox = true,
+ TrackInfo = currentParent.TrackInfo,
+ SongInfo = song,
+ };
+
+ songTreeData.UpdateCompletedFlag();
+ songTreeData.DisplayIsCompleteIcon = _viewModel.DisplayIsCompleteIcon;
+ songTreeData.DisplayHasSongIcon = _viewModel.DisplayHasSongIcon;
+ songTreeData.DisplayCopyrightSafeIcon = _viewModel.DisplayCopyrightSafeIcon;
+ songTreeData.DisplayCheckCopyrightIcon = _viewModel.DisplayCheckCopyrightIcon;
+
+ currentParent.ChildTreeData.Add(songTreeData);
+ _viewModel.TreeItems.Insert(_viewModel.TreeItems.IndexOf(currentParent) + i + 1, songTreeData);
+ }
+
+ currentParent.SongInfo = null;
+ currentParent.UpdateCompletedFlag();
+ currentParent.ToggleAsParent(true, false);
+ }
+
+ if (destinationParent.TrackInfo.Songs.Count <= 1)
+ {
+ destinationParent.SongInfo = destinationParent.TrackInfo.Songs.FirstOrDefault();
+ destinationParent.UpdateCompletedFlag();
+ destinationParent.ToggleAsParent(false, false);
+ newTreeData = destinationParent;
+ }
+ else
+ {
+ for (var i = 0; i < destinationParent.TrackInfo.Songs.Count; i++)
+ {
+ var song = destinationParent.TrackInfo.Songs[i];
+
+ var songTreeData = new MsuProjectWindowViewModelTreeData
+ {
+ Name = string.IsNullOrEmpty(song.SongName) ? $"Song {i + 1}" : song.SongName,
+ CollapseIcon = MaterialIconKind.MusicNote,
+ CollapseIconOpacity = 0.4,
+ LeftSpacing = 12,
+ SortIndex = destinationParent.SortIndex + 1 + i,
+ ParentIndex = destinationParent.SortIndex,
+ ParentTreeData = destinationParent,
+ ShowCheckbox = true,
+ TrackInfo = destinationParent.TrackInfo,
+ SongInfo = song,
+ };
+
+ if (song == fromSongInfo)
+ {
+ newTreeData = songTreeData;
+ }
+
+ songTreeData.UpdateCompletedFlag();
+ songTreeData.DisplayIsCompleteIcon = _viewModel.DisplayIsCompleteIcon;
+ songTreeData.DisplayHasSongIcon = _viewModel.DisplayHasSongIcon;
+ songTreeData.DisplayCopyrightSafeIcon = _viewModel.DisplayCopyrightSafeIcon;
+ songTreeData.DisplayCheckCopyrightIcon = _viewModel.DisplayCheckCopyrightIcon;
+
+ destinationParent.ChildTreeData.Add(songTreeData);
+ _viewModel.TreeItems.Insert(_viewModel.TreeItems.IndexOf(destinationParent) + i + 1, songTreeData);
+ }
+
+ destinationParent.SongInfo = null;
+ destinationParent.UpdateCompletedFlag();
+ destinationParent.ToggleAsParent(true, false);
+ }
+
+ _viewModel.MsuSongViewModel.UpdateViewModel(_project, destinationTrack, fromSongInfo, newTreeData);
+ _viewModel.SelectedTreeItem = newTreeData;
+ }
+
+ if (_viewModel.SelectedTreeItem.SongInfo != null)
+ {
+ _viewModel.LastModifiedDate = DateTime.Now;
+ }
+
+ logger.LogInformation("HandleDragged complete");
+ }
+
+ public void UpdateHover(MsuProjectWindowViewModelTreeData? treeData)
+ {
+ if (_hoveredItem != null)
+ {
+ _hoveredItem.GridBackground = Brushes.Transparent;
+ _hoveredItem.BorderColor = Brushes.Transparent;
+ }
+
+ _hoveredItem = treeData;
+
+ if (treeData != null)
+ {
+ treeData.BorderColor = MsuProjectWindowViewModelTreeData.HighlightColor;
+ }
+ }
+
+ public void ToggleCompletedIcons(bool? value = null)
+ {
+ bool newValue;
+ if (value == null)
+ {
+ newValue = _viewModel.DisplayIsCompleteIcon = !_viewModel.DisplayIsCompleteIcon;
+ }
+ else
+ {
+ newValue = _viewModel.DisplayIsCompleteIcon = value.Value;
+ }
+
+ foreach (var treeData in _viewModel.TreeItems)
+ {
+ if (treeData.IsSongOrTrack)
+ {
+ treeData.DisplayIsCompleteIcon = newValue;
+ }
+ }
+
+ settingsService.Settings.ProjectTreeDisplayIsCompleteIcon = _viewModel.DisplayIsCompleteIcon;
+ settingsService.TrySaveSettings();
+ }
+
+ public void ToggleHasAudioIcons(bool? value = null)
+ {
+ bool newValue;
+ if (value == null)
+ {
+ newValue = _viewModel.DisplayHasSongIcon = !_viewModel.DisplayHasSongIcon;
+ }
+ else
+ {
+ newValue = _viewModel.DisplayHasSongIcon = value.Value;
+ }
+
+ foreach (var treeData in _viewModel.TreeItems)
+ {
+ if (treeData.IsSongOrTrack)
+ {
+ treeData.DisplayHasSongIcon = newValue;
+ }
+ }
+
+ settingsService.Settings.ProjectTreeDisplayHasSongIcon = _viewModel.DisplayHasSongIcon;
+ settingsService.TrySaveSettings();
+ }
+
+ public void ToggleCheckCopyrightIcons(bool? value = null)
+ {
+ bool newValue;
+ if (value == null)
+ {
+ newValue = _viewModel.DisplayCheckCopyrightIcon = !_viewModel.DisplayCheckCopyrightIcon;
+ }
+ else
+ {
+ newValue = _viewModel.DisplayCheckCopyrightIcon = value.Value;
+ }
+
+ foreach (var treeData in _viewModel.TreeItems)
+ {
+ if (treeData.IsSongOrTrack)
+ {
+ treeData.DisplayCheckCopyrightIcon = newValue;
+ }
+ }
+
+ settingsService.Settings.ProjectTreeDisplayCheckCopyrightIcon = _viewModel.DisplayCheckCopyrightIcon;
+ settingsService.TrySaveSettings();
+ }
+
+ public void ToggleCopyrightStatusIcons(bool? value = null)
+ {
+ bool newValue;
+ if (value == null)
+ {
+ newValue = _viewModel.DisplayCopyrightSafeIcon = !_viewModel.DisplayCopyrightSafeIcon;
+ }
+ else
+ {
+ newValue = _viewModel.DisplayCopyrightSafeIcon = value.Value;
+ }
+
+ foreach (var treeData in _viewModel.TreeItems)
+ {
+ if (treeData.IsSongOrTrack)
+ {
+ treeData.DisplayCopyrightSafeIcon = newValue;
+ }
+ }
+
+ settingsService.Settings.ProjectTreeDisplayCopyrightSafeIcon = _viewModel.DisplayCopyrightSafeIcon;
+ settingsService.TrySaveSettings();
+ }
+
+ public string? GetSongCopyDetails(MsuProjectWindowViewModelTreeData treeData)
+ {
+
+ MsuSongInfo output = new();
+ if (treeData.SongInfo == null || !converterService.CloneModel(treeData.SongInfo, output) || !converterService.CloneModel(treeData.SongInfo.MsuPcmInfo, output.MsuPcmInfo))
+ {
+ return null;
+ }
+
+ output.TrackNumber = 0;
+ output.TrackName = null;
+ output.OutputPath = null;
+ output.LastModifiedDate = new DateTime();
+ output.IsComplete = false;
+ output.MsuPcmInfo.ClearFieldsForYaml();
+ output.IsAlt = false;
+ var yamlText = yamlService.ToYaml(output, YamlType.PascalIgnoreDefaults);
+
+ return
+ """
+ # yaml-language-server: $schema=https://raw.githubusercontent.com/MattEqualsCoder/MSUScripter/main/Schemas/MsuSongInfo.json
+ # Use Visual Studio Code with the YAML plugin from redhat for schema support (make sure the language is set to YAML)
+
+ """ + yamlText;
+ }
+
+ public string? PasteSongDetails(MsuProjectWindowViewModelTreeData treeData, string yaml)
+ {
+ if (yamlService.FromYaml(yaml, YamlType.PascalIgnoreDefaults, out var songInfo, out var error) && songInfo != null)
+ {
+ var originalSongInfo = treeData.SongInfo;
+ treeData.SongInfo = songInfo;
+
+ if (originalSongInfo != null)
+ {
+ songInfo.Id = originalSongInfo.Id;
+ songInfo.OutputPath = originalSongInfo.OutputPath;
+ songInfo.MsuPcmInfo.Output = originalSongInfo.MsuPcmInfo.Output;
+ songInfo.TrackNumber = originalSongInfo.TrackNumber;
+ songInfo.TrackName = originalSongInfo.TrackName;
+
+ var index = treeData.TrackInfo!.Songs.IndexOf(originalSongInfo);
+ treeData.TrackInfo.Songs[index] = songInfo;
+ }
+ else
+ {
+ songInfo.Id = Guid.NewGuid().ToString();
+ songInfo.TrackName = treeData.TrackInfo!.TrackName;
+ songInfo.TrackNumber = treeData.TrackInfo!.TrackNumber;
+ treeData.TrackInfo.Songs.Add(songInfo);
+ }
+
+ if (treeData.ParentTreeData != null && !string.IsNullOrEmpty(treeData.SongInfo?.SongName))
+ {
+ treeData.Name = treeData.SongInfo.SongName;
+ }
+
+ UpdateCompletedSummary();
+
+ return null;
+ }
+
+ return error ?? "Unable to copy data";
+ }
+
+ public void InputFileUpdated()
+ {
+ _viewModel.CurrentTreeItem?.UpdateCompletedFlag();
+ }
+
+ public void OnClose()
+ {
+ statusBarService.StatusBarTextUpdated -= StatusBarServiceOnStatusBarTextUpdated;
+ msuPcmService.GeneratingPcm -= MsuPcmServiceOnGeneratingPcm;
+ _ = audioPlayerService.StopSongAsync();
+ }
+
+ public void DragDropFile(string filePath)
+ {
+ if (_viewModel.CurrentTreeItem?.TrackInfo == null || _viewModel.CurrentTreeItem?.SongInfo == null)
+ {
+ return;
+ }
+
+ logger.LogInformation("Dragged {File} to {Track}", filePath, _viewModel.MsuSongViewModel.TrackInfo!.TrackNumber);
+
+ if (_viewModel.MsuSongViewModel.BasicPanelViewModel.IsEnabled)
+ {
+ _viewModel.MsuSongViewModel.BasicPanelViewModel.DragDropFile(filePath);
+ }
+ else if (_viewModel.MsuSongViewModel.AdvancedPanelViewModel is { IsEnabled: true, CurrentTreeItem.MsuPcmInfo: not null })
+ {
+ _viewModel.MsuSongViewModel.AdvancedPanelViewModel.DragDropFile(filePath);
+ }
+ }
+
+ public bool CanCreateVideos()
+ {
+ return pythonCompanionService.IsValid;
+ }
+
+ public void LogError(Exception ex, string message)
+ {
+ logger.LogError(ex, "{Message}", message);
+ }
+}
\ No newline at end of file
diff --git a/MSUScripter/Services/ControlServices/MsuSongInfoPanelService.cs b/MSUScripter/Services/ControlServices/MsuSongInfoPanelService.cs
deleted file mode 100644
index fbc83b1..0000000
--- a/MSUScripter/Services/ControlServices/MsuSongInfoPanelService.cs
+++ /dev/null
@@ -1,179 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-using AvaloniaControls.ControlServices;
-using AvaloniaControls.Services;
-using MSUScripter.Configs;
-using MSUScripter.Models;
-using MSUScripter.ViewModels;
-
-namespace MSUScripter.Services.ControlServices;
-
-public class MsuSongInfoPanelService(SharedPcmService sharedPcmService, Settings settings, AudioMetadataService audioMetadataService, ConverterService converterService, YamlService yamlService, AudioAnalysisService audioAnalysisService) : ControlService
-{
- private MsuSongInfoViewModel _model = new();
-
- public void InitializeModel(MsuSongInfoViewModel model)
- {
- _model = model;
- _model.Track = _model.Project.Tracks.First(x => x.TrackNumber == model.TrackNumber);
- _model.CanPlaySongs = sharedPcmService.CanPlaySongs;
- _model.PauseStopIcon = sharedPcmService.CanPauseSongs ? Material.Icons.MaterialIconKind.Pause : Material.Icons.MaterialIconKind.Stop;
- _model.PauseStopText = sharedPcmService.CanPauseSongs ? "Pause Music" : "Stop Music";
- }
-
- public async Task PlaySong(bool testLoop)
- {
- return await sharedPcmService.PlaySong(_model, testLoop);
- }
-
- public void DeleteSong()
- {
- _model.Track.Songs.Remove(_model);
- _model.Track.FixTrackSuffixes(_model.CanPlaySongs);
- }
-
- public async Task PauseSong()
- {
- await sharedPcmService.PauseSong();
- }
-
- public async Task StopSong()
- {
- await sharedPcmService.StopSong();
- }
-
- public string? GetOpenMusicFilePath()
- {
- if (!string.IsNullOrEmpty(_model.MsuPcmInfo.File) && File.Exists(_model.MsuPcmInfo.File))
- {
- var file = new FileInfo(_model.MsuPcmInfo.File);
- if (file.Directory?.Exists == true)
- {
- return file.Directory.FullName;
- }
- }
- else if (!string.IsNullOrEmpty(settings.PreviousPath) && Directory.Exists(settings.PreviousPath))
- {
- return settings.PreviousPath;
- }
-
- return null;
- }
-
- public void ImportAudioMetadata(string file)
- {
- if (string.IsNullOrEmpty(file) || !File.Exists(file))
- {
- return;
- }
-
- var metadata = audioMetadataService.GetAudioMetadata(file);
- _model.ApplyAudioMetadata(metadata, true);
- }
-
- public string? GetCopyDetailsString()
- {
- MsuSongInfo output = new();
- if (!converterService.ConvertViewModel(_model, output) || !converterService.ConvertViewModel(_model.MsuPcmInfo, output.MsuPcmInfo))
- {
- return null;
- }
-
- output.TrackNumber = 0;
- output.TrackName = null;
- output.OutputPath = null;
- output.LastGeneratedDate = new DateTime();
- output.LastModifiedDate = new DateTime();
- output.IsComplete = false;
- output.ShowPanel = false;
- output.MsuPcmInfo.ClearFieldsForYaml();
- var yamlText = yamlService.ToYaml(output, YamlType.PascalIgnoreDefaults);
-
- return
- """
- # yaml-language-server: $schema=https://raw.githubusercontent.com/MattEqualsCoder/MSUScripter/main/Schemas/MsuSongInfo.json
- # Use Visual Studio Code with the YAML plugin from redhat for schema support (make sure the language is set to YAML)
-
- """ + yamlText;
- }
-
- public string? CopyDetailsFromString(string yamlText)
- {
- if (!yamlService.FromYaml(yamlText, YamlType.PascalIgnoreDefaults, out var yamlSongDetails, out _) || yamlSongDetails == null)
- {
- return "Invalid song details";
- }
-
- var originalProject = _model.Project;
- var originalTrack = _model.Track;
- var originalTrackName = _model.TrackName;
- var originalTrackNumber = _model.TrackNumber;
- var originalIsAlt = _model.IsAlt;
- var originalCanPlaySongs = _model.CanPlaySongs;
- var originalOutputPath = _model.OutputPath;
- _model.MsuPcmInfo.SubChannels.Clear();
- _model.MsuPcmInfo.SubTracks.Clear();
-
- if (!converterService.ConvertViewModel(yamlSongDetails, _model) || !converterService.ConvertViewModel(yamlSongDetails.MsuPcmInfo, _model.MsuPcmInfo))
- {
- return "Invalid song details";
- }
-
- _model.OutputPath = originalOutputPath;
-
- _model.ApplyCascadingSettings(originalProject, originalTrack, originalIsAlt, originalCanPlaySongs, true, true);
- return null;
- }
-
- public void IgnoreMsuPcmError()
- {
- if (string.IsNullOrEmpty(_model.OutputPath)) return;
- _model.Project.IgnoreWarnings.Add(_model.OutputPath);
- }
-
- public Task GeneratePcmFile(bool asPrimary, bool asEmpty)
- {
- return sharedPcmService.GeneratePcmFile(_model, asPrimary, asEmpty);
- }
-
- public void AnalyzeAudio()
- {
- _model.AverageAudio = "Running";
- _model.PeakAudio = null;
-
- ITaskService.Run(async () =>
- {
- await PauseSong();
-
- var generateResponse = await GeneratePcmFile(false, false);
- if (!generateResponse.Successful)
- {
- _model.AverageAudio = "Error generating PCM";
- _model.PeakAudio = null;
- }
-
- if (!string.IsNullOrEmpty(_model.OutputPath))
- {
- var output = await audioAnalysisService.AnalyzeAudio(_model.OutputPath);
-
- if (output is { AvgDecibels: not null, MaxDecibels: not null })
- {
- _model.AverageAudio = $"Average: {Math.Round(output.AvgDecibels.Value, 2)}db";
- _model.PeakAudio = $"Peak: {Math.Round(output.MaxDecibels.Value, 2)}db";
- }
- else
- {
- _model.AverageAudio = "Error analyzing audio";
- _model.PeakAudio = null;
- }
- }
- else
- {
- _model.AverageAudio = "Error generating PCM";
- _model.PeakAudio = null;
- }
- });
- }
-}
\ No newline at end of file
diff --git a/MSUScripter/Services/ControlServices/MsuSongMsuPcmInfoPanelService.cs b/MSUScripter/Services/ControlServices/MsuSongMsuPcmInfoPanelService.cs
deleted file mode 100644
index 3c02b33..0000000
--- a/MSUScripter/Services/ControlServices/MsuSongMsuPcmInfoPanelService.cs
+++ /dev/null
@@ -1,282 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using AvaloniaControls.ControlServices;
-using MSUScripter.Configs;
-using MSUScripter.ViewModels;
-
-namespace MSUScripter.Services.ControlServices;
-
-public class MsuSongMsuPcmInfoPanelService(
- Settings settings,
- SettingsService settingsService,
- IAudioPlayerService audioPlayerService,
- ConverterService converterService,
- AudioAnalysisService audioAnalysisService,
- AudioMetadataService audioMetadataService,
- YamlService yamlService) : ControlService
-{
- private MsuSongMsuPcmInfoViewModel _model = new();
-
- public void InitializeModel(MsuSongMsuPcmInfoViewModel model)
- {
- _model = model;
- _model.CanPlaySongs = audioPlayerService.CanPlayMusic;
- }
-
- public void AddSubTrack(int? index = null, bool addToParent = false)
- {
- if (addToParent)
- {
- if (index is null or -1)
- {
- _model.ParentMsuPcmInfo?.SubTracks.Add(new MsuSongMsuPcmInfoViewModel
- { Project = _model.Project, Song = _model.Song, IsAlt = _model.IsAlt, ParentMsuPcmInfo = _model.ParentMsuPcmInfo });
- }
- else
- {
- _model.ParentMsuPcmInfo?.SubTracks.Insert(index.Value, new MsuSongMsuPcmInfoViewModel
- { Project = _model.Project, Song = _model.Song, IsAlt = _model.IsAlt, ParentMsuPcmInfo = _model.ParentMsuPcmInfo });
- }
- }
- else
- {
- if (index is null or -1)
- {
- _model.SubTracks.Add(new MsuSongMsuPcmInfoViewModel
- { Project = _model.Project, Song = _model.Song, IsAlt = _model.IsAlt, ParentMsuPcmInfo = _model });
- }
- else
- {
- _model.SubTracks.Insert(index.Value, new MsuSongMsuPcmInfoViewModel
- { Project = _model.Project, Song = _model.Song, IsAlt = _model.IsAlt, ParentMsuPcmInfo = _model });
- }
- }
-
- var topLevelPcmInfo = _model.TopLevel;
- topLevelPcmInfo.UpdateSubTrackSubChannelWarning();
- }
-
- public void AddSubChannel(int? index = null, bool addToParent = false)
- {
- if (addToParent)
- {
- if (index is null or -1)
- {
- _model.ParentMsuPcmInfo?.SubChannels.Add(new MsuSongMsuPcmInfoViewModel
- { Project = _model.Project, Song = _model.Song, IsAlt = _model.IsAlt, ParentMsuPcmInfo = _model.ParentMsuPcmInfo });
- }
- else
- {
- _model.ParentMsuPcmInfo?.SubChannels.Insert(index.Value, new MsuSongMsuPcmInfoViewModel
- { Project = _model.Project, Song = _model.Song, IsAlt = _model.IsAlt, ParentMsuPcmInfo = _model.ParentMsuPcmInfo });
- }
- }
- else
- {
- if (index is null or -1)
- {
- _model.SubChannels.Add(new MsuSongMsuPcmInfoViewModel
- { Project = _model.Project, Song = _model.Song, IsAlt = _model.IsAlt, ParentMsuPcmInfo = _model });
- }
- else
- {
- _model.SubChannels.Insert(index.Value, new MsuSongMsuPcmInfoViewModel
- { Project = _model.Project, Song = _model.Song, IsAlt = _model.IsAlt, ParentMsuPcmInfo = _model });
- }
- }
-
- var topLevelPcmInfo = _model.TopLevel;
- topLevelPcmInfo.UpdateSubTrackSubChannelWarning();
- }
-
- public void Delete()
- {
- if (_model.ParentMsuPcmInfo == null)
- {
- return;
- }
-
- if (_model.IsSubChannel)
- {
- _model.ParentMsuPcmInfo.SubChannels.Remove(_model);
- }
- else if (_model.IsSubTrack)
- {
- _model.ParentMsuPcmInfo.SubTracks.Remove(_model);
- }
-
- var topLevelPcmInfo = _model.TopLevel;
- topLevelPcmInfo.UpdateSubTrackSubChannelWarning();
- }
-
- public bool ShouldShowSubTracksSubChannelsWarningPopup(bool newSubTrack, bool newSubChannel)
- {
- var numSubTracks = _model.SubTracks.Count + (newSubTrack ? 1 : 0);
- var numSubChannels = _model.SubChannels.Count + (newSubChannel ? 1 : 0);
- return !settings.HideSubTracksSubChannelsWarning && numSubTracks > 0 && numSubChannels > 0;
- }
-
- public void HideSubTracksSubChannelsWarning()
- {
- settings.HideSubTracksSubChannelsWarning = true;
- settingsService.SaveSettings();
- }
-
- public string? GetCopyDetailsString()
- {
- MsuSongMsuPcmInfo output = new();
- if (!converterService.ConvertViewModel(_model, output))
- {
- return null;
- }
- output.ClearFieldsForYaml();
- var yamlText = yamlService.ToYaml(output, YamlType.PascalIgnoreDefaults);
-
- return
- """
- # yaml-language-server: $schema=https://raw.githubusercontent.com/MattEqualsCoder/MSUScripter/main/Schemas/MsuSongMsuPcmInfo.json
- # Use Visual Studio Code with the YAML plugin from redhat for schema support (make sure the language is set to YAML)
-
- """ + yamlText;
- }
-
- public string? CopyDetailsFromString(string yamlText)
- {
- if (!yamlService.FromYaml(yamlText, YamlType.PascalIgnoreDefaults, out var yamlMsuPcmDetails, out _) || yamlMsuPcmDetails == null)
- {
- return "Invalid msupcm++ track details";
- }
-
- var originalProject = _model.Project;
- var originalSong = _model.Song;
- var originalIsAlt = _model.IsAlt;
- var originalParent = _model.ParentMsuPcmInfo;
- var originalCanPlaySongs = _model.CanPlaySongs;
-
- _model.SubTracks.Clear();
- _model.SubChannels.Clear();
-
- if (!converterService.ConvertViewModel(yamlMsuPcmDetails, _model))
- {
- return "Invalid msupcm++ track details";
- }
-
- _model.ApplyCascadingSettings(originalProject, originalSong, originalIsAlt, originalParent, originalCanPlaySongs, true, true);
- return null;
- }
-
- public void UpdateLoopSettings(PyMusicLooperResultViewModel loopResult)
- {
- _model.Loop = loopResult.LoopStart;
- _model.TrimEnd = loopResult.LoopEnd;
- }
-
- public void UpdateContextMenuOptions()
- {
- if (_model.IsTopLevel)
- {
- _model.CanMoveUp = false;
- _model.CanMoveDown = false;
- return;
- }
-
- var parentObjectList = _model.IsSubChannel
- ? _model.ParentMsuPcmInfo!.SubChannels.ToList()
- : _model.ParentMsuPcmInfo!.SubTracks.ToList();
-
- _model.CanMoveUp = parentObjectList.IndexOf(_model) > 0;
- _model.CanMoveDown = parentObjectList.IndexOf(_model) < parentObjectList.Count - 1;
- }
-
- public void MoveUp()
- {
- if (!_model.CanMoveUp) return;
-
- var parentObjectList = _model.IsSubChannel
- ? _model.ParentMsuPcmInfo!.SubChannels
- : _model.ParentMsuPcmInfo!.SubTracks;
-
- var currentIndex = parentObjectList.IndexOf(_model);
- parentObjectList.Remove(_model);
- parentObjectList.Insert(currentIndex - 1, _model);
- }
-
- public void MoveDown()
- {
- if (!_model.CanMoveDown) return;
-
- var parentObjectList = _model.IsSubChannel
- ? _model.ParentMsuPcmInfo!.SubChannels
- : _model.ParentMsuPcmInfo!.SubTracks;
-
- var currentIndex = parentObjectList.IndexOf(_model);
- parentObjectList.Remove(_model);
- parentObjectList.Insert(currentIndex + 1, _model);
- }
-
- public bool HasLoopDetails()
- {
- return _model.Loop > 0 || _model.TrimEnd > 0;
- }
-
- public string? GetStartingSamples()
- {
- var file = _model.GetEffectiveFile();
- if (string.IsNullOrEmpty(file) || !File.Exists(file))
- {
- return "No input file selected";
- }
-
- try
- {
- var samples = audioAnalysisService.GetAudioStartingSample(file);
- _model.TrimStart = samples;
- return null;
- }
- catch
- {
- return "Unable to get starting samples for file";
- }
- }
-
- public string? GetEndingSamples()
- {
- var file = _model.GetEffectiveFile();
- if (string.IsNullOrEmpty(file) || !File.Exists(file))
- {
- return "No input file selected";
- }
-
- try
- {
- var samples = audioAnalysisService.GetAudioEndingSample(file);
- _model.TrimEnd = samples;
- return null;
- }
- catch
- {
- return "Unable to get ending samples for file";
- }
- }
-
- public void ImportAudioMetadata()
- {
- var topLevelPcmInfo = _model.TopLevel;
-
- _model.UpdatePyMusicLooperButtonStatus();
-
- if (string.IsNullOrEmpty(_model.File) || !File.Exists(_model.File))
- {
- topLevelPcmInfo.UpdateHertzWarning(audioAnalysisService.GetAudioSampleRate(_model.File));
- topLevelPcmInfo.UpdateMultiWarning();
- return;
- }
-
- var metadata = audioMetadataService.GetAudioMetadata(_model.File);
- _model.Song.ApplyAudioMetadata(metadata, false);
-
- topLevelPcmInfo.UpdateHertzWarning(audioAnalysisService.GetAudioSampleRate(_model.File));
- topLevelPcmInfo.UpdateMultiWarning();
- }
-}
\ No newline at end of file
diff --git a/MSUScripter/Services/ControlServices/MsuSongPanelService.cs b/MSUScripter/Services/ControlServices/MsuSongPanelService.cs
new file mode 100644
index 0000000..4642b4f
--- /dev/null
+++ b/MSUScripter/Services/ControlServices/MsuSongPanelService.cs
@@ -0,0 +1,293 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using AvaloniaControls.ControlServices;
+using AvaloniaControls.Services;
+using Microsoft.Extensions.Logging;
+using MSUScripter.Configs;
+using MSUScripter.Models;
+using MSUScripter.ViewModels;
+
+namespace MSUScripter.Services.ControlServices;
+
+// ReSharper disable once ClassNeverInstantiated.Global
+public class MsuSongPanelService(
+ ConverterService converterService,
+ YamlService yamlService,
+ AudioMetadataService audioMetadataService,
+ MsuPcmService msuPcmService,
+ IAudioPlayerService audioPlayerService,
+ AudioAnalysisService audioAnalysisService,
+ PythonCompanionService pythonCompanionService,
+ ILogger logger) : ControlService
+{
+ public string? GetMsuPcmInfoCopyText(MsuSongMsuPcmInfo msuPcmInfo)
+ {
+ MsuSongMsuPcmInfo output = new();
+ if (!converterService.CloneModel(msuPcmInfo, output))
+ {
+ return null;
+ }
+
+ Sanitize(output);
+
+ var yamlText = yamlService.ToYaml(output, YamlType.PascalIgnoreDefaults);
+
+ return
+ """
+ # yaml-language-server: $schema=https://raw.githubusercontent.com/MattEqualsCoder/MSUScripter/main/Schemas/MsuSongMsuPcmInfo.json
+ # Use Visual Studio Code with the YAML plugin from redhat for schema support (make sure the language is set to YAML)
+
+ """ + yamlText;
+
+ void Sanitize(MsuSongMsuPcmInfo subInfo)
+ {
+ subInfo.ShowPanel = false;
+ subInfo.LastModifiedDate = DateTime.MinValue;
+ subInfo.Output = null;
+ foreach (var info in subInfo.SubChannels.Concat(subInfo.SubTracks))
+ {
+ Sanitize(info);
+ }
+ }
+ }
+
+ public MsuSongMsuPcmInfo? GetMsuPcmInfoFromText(string yaml)
+ {
+ yamlService.FromYaml(yaml, YamlType.PascalIgnoreDefaults, out var data, out _);
+ return data;
+ }
+
+ public MsuSongMsuPcmInfo? DuplicateMsuPcmInfo(MsuSongMsuPcmInfo info)
+ {
+ MsuSongMsuPcmInfo output = new();
+ return !converterService.CloneModel(info, output) ? null : output;
+ }
+
+ public AudioMetadata GetAudioMetadata(string filename)
+ {
+ return audioMetadataService.GetAudioMetadata(filename);
+ }
+
+ public void CheckSampleRate(MsuSongBasicPanelViewModel model)
+ {
+ var path = model.InputFilePath;
+ if (string.IsNullOrEmpty(path) || !File.Exists(path) || model.Project == null)
+ {
+ model.SetSampleRate(44100);
+ return;
+ }
+
+ var savedSampleRate = GetSavedSampleInfo(model.Project, path);
+ if (savedSampleRate != null)
+ {
+ model.SetSampleRate(savedSampleRate.Value);
+ return;
+ }
+
+ ITaskService.Run(async () =>
+ {
+ model.SetSampleRate(44100);
+ var sampleRate = await GetSampleRateAsync(model.Project, path);
+ if (path == model.InputFilePath)
+ {
+ model.SetSampleRate(sampleRate);
+ }
+ });
+ }
+
+ public void CheckFileErrors(MsuSongAdvancedPanelViewModel model)
+ {
+ var project = model.Project;
+ var song = model.CurrentSongInfo;
+
+ var hasBothSubTracksAndSubChannels = false;
+ var hasSoloSubChannel = false;
+ var hasSameParentChildType = false;
+ var hasIgnoredFile = false;
+
+ List files = [];
+ foreach (var treeItem in model.TreeItems.Where(x => x.MsuPcmInfo != null))
+ {
+ if (!string.IsNullOrEmpty(treeItem.MsuPcmInfo!.File) && File.Exists(treeItem.MsuPcmInfo!.File))
+ {
+ files.Add(treeItem.MsuPcmInfo.File);
+ }
+
+ var subTracks = treeItem.ChildrenTreeData.FirstOrDefault();
+ var subChannels = treeItem.ChildrenTreeData.LastOrDefault();
+
+ if (subChannels != null && subTracks != null && subTracks != subChannels &&
+ subTracks.ChildrenTreeData.Count > 0 && subChannels.ChildrenTreeData.Count > 0)
+ {
+ hasBothSubTracksAndSubChannels = true;
+
+ if (subChannels.ChildrenTreeData.Count == 1)
+ {
+ hasSoloSubChannel = true;
+ }
+ }
+
+ if (treeItem is { IsSubChannel: true, ParentTreeData.ParentTreeData.IsSubChannel: true } or { IsSubTrack: true, ParentTreeData.ParentTreeData.IsSubTrack: true })
+ {
+ hasSameParentChildType = true;
+ }
+
+ if (!string.IsNullOrEmpty(treeItem.MsuPcmInfo.File) &&
+ (subTracks?.ChildrenTreeData.Count > 0 || subChannels?.ChildrenTreeData.Count > 0))
+ {
+ hasIgnoredFile = true;
+ }
+ }
+
+ if (files.Count == 0)
+ {
+ model.UpdateTrackWarnings(false, false, false, false, false, false);
+ return;
+ }
+
+ files = files.Distinct().ToList();
+
+ if (files.Count > 1)
+ {
+ model.UpdateTrackWarnings(false, true, hasBothSubTracksAndSubChannels, hasSoloSubChannel, hasSameParentChildType, hasIgnoredFile);
+ return;
+ }
+
+ ITaskService.Run(async() =>
+ {
+ var path = files.First();
+ var sampleRate = GetSavedSampleInfo(project, path) ?? await GetSampleRateAsync(project, path);
+ if (model.CurrentSongInfo == song)
+ {
+ model.UpdateTrackWarnings(sampleRate != 44100, false, hasBothSubTracksAndSubChannels, hasSoloSubChannel, hasSameParentChildType, hasIgnoredFile);
+ }
+ });
+ }
+
+ private int? GetSavedSampleInfo(MsuProject project, string path)
+ {
+ if (project.SampleRates.TryGetValue(path, out var sampleRate))
+ {
+ var fileInfo = new FileInfo(path);
+ if (fileInfo.Length == sampleRate.FileLength)
+ {
+ return sampleRate.SampleRate;
+ }
+ }
+
+ return null;
+ }
+
+ private async Task GetSampleRateAsync(MsuProject project, string path)
+ {
+ var response = await audioAnalysisService.GetAudioSampleRateAsync(path);
+ var fileInfo = new FileInfo(path);
+ if (response.Successful)
+ {
+ project.SampleRates[path] = new FileSampleInfo
+ {
+ FileLength = fileInfo.Length,
+ SampleRate = response.SampleRate
+ };
+ }
+ return response.SampleRate;
+ }
+
+ public async Task PlaySong(MsuProject project, MsuSongInfo song, bool testLoop)
+ {
+ GeneratePcmFileResponse? generateResponse = null;
+
+ if (song.HasAudioFiles())
+ {
+ generateResponse = await GeneratePcm(project, song, false, false);
+ if (!generateResponse.GeneratedPcmFile)
+ {
+ return generateResponse;
+ }
+
+ if (string.IsNullOrEmpty(song.OutputPath) || !File.Exists(song.OutputPath))
+ {
+ return generateResponse;
+ }
+ }
+ else if (!File.Exists(song.OutputPath))
+ {
+ return new GeneratePcmFileResponse(false, false, "PCM file not found", song.OutputPath);
+ }
+
+ var msuTypeTrackInfo = project.MsuType.Tracks.FirstOrDefault(x => x.Number == song.TrackNumber);
+
+ if (await audioPlayerService.PlaySongAsync(song.OutputPath, testLoop, msuTypeTrackInfo?.NonLooping != true))
+ {
+ return new GeneratePcmFileResponse(generateResponse?.Successful ?? true, generateResponse?.GeneratedPcmFile ?? false, generateResponse?.Message ?? string.Empty, song.OutputPath);
+ }
+
+ return new GeneratePcmFileResponse(false, false, "Error playing PCM file", song.OutputPath);
+ }
+
+ public async Task GeneratePcm(MsuProject project, MsuSongInfo song, bool asPrimary, bool asEmpty)
+ {
+ await audioPlayerService.StopSongAsync(song.OutputPath);
+ if (asEmpty)
+ {
+ return msuPcmService.CreateEmptyPcm(song);
+ }
+ return await msuPcmService.CreatePcm(project, song, asPrimary, false, true);
+ }
+
+ public MsuPcmJsonInfo GenerateSongTracksFile(MsuProject project, MsuSongInfo song, string path)
+ {
+ return msuPcmService.ExportMsuPcmTracksJson(project, song, path, song.OutputPath);
+ }
+
+ public async Task AnalyzeAudio(MsuProject project, MsuSongInfo song)
+ {
+ if (string.IsNullOrEmpty(song.OutputPath))
+ {
+ return null;
+ }
+
+ await audioPlayerService.StopSongAsync(null, true);
+ var response = await msuPcmService.CreatePcm(project, song, false, false, true);
+ if (!response.Successful)
+ {
+ return null;
+ }
+
+ var output = await audioAnalysisService.AnalyzeAudio(song.OutputPath);
+ return output;
+ }
+
+ public int DetectStartingSamples(string file)
+ {
+ return audioAnalysisService.GetAudioStartingSample(file);
+ }
+
+ public int DetectEndingSamples(string file)
+ {
+ return audioAnalysisService.GetAudioEndingSample(file);
+ }
+
+ public void LogError(Exception ex, string message)
+ {
+ logger.LogError(ex, "{Message}", message);
+ }
+
+ public bool CanGenerateMsuPcmFiles()
+ {
+ return msuPcmService.IsValid;
+ }
+
+ public bool CanGenerateVideos()
+ {
+ return pythonCompanionService.IsValid;
+ }
+
+ public bool CanRunPyMusicLooper()
+ {
+ return pythonCompanionService.IsValid;
+ }
+}
\ No newline at end of file
diff --git a/MSUScripter/Services/ControlServices/MsuTrackInfoPanelService.cs b/MSUScripter/Services/ControlServices/MsuTrackInfoPanelService.cs
deleted file mode 100644
index 1bb07fe..0000000
--- a/MSUScripter/Services/ControlServices/MsuTrackInfoPanelService.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System.IO;
-using AvaloniaControls.ControlServices;
-using MSUScripter.ViewModels;
-
-namespace MSUScripter.Services.ControlServices;
-
-public class MsuTrackInfoPanelService : ControlService
-{
- private MsuTrackInfoViewModel _model = new();
-
- public void InitializeModel(MsuTrackInfoViewModel model)
- {
- _model = model;
- }
-
- public void AddSong()
- {
- var songInfo = new MsuSongInfoViewModel();
-
- var isAlt = _model.Songs.Count > 0;
-
- var msu = new FileInfo(_model.Project.MsuPath);
- if (!isAlt)
- {
- songInfo.OutputPath = msu.FullName.Replace(msu.Extension, $"-{_model.TrackNumber}.pcm");
- }
- else
- {
- var altSuffix = _model.Songs.Count == 1 ? "alt" : $"alt{_model.Songs.Count}";
- songInfo.OutputPath = msu.FullName.Replace(msu.Extension, $"-{_model.TrackNumber}_{altSuffix}.pcm");
- }
-
- songInfo.ApplyCascadingSettings(_model.Project, _model, isAlt, true, true, true);
- _model.Songs.Add(songInfo);
- }
-}
\ No newline at end of file
diff --git a/MSUScripter/Services/ControlServices/NewProjectPanelService.cs b/MSUScripter/Services/ControlServices/NewProjectPanelService.cs
deleted file mode 100644
index 053c2b4..0000000
--- a/MSUScripter/Services/ControlServices/NewProjectPanelService.cs
+++ /dev/null
@@ -1,120 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using AvaloniaControls.Controls;
-using AvaloniaControls.ControlServices;
-using AvaloniaControls.Models;
-using Microsoft.Extensions.Logging;
-using MSURandomizerLibrary.Services;
-using MSUScripter.Configs;
-using MSUScripter.ViewModels;
-using MSUScripter.Views;
-
-namespace MSUScripter.Services.ControlServices;
-
-public class NewProjectPanelService(IMsuTypeService msuTypeService, ProjectService projectService, Settings settings, ILogger logger) : ControlService
-{
- private NewProjectPanelViewModel _model = new();
-
- public NewProjectPanelViewModel InitializeModel()
- {
- _model.MsuTypes = msuTypeService.MsuTypes
- .OrderBy(x => x.DisplayName)
- .ToList();
- return _model;
- }
-
- public void ResetModel()
- {
- _model.SelectedMsuType = null;
- _model.MsuPath = "";
- _model.MsuPcmTracksJsonPath = "";
- _model.MsuPcmWorkingDirectoryPath = "";
- _model.RecentProjects = settings.RecentProjects.Where(x => File.Exists(x.ProjectPath)).OrderByDescending(x => x.Time).ToList();
- }
-
- public bool CreateNewProject(string path, out MsuProject? newProject, out bool isLegacySmz3, out string? error)
- {
- if (string.IsNullOrEmpty(path) || _model.SelectedMsuType == null ||
- string.IsNullOrEmpty(_model.MsuPath))
- {
- newProject = null;
- isLegacySmz3 = false;
- error = "Missing data. Please enter the project path, msu path, and MSU type.";
- return false;
- }
-
- try
- {
- newProject = projectService.NewMsuProject(path, _model.SelectedMsuType, _model.MsuPath, _model.MsuPcmTracksJsonPath, _model.MsuPcmWorkingDirectoryPath);
- isLegacySmz3 = newProject.MsuType == msuTypeService.GetSMZ3LegacyMSUType() &&
- msuTypeService.GetSMZ3MsuType() != null;
- error = null;
- return true;
- }
- catch (Exception exception)
- {
- logger.LogError(exception, "Unable to create new MSU Scripter project");
- newProject = null;
- isLegacySmz3 = false;
- error = exception.Message;
- return false;
- }
- }
-
- public bool UpdateLegacySmz3Msu(MsuProject project)
- {
- try
- {
- projectService.ConvertProjectMsuType(project, msuTypeService.GetSMZ3MsuType()!, true);
- SaveProject(project);
- return true;
- }
- catch (Exception e)
- {
- logger.LogError(e, "Unable to update legacy SMZ3 MSU");
- return false;
- }
- }
-
- public bool LoadProject(string path, out MsuProject? project, out MsuProject? backupProject, out string? error)
- {
- try
- {
- project = projectService.LoadMsuProject(path, false);
- backupProject = null;
- error = null;
-
- if (project == null)
- {
- error = "MSU Project file not found";
- backupProject = null;
- return false;
- }
-
- if (!string.IsNullOrEmpty(project.BackupFilePath))
- {
- var potentialBackupProject = projectService.LoadMsuProject(project.BackupFilePath, true);
- if (potentialBackupProject != null && potentialBackupProject.LastSaveTime > project.LastSaveTime)
- {
- backupProject = potentialBackupProject;
- }
- }
-
- return true;
- }
- catch (Exception e)
- {
- logger.LogError(e, "Error opening project");
- project = null;
- backupProject = null;
- error = "Error opening project. Please contact MattEqualsCoder or post an issue on GitHub";
- return false;
- }
- }
-
- public void SaveProject(MsuProject project)
- {
- projectService.SaveMsuProject(project, false);
- }
-}
\ No newline at end of file
diff --git a/MSUScripter/Services/ControlServices/PackageMsuWindowService.cs b/MSUScripter/Services/ControlServices/PackageMsuWindowService.cs
deleted file mode 100644
index d9f24b5..0000000
--- a/MSUScripter/Services/ControlServices/PackageMsuWindowService.cs
+++ /dev/null
@@ -1,143 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.IO.Compression;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using Avalonia.Platform.Storage;
-using AvaloniaControls.ControlServices;
-using AvaloniaControls.Services;
-using MSUScripter.ViewModels;
-
-namespace MSUScripter.Services.ControlServices;
-
-public class PackageMsuWindowService : ControlService
-{
- private readonly PackageMsuWindowViewModel _model = new();
-
- private readonly HashSet _extensions =
- [
- ".txt",
- ".pcm",
- ".msu",
- ".bat",
- ".yml"
- ];
-
- private readonly CancellationTokenSource _cts = new();
-
- public PackageMsuWindowViewModel InitializeModel(MsuProjectViewModel project)
- {
- _model.Project = project;
- _model.ValidPcmPaths = project.Tracks.Where(x => !x.IsScratchPad).SelectMany(x => x.Songs)
- .Select(x => x.OutputPath).Where(x => !string.IsNullOrEmpty(x)).Cast().ToList();
- return _model;
- }
-
- public void PackageProject(string zipPath)
- {
- ITaskService.Run(() => PackageProjectTask(zipPath));
- }
-
- public void PackageProjectTask(string zipPath)
- {
- _model.IsRunning = true;
-
- var sb = new StringBuilder();
- sb.AppendLine($"Creating zip file {zipPath}");
- _model.Response = sb.ToString();
-
- try
- {
- if (File.Exists(zipPath))
- {
- File.Delete(zipPath);
- }
-
- using var zip = ZipFile.Open(zipPath, ZipArchiveMode.Create);
-
- foreach (var file in Directory.EnumerateFiles(MsuDirectory, "*.*"))
- {
- if (_cts.Token.IsCancellationRequested)
- {
- break;
- }
-
- if (!_extensions.Contains(Path.GetExtension(file)))
- {
- continue;
- }
-
- if (string.Equals(Path.GetExtension(file), ".pcm", StringComparison.OrdinalIgnoreCase) &&
- !_model.ValidPcmPaths.Contains(file))
- {
- continue;
- }
-
- sb.AppendLine($"... adding {file}");
- _model.Response = sb.ToString();
-
- try
- {
- zip.CreateEntryFromFile(file, Path.GetFileName(file));
- }
- catch (Exception e2)
- {
- sb.AppendLine($"Could not add {file} to zip file: {e2.Message}");
- _model.Response = sb.ToString();
- _model.ButtonText = "Close";
- return;
- }
- }
-
- }
- catch (Exception e)
- {
- sb.AppendLine($"Could not create zip file: {e.Message}");
- _model.Response = sb.ToString();
- _model.ButtonText = "Close";
- return;
- }
-
- if (_cts.Token.IsCancellationRequested)
- {
- try
- {
- if (File.Exists(zipPath))
- {
- File.Delete(zipPath);
- }
- }
- catch
- {
- // Do nothing
- }
- }
- else
- {
- _model.IsRunning = false;
- sb.AppendLine("Complete!");
- _model.Response = sb.ToString();
- _model.ButtonText = "Close";
- }
- }
-
- public void Cancel()
- {
- if (_model.IsRunning)
- {
- _cts.Cancel();
- }
- }
-
- public string MsuDirectory
- {
- get
- {
- var msuFileInfo = new FileInfo(_model.Project.MsuPath);
- return msuFileInfo.DirectoryName!;
- }
- }
-}
\ No newline at end of file
diff --git a/MSUScripter/Services/ControlServices/PyMusicLooperPanelService.cs b/MSUScripter/Services/ControlServices/PyMusicLooperPanelService.cs
index 0daec01..7b67c37 100644
--- a/MSUScripter/Services/ControlServices/PyMusicLooperPanelService.cs
+++ b/MSUScripter/Services/ControlServices/PyMusicLooperPanelService.cs
@@ -12,9 +12,19 @@
namespace MSUScripter.Services.ControlServices;
+public class PyMusicLooperDetails
+{
+ public required MsuProject Project { get; init; }
+ public required string? FilePath { get; init; }
+ public required int? FilterStart { get; init; }
+ public double? Normalization { get; init; }
+ public bool AllowRunByDefault { get; init; }
+ public bool ForceRun { get; init; }
+}
+
+// ReSharper disable once ClassNeverInstantiated.Global
public class PyMusicLooperPanelService(
- PyMusicLooperService pyMusicLooperService,
- ConverterService converterService,
+ PythonCompanionService pythonCompanionService,
MsuPcmService msuPcmService,
IAudioPlayerService audioPlayerService,
SettingsService settingsService) : ControlService
@@ -23,6 +33,7 @@ public class PyMusicLooperPanelService(
private CancellationTokenSource? _cts;
private Settings Settings => settingsService.Settings;
public event EventHandler? OnUpdated;
+ public event EventHandler? RunningUpdated;
public PyMusicLooperPanelViewModel InitializeModel()
{
@@ -33,7 +44,8 @@ public PyMusicLooperPanelViewModel InitializeModel()
_ = ITaskService.Run(() =>
{
- RunMsuPcm(false);
+ var cts = _cts = new CancellationTokenSource();
+ RunMsuPcm(false, cts);
});
};
@@ -41,18 +53,30 @@ public PyMusicLooperPanelViewModel InitializeModel()
return _model;
}
- public void UpdateModel(MsuProjectViewModel msuProjectViewModel, MsuSongInfoViewModel msuSongInfoViewModel, MsuSongMsuPcmInfoViewModel msuSongMsuPcmInfoViewModel)
+ public void UpdateDetails(PyMusicLooperDetails details)
{
- _model.MsuProjectViewModel = msuProjectViewModel;
- _model.MsuProject = converterService.ConvertProject(_model.MsuProjectViewModel);
- _model.MsuSongInfoViewModel = msuSongInfoViewModel;
- _model.MsuSongMsuPcmInfoViewModel = msuSongMsuPcmInfoViewModel;
- _model.FilterStart = msuSongMsuPcmInfoViewModel.TrimStart;
+ _model.AutoRun = Settings.AutomaticallyRunPyMusicLooper;
+ _model.FilePath = details.FilePath;
+ _model.FilterStart = details.FilterStart;
+ _model.MsuProject = details.Project;
+ _model.Normalization = details.Normalization;
- if (Settings.AutomaticallyRunPyMusicLooper)
+ if (string.IsNullOrEmpty(_model.FilePath))
+ {
+ _model.Message = "No file selected. Please select a file and click run.";
+ _model.CanRun = false;
+ _model.DisplayAutoRun = true;
+ }
+ else if (details.ForceRun || (Settings.AutomaticallyRunPyMusicLooper && details.AllowRunByDefault))
{
RunPyMusicLooper();
}
+ else
+ {
+ _model.Message = "Click run to execute PyMusicLooper.";
+ _model.CanRun = true;
+ _model.DisplayAutoRun = true;
+ }
}
public void UpdateFilterStart(int? filterStart)
@@ -74,8 +98,8 @@ public async Task ChangePage(int mod)
_ = ITaskService.Run(() =>
{
- RunMsuPcm();
- _model.Message = null;
+ var cts = _cts = new CancellationTokenSource();
+ RunMsuPcm(true, cts);
});
}
@@ -103,7 +127,7 @@ await ITaskService.Run(async () =>
if (playSong && !string.IsNullOrEmpty(songPath))
{
- _ = audioPlayerService.PlaySongAsync(songPath, true);
+ _ = audioPlayerService.PlaySongAsync(songPath, true, true);
}
else
{
@@ -142,41 +166,40 @@ private void FilterResults()
_model.Page = 0;
_model.LastPage = _model.FilteredResults.Count / _model.NumPerPage;
}
-
- public void TestPyMusicLooper()
+
+ private void TestPyMusicLooper()
{
- if (!pyMusicLooperService.TestService(out string message))
+ if (pythonCompanionService.IsValid)
{
- _model.Message = message;
- _model.DisplayGitHubLink = true;
- OnUpdated?.Invoke(this, new PyMusicLooperPanelUpdatedArgs(null));
return;
}
-
- if (!pyMusicLooperService.CanReturnMultipleResults)
- {
- _model.DisplayOldVersionWarning = true;
- }
+
+ _model.Message = "Companion PyMsuScripterApp is not detected";
+ _model.DisplayGitHubLink = true;
+ OnUpdated?.Invoke(this, new PyMusicLooperPanelUpdatedArgs(null));
}
public void RunPyMusicLooper()
{
+ _model.DisplayAutoRun = false;
+ RunningUpdated?.Invoke(this, true);
+
if (!_model.HasTestedPyMusicLooper)
{
TestPyMusicLooper();
if (_model.DisplayGitHubLink)
{
+ RunningUpdated?.Invoke(this, false);
return;
}
- else if (!_model.DisplayOldVersionWarning)
- {
- _model.HasTestedPyMusicLooper = true;
- }
+
+ _model.HasTestedPyMusicLooper = true;
}
- if (string.IsNullOrEmpty(_model.MsuSongMsuPcmInfoViewModel.GetEffectiveFile()))
+ if (string.IsNullOrEmpty(_model.FilePath))
{
+ RunningUpdated?.Invoke(this, false);
return;
}
@@ -184,70 +207,106 @@ public void RunPyMusicLooper()
{
_model.Message = "Both approximate loop start and end times must be filled out";
OnUpdated?.Invoke(this, new PyMusicLooperPanelUpdatedArgs(null));
+ RunningUpdated?.Invoke(this, false);
return;
}
- _cts = new CancellationTokenSource();
+ var cts = _cts = new CancellationTokenSource();
- ITaskService.Run(() =>
+ ITaskService.Run(async() =>
{
_model.IsRunning = true;
+ _model.CanRun = false;
_model.Message = "Running PyMusicLooper";
- var inputFile = _model.MsuSongMsuPcmInfoViewModel.GetEffectiveFile()!;
- var loopPoints = pyMusicLooperService.GetLoopPoints(inputFile, out string message,
- _model.MinDurationMultiplier,
- _model.MinLoopDuration, _model.MaxLoopDuration,
- _model.ApproximateStart, _model.ApproximateEnd,
- _cts.Token);
-
- if (_cts?.IsCancellationRequested == true)
+ var inputFile = _model.FilePath;
+
+ var response = await pythonCompanionService.RunPyMusicLooperAsync(new RunPyMusicLooperRequest()
+ {
+ File = inputFile,
+ MinDurationMultiplier = _model.MinDurationMultiplier,
+ MinLoopDuration = _model.MinLoopDuration,
+ MaxLoopDuration = _model.MaxLoopDuration,
+ ApproxLoopStart = _model.ApproximateStart,
+ ApproxLoopEnd = _model.ApproximateEnd,
+ }, cts.Token);
+
+ if (cts.IsCancellationRequested)
{
_model.Message = "PyMusicLooper canceled";
_model.IsRunning = false;
+ _model.CanRun = true;
OnUpdated?.Invoke(this, new PyMusicLooperPanelUpdatedArgs(null));
+ RunningUpdated?.Invoke(this, false);
return;
}
- else if (loopPoints?.Any() == true)
+ else if (response is { Successful: true, Pairs.Count: > 0 })
{
_model.PyMusicLooperResults =
- loopPoints.Select(x => new PyMusicLooperResultViewModel(x.LoopStart, x.LoopEnd, x.Score)).ToList();
+ response.Pairs.Select(x => new PyMusicLooperResultViewModel(x.LoopStart, x.LoopEnd, x.Score)).ToList();
FilterResults();
- _model.SelectedResult = _model.FilteredResults.First();
- _model.SelectedResult.IsSelected = true;
- _model.Message = "Generating Preview Files";
- RunMsuPcm();
- if (_cts?.IsCancellationRequested == true)
+
+ if (_model.FilteredResults.Count > 0)
{
- _model.Message = "PyMusicLooper canceled";
- _model.IsRunning = false;
- OnUpdated?.Invoke(this, new PyMusicLooperPanelUpdatedArgs(_model.SelectedResult));
- return;
+ _model.SelectedResult = _model.FilteredResults.First();
+ _model.SelectedResult.IsSelected = true;
+ _model.Message = "Generating Preview Files";
+ RunMsuPcm(true, cts);
+ if (cts.IsCancellationRequested)
+ {
+ _model.Message = "PyMusicLooper canceled";
+ _model.IsRunning = false;
+ _model.CanRun = true;
+ OnUpdated?.Invoke(this, new PyMusicLooperPanelUpdatedArgs(_model.SelectedResult));
+ RunningUpdated?.Invoke(this, false);
+ return;
+ }
+ else
+ {
+ _model.Message = null;
+ _model.IsRunning = false;
+ _model.CanRun = true;
+ OnUpdated?.Invoke(this, new PyMusicLooperPanelUpdatedArgs(_model.SelectedResult));
+ RunningUpdated?.Invoke(this, false);
+ return;
+ }
}
else
{
- _model.Message = null;
+ _model.Message = "No matching results found";
_model.IsRunning = false;
- OnUpdated?.Invoke(this, new PyMusicLooperPanelUpdatedArgs(_model.SelectedResult));
+ _model.CanRun = true;
+ OnUpdated?.Invoke(this, new PyMusicLooperPanelUpdatedArgs(null));
+ RunningUpdated?.Invoke(this, false);
return;
}
}
_model.IsRunning = false;
+ _model.CanRun = true;
OnUpdated?.Invoke(this, new PyMusicLooperPanelUpdatedArgs(_model.SelectedResult));
- _model.Message = message;
- }, _cts.Token);
+ RunningUpdated?.Invoke(this, false);
+ _model.Message = response.Error;
+ }, cts.Token);
}
-
+
public void StopPyMusicLooper()
{
_cts?.Cancel();
}
- private void RunMsuPcm(bool fullReload = true)
+ public void SaveAutoRun(bool? value)
{
- if (_model.CurrentPageResults.All(x => x.Generated))
+ Settings.AutomaticallyRunPyMusicLooper = value ?? false;
+ settingsService.SaveSettings();
+ }
+
+ private void RunMsuPcm(bool fullReload, CancellationTokenSource cts)
+ {
+ if (_model.CurrentPageResults.All(x => x.Generated && File.Exists(x.TempPath)))
{
+ _model.Message = null;
+ _model.GeneratingPcms = false;
return;
}
@@ -262,36 +321,47 @@ private void RunMsuPcm(bool fullReload = true)
{
async void GenerateTempPcm(PyMusicLooperResultViewModel result)
{
- result.Status = "Generating Preview .pcm File";
+ try
+ {
+ result.Status = "Generating Preview .pcm File";
- var response = await CreateTempPcm(result, true);
+ var response = await CreateTempPcm(result, true);
- if (!response.Successful)
- {
- if (response.GeneratedPcmFile)
+ if (!response.Successful)
{
- result.Status = $"Generated with message: {response.Message}";
- result.TempPath = response.OutputPath ?? throw new InvalidOperationException("GeneratePcmFileResponse for generated PCM missing output path");
- GetLoopDuration(result);
- result.Generated = true;
+ if (response.GeneratedPcmFile)
+ {
+ result.Status = $"Generated with message: {response.Message}";
+ result.TempPath = response.OutputPath ??
+ throw new InvalidOperationException(
+ "GeneratePcmFileResponse for generated PCM missing output path");
+ GetLoopDuration(result);
+ result.Generated = true;
+ }
+ else
+ {
+ result.Status = $"Error: {response.Message}";
+ }
}
else
{
- result.Status = $"Error: {response.Message}";
+ result.Status = "Generated";
+ result.TempPath = response.OutputPath ??
+ throw new InvalidOperationException(
+ "GeneratePcmFileResponse for generated PCM missing output path");
+ GetLoopDuration(result);
+ result.Generated = true;
}
}
- else
+ catch (Exception ex)
{
- result.Status = "Generated";
- result.TempPath = response.OutputPath ?? throw new InvalidOperationException("GeneratePcmFileResponse for generated PCM missing output path");
- GetLoopDuration(result);
- result.Generated = true;
+ result.Status = $"Error: {ex.Message}";
}
}
- Parallel.ForEach(_model.CurrentPageResults.Where(x => !x.Generated), new ParallelOptions()
+ Parallel.ForEach(_model.CurrentPageResults.Where(x => !x.Generated || !File.Exists(x.TempPath)), new ParallelOptions()
{
- CancellationToken = _cts?.Token ?? CancellationToken.None
+ CancellationToken = cts.Token
},
GenerateTempPcm);
}
@@ -299,15 +369,30 @@ async void GenerateTempPcm(PyMusicLooperResultViewModel result)
{
// Do nothing
}
-
+
+ if (_cts != cts || cts.IsCancellationRequested)
+ {
+ _model.PyMusicLooperResults = [];
+ _model.FilteredResults = [];
+ _model.Message = "Click run to execute PyMusicLooper.";
+ _model.CanRun = true;
+ _model.DisplayAutoRun = true;
+ }
+ else
+ {
+ _model.Message = null;
+ }
_model.GeneratingPcms = false;
}
private async Task CreateTempPcm(PyMusicLooperResultViewModel result, bool skipCleanup)
{
- var normalization = _model.MsuSongMsuPcmInfoViewModel.Normalization ??
- _model.MsuProjectViewModel.BasicInfo.Normalization;
- return await msuPcmService.CreateTempPcm(false, _model.MsuProject, _model.MsuSongMsuPcmInfoViewModel.GetEffectiveFile()!, result.LoopStart, result.LoopEnd, normalization, skipCleanup: skipCleanup);
+ if (string.IsNullOrEmpty(_model.FilePath))
+ {
+ throw new InvalidOperationException("Attempted to create a temp PCM without a file path");
+ }
+ var normalization = _model.Normalization ?? _model.MsuProject.BasicInfo.Normalization;
+ return await msuPcmService.CreateTempPcm(_model.MsuProject, _model.FilePath, result.LoopStart, result.LoopEnd, normalization, skipCleanup: skipCleanup);
}
private void GetLoopDuration(PyMusicLooperResultViewModel song)
diff --git a/MSUScripter/Services/ControlServices/SettingsWindowService.cs b/MSUScripter/Services/ControlServices/SettingsWindowService.cs
index f469039..4072d20 100644
--- a/MSUScripter/Services/ControlServices/SettingsWindowService.cs
+++ b/MSUScripter/Services/ControlServices/SettingsWindowService.cs
@@ -1,36 +1,22 @@
-using System;
-using Avalonia;
-using Avalonia.Styling;
using AvaloniaControls.ControlServices;
using MSUScripter.ViewModels;
namespace MSUScripter.Services.ControlServices;
-public class SettingsWindowService(SettingsService settingsService, ConverterService converterService, MsuPcmService msuPcmService, PyMusicLooperService pyMusicLooperService) : ControlService
+// ReSharper disable once ClassNeverInstantiated.Global
+public class SettingsWindowService(SettingsService settingsService) : ControlService
{
- private SettingsWindowViewModel _model = new();
+ private readonly SettingsWindowViewModel _model = new();
public SettingsWindowViewModel InitializeModel()
{
- converterService.ConvertViewModel(settingsService.Settings, _model);
- _model.CanSetPyMusicLooperPath = OperatingSystem.IsWindows();
+ _model.SettingsPanelViewModel.LoadSettings(settingsService.Settings);
return _model;
}
public void SaveSettings()
{
- converterService.ConvertViewModel(_model, settingsService.Settings);
+ _model.SettingsPanelViewModel.SaveChanges();
settingsService.SaveSettings();
- Application.Current!.RequestedThemeVariant = settingsService.Settings.DarkTheme ? ThemeVariant.Dark : ThemeVariant.Light;
- }
-
- public bool ValidateMsuPcm()
- {
- return msuPcmService.ValidateMsuPcmPath(_model.MsuPcmPath!, out _);
- }
-
- public bool ValidatePyMusicLooper()
- {
- return pyMusicLooperService.TestService(out _, _model.PyMusicLooperPath);
}
}
\ No newline at end of file
diff --git a/MSUScripter/Services/ControlServices/TrackOverviewPanelService.cs b/MSUScripter/Services/ControlServices/TrackOverviewPanelService.cs
deleted file mode 100644
index fe45c8c..0000000
--- a/MSUScripter/Services/ControlServices/TrackOverviewPanelService.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
-using AvaloniaControls.ControlServices;
-using MSUScripter.ViewModels;
-
-namespace MSUScripter.Services.ControlServices;
-
-public class TrackOverviewPanelService : ControlService
-{
- private readonly TrackOverviewPanelViewModel _model = new();
-
- public TrackOverviewPanelViewModel InitializeModel(EditProjectPanelViewModel editProjectPanelViewModel)
- {
- _model.MsuProjectViewModel = editProjectPanelViewModel.MsuProjectViewModel ?? new MsuProjectViewModel();
- RefreshTracks();
- return _model;
- }
-
- public void RefreshTracks()
- {
- var tracks = _model.MsuProjectViewModel.Tracks;
-
- _model.Rows.Clear();
-
- foreach (var track in tracks.Where(x => !x.IsScratchPad).OrderBy(x => x.TrackNumber))
- {
- if (!track.Songs.Any())
- {
- _model.Rows.Add(new TrackOverviewPanelViewModel.TrackOverviewRow(track.TrackNumber, track.TrackName));
- }
- else
- {
- foreach (var song in track.Songs)
- {
- _model.Rows.Add(new TrackOverviewPanelViewModel.TrackOverviewRow(track.TrackNumber,
- track.TrackName + (song.IsAlt ? " (Alt)" : ""), song));
- }
-
- }
- }
-
- UpdateCompletedTrackDetails();
- }
-
- public void UpdateCompletedTrackDetails()
- {
- _model.UpdateCompletedTrackDetails();
- }
-
- public MsuProjectViewModel GetProject()
- {
- return _model.MsuProjectViewModel;
- }
-
- internal void AddSong(TrackOverviewPanelViewModel.TrackOverviewRow row, MsuSongInfoViewModel newSongInfo)
- {
- if (row.HasSong)
- {
- var oldRowIndex = _model.Rows.IndexOf(row);
- _model.Rows.Insert(oldRowIndex + 1, new TrackOverviewPanelViewModel.TrackOverviewRow(row.TrackNumber, row.TrackName, newSongInfo));
- }
- else
- {
- row.SongInfo = newSongInfo;
- }
- }
-}
\ No newline at end of file
diff --git a/MSUScripter/Services/ControlServices/VideoCreatorWindowService.cs b/MSUScripter/Services/ControlServices/VideoCreatorWindowService.cs
deleted file mode 100644
index b7e41da..0000000
--- a/MSUScripter/Services/ControlServices/VideoCreatorWindowService.cs
+++ /dev/null
@@ -1,177 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Management;
-using System.Runtime.Versioning;
-using System.Text.RegularExpressions;
-using AvaloniaControls.ControlServices;
-using AvaloniaControls.Services;
-using Microsoft.Extensions.Logging;
-using MSUScripter.Configs;
-using MSUScripter.Models;
-using MSUScripter.ViewModels;
-
-namespace MSUScripter.Services.ControlServices;
-
-public class VideoCreatorWindowService(ILogger logger, PythonCommandRunnerService python, YamlService yamlService, Settings settings) : ControlService
-{
- private Process? _process;
- private const string MinVersion = "0.2.0";
- private static readonly Regex DigitsOnly = new(@"[^\d.]");
- private readonly VideoCreatorWindowViewModel _model = new();
-
- public VideoCreatorWindowViewModel InitializeModel(MsuProjectViewModel project)
- {
- _model.PreviousPath = settings.PreviousPath;
-
- _model.PcmPaths = project.Tracks.Where(x => !x.IsScratchPad).SelectMany(x => x.Songs)
- .Where(x => x.CheckCopyright == true && File.Exists(x.OutputPath)).Select(x => x.OutputPath).ToList();
-
- if (_model.PcmPaths.Count == 0)
- {
- _model.DisplayText = "No songs are set to be added to the copyright test";
- return _model;
- }
-
- if (python.SetBaseCommand("msu_test_video_creator", "--version", out var result, out var error) &&
- result.StartsWith("msu_test_video_creator "))
- {
- logger.LogInformation("{Version} found", result);
- var version = DigitsOnly.Replace(result, "").Split(".").Select(int.Parse).ToList();
- var currentVersionNumber = ConvertVersionNumber(version[0], version[1], version[2]);
- var minVersion = DigitsOnly.Replace(MinVersion, "").Split(".").Select(int.Parse).ToList();
- var minVersionNumber = ConvertVersionNumber(minVersion[0], minVersion[1], minVersion[2]);
-
- _model.CanRunVideoCreator = currentVersionNumber >= minVersionNumber;
-
- if (!_model.CanRunVideoCreator)
- {
- _model.DisplayText = $"msu_test_video_creator is out of date. Please upgrade to version {MinVersion}";
- _model.DisplayGitHubLink = true;
- logger.LogWarning("{Warning}", _model.DisplayText);
- }
- }
- else
- {
- _model.DisplayText = "Unable to run msu_test_video_creator. Make sure you can run it via command line.";
- _model.DisplayGitHubLink = true;
- logger.LogWarning("Unable to run msu_test_video_creator: {Error}", error);
- }
-
- return _model;
- }
-
- public bool IsRunning { get; private set; }
-
- public bool CanCreateVideo => _model.CanRunVideoCreator;
-
- public void CreateVideo(string outputPath)
- {
- if (!_model.CanRunVideoCreator) return;
-
- logger.LogInformation("Creating test video");
- _model.DisplayText = "Creating video (this could take a while)";
-
- var pcmFilesData = new Dictionary>()
- {
- { "Files", _model.PcmPaths }
- };
-
- var yaml = yamlService.ToYaml(pcmFilesData, YamlType.Pascal);
- var path = Path.Combine(Directories.TempFolder, "video-creator-list.yml");
- var directory = new FileInfo(path).DirectoryName;
- if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
- {
- Directory.CreateDirectory(directory);
- }
- File.WriteAllText(path, yaml);
-
- _process = python.RunCommandAsync($"-i \"{path}\" -o \"{outputPath}\"", false);
-
- if (_process == null)
- {
- _model.DisplayText = "Error calling msu_test_video_creator. Make sure you can call it manually via console.";
- _model.DisplayGitHubLink = true;
- logger.LogError("Error in VideoCreatorService: {Message}", _model.DisplayText);
- return;
- }
-
- ITaskService.Run(() =>
- {
- IsRunning = true;
- _process.WaitForExit();
- IsRunning = false;
- var code = _process.ExitCode;
- if (code != 0)
- {
- logger.LogError("Error {Code} calling msu_test_video_creator", code);
- _model.DisplayText =
- "Error calling msu_test_video_creator. Make sure you can call it manually via console.";
- }
- else
- {
- _model.DisplayText = "Video generation successful!";
- _model.CloseButtonText = "Close";
- }
- });
- }
-
- public void Cancel()
- {
- if (_process == null)
- {
- return;
- }
-
- logger.LogInformation("Video creation cancellation requested");
-
- if (OperatingSystem.IsWindows())
- {
- KillProcessWindows(_process.Id);
- }
- else
- {
- try
- {
- _process?.Kill();
- }
- catch (Exception e)
- {
- logger.LogError(e, "Unable to kill Video Creator");
- }
- }
- }
-
- [SupportedOSPlatform("windows")]
- private static void KillProcessWindows(int pid)
- {
- var processSearch = new ManagementObjectSearcher($"Select * From Win32_Process Where ParentProcessID={pid}");
- var processCollection = processSearch.Get();
-
- if (processCollection.Count > 0)
- {
- foreach (var managementObject in processCollection)
- {
- KillProcessWindows(Convert.ToInt32(managementObject["ProcessID"]));
- }
- }
-
- // Then kill parents.
- try
- {
- Process proc = Process.GetProcessById(pid);
- if (!proc.HasExited) proc.Kill();
- }
- catch (ArgumentException)
- {
- // Process already exited.
- }
- }
-
- private int ConvertVersionNumber(int a, int b, int c)
- {
- return a * 10000 + b * 100 + c;
- }
-}
\ No newline at end of file
diff --git a/MSUScripter/Services/ConverterService.cs b/MSUScripter/Services/ConverterService.cs
index 5215848..9dba65b 100644
--- a/MSUScripter/Services/ConverterService.cs
+++ b/MSUScripter/Services/ConverterService.cs
@@ -5,25 +5,23 @@
using System.Linq;
using System.Reflection;
using MSURandomizerLibrary.Configs;
-using MSURandomizerLibrary.Services;
using MSUScripter.Configs;
using MSUScripter.Models;
-using MSUScripter.ViewModels;
using Track = MSUScripter.Configs.Track;
namespace MSUScripter.Services;
-public class ConverterService(IMsuTypeService msuTypeService)
+public class ConverterService
{
- public bool ConvertViewModel(A input, B output, bool recursive = true) where B : new()
+ public bool CloneModel(TA input, TA output, bool recursive = true) where TA : new()
{
- var propertiesA = typeof(A).GetProperties().Where(x => x.CanWrite && x.PropertyType.Namespace?.Contains("MSU") != true && x.GetCustomAttribute() == null).ToDictionary(x => x.Name, x => x);
- var propertiesB = typeof(B).GetProperties().Where(x => x.CanWrite && x.PropertyType.Namespace?.Contains("MSU") != true && x.GetCustomAttribute() == null).ToDictionary(x => x.Name, x => x);
+ var propertiesA = typeof(TA).GetProperties().Where(x => x.CanWrite && x.PropertyType.Namespace?.Contains("MSU") != true && x.GetCustomAttribute() == null).ToDictionary(x => x.Name, x => x);
+ var propertiesB = typeof(TA).GetProperties().Where(x => x.CanWrite && x.PropertyType.Namespace?.Contains("MSU") != true && x.GetCustomAttribute() == null).ToDictionary(x => x.Name, x => x);
var updated = false;
if (propertiesA.Count != propertiesB.Count)
{
- throw new InvalidOperationException($"Types {typeof(A).Name} and {typeof(B).Name} are not compatible");
+ throw new InvalidOperationException($"Types {typeof(TA).Name} and {typeof(TA).Name} are not compatible");
}
foreach (var propA in propertiesA.Values)
@@ -33,18 +31,18 @@ public class ConverterService(IMsuTypeService msuTypeService)
continue;
}
- if (propA.PropertyType == typeof(List) || propA.PropertyType == typeof(ObservableCollection))
+ if (propA.PropertyType == typeof(List) || propA.PropertyType == typeof(ObservableCollection))
{
if (recursive)
{
- IList? aValue = propA.GetValue(input) as IList;
- IList bValue = propB.GetValue(output) as IList ?? (propB.PropertyType == typeof(ObservableCollection) ? new ObservableCollection() : new List());
+ IList? aValue = propA.GetValue(input) as IList;
+ IList bValue = propB.GetValue(output) as IList ?? (propB.PropertyType == typeof(ObservableCollection) ? new ObservableCollection() : new List());
if (aValue != null)
{
foreach (var aSubItem in aValue)
{
- var bSubItem = new B();
- ConvertViewModel(aSubItem, bSubItem);
+ var bSubItem = new TA();
+ CloneModel(aSubItem, bSubItem);
bValue.Add(bSubItem);
}
}
@@ -63,71 +61,33 @@ public class ConverterService(IMsuTypeService msuTypeService)
return updated;
}
- public MsuProjectViewModel ConvertProject(MsuProject project)
+ public MsuProject CloneProject(MsuProject project)
{
- var viewModel = new MsuProjectViewModel();
- ConvertViewModel(project, viewModel);
- ConvertViewModel(project.BasicInfo, viewModel.BasicInfo);
- viewModel.BasicInfo.Project = viewModel;
+ var newProject = new MsuProject();
+ CloneModel(project, newProject);
+ CloneModel(project.BasicInfo, newProject.BasicInfo);
foreach (var track in project.Tracks)
{
- var msuTypeTrack = project.MsuType.Tracks.FirstOrDefault(x => x.Number == track.TrackNumber);
-
- var trackViewModel = new MsuTrackInfoViewModel()
- {
- Project = viewModel,
- Description = track.IsScratchPad
- ? "Use this page to add songs for keeping and editing without including them in the MSU. All songs included in this will not be included when generating and packaging the MSU."
- : msuTypeTrack?.Description
- };
-
- ConvertViewModel(track, trackViewModel);
+ var trackViewModel = new MsuTrackInfo();
+ CloneModel(track, trackViewModel);
foreach (var song in track.Songs)
{
- var songViewModel = new MsuSongInfoViewModel();
- ConvertViewModel(song, songViewModel);
- ConvertViewModel(song.MsuPcmInfo, songViewModel.MsuPcmInfo);
- songViewModel.ApplyCascadingSettings(viewModel, trackViewModel, songViewModel.IsAlt, songViewModel.CanPlaySongs, false, false);
- trackViewModel.Songs.Add(songViewModel);
+ var songViewModel = new MsuSongInfo();
+ CloneModel(song, songViewModel);
+ CloneModel(song.MsuPcmInfo, songViewModel.MsuPcmInfo);
}
- viewModel.Tracks.Add(trackViewModel);
+ newProject.Tracks.Add(trackViewModel);
}
- viewModel.LastSaveTime = DateTime.Now;
+ newProject.MsuType = project.MsuType;
+ newProject.LastSaveTime = DateTime.Now;
- return viewModel;
+ return newProject;
}
- public MsuProject ConvertProject(MsuProjectViewModel viewModel)
- {
- var project = new MsuProject();
- ConvertViewModel(viewModel, project);
- ConvertViewModel(viewModel.BasicInfo, project.BasicInfo);
- project.MsuType = msuTypeService.GetMsuType(project.MsuTypeName) ??
- throw new InvalidOperationException($"Invalid MSU Type {project.MsuTypeName}");
-
- foreach (var trackViewModel in viewModel.Tracks)
- {
- var track = new MsuTrackInfo();
- ConvertViewModel(trackViewModel, track);
-
- foreach (var songViewModel in trackViewModel.Songs)
- {
- var song = new MsuSongInfo();
- ConvertViewModel(songViewModel, song);
- ConvertViewModel(songViewModel.MsuPcmInfo, song.MsuPcmInfo);
- track.Songs.Add(song);
- }
-
- project.Tracks.Add(track);
- }
-
- return project;
- }
-
public ICollection ConvertMsuPcmTrackInfo(Track_base trackBase, string rootPath)
{
var outputList = new List();
@@ -226,7 +186,15 @@ public Track_base ConvertMsuPcmTrackInfo(MsuSongMsuPcmInfo trackBase, bool isSub
if (!isSubTrack && !isSubChannel && output is Track track)
{
if (trackBase.SubChannels.Any())
- track.Sub_channels = trackBase.SubChannels.Select(x => ConvertMsuPcmTrackInfo(x, false, true)).Cast().ToList();
+ {
+ if (string.IsNullOrEmpty(track.File))
+ {
+ track.File = trackBase.GetFiles().FirstOrDefault();
+ }
+ track.Sub_channels = trackBase.SubChannels.Select(x => ConvertMsuPcmTrackInfo(x, false, true))
+ .Cast().ToList();
+ }
+
if (trackBase.SubTracks.Any())
track.Sub_tracks = trackBase.SubTracks.Select(x => ConvertMsuPcmTrackInfo(x, true, false)).Cast().ToList();
}
@@ -236,6 +204,10 @@ public Track_base ConvertMsuPcmTrackInfo(MsuSongMsuPcmInfo trackBase, bool isSub
}
else if (isSubTrack && output is Sub_track subTrack && trackBase.SubChannels.Any())
{
+ if (string.IsNullOrEmpty(subTrack.File))
+ {
+ subTrack.File = trackBase.GetFiles().FirstOrDefault();
+ }
subTrack.Sub_channels = trackBase.SubChannels.Select(x => ConvertMsuPcmTrackInfo(x, false, true)).Cast().ToList();
}
diff --git a/MSUScripter/Services/DependencyInstallerService.cs b/MSUScripter/Services/DependencyInstallerService.cs
new file mode 100644
index 0000000..c050241
--- /dev/null
+++ b/MSUScripter/Services/DependencyInstallerService.cs
@@ -0,0 +1,437 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using AvaloniaControls.Services;
+using ICSharpCode.SharpZipLib.GZip;
+using ICSharpCode.SharpZipLib.Tar;
+using ICSharpCode.SharpZipLib.Zip;
+using Microsoft.Extensions.Logging;
+using MSUScripter.Models;
+
+namespace MSUScripter.Services;
+
+public class DependencyInstallerService(ILogger logger)
+{
+ private const string PythonWindowsDownloadUrl = "https://github.com/astral-sh/python-build-standalone/releases/download/20250828/cpython-3.13.7+20250828-x86_64-pc-windows-msvc-install_only_stripped.tar.gz";
+ private const string PythonLinuxDownloadUrl = "https://github.com/astral-sh/python-build-standalone/releases/download/20250828/cpython-3.13.7+20250828-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz";
+ private const string MsuPcmWindowsDownloadUrl = "https://github.com/qwertymodo/msupcmplusplus/releases/download/v1.0RC3/msupcm.exe";
+ private const string MsuPcmLinuxDownloadUrl = "https://github.com/MattEqualsCoder/msupcmplusplus/releases/download/v1.0RC3/msupcm.AppImage";
+ private const string FfmpegWindowsDownloadUrl = "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2024-08-31-12-50/ffmpeg-n7.0.2-6-g7e69129d2f-win64-lgpl-shared-7.0.zip";
+ private const string FfmpegWindows32BitDownloadUrl = "https://github.com/defisym/FFmpeg-Builds-Win32/releases/download/latest/ffmpeg-n7.1-latest-win32-gpl-7.1.zip";
+ private const string FfmpegLinuxDownloadUrl = "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2024-08-31-12-50/ffmpeg-n7.0.2-6-g7e69129d2f-linux64-lgpl-shared-7.0.tar.xz";
+
+ public async Task InstallPyApp(Action response, Func> runPyFunc)
+ {
+ var successful = false;
+
+ try
+ {
+ response.Invoke("Setting up directories");
+ var destination = Path.Combine(Directories.Dependencies, "python");
+
+ if (Directory.Exists(destination))
+ {
+ logger.LogInformation("Deleting prior Python installation");
+ try
+ {
+ await ITaskService.Run(() =>
+ {
+ Directory.Delete(destination, true);
+ });
+ }
+ catch (TaskCanceledException)
+ {
+ // Do Nothing
+ }
+ }
+
+ EnsureFolders(destination);
+
+ var tempFile = Path.Combine(Directories.TempFolder, "python.tar.gz");
+ var url = OperatingSystem.IsWindows() ? PythonWindowsDownloadUrl : PythonLinuxDownloadUrl;
+
+ response.Invoke("Downloading Python");
+ if (!await DownloadFileAsync(url, tempFile))
+ {
+ return false;
+ }
+
+ response.Invoke("Extracting Python files");
+ if (!await ExtractTarGzFile(tempFile, Directories.Dependencies))
+ {
+ return false;
+ }
+
+ var pythonPath = OperatingSystem.IsWindows()
+ ? Path.Combine(destination, "python.exe")
+ : Path.Combine(destination, "bin", "python3.13");
+
+ if (OperatingSystem.IsLinux())
+ {
+ File.SetUnixFileMode(pythonPath,
+ UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
+ }
+
+ response.Invoke("Verifying Python version");
+
+ var runPyResult = await runPyFunc(pythonPath, "--version");
+ if (!runPyResult.Success || !runPyResult.Result.StartsWith("Python 3"))
+ {
+ logger.LogError("Python version response incorrect: {Response} | {Error}", runPyResult.Result,
+ runPyResult.Error);
+ return false;
+ }
+
+ response.Invoke("Installing companion app");
+
+ runPyResult = await runPyFunc(pythonPath, "-m pip install py-msu-scripter-app");
+ if (!runPyResult.Success && !runPyResult.Error.StartsWith("[notice]"))
+ {
+ logger.LogError("Failed to install Python companion app: {Error}", runPyResult.Error);
+ return false;
+ }
+
+ successful = true;
+ return true;
+ }
+ catch (TaskCanceledException)
+ {
+ // Do nothing
+ return successful;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Unknown error installing Python companion app");
+ return false;
+ }
+ }
+
+ public async Task InstallMsuPcm(Action progress)
+ {
+ var successful = false;
+
+ try
+ {
+ progress.Invoke("Setting up directories");
+ EnsureFolders();
+
+ var destination = Path.Combine(Directories.Dependencies, OperatingSystem.IsWindows() ? "msupcm.exe" : "msupcm.AppImage");
+
+ if (File.Exists(destination))
+ {
+ File.Delete(destination);
+ }
+
+ var url = OperatingSystem.IsWindows() ? MsuPcmWindowsDownloadUrl : MsuPcmLinuxDownloadUrl;
+ progress.Invoke("Downloading MsuPcm++");
+ if (!await DownloadFileAsync(url, destination))
+ {
+ return false;
+ }
+
+ if (OperatingSystem.IsLinux())
+ {
+ File.SetUnixFileMode(destination, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
+ }
+
+ successful = File.Exists(destination);
+ return successful;
+ }
+ catch (TaskCanceledException)
+ {
+ // Do nothing
+ return successful;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Unknown error installing MsuPcm++");
+ return false;
+ }
+ }
+
+ public async Task InstallFfmpeg(Action progress)
+ {
+ var successful = false;
+
+ try
+ {
+ progress.Invoke("Setting up directories");
+ var tempExtractionPath = Path.Combine(Directories.TempFolder, "ffmpeg");
+ var destination = Path.Combine(Directories.Dependencies, "ffmpeg");
+
+ if (Directory.Exists(tempExtractionPath))
+ {
+ logger.LogInformation("Deleting prior FFmpeg installation");
+
+ try
+ {
+ await ITaskService.Run(() =>
+ {
+ Directory.Delete(tempExtractionPath, true);
+ });
+ }
+ catch (TaskCanceledException)
+ {
+ // Do Nothing
+ }
+ }
+
+ if (Directory.Exists(destination))
+ {
+ logger.LogInformation("Deleting prior FFmpeg installation");
+
+ try
+ {
+ await ITaskService.Run(() =>
+ {
+ Directory.Delete(destination, true);
+ });
+ }
+ catch (TaskCanceledException)
+ {
+ // Do Nothing
+ }
+ }
+
+ EnsureFolders(tempExtractionPath, destination);
+
+ var tempFileName = OperatingSystem.IsWindows() ? "ffmpeg.zip" : "ffmpeg.tar.xz";
+ var tempFilePath = Path.Combine(Directories.TempFolder, tempFileName);
+ var url = OperatingSystem.IsLinux()
+ ? FfmpegLinuxDownloadUrl
+ : Environment.Is64BitOperatingSystem
+ ? FfmpegWindowsDownloadUrl
+ : FfmpegWindows32BitDownloadUrl;
+
+ progress.Invoke("Downloading FFmpeg");
+ if (!await DownloadFileAsync(url, tempFilePath))
+ {
+ return false;
+ }
+
+ progress.Invoke("Extracting files");
+ if (OperatingSystem.IsWindows() && !await ExtractZipFile(tempFilePath, tempExtractionPath))
+ {
+ return false;
+ }
+ if (OperatingSystem.IsLinux() && !await ExtractTarXzFile(tempFilePath, tempExtractionPath))
+ {
+ return false;
+ }
+
+ var subDirectory = Directory.GetDirectories(tempExtractionPath).FirstOrDefault();
+ if (!Directory.Exists(subDirectory))
+ {
+ return false;
+ }
+
+ progress.Invoke("Copying files");
+ successful = await CopyDirectory(subDirectory, destination);
+
+ if (OperatingSystem.IsLinux())
+ {
+ var executablePath = Path.Combine(destination, "bin", "ffmpeg");
+ File.SetUnixFileMode(executablePath, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
+ executablePath = Path.Combine(destination, "bin", "ffprobe");
+ File.SetUnixFileMode(executablePath, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
+ executablePath = Path.Combine(destination, "bin", "ffplay");
+ File.SetUnixFileMode(executablePath, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
+ }
+
+ return successful;
+
+ }
+ catch (TaskCanceledException)
+ {
+ // Do nothing
+ return successful;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Unknown error installing FFmpeg");
+ return false;
+ }
+ }
+
+ private async Task ExtractTarGzFile(string inputPath, string outputDirectory)
+ {
+ logger.LogInformation("Extracting files from {File} to {Destination}", inputPath, outputDirectory);
+
+ var success = false;
+ try
+ {
+ await ITaskService.Run(() =>
+ {
+ try
+ {
+ using var inStream = File.OpenRead(inputPath);
+ using var gzipStream = new GZipInputStream(inStream);
+ var tarArchive = TarArchive.CreateInputTarArchive(gzipStream, Encoding.UTF8);
+ tarArchive.ExtractContents(outputDirectory);
+ tarArchive.Close();
+ success = true;
+ }
+ catch (Exception e)
+ {
+ logger.LogError(e, "Error extracting files");
+ }
+ });
+ }
+ catch (TaskCanceledException)
+ {
+ // Do nothing
+ }
+
+ return success;
+ }
+
+ private async Task ExtractTarXzFile(string inputPath, string outputDirectory)
+ {
+ logger.LogInformation("Extracting files from {File} to {Destination}", inputPath, outputDirectory);
+ var procStartInfo = new ProcessStartInfo("tar")
+ {
+ Arguments = $"xf \"{inputPath}\"",
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WorkingDirectory = outputDirectory
+ };
+ using var process = new Process();
+ process.StartInfo = procStartInfo;
+ process.Start();
+ await process.WaitForExitAsync();
+ return process.ExitCode == 0;
+ }
+
+ private async Task ExtractZipFile(string inputPath, string outputDirectory)
+ {
+ logger.LogInformation("Extracting files from {File} to {Destination}", inputPath, outputDirectory);
+
+ var success = false;
+ try
+ {
+ await ITaskService.Run(() =>
+ {
+ try
+ {
+ var fastZip = new FastZip();
+ fastZip.ExtractZip(inputPath, outputDirectory, null);
+ success = true;
+ }
+ catch (Exception e)
+ {
+ logger.LogError(e, "Error extracting files");
+ }
+ });
+ }
+ catch (TaskCanceledException)
+ {
+ // Do nothing
+ }
+
+ return success;
+ }
+
+ private async Task CopyDirectory(string sourceDir, string targetDir)
+ {
+ logger.LogInformation("Copying {Source} to {Destination}", sourceDir, targetDir);
+
+ var success = false;
+ try
+ {
+ await ITaskService.Run(() =>
+ {
+ try
+ {
+ CopyDirectoryInternal(sourceDir, targetDir);
+ logger.LogInformation("Successfully copied files");
+ success = true;
+ }
+ catch (Exception e)
+ {
+ logger.LogError(e, "Error copying directory");
+ }
+ });
+ }
+ catch (TaskCanceledException)
+ {
+ // Do nothing
+ }
+
+ return success;
+ }
+
+ private void CopyDirectoryInternal(string sourceDir, string targetDir)
+ {
+ Directory.CreateDirectory(targetDir);
+
+ foreach(var file in Directory.GetFiles(sourceDir))
+ File.Copy(file, Path.Combine(targetDir, Path.GetFileName(file)));
+
+ foreach(var directory in Directory.GetDirectories(sourceDir))
+ CopyDirectoryInternal(directory, Path.Combine(targetDir, Path.GetFileName(directory)));
+ }
+
+ private async Task DownloadFileAsync(string url, string target, int attempts = 3)
+ {
+ logger.LogInformation("Downloading {Url} to {Target}", url, target);
+ for (var i = 0; i < attempts; i++)
+ {
+ var result = await DownloadFileAsyncAttempt(url, target);
+ if (result)
+ {
+ return true;
+ }
+
+ await Task.Delay(TimeSpan.FromSeconds(15));
+ }
+
+ return false;
+ }
+
+ private async Task DownloadFileAsyncAttempt(string url, string target)
+ {
+
+ using var httpClient = new HttpClient();
+
+ try
+ {
+ await using (var downloadStream = await httpClient.GetStreamAsync(url))
+ await using (var fileStream = new FileStream(target, FileMode.Create))
+ {
+ await downloadStream.CopyToAsync(fileStream);
+ }
+ logger.LogInformation("Successfully downloaded file");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ logger.LogInformation(ex, "Error downloading file");
+ return false;
+ }
+ }
+
+ private void EnsureFolders(params string[] additionalFolders)
+ {
+ if (!Directory.Exists(Directories.Dependencies))
+ {
+ Directory.CreateDirectory(Directories.Dependencies);
+ }
+
+ if (!Directory.Exists(Directories.TempFolder))
+ {
+ Directory.CreateDirectory(Directories.TempFolder);
+ }
+
+ foreach (var additionalFolder in additionalFolders)
+ {
+ if (!string.IsNullOrEmpty(additionalFolder) && !Directory.Exists(additionalFolder))
+ {
+ Directory.CreateDirectory(additionalFolder);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/MSUScripter/Services/IAudioPlayerService.cs b/MSUScripter/Services/IAudioPlayerService.cs
index e7aacfe..257fb6f 100644
--- a/MSUScripter/Services/IAudioPlayerService.cs
+++ b/MSUScripter/Services/IAudioPlayerService.cs
@@ -5,10 +5,6 @@ namespace MSUScripter.Services;
public interface IAudioPlayerService
{
- public static bool CanPlaySongs { get; protected set; }
-
- public string CurrentPlayingFile { get; protected set; }
-
public void Pause();
public void PlayPause();
@@ -33,7 +29,7 @@ public interface IAudioPlayerService
public bool IsStopped { get; }
- public Task PlaySongAsync(string path, bool fromEnd);
+ public Task PlaySongAsync(string path, bool fromEnd, bool isLoopingSong);
public Task StopSongAsync(string? newSongPath = null, bool waitForFile = false);
diff --git a/MSUScripter/Services/MsuPcmService.cs b/MSUScripter/Services/MsuPcmService.cs
index 7cbd802..b34319d 100644
--- a/MSUScripter/Services/MsuPcmService.cs
+++ b/MSUScripter/Services/MsuPcmService.cs
@@ -5,10 +5,9 @@
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
-using System.Security.Cryptography;
using System.Text;
-using System.Text.RegularExpressions;
using System.Threading.Tasks;
+using K4os.Hash.xxHash;
using Microsoft.Extensions.Logging;
using MSUScripter.Configs;
using MSUScripter.Models;
@@ -16,26 +15,57 @@
namespace MSUScripter.Services;
-public class MsuPcmService
+public class MsuPcmInstallResponse
{
- private readonly StatusBarService _statusBarService;
- private readonly ConverterService _converterService;
- private readonly ILogger _logger;
- private readonly Settings _settings;
- private readonly IAudioPlayerService _audioPlayerService;
- private readonly string _cacheFolder;
-
- public MsuPcmService(ILogger logger, Settings settings, StatusBarService statusBarService, ConverterService converterService, IAudioPlayerService audioPlayerService)
+ public bool Success { get; init; }
+ public bool MissingSharedLibraries { get; init; }
+}
+
+public class MsuPcmJsonInfo
+{
+ // ReSharper disable once UnusedAutoPropertyAccessor.Global
+ public required string JsonFilePath { get; init; }
+ public required string JsonText { get; init; }
+ public string? ErrorMessage { get; init; }
+}
+
+public class MsuPcmResult
+{
+ public bool Successful { get; set; }
+ public string Result { get; set; } = string.Empty;
+ public string Error { get; set; } = string.Empty;
+}
+
+public class MsuPcmService(
+ ILogger logger,
+ Settings settings,
+ StatusBarService statusBarService,
+ ConverterService converterService,
+ YamlService yamlService,
+ IAudioPlayerService audioPlayerService,
+ DependencyInstallerService dependencyInstallerService)
+{
+ private string _cacheFolder2 = "";
+ private string _msuPcmPath = string.Empty;
+
+ public bool IsGeneratingPcm { get; private set; }
+
+ public event EventHandler? GeneratingPcm;
+
+ private string CacheFolder
{
- _logger = logger;
- _settings = settings;
- _statusBarService = statusBarService;
- _converterService = converterService;
- _audioPlayerService = audioPlayerService;
- _cacheFolder = Path.Combine(Directories.CacheFolder, "msupcm");
- if (!Directory.Exists(_cacheFolder))
+ get
{
- Directory.CreateDirectory(_cacheFolder);
+ if (string.IsNullOrEmpty(_cacheFolder2))
+ {
+ _cacheFolder2 = Path.Combine(Directories.CacheFolder, "msupcm");
+ if (!Directory.Exists(_cacheFolder2))
+ {
+ Directory.CreateDirectory(_cacheFolder2);
+ }
+ }
+
+ return _cacheFolder2;
}
}
@@ -53,7 +83,7 @@ public void DeleteTempPcms(int capPcms = -1)
}
catch (Exception)
{
- _logger.LogWarning("Could not delete {File}", tempPcm.FullName);
+ logger.LogWarning("Could not delete {File}", tempPcm.FullName);
}
}
}
@@ -70,13 +100,15 @@ public void DeleteTempPcms(int capPcms = -1)
}
catch (Exception)
{
- _logger.LogWarning("Could not delete {File}", tempPcm.FullName);
+ logger.LogWarning("Could not delete {File}", tempPcm.FullName);
}
}
}
}
+
+ public bool IsValid { get; private set; }
- public async Task CreateTempPcm(bool standAlone, MsuProject project, string inputFile, int? loop = null, int? trimEnd = null, double? normalization = -25, int? trimStart = null, bool skipCleanup = false)
+ public async Task CreateTempPcm(MsuProject project, string inputFile, int? loop = null, int? trimEnd = null, double? normalization = -25, int? trimStart = null, bool skipCleanup = false)
{
if (!skipCleanup)
{
@@ -87,9 +119,10 @@ public async Task CreateTempPcm(bool standAlone, MsuPro
{
File.Delete(outputPath);
}
- var result = await CreatePcm(standAlone, project, new MsuSongInfo()
+ var result = await CreatePcm(project, new MsuSongInfo()
{
- TrackNumber = project.MsuType.Tracks.First().Number,
+ Id = Guid.NewGuid().ToString("N"),
+ TrackNumber = -1,
OutputPath = outputPath,
MsuPcmInfo = new MsuSongMsuPcmInfo()
{
@@ -100,208 +133,233 @@ public async Task CreateTempPcm(bool standAlone, MsuPro
TrimEnd = trimEnd,
Normalization = normalization
}
- }, false);
+ }, false, false, false);
if (result is { Successful: true, GeneratedPcmFile: true })
{
- _logger.LogInformation("Temp PCM {Path} created successfully", outputPath);
+ logger.LogInformation("Temp PCM {Path} created successfully", outputPath);
}
else if (result.GeneratedPcmFile)
{
- _logger.LogInformation("Temp PCM {Path} created with warning: {Warning}", outputPath, result.Message);
+ logger.LogInformation("Temp PCM {Path} created with warning: {Warning}", outputPath, result.Message);
}
else
{
- _logger.LogInformation("Temp PCM {Path} had an error: {Error}", outputPath, result.Message);
+ logger.LogInformation("Temp PCM {Path} had an error: {Error}", outputPath, result.Message);
}
return result;
}
- public void ClearCache()
+ public async Task CreatePcm(MsuProject project, MsuSongInfo song, bool asPrimary, bool isBulkGeneration, bool cacheResults)
{
- var cacheDirectory = new DirectoryInfo(_cacheFolder);
- foreach (var file in cacheDirectory.EnumerateFiles())
+ if (!IsValid)
{
- if (file.CreationTime < DateTime.Now.AddMonths(-1))
- {
- try
- {
- file.Delete();
- }
- catch
- {
- _logger.LogWarning("Could not delete {File}", file.FullName);
- }
- }
+ return new GeneratePcmFileResponse(false, false,
+ "MsuPcm++ is not installed and configured. Please install MsuPcm++ and reverify in the MSU Scripter settings.",
+ null);
}
- }
- public void DeleteTempJsonFiles()
- {
- var jsonDirectory = Path.Combine(Directories.TempFolder, "msupcm");
- var tempDirectory = new DirectoryInfo(jsonDirectory);
- if (tempDirectory.Exists)
+ GeneratingPcm?.Invoke(this, true);
+
+ if (!audioPlayerService.IsStopped)
{
- foreach (var tempPcm in tempDirectory.EnumerateFiles("*.json"))
- {
- try
- {
- tempPcm.Delete();
- }
- catch
- {
- _logger.LogWarning("Could not delete {File}", tempPcm.FullName);
- }
- }
+ await audioPlayerService.StopSongAsync(song.OutputPath);
}
- }
-
- public async Task CreatePcm(bool standAlone, MsuProject project, MsuSongInfo song, bool addTrackDetailsToMessage = true)
- {
- if (!_audioPlayerService.IsStopped)
+
+ if (song.TrackNumber < 0)
{
- await _audioPlayerService.StopSongAsync(song.OutputPath);
+ logger.LogInformation("Generating PCM file for song {Song}", string.IsNullOrEmpty(song.SongName) ? song.TrackName : song.SongName);
+ }
+ else
+ {
+ logger.LogInformation("Generating temp PCM file for {SongFile}", song.MsuPcmInfo.File);
+ }
+
+ var tempPath = project.GetMsuGenerationTempFilePath(song);
+ if (!Directory.Exists(tempPath))
+ {
+ Directory.CreateDirectory(tempPath);
+ }
+ var tempJsonPath = Path.Combine(tempPath, "temp.json");
+ var tempPcmPath = Path.Combine(tempPath, "temp.pcm");
+ var jsonResponse = ExportMsuPcmTracksJson(project, song, tempJsonPath, tempPcmPath);
+
+ if (string.IsNullOrEmpty(jsonResponse.JsonText))
+ {
+ WriteFailureToStatusBar();
+ GeneratingPcm?.Invoke(this, false);
+ return new GeneratePcmFileResponse(false, false, jsonResponse.ErrorMessage ?? "Failed to generate MsuPcm++ JSON file", null);
}
- var message = "";
- _statusBarService.UpdateStatusBar("Generating PCM");
-
- var hasAlts = project.Tracks.First(x => x.TrackNumber == song.TrackNumber).Songs.Count > 1;
+ var outputPath = song.OutputPath;
+ if (asPrimary && song.IsAlt)
+ {
+ outputPath = project.Tracks.First(x => x.TrackNumber == song.TrackNumber).Songs.First(x => !x.IsAlt).OutputPath;
+ }
+ else if (song.TrackNumber is >= 1000 or < 0)
+ {
+ song.OutputPath = tempPcmPath;
+ outputPath = tempPcmPath;
+ }
- if (string.IsNullOrEmpty(song.OutputPath))
+ if (string.IsNullOrEmpty(outputPath))
{
WriteFailureToStatusBar();
+ GeneratingPcm?.Invoke(this, false);
return new GeneratePcmFileResponse(false, false, $"Track #{song.TrackNumber} - Missing output PCM path", null);
}
+
+ project.GenerationCache.Songs.TryGetValue(song.Id, out var previousCache);
+ var currentCache = cacheResults ? GetSongCacheData(jsonResponse.JsonText, outputPath) : null;
- if (song.MsuPcmInfo.HasBothSubTracksAndSubChannels)
+ if (MsuProjectSongCache.IsValid(previousCache, currentCache))
{
- message = $"Track #{song.TrackNumber} - Subtracks and subchannels can't be at the same level and be generated by msupcm++.";
- WriteFailureToStatusBar();
- return new GeneratePcmFileResponse(false, false, message, null);
+ logger.LogInformation("Song {SongId} matches cached data", outputPath);
+ statusBarService.UpdateStatusBar("PCM Generated");
+ GeneratingPcm?.Invoke(this, false);
+ return new GeneratePcmFileResponse(true, true, null, outputPath);
}
- var jsonDirectory = Path.Combine(Directories.TempFolder, "msupcm");
- if (!Directory.Exists(jsonDirectory))
+ logger.LogInformation("Generating PCM at {Path}", tempPcmPath);
+ statusBarService.UpdateStatusBar("Generating PCM");
+
+ // Generate the file and make sure the file exists
+ var msuPcmResult = await RunMsuPcmAsync(tempJsonPath, tempPcmPath);
+ if (!msuPcmResult.Successful && !File.Exists(tempPcmPath))
{
- Directory.CreateDirectory(jsonDirectory);
+ logger.LogError("MsuPcm++ returned the following error: {Error}", msuPcmResult.Error);
+ GeneratingPcm?.Invoke(this, false);
+ WriteFailureToStatusBar();
+ return new GeneratePcmFileResponse(false, false, msuPcmResult.Error, outputPath);
}
-
- var msu = new FileInfo(project.MsuPath);
- var guid = Guid.NewGuid().ToString("N");
- var jsonPath = Path.Combine(jsonDirectory, msu.Name.Replace(msu.Extension, $"-msupcm-temp-{guid}.json"));
- try
+ logger.LogInformation("MsuPcm++ generated PCM file {File}", tempPcmPath);
+
+ // Clean up msupcm++ errors
+ bool msuPcmSuccessful = msuPcmResult.Successful;
+ var msuPcmMessage = msuPcmResult.Error;
+ if (!msuPcmResult.Successful && project.IgnoreWarnings.Contains(msuPcmResult.Error))
{
- ExportMsuPcmTracksJson(standAlone, project, song, jsonPath);
-
- var msuPath = new FileInfo(project.MsuPath).DirectoryName;
- var relativePath = Path.GetRelativePath(msuPath!, song.OutputPath);
+ logger.LogWarning("Ignoring MsuPcm++ warning: {Warning}", msuPcmResult.Error);
+ msuPcmSuccessful = true;
+ msuPcmMessage = "";
+ }
- if (!File.Exists(jsonPath))
- {
- message = "Valid MsuPcm++ json was not able to be created";
- if (addTrackDetailsToMessage)
- message = $"Track #{song.TrackNumber} - {relativePath} - {message}";
- WriteFailureToStatusBar();
- return new GeneratePcmFileResponse(false, false, message, null);
- }
+ // Validate the MsuPcm++ header
+ var validationSuccessful = ValidatePcm(tempPcmPath, out var validationResponse);
+ if (!validationSuccessful)
+ {
+ logger.LogError("Generated PCM file has an error: {Error}", validationResponse);
+ GeneratingPcm?.Invoke(this, false);
+ WriteFailureToStatusBar();
+ return new GeneratePcmFileResponse(false, false, validationResponse, outputPath);
+ }
- if (!ValidateMsuPcmInfo(song.MsuPcmInfo, out message, out var numFiles))
+ // Move the file to the target location
+ if (outputPath != tempPcmPath)
+ {
+ try
{
- if (addTrackDetailsToMessage)
- message = $"Track #{song.TrackNumber} - {relativePath} - {message}";
- File.Delete(jsonPath);
- WriteFailureToStatusBar();
- return new GeneratePcmFileResponse(false, false, message, null);
- }
+ if (File.Exists(outputPath))
+ {
+ File.Delete(outputPath);
+ }
+
+ var directory = Path.GetDirectoryName(outputPath);
+ if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
- if (numFiles == 0)
+ logger.LogInformation("Moving generated PCM to {Path}", outputPath);
+ File.Move(tempPcmPath, outputPath);
+ }
+ catch (Exception ex)
{
- message = "No input files specified";
- if (addTrackDetailsToMessage)
- message = $"Track #{song.TrackNumber} - {relativePath} - {message}";
+ logger.LogError(ex, "Error while generating PCM file {Path}", outputPath);
+ GeneratingPcm?.Invoke(this, false);
WriteFailureToStatusBar();
- File.Delete(jsonPath);
- return new GeneratePcmFileResponse(false, false, message, null);
+ return new GeneratePcmFileResponse(false, false, "MsuPcm++ succeeded, but could not move generated file", outputPath);
}
+ }
- var file = new FileInfo(song.OutputPath);
-
- if (IsCached(song.MsuPcmInfo.GetFiles(), file.FullName, jsonPath))
- {
- message = "Success!";
- if (addTrackDetailsToMessage)
- message = $"Track #{song.TrackNumber} - {relativePath} - {message}";
- _statusBarService.UpdateStatusBar(hasAlts ? "PCM Generated - YAML Regeneration Needed" : "PCM Generated");
- return new GeneratePcmFileResponse(true, true, message, song.OutputPath);
- }
-
- var lastModifiedDate = file.Exists ? file.LastWriteTime : DateTime.MinValue;
+ // Move to the cache
+ if (cacheResults)
+ {
+ currentCache = GetSongCacheData(jsonResponse.JsonText, outputPath);
- if (RunMsuPcm(jsonPath, song.OutputPath, out message))
+ if (currentCache != null)
{
- if (!ValidatePcm(song.OutputPath, out message))
+ project.GenerationCache.Songs[song.Id] = currentCache;
+
+ if (!isBulkGeneration)
{
- if (addTrackDetailsToMessage)
- message = $"Track #{song.TrackNumber} - {relativePath} - {message}";
- File.Delete(jsonPath);
- return new GeneratePcmFileResponse(false, false, message, null);
+ SaveGenerationCache(project);
}
- Cache(song.MsuPcmInfo.GetFiles(), file.FullName, jsonPath);
- message = "Success!";
- if (addTrackDetailsToMessage)
- message = $"Track #{song.TrackNumber} - {relativePath} - {message}";
- File.Delete(jsonPath);
-
- _statusBarService.UpdateStatusBar(hasAlts ? "PCM Generated - YAML Regeneration Needed" : "PCM Generated");
- return new GeneratePcmFileResponse(true, true, message, song.OutputPath);
}
+ }
- file = new FileInfo(song.OutputPath);
- var newModifiedDate = file.Exists ? file.LastWriteTime : DateTime.MinValue;
- var generated = newModifiedDate > lastModifiedDate;
-
- if (generated)
- {
- message = $"PCM Generated with msupcm++ warning: {CleanMsuPcmResponse(message)}";
- if (addTrackDetailsToMessage)
- message = $"Track #{song.TrackNumber} - {relativePath} - {message}";
- _statusBarService.UpdateStatusBar("PCM Generated with Warning");
- }
- else
- {
- message = CleanMsuPcmResponse(message);
- if (addTrackDetailsToMessage)
- message = $"Track #{song.TrackNumber} - {relativePath} - {message}";
- WriteFailureToStatusBar();
- }
-
- File.Delete(jsonPath);
-
- return new GeneratePcmFileResponse(false, generated, message, generated ? song.OutputPath : null);
+ if (!asPrimary)
+ {
+ song.LastGeneratedDate = DateTime.Now;
}
- catch (Exception e)
+
+ var hasAlts = song.TrackNumber < 1000 &&
+ project.Tracks.FirstOrDefault(x => x.TrackNumber == song.TrackNumber)?.Songs.Count > 1;
+
+ if (msuPcmSuccessful && validationSuccessful)
{
- _logger.LogError(e, "Error creating PCM file for Track #{TrackNum} - {SongPath}", song.TrackNumber, song.OutputPath);
- message = "Unknown error";
- if (addTrackDetailsToMessage)
- message = $"Track #{song.TrackNumber} - {song.OutputPath} - {message}";
- if (File.Exists(jsonPath))
- {
- File.Delete(jsonPath);
- }
-
- WriteFailureToStatusBar();
- return new GeneratePcmFileResponse(false, false, message, null);
+ logger.LogInformation("Generated PCM file {File} successfully", outputPath);
+ }
+ else
+ {
+ logger.LogWarning("Generated PCM file {File} with warnings: {Warning}", outputPath, msuPcmMessage);
+ }
+
+ statusBarService.UpdateStatusBar(hasAlts ? "PCM Generated - YAML Regeneration Needed" : "PCM Generated");
+ GeneratingPcm?.Invoke(this, false);
+ return new GeneratePcmFileResponse(msuPcmSuccessful && validationSuccessful, true, msuPcmMessage, outputPath);
+ }
+
+ public void SaveGenerationCache(MsuProject project)
+ {
+ var generationCacheFile = project.GetMsuGenerationCacheFilePath();
+ var cacheYaml = yamlService.ToYaml(project.GenerationCache, YamlType.Pascal);
+ try
+ {
+ File.WriteAllText(generationCacheFile, cacheYaml);
+ logger.LogInformation("Saved project msupcm++ generation cache to {Path}", generationCacheFile);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error while saving generation cache");
}
}
- public bool CreateEmptyPcm(MsuSongInfo song)
+ private MsuProjectSongCache? GetSongCacheData(string json, string outputPath)
+ {
+ if (!File.Exists(outputPath))
+ {
+ return null;
+ }
+
+ var data = Encoding.UTF8.GetBytes(json);
+ var hash = XXH64.DigestOf(data);
+ var fileInfo = new FileInfo(outputPath);
+ return new MsuProjectSongCache
+ {
+ JsonHash = hash,
+ JsonLength = json.Length,
+ FileGenerationTime = fileInfo.LastWriteTime,
+ FileLength = fileInfo.Length,
+ };
+ }
+
+ public GeneratePcmFileResponse CreateEmptyPcm(MsuSongInfo song)
{
if (string.IsNullOrEmpty(song.OutputPath))
{
- return false;
+ return new GeneratePcmFileResponse(false, false, "Missing song output path", null);
}
try
@@ -313,14 +371,16 @@ public bool CreateEmptyPcm(MsuSongInfo song)
}
catch (Exception e)
{
- _logger.LogError(e, "Could not delete output file");
- _statusBarService.UpdateStatusBar("Error Creating Empty PCM File");
- return false;
+ logger.LogError(e, "Could not delete output file");
+ statusBarService.UpdateStatusBar("Error Creating Empty PCM File");
+ return new GeneratePcmFileResponse(false, false, "Could not delete output file", null);
}
- var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MSUScripter.empty.pcm");
+ var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MSUScripter.Assets.empty.pcm");
if (stream == null)
- return false;
+ {
+ return new GeneratePcmFileResponse(false, false, "Error Creating Empty PCM File", null);
+ }
try
{
@@ -329,67 +389,28 @@ public bool CreateEmptyPcm(MsuSongInfo song)
}
catch (Exception e)
{
- _logger.LogError(e, "Could not copy empty pcm file");
- _statusBarService.UpdateStatusBar("Error Creating Empty PCM File");
- return false;
+ logger.LogError(e, "Could not copy empty pcm file");
+ statusBarService.UpdateStatusBar("Error Creating Empty PCM File");
+ return new GeneratePcmFileResponse(false, false, "Error Creating Empty PCM File", null);
}
- _statusBarService.UpdateStatusBar("Generated Empty PCM File");
- return true;
+ statusBarService.UpdateStatusBar("Generated Empty PCM File");
+ return new GeneratePcmFileResponse(true, true, "Error Creating Empty PCM File", song.OutputPath);
}
- public bool ValidateMsuPcmInfo(MsuSongMsuPcmInfo info, out string? error, out int numFiles)
+ private bool ValidatePcm(string path, out string validationError)
{
- numFiles = 0;
-
- if (!string.IsNullOrEmpty(info.File))
- {
- if (!File.Exists(info.File))
- {
- error = $"{info.File} not found";
- return false;
- }
-
- numFiles = 1;
- }
-
- foreach (var subTrack in info.SubTracks)
- {
- if (!ValidateMsuPcmInfo(subTrack, out error, out var numSubFiles))
- return false;
- numFiles += numSubFiles;
- }
-
- foreach (var subChannel in info.SubChannels)
- {
- if (!ValidateMsuPcmInfo(subChannel, out error, out var numSubFiles))
- return false;
- numFiles += numSubFiles;
- }
-
- error = null;
- return true;
- }
-
- public bool ValidatePcm(string path, out string? error)
- {
- if (!File.Exists(path))
- {
- error = "msupcm++ did not create the file, but did not return an error.";
- return false;
- }
-
var testBytes = new byte[8];
using (var reader = new BinaryReader(new FileStream(path, FileMode.Open)))
{
reader.BaseStream.Seek(0, SeekOrigin.Begin);
- reader.Read(testBytes, 0, 8);
+ _ = reader.Read(testBytes, 0, 8);
}
if (Encoding.UTF8.GetString(testBytes, 0, 4) != "MSU1")
{
- error = "Bad Header";
+ validationError = "MsuPcm++ generated the file with an invalid header.";
return false;
}
@@ -398,90 +419,88 @@ public bool ValidatePcm(string path, out string? error)
if (loop < totalSamples)
{
- error = null;
+ validationError = "";
return true;
}
- else
- {
- error = "Bad loop point specified";
- return false;
- }
+
+ validationError = "Bad loop point specified. Continuing could cause some emulators to crash.";
+ return false;
}
- public bool RunMsuPcm(string trackJson, string expectedOutputPath, out string error)
+ private async Task RunMsuPcmAsync(string trackJson, string expectedOutputPath)
{
IsGeneratingPcm = true;
- var toReturn = RunMsuPcmInternal("\"" + trackJson + "\"", expectedOutputPath, out _, out error);
+ logger.LogInformation("Running MsuPcm++ for file {File}", trackJson);
+ var toReturn = RunMsuPcmInternalAsync(trackJson, expectedOutputPath);
IsGeneratingPcm = false;
- return toReturn;
+ return await toReturn;
}
- public bool ValidateMsuPcmPath(string msuPcmPath, out string error)
+ public async Task VerifyInstalledAsync()
{
- var successful = RunMsuPcmInternal("-v", "", out var result, out error, msuPcmPath);
- return successful && result.StartsWith("msupcm v");
- }
-
- private bool IsCached(ICollection inputPaths, string outputPath, string jsonPath)
- {
- if (!File.Exists(outputPath))
+ var fileName = OperatingSystem.IsWindows() ? "msupcm.exe" : "msupcm.AppImage";
+ _msuPcmPath = Path.Combine(Directories.Dependencies, fileName);
+ var result = await RunMsuPcmInternalAsync("-v", "");
+ IsValid = result.Successful && result.Result.StartsWith("msupcm v");
+
+ if (!IsValid)
{
- return false;
+ logger.LogError("msupcm++ could not be validated at path {Path}: {Error}", _msuPcmPath, result.Error);
}
-
- var expectedCache = GetCacheDetails(inputPaths, outputPath, jsonPath);
-
- if (!File.Exists(expectedCache.CachePath))
+ else
{
- return false;
+ logger.LogInformation("msupcm++ validated successfully at {Path}: {Result}", _msuPcmPath, result.Result);
}
- var currentCache = File.ReadAllText(expectedCache.CachePath);
-
- return currentCache == expectedCache.CacheValue;
- }
-
- private void Cache(ICollection inputPaths, string outputPath, string jsonPath)
- {
- var cache = GetCacheDetails(inputPaths, outputPath, jsonPath);
- File.WriteAllText(cache.CachePath, cache.CacheValue);
+ return new MsuPcmResult()
+ {
+ Successful = IsValid,
+ Result = result.Result,
+ Error = result.Error
+ };
}
- private (string CachePath, string CacheValue) GetCacheDetails(ICollection inputPaths, string outputPath,
- string jsonPath)
+ public async Task InstallAsync(Action progress)
{
- using var sha1 = SHA1.Create();
+ var response = await dependencyInstallerService.InstallMsuPcm(progress);
- var key = BitConverter.ToString(sha1.ComputeHash(Encoding.UTF8.GetBytes(outputPath))).Replace("-", "");
-
- var paths = new List();
- paths.Add(jsonPath);
- paths.Add(outputPath);
- paths.AddRange(inputPaths);
-
- var hashes = new List();
- foreach (var inputPath in paths)
+ if (!response)
{
- using var stream = File.OpenRead(inputPath);
- hashes.Add(BitConverter.ToString(sha1.ComputeHash(stream)).Replace("-", ""));
+ return new MsuPcmInstallResponse
+ {
+ Success = false,
+ };
}
- var value = string.Join("|", hashes);
+ var verified = await VerifyInstalledAsync();
- var filePath = Path.Combine(_cacheFolder, key);
-
- return (filePath, value);
+ if (verified.Error.Contains("error while loading shared libraries"))
+ {
+ return new MsuPcmInstallResponse
+ {
+ Success = false,
+ MissingSharedLibraries = true
+ };
+ }
+
+ return new MsuPcmInstallResponse
+ {
+ Success = verified.Successful,
+ MissingSharedLibraries = false
+ };
}
-
- private bool RunMsuPcmInternal(string innerCommand, string expectedOutputPath, out string result, out string error, string? msuPcmPath = null)
+
+ private async Task RunMsuPcmInternalAsync(string innerCommand, string expectedOutputPath)
{
- msuPcmPath ??= _settings.MsuPcmPath;
- if (string.IsNullOrEmpty(msuPcmPath) ||
- !File.Exists(msuPcmPath))
+ if (string.IsNullOrEmpty(_msuPcmPath) ||
+ !File.Exists(_msuPcmPath))
{
- result = "";
- error = "MsuPcm++ path not specified or is invalid";
- return false;
+ return new MsuPcmResult()
+ {
+ Successful = false,
+ Result = "",
+ Error = "MsuPcm++ path not specified or is invalid"
+ };
}
try
@@ -493,16 +512,28 @@ private bool RunMsuPcmInternal(string innerCommand, string expectedOutputPath, o
modifiedTime = fileInfo.LastWriteTime;
}
- var msuPcmFile = new FileInfo(msuPcmPath);
+ var msuPcmFile = new FileInfo(_msuPcmPath);
+
+ var workingDirectory = msuPcmFile.DirectoryName;
+ var command = msuPcmFile.FullName;
+ var arguments = innerCommand;
+
+ if (innerCommand != "-v" && File.Exists(innerCommand))
+ {
+ workingDirectory = Path.GetDirectoryName(innerCommand);
+ arguments = "\"" + Path.GetFileName(innerCommand) + "\"";
+ }
ProcessStartInfo procStartInfo;
+
+ logger.LogInformation("Running msupcm++ command: {Command} {Arguments}", command, arguments);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
- var command = msuPcmFile.Name + " " + innerCommand;
- procStartInfo= new ProcessStartInfo("cmd", "/c " + command)
+ procStartInfo= new ProcessStartInfo(command)
{
- WorkingDirectory = msuPcmFile.DirectoryName,
+ Arguments = arguments,
+ WorkingDirectory = workingDirectory,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
@@ -511,10 +542,10 @@ private bool RunMsuPcmInternal(string innerCommand, string expectedOutputPath, o
}
else
{
- procStartInfo= new ProcessStartInfo(msuPcmFile.FullName)
+ procStartInfo= new ProcessStartInfo(command)
{
- Arguments = innerCommand,
- WorkingDirectory = msuPcmFile.DirectoryName,
+ Arguments = arguments,
+ WorkingDirectory = workingDirectory,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
@@ -528,16 +559,26 @@ private bool RunMsuPcmInternal(string innerCommand, string expectedOutputPath, o
process.Start();
// Add this: wait until process does its work
- process.WaitForExit();
+ await process.WaitForExitAsync();
// and only then read the result
- result = process.StandardOutput.ReadToEnd().Replace("\0", "").Trim();
- error = process.StandardError.ReadToEnd().Replace("\0", "").Trim();
+ var result = (await process.StandardOutput.ReadToEndAsync()).Replace("\0", "").Trim();
+ var error = (await process.StandardError.ReadToEndAsync()).Replace("\0", "").Trim();
if (!string.IsNullOrEmpty(error))
{
- _logger.LogError("Error running MsuPcm++: {Error}", error);
- return false;
+ logger.LogError("Error running MsuPcm++: {Error}", error);
+ if (error.Contains("decrease volume?"))
+ {
+ error =
+ "MsuPcm settings for audio file caused audio clipping. Consider lowering the normalization value.";
+ }
+ return new MsuPcmResult()
+ {
+ Successful = false,
+ Result = result,
+ Error = error
+ };
}
// Validate that the file generated if applicable
@@ -548,14 +589,24 @@ private bool RunMsuPcmInternal(string innerCommand, string expectedOutputPath, o
if (!string.IsNullOrEmpty(result))
{
error = $"MsuPcm++ did not create the expected file and returned with the following Message: {result}";
- _logger.LogError("Error running MsuPcm++: {Error}", error);
- return false;
+ logger.LogError("Error running MsuPcm++: {Error}", error);
+ return new MsuPcmResult()
+ {
+ Successful = false,
+ Result = result,
+ Error = error
+ };
}
else
{
error = "$MsuPcm++ ran but did not create the expected file or return an error message.";
- _logger.LogError("Error running MsuPcm++: {Error}", error);
- return false;
+ logger.LogError("Error running MsuPcm++: {Error}", error);
+ return new MsuPcmResult()
+ {
+ Successful = false,
+ Result = result,
+ Error = error
+ };
}
}
else
@@ -566,40 +617,120 @@ private bool RunMsuPcmInternal(string innerCommand, string expectedOutputPath, o
if (!string.IsNullOrEmpty(result))
{
error = $"MsuPcm++ did not create the expected file and returned with the following Message: {result}";
- _logger.LogError("Error running MsuPcm++: {Error}", error);
- return false;
+ logger.LogError("Error running MsuPcm++: {Error}", error);
+ return new MsuPcmResult()
+ {
+ Successful = false,
+ Result = result,
+ Error = error
+ };
}
else
{
error = "$MsuPcm++ ran but did not create the expected file or return an error message.";
- _logger.LogError("Error running MsuPcm++: {Error}", error);
- return false;
+ logger.LogError("Error running MsuPcm++: {Error}", error);
+ return new MsuPcmResult()
+ {
+ Successful = false,
+ Result = result,
+ Error = error
+ };
}
}
}
}
- return true;
+ return new MsuPcmResult()
+ {
+ Successful = true,
+ Result = result,
+ Error = error
+ };
}
catch (Exception e)
{
- result = "";
- error = "Unknown error running MsuPcm++";
- _logger.LogError(e, "Unknown error running MsuPcm++");
- return false;
+ var result = "";
+ var error = "Unknown error running MsuPcm++";
+ logger.LogError(e, "Unknown error running MsuPcm++");
+ return new MsuPcmResult()
+ {
+ Successful = false,
+ Result = result,
+ Error = error
+ };
}
}
+
+ private string? CheckSongForErrors(MsuSongInfo song)
+ {
+ var audioFiles = song.MsuPcmInfo.GetFiles();
+
+ if (audioFiles.Count == 0)
+ {
+ return "No input files selected.";
+ }
+
+ foreach (var path in audioFiles.Where(path => !File.Exists(path)))
+ {
+ return $"Audio file {path} is not found.";
+ }
+
+ if (song.MsuPcmInfo.HasBothSubTracksAndSubChannels)
+ {
+ return "Subtracks and subchannels can't be at the same level and be generated by msupcm++.";
+ }
+
+ if (!song.MsuPcmInfo.HasValidSubChannelCount)
+ {
+ return "If subchannels are to be used, you need at least two of them.";
+ }
+
+ if (!song.MsuPcmInfo.HasValidChildTypes)
+ {
+ return "Subchannels cannot contain more subchannels and subtracks cannot contain more subtracks. You must alternate between subchannel and subtracks when creating multiple layers.";
+ }
+
+ return null;
+ }
- public string? ExportMsuPcmTracksJson(bool standAlone, MsuProject project, MsuSongInfo? singleSong = null, string? exportPath = null)
+ public MsuPcmJsonInfo ExportMsuPcmTracksJson(MsuProject project, MsuSongInfo? singleSong = null, string? exportPath = null, string? pcmPath = null)
{
+ if (singleSong != null)
+ {
+ var error = CheckSongForErrors(singleSong);
+ if (!string.IsNullOrEmpty(error))
+ {
+ return new MsuPcmJsonInfo
+ {
+ JsonText = "",
+ JsonFilePath = "",
+ ErrorMessage = error,
+ };
+ }
+ }
+
var msu = new FileInfo(project.MsuPath);
if (string.IsNullOrEmpty(exportPath))
{
- exportPath = msu.FullName.Replace(msu.Extension, "-tracks.json");
+ exportPath = project.GetTracksJsonPath();
}
+ bool? ditherValue = project.BasicInfo.DitherType switch
+ {
+ DitherType.Default => null,
+ DitherType.All => true,
+ DitherType.None => false,
+ DitherType.DefaultOn => singleSong?.MsuPcmInfo.Dither ?? true,
+ DitherType.DefaultOff => singleSong?.MsuPcmInfo.Dither ?? false,
+ _ => null
+ };
+
+ var songs = singleSong == null
+ ? project.Tracks.Where(x => !x.IsScratchPad).SelectMany(x => x.Songs).ToList()
+ : [singleSong];
+
var output = new MsuPcmPlusPlusConfig()
{
Game = project.BasicInfo.Game,
@@ -607,22 +738,18 @@ private bool RunMsuPcmInternal(string innerCommand, string expectedOutputPath, o
Artist = project.BasicInfo.Artist,
Output_prefix = msu.FullName.Replace(msu.Extension, ""),
Normalization = project.BasicInfo.Normalization,
- Dither = project.BasicInfo.Dither,
+ Dither = ditherValue,
Verbosity = 2,
- Keep_temps = standAlone && _settings.RunMsuPcmWithKeepTemps,
- First_track = singleSong?.TrackNumber ?? project.Tracks.Min(x => x.TrackNumber),
- Last_track = singleSong?.TrackNumber ?? project.Tracks.Where(x => !x.IsScratchPad).Max(x => x.TrackNumber)
+ Keep_temps = settings.RunMsuPcmWithKeepTemps,
+ First_track = songs.Min(x => x.TrackNumber),
+ Last_track = songs.Max(x => x.TrackNumber)
};
- var tracks = new List