we can export!!

This commit is contained in:
Alex
2025-08-27 23:03:30 -07:00
parent 54ea7f09a0
commit 5c6e144e8a
6 changed files with 139 additions and 19 deletions
+44 -10
View File
@@ -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<Song> Songs = new();
public static Dictionary<string, string> AudioPaths { get; } = new();
public static ObservableCollection<Song> 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()!,
+5 -5
View File
@@ -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<uint> { 1, 2 }.Contains(Source))
{
+54 -2
View File
@@ -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<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" };
}
}
+2 -1
View File
@@ -30,13 +30,14 @@
<TextBlock Text="Audio Format" FontWeight="Medium" Margin="0 0 0 4"/>
<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">
<ComboBox Name="ListSelectAudioConvertFormat" IsEnabled="false" PlaceholderText="Convert to...">
<ComboBoxItem Content="mp3"/>
<ComboBoxItem Content="ogg"/>
</ComboBox>
</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>
<CheckBox Name="TickExcludeVideos" Content="Exclude videos"/>
+16 -1
View File
@@ -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));
}
);
+18
View File
@@ -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
/// <param name="path">Forward-slash (/)-separated path to asset.</param>
/// <returns></returns>
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;
}
}