diff --git a/src/Data/Database.cs b/src/Data/Database.cs index 77c0249..490cabd 100644 --- a/src/Data/Database.cs +++ b/src/Data/Database.cs @@ -1,31 +1,64 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Data.Common; using System.IO; -using System.Linq; -using System.Linq.Expressions; -using System.Text.Json; using Avalonia.Threading; -using MercuryConverter.UI.Views; +using FFMpegCore.Arguments; +using MercuryConverter.Utility; using SaturnData.Notation.Core; -using SaturnData.Notation.Serialization; -using SaturnData.Notation.Serialization.Mer; +using Tmds.DBus.Protocol; using UAssetAPI; using UAssetAPI.ExportTypes; using UAssetAPI.PropertyTypes.Objects; -using UAssetAPI.PropertyTypes.Structs; using UAssetAPI.UnrealTypes; namespace MercuryConverter.Data; public static class Database { - public static ObservableCollection Songs = new(); + public static Dictionary AudioPaths { get; } = new(); + public static ObservableCollection Songs { get; } = new(); 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 metadataAsset = new UAsset(metadataTablePath, EngineVersion.VER_UE4_19); @@ -50,6 +83,7 @@ public static class Database var song = new Song { Id = data["AssetDirectory"].ToString()!, + Uid = ((UInt32PropertyData)data["UniqueID"]).Value, Rubi = data["Rubi"].ToString()!, Name = data["MusicMessage"].ToString()!, Artist = data["ArtistMessage"].ToString()!, diff --git a/src/Data/Song/Song.cs b/src/Data/Song/Song.cs index 90b9fde..20d8f95 100644 --- a/src/Data/Song/Song.cs +++ b/src/Data/Song/Song.cs @@ -17,6 +17,7 @@ namespace MercuryConverter.Data; public class Song { public required string Id { get; set; } // Snn-nnn + public required uint Uid { get; set; } // nnnn public required string Name { get; set; } public required string Artist { get; set; } public required uint Source { get; set; } @@ -53,7 +54,7 @@ public class Song ========= Entry.Guid format ========= MERCURY_[SONGID]_[DIFF] (each var is int) */ - public IEnumerable<(Entry, Chart)> GetEntryCharts(string dataPath) + public IEnumerable<(Entry, Chart)> GetEntryCharts() { List<(Entry, Chart)> ret = new(); @@ -64,7 +65,7 @@ public class Song 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 e = NotationSerializer.ToEntry(chartFilePath, new NotationReadArgs @@ -81,12 +82,11 @@ public class Song e.Difficulty = diff; e.Level = l.Value.Item1; e.NotesDesigner = l.Value.Item2; - e.JacketPath = Path.GetFileName(Jacket)!; + e.JacketPath = "jacket.png"; // 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 { 1, 2 }.Contains(Source)) { diff --git a/src/ExportOperation/Exporter.cs b/src/ExportOperation/Exporter.cs index 91e013f..78fc5d8 100644 --- a/src/ExportOperation/Exporter.cs +++ b/src/ExportOperation/Exporter.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics.Contracts; using System.IO; using MercuryConverter.Data; @@ -35,9 +36,60 @@ public class Exporter { 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 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" }; } } \ No newline at end of file diff --git a/src/UI/Views/Export/Export.axaml b/src/UI/Views/Export/Export.axaml index bf9cb71..97297fa 100644 --- a/src/UI/Views/Export/Export.axaml +++ b/src/UI/Views/Export/Export.axaml @@ -30,13 +30,14 @@ - + + diff --git a/src/UI/Views/Export/Export.axaml.cs b/src/UI/Views/Export/Export.axaml.cs index 0a69ff3..80eaabb 100644 --- a/src/UI/Views/Export/Export.axaml.cs +++ b/src/UI/Views/Export/Export.axaml.cs @@ -132,6 +132,14 @@ public partial class Export : Panel // enabled export worker count is in good range 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) @@ -149,7 +157,13 @@ public partial class Export : Panel { 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(() => { @@ -176,6 +190,7 @@ public partial class Export : Panel { await Dispatcher.UIThread.InvokeAsync(() => row.SetStatus(ExportStatus.Working)); Exporter.Run(path, row.Song, options); + await Dispatcher.UIThread.InvokeAsync(() => row.SetStatus(ExportStatus.Finished)); } ); diff --git a/src/Utility/Utils.cs b/src/Utility/Utils.cs index 0406921..f180966 100644 --- a/src/Utility/Utils.cs +++ b/src/Utility/Utils.cs @@ -2,11 +2,14 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection.Metadata.Ecma335; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Threading; +using FFMpegCore; +using FFMpegCore.Arguments; using MercuryConverter.UI; namespace MercuryConverter.Utility; @@ -47,4 +50,19 @@ public static class Utils /// Forward-slash (/)-separated path to asset. /// 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; + } } \ No newline at end of file