mirror of
https://github.com/muskit/MercuryConverter.git
synced 2026-06-02 20:24:26 -07:00
we can export!!
This commit is contained in:
+44
-10
@@ -1,31 +1,64 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Data.Common;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Linq.Expressions;
|
|
||||||
using System.Text.Json;
|
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using MercuryConverter.UI.Views;
|
using FFMpegCore.Arguments;
|
||||||
|
using MercuryConverter.Utility;
|
||||||
using SaturnData.Notation.Core;
|
using SaturnData.Notation.Core;
|
||||||
using SaturnData.Notation.Serialization;
|
using Tmds.DBus.Protocol;
|
||||||
using SaturnData.Notation.Serialization.Mer;
|
|
||||||
using UAssetAPI;
|
using UAssetAPI;
|
||||||
using UAssetAPI.ExportTypes;
|
using UAssetAPI.ExportTypes;
|
||||||
using UAssetAPI.PropertyTypes.Objects;
|
using UAssetAPI.PropertyTypes.Objects;
|
||||||
using UAssetAPI.PropertyTypes.Structs;
|
|
||||||
using UAssetAPI.UnrealTypes;
|
using UAssetAPI.UnrealTypes;
|
||||||
|
|
||||||
namespace MercuryConverter.Data;
|
namespace MercuryConverter.Data;
|
||||||
|
|
||||||
public static class Database
|
public static class Database
|
||||||
{
|
{
|
||||||
public static ObservableCollection<Song> Songs = new();
|
public static Dictionary<string, string> AudioPaths { get; } = new();
|
||||||
|
public static ObservableCollection<Song> Songs { get; } = new();
|
||||||
|
|
||||||
public static void SetupNew(string dataPath)
|
public static void SetupNew(string dataPath)
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.Invoke(() => Songs.Clear());
|
SetupAudio();
|
||||||
|
SetupSongs(dataPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetupAudio()
|
||||||
|
{
|
||||||
|
AudioPaths.Clear();
|
||||||
|
|
||||||
|
using (var reader = new StreamReader(Utils.AssetPath("awb.csv")))
|
||||||
|
{
|
||||||
|
string? line;
|
||||||
|
while ((line = reader.ReadLine()) != null)
|
||||||
|
{
|
||||||
|
// skip header
|
||||||
|
if (line.Contains("songID")) continue;
|
||||||
|
|
||||||
|
var tokens = line.Split(",");
|
||||||
|
var id = tokens[0];
|
||||||
|
var path = tokens[1];
|
||||||
|
|
||||||
|
if (path.Length <= 0)
|
||||||
|
{
|
||||||
|
// TODO: warn of missing audio
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = Utils.IIDToMusicFilePath(Convert.ToUInt32(id));
|
||||||
|
|
||||||
|
var audFilePath = path.Split("_");
|
||||||
|
var audPath = Path.Combine(Settings.I!.DataPath, "MER_BGM", audFilePath[0], $"{audFilePath[1]}.wav");
|
||||||
|
AudioPaths[key] = audPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetupSongs(string dataPath)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Invoke(Songs.Clear);
|
||||||
|
|
||||||
var metadataTablePath = Path.Combine(dataPath, "MusicParameterTable.uasset");
|
var metadataTablePath = Path.Combine(dataPath, "MusicParameterTable.uasset");
|
||||||
var metadataAsset = new UAsset(metadataTablePath, EngineVersion.VER_UE4_19);
|
var metadataAsset = new UAsset(metadataTablePath, EngineVersion.VER_UE4_19);
|
||||||
@@ -50,6 +83,7 @@ public static class Database
|
|||||||
var song = new Song
|
var song = new Song
|
||||||
{
|
{
|
||||||
Id = data["AssetDirectory"].ToString()!,
|
Id = data["AssetDirectory"].ToString()!,
|
||||||
|
Uid = ((UInt32PropertyData)data["UniqueID"]).Value,
|
||||||
Rubi = data["Rubi"].ToString()!,
|
Rubi = data["Rubi"].ToString()!,
|
||||||
Name = data["MusicMessage"].ToString()!,
|
Name = data["MusicMessage"].ToString()!,
|
||||||
Artist = data["ArtistMessage"].ToString()!,
|
Artist = data["ArtistMessage"].ToString()!,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ namespace MercuryConverter.Data;
|
|||||||
public class Song
|
public class Song
|
||||||
{
|
{
|
||||||
public required string Id { get; set; } // Snn-nnn
|
public required string Id { get; set; } // Snn-nnn
|
||||||
|
public required uint Uid { get; set; } // nnnn
|
||||||
public required string Name { get; set; }
|
public required string Name { get; set; }
|
||||||
public required string Artist { get; set; }
|
public required string Artist { get; set; }
|
||||||
public required uint Source { get; set; }
|
public required uint Source { get; set; }
|
||||||
@@ -53,7 +54,7 @@ public class Song
|
|||||||
========= Entry.Guid format =========
|
========= Entry.Guid format =========
|
||||||
MERCURY_[SONGID]_[DIFF] (each var is int)
|
MERCURY_[SONGID]_[DIFF] (each var is int)
|
||||||
*/
|
*/
|
||||||
public IEnumerable<(Entry, Chart)> GetEntryCharts(string dataPath)
|
public IEnumerable<(Entry, Chart)> GetEntryCharts()
|
||||||
{
|
{
|
||||||
List<(Entry, Chart)> ret = new();
|
List<(Entry, Chart)> ret = new();
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ public class Song
|
|||||||
|
|
||||||
var diff = (Difficulty)i;
|
var diff = (Difficulty)i;
|
||||||
|
|
||||||
var chartFilePath = Path.Combine(dataPath, "MusicData", Id, $"{Id}_{Consts.DIFF_FILENAME_APPEND[diff]}.mer");
|
var chartFilePath = Path.Combine(Settings.I!.DataPath, "MusicData", Id, $"{Id}_{Consts.DIFF_FILENAME_APPEND[diff]}.mer");
|
||||||
var clearThreshold = ((FloatPropertyData)assetData[Consts.DIFF_CLEAR_KEY[diff]]).Value;
|
var clearThreshold = ((FloatPropertyData)assetData[Consts.DIFF_CLEAR_KEY[diff]]).Value;
|
||||||
|
|
||||||
var e = NotationSerializer.ToEntry(chartFilePath, new NotationReadArgs
|
var e = NotationSerializer.ToEntry(chartFilePath, new NotationReadArgs
|
||||||
@@ -81,12 +82,11 @@ public class Song
|
|||||||
e.Difficulty = diff;
|
e.Difficulty = diff;
|
||||||
e.Level = l.Value.Item1;
|
e.Level = l.Value.Item1;
|
||||||
e.NotesDesigner = l.Value.Item2;
|
e.NotesDesigner = l.Value.Item2;
|
||||||
e.JacketPath = Path.GetFileName(Jacket)!;
|
e.JacketPath = "jacket.png";
|
||||||
|
|
||||||
// TODO: video
|
// TODO: video
|
||||||
|
|
||||||
var uid = ((UInt32PropertyData)assetData["UniqueID"]).Value;
|
e.Guid = $"MERCURY_{Uid}_0{(int)diff}";
|
||||||
e.Guid = $"MERCURY_{uid}_0{(int)diff}";
|
|
||||||
|
|
||||||
if (new List<uint> { 1, 2 }.Contains(Source))
|
if (new List<uint> { 1, 2 }.Contains(Source))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.Contracts;
|
using System.Diagnostics.Contracts;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using MercuryConverter.Data;
|
using MercuryConverter.Data;
|
||||||
@@ -35,9 +36,60 @@ public class Exporter
|
|||||||
{
|
{
|
||||||
public static ExportResult Run(string outputPath, Song song, ExportOptions options)
|
public static ExportResult Run(string outputPath, Song song, ExportOptions options)
|
||||||
{
|
{
|
||||||
var exportPath = Path.Combine(outputPath, options.SourceSubdir ? song.SourceName : "", song.FolderName);
|
var exportDir = Path.Combine(outputPath, options.SourceSubdir ? song.SourceName : "");
|
||||||
|
var exportSongPath = Path.Combine(exportDir, song.FolderName);
|
||||||
|
Directory.CreateDirectory(exportSongPath);
|
||||||
|
|
||||||
|
Console.WriteLine($"Exporting {song.Id} to {exportSongPath}");
|
||||||
|
|
||||||
|
var entryCharts = song.GetEntryCharts();
|
||||||
|
HashSet<string> processedAudio = new();
|
||||||
|
|
||||||
|
foreach (var ec in entryCharts)
|
||||||
|
{
|
||||||
|
/// AUDIO ///
|
||||||
|
var audioKey = ec.Item1.AudioPath;
|
||||||
|
var audioExportFileName = $"{audioKey}.{options.AudioFormat.ToString().ToLower()}";
|
||||||
|
if (!processedAudio.Contains(audioKey) && Database.AudioPaths.ContainsKey(audioKey))
|
||||||
|
{
|
||||||
|
var audioSourcePath = Database.AudioPaths[audioKey];
|
||||||
|
|
||||||
|
// Copy/convert audio -- TODO
|
||||||
|
switch (options.AudioFormat)
|
||||||
|
{
|
||||||
|
case AudioFormat.MP3:
|
||||||
|
break;
|
||||||
|
case AudioFormat.OGG:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
File.Copy(audioSourcePath, Path.Combine(exportSongPath, audioExportFileName), true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
processedAudio.Add(audioKey);
|
||||||
|
}
|
||||||
|
ec.Item1.AudioPath = audioExportFileName;
|
||||||
|
|
||||||
|
/// CHART ///
|
||||||
|
var chartExt =
|
||||||
|
options.ChartFormat == FormatVersion.SatV1 ||
|
||||||
|
options.ChartFormat == FormatVersion.SatV2 ||
|
||||||
|
options.ChartFormat == FormatVersion.SatV3 ?
|
||||||
|
"sat" : "mer";
|
||||||
|
|
||||||
|
NotationSerializer.ToFile(
|
||||||
|
Path.Combine(exportSongPath, $"{(int)ec.Item1.Difficulty}.{chartExt}"),
|
||||||
|
ec.Item1, ec.Item2,
|
||||||
|
new NotationWriteArgs { FormatVersion = options.ChartFormat }
|
||||||
|
);
|
||||||
|
|
||||||
|
// restore audio key in db AFTER exporting metadata / chart
|
||||||
|
ec.Item1.AudioPath = audioKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// JACKET ///
|
||||||
|
if (song.Jacket != null)
|
||||||
|
File.Copy(song.Jacket, Path.Combine(exportSongPath, "jacket.png"));
|
||||||
|
|
||||||
Console.WriteLine($"Exporting {song.Id} to {exportPath}");
|
|
||||||
return new ExportResult { status = ExportResult.Status.Failed, message = "Unimplemented" };
|
return new ExportResult { status = ExportResult.Status.Failed, message = "Unimplemented" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,13 +30,14 @@
|
|||||||
|
|
||||||
<TextBlock Text="Audio Format" FontWeight="Medium" Margin="0 0 0 4"/>
|
<TextBlock Text="Audio Format" FontWeight="Medium" Margin="0 0 0 4"/>
|
||||||
<StackPanel Margin="10 0 0 42">
|
<StackPanel Margin="10 0 0 42">
|
||||||
<RadioButton GroupName="AudioFormat" IsChecked="true" Content="Leave as WAV"/>
|
<RadioButton Name="RadioLeaveAudioWAV" GroupName="AudioFormat" IsChecked="true" Content="Leave as WAV"/>
|
||||||
<RadioButton Name="RadioShouldAudioConvert" GroupName="AudioFormat">
|
<RadioButton Name="RadioShouldAudioConvert" GroupName="AudioFormat">
|
||||||
<ComboBox Name="ListSelectAudioConvertFormat" IsEnabled="false" PlaceholderText="Convert to...">
|
<ComboBox Name="ListSelectAudioConvertFormat" IsEnabled="false" PlaceholderText="Convert to...">
|
||||||
<ComboBoxItem Content="mp3"/>
|
<ComboBoxItem Content="mp3"/>
|
||||||
<ComboBoxItem Content="ogg"/>
|
<ComboBoxItem Content="ogg"/>
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
</RadioButton>
|
</RadioButton>
|
||||||
|
<TextBlock Name="NoFFMpegMessage" Text="Could not find FFmpeg. Make sure it is installed and on PATH!" TextWrapping="Wrap" Foreground="Red" FontWeight="DemiBold" Opacity="0.8" FontSize="12"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<CheckBox Name="TickExcludeVideos" Content="Exclude videos"/>
|
<CheckBox Name="TickExcludeVideos" Content="Exclude videos"/>
|
||||||
|
|||||||
@@ -132,6 +132,14 @@ public partial class Export : Panel
|
|||||||
// enabled export worker count is in good range
|
// enabled export worker count is in good range
|
||||||
int.TryParse(NumThreads.Text, out var thr) && 1 <= thr && thr <= Environment.ProcessorCount
|
int.TryParse(NumThreads.Text, out var thr) && 1 <= thr && thr <= Environment.ProcessorCount
|
||||||
);
|
);
|
||||||
|
|
||||||
|
var ffmpegAvail = Utils.IsFFMpegAvailable();
|
||||||
|
Console.WriteLine($"FFMpeg available: {ffmpegAvail}");
|
||||||
|
if (!ffmpegAvail)
|
||||||
|
RadioLeaveAudioWAV.IsChecked = true;
|
||||||
|
RadioLeaveAudioWAV.IsEnabled = ffmpegAvail;
|
||||||
|
RadioShouldAudioConvert.IsEnabled = ffmpegAvail;
|
||||||
|
NoFFMpegMessage.IsVisible = !ffmpegAvail;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UIExportingMode(bool isExporting)
|
private void UIExportingMode(bool isExporting)
|
||||||
@@ -149,7 +157,13 @@ public partial class Export : Panel
|
|||||||
{
|
{
|
||||||
UIExportingMode(true);
|
UIExportingMode(true);
|
||||||
|
|
||||||
string path = await Utils.BeginDirSelection("Choose your export path...");
|
var path = await Utils.BeginDirSelection("Choose your export path...", Settings.I!.ExportPath);
|
||||||
|
if (string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
|
UIExportingMode(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Settings.I!.ExportPath = path;
|
||||||
|
|
||||||
var options = await Dispatcher.UIThread.InvokeAsync(() =>
|
var options = await Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
{
|
{
|
||||||
@@ -176,6 +190,7 @@ public partial class Export : Panel
|
|||||||
{
|
{
|
||||||
await Dispatcher.UIThread.InvokeAsync(() => row.SetStatus(ExportStatus.Working));
|
await Dispatcher.UIThread.InvokeAsync(() => row.SetStatus(ExportStatus.Working));
|
||||||
Exporter.Run(path, row.Song, options);
|
Exporter.Run(path, row.Song, options);
|
||||||
|
await Dispatcher.UIThread.InvokeAsync(() => row.SetStatus(ExportStatus.Finished));
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection.Metadata.Ecma335;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Platform;
|
using Avalonia.Platform;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
using FFMpegCore;
|
||||||
|
using FFMpegCore.Arguments;
|
||||||
using MercuryConverter.UI;
|
using MercuryConverter.UI;
|
||||||
|
|
||||||
namespace MercuryConverter.Utility;
|
namespace MercuryConverter.Utility;
|
||||||
@@ -47,4 +50,19 @@ public static class Utils
|
|||||||
/// <param name="path">Forward-slash (/)-separated path to asset.</param>
|
/// <param name="path">Forward-slash (/)-separated path to asset.</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static Stream AssetPath(string path) => AssetLoader.Open(new Uri("avares://MercuryConverter/Assets/" + path));
|
public static Stream AssetPath(string path) => AssetLoader.Open(new Uri("avares://MercuryConverter/Assets/" + path));
|
||||||
|
|
||||||
|
public static string IIDToMusicFilePath(uint id)
|
||||||
|
{
|
||||||
|
return $"MER_BGM_S{id / 1000:D2}_{id % 1000:D3}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsFFMpegAvailable()
|
||||||
|
{
|
||||||
|
// FFMpegArguments
|
||||||
|
// .FromFileInput("dummy_input.mp4") // Use a dummy input, as it won't be processed
|
||||||
|
// .OutputToFile("dummy_output.mp4", true, options => options.WithArgument(new CustomArgument("-version"))) // Request FFmpeg version
|
||||||
|
// .ProcessSynchronously();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user