From f5fcc41e5bb6695ee71a892078e87b0fccb3861c Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Wed, 9 Jul 2025 16:50:47 -0400 Subject: [PATCH 01/15] use hashes instead of index for imported splicer SNRs --- .../CLI/Commands/ImportResourceCommand.cs | 90 ++++++++++--------- 1 file changed, 48 insertions(+), 42 deletions(-) diff --git a/Volatility/CLI/Commands/ImportResourceCommand.cs b/Volatility/CLI/Commands/ImportResourceCommand.cs index 2f103d6..291c7fe 100644 --- a/Volatility/CLI/Commands/ImportResourceCommand.cs +++ b/Volatility/CLI/Commands/ImportResourceCommand.cs @@ -1,8 +1,9 @@ +using System.Text; using System.Diagnostics; +using System.Security.Cryptography; using System.Text.RegularExpressions; using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; using Volatility.Resources; using Volatility.Utilities; @@ -157,60 +158,65 @@ public async Task Execute() Splicer? splicer = resource as Splicer; byte[][]? samples = splicer?.GetLoadedSamples(); - + using var hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); + byte[] salt = Encoding.UTF8.GetBytes("Volatility_"); for (int i = 0; i < samples?.Length; i++) - { - string sampleName = $"{resource.AssetName}_{i}"; - - string sampleDirectory = Path.Combine(directoryPath, $"{resource.AssetName}_Samples"); + { + string sampleDirectory = Path.Combine(directoryPath, $"{resource.AssetName}_Samples"); Directory.CreateDirectory(sampleDirectory); + + hasher.AppendData(salt); + hasher.AppendData(samples[i]); - Console.WriteLine($"Writing extracted sample {sampleName}.snr"); - await File.WriteAllBytesAsync(Path.Combine(sampleDirectory, $"{sampleName}.snr"), samples[i]); + string sampleName = $"{resource.AssetName}_{Convert.ToHexString(hasher.GetHashAndReset())}"; + + Console.WriteLine($"Writing extracted sample {sampleName}.snr"); + await File.WriteAllBytesAsync(Path.Combine(sampleDirectory, $"{sampleName}.snr"), samples[i]); - if (sxExists) - { - string samplePathName = Path.Combine(sampleDirectory, sampleName); + if (sxExists) + { + string samplePathName = Path.Combine(sampleDirectory, sampleName); - string convertedSamplePathName = Path.Combine(sampleDirectory, "_extracted"); + string convertedSamplePathName = Path.Combine(sampleDirectory, "_extracted"); - Directory.CreateDirectory(convertedSamplePathName); + Directory.CreateDirectory(convertedSamplePathName); - convertedSamplePathName = Path.Combine(convertedSamplePathName, sampleName); + convertedSamplePathName = Path.Combine(convertedSamplePathName, sampleName); ProcessStartInfo start = new ProcessStartInfo - { - FileName = sxPath, - Arguments = $"-wave -s16l_int -v0 \"{samplePathName}.snr\" -=\"{convertedSamplePathName}.wav\"", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - using (Process process = new Process()) - { - process.StartInfo = start; - process.OutputDataReceived += (sender, e) => - { - if (!string.IsNullOrEmpty(e.Data)) Console.WriteLine(e.Data); - }; - - process.ErrorDataReceived += (sender, e) => - { - if (!string.IsNullOrEmpty(e.Data)) Console.WriteLine(e.Data); - }; + { + FileName = sxPath, + Arguments = $"-wave -s16l_int -v0 \"{samplePathName}.snr\" -=\"{convertedSamplePathName}.wav\"", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (Process process = new Process()) + { + process.StartInfo = start; + process.OutputDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) Console.WriteLine(e.Data); + }; + + process.ErrorDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) Console.WriteLine(e.Data); + }; Console.WriteLine($"Converting extracted sample {sampleName}.snr to wave..."); process.Start(); - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - process.WaitForExit(); - } - } - } - } + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + process.WaitForExit(); + } + } + } + + } Console.WriteLine($"Imported {Path.GetFileName(ImportPath)} as {Path.GetFullPath(filePath)}."); })); } From 0b8c568997afbdd3fc2e3c3228b5277899163850 Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Wed, 23 Jul 2025 17:28:24 -0400 Subject: [PATCH 02/15] StrongID, SnrID, use SnrID in imported samples --- Volatility/CLI/Commands/AutotestCommand.cs | 10 +- .../CLI/Commands/ImportResourceCommand.cs | 13 +-- Volatility/Resources/Resource.cs | 2 +- Volatility/Resources/Splicer/Splicer.cs | 22 +++- Volatility/StrongID.cs | 102 ++++++++++++++++++ Volatility/Types.cs | 13 +-- Volatility/Utilities/ResourceIDUtilities.cs | 17 --- 7 files changed, 132 insertions(+), 47 deletions(-) create mode 100644 Volatility/StrongID.cs diff --git a/Volatility/CLI/Commands/AutotestCommand.cs b/Volatility/CLI/Commands/AutotestCommand.cs index f6a8d78..a8888a9 100644 --- a/Volatility/CLI/Commands/AutotestCommand.cs +++ b/Volatility/CLI/Commands/AutotestCommand.cs @@ -48,7 +48,7 @@ public async Task Execute() TexturePC textureHeaderPC = new TexturePC { AssetName = "autotest_header_PC", - ResourceID = GetResourceIDFromName("autotest_header_PC"), + ResourceID = ResourceID.HashFromString("autotest_header_PC"), Format = D3DFORMAT.D3DFMT_DXT1, Width = 1024, Height = 512, @@ -62,7 +62,7 @@ public async Task Execute() TextureBPR textureHeaderBPR = new TextureBPR { AssetName = "autotest_header_BPR", - ResourceID = GetResourceIDFromName("autotest_header_BPR"), + ResourceID = ResourceID.HashFromString("autotest_header_BPR"), Format = DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM, Width = 1024, Height = 512, @@ -77,7 +77,7 @@ public async Task Execute() textureHeaderBPR.SetResourceArch(Arch.x64); textureHeaderBPR.AssetName = "autotest_header_BPRx64"; - textureHeaderBPR.ResourceID = GetResourceIDFromName(textureHeaderBPR.AssetName); + textureHeaderBPR.ResourceID = ResourceID.HashFromString(textureHeaderBPR.AssetName); // Write 64 bit test BPR header TestHeaderRW("autotest_header_BPRx64.dat", textureHeaderBPR); @@ -86,7 +86,7 @@ public async Task Execute() TexturePS3 textureHeaderPS3 = new TexturePS3 { AssetName = "autotest_header_PS3", - ResourceID = GetResourceIDFromName("autotest_header_PS3"), + ResourceID = ResourceID.HashFromString("autotest_header_PS3"), Format = CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT45, Width = 1024, Height = 512, @@ -100,7 +100,7 @@ public async Task Execute() TextureX360 textureHeaderX360 = new TextureX360 { AssetName = "autotest_header_X360", - ResourceID = GetResourceIDFromName("autotest_header_X360"), + ResourceID = ResourceID.HashFromString("autotest_header_X360"), Format = new GPUTEXTURE_FETCH_CONSTANT { Tiled = true, diff --git a/Volatility/CLI/Commands/ImportResourceCommand.cs b/Volatility/CLI/Commands/ImportResourceCommand.cs index 25dc3d9..107be28 100644 --- a/Volatility/CLI/Commands/ImportResourceCommand.cs +++ b/Volatility/CLI/Commands/ImportResourceCommand.cs @@ -158,22 +158,17 @@ public async Task Execute() Splicer? splicer = resource as Splicer; - byte[][]? samples = splicer?.GetLoadedSamples(); - using var hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); - byte[] salt = Encoding.UTF8.GetBytes("Volatility_"); - for (int i = 0; i < samples?.Length; i++) + List? samples = splicer?.GetLoadedSamples(); + for (int i = 0; i < samples?.Count; i++) { string sampleDirectory = Path.Combine(directoryPath, $"{resource.AssetName}_Samples"); Directory.CreateDirectory(sampleDirectory); - - hasher.AppendData(salt); - hasher.AppendData(samples[i]); - string sampleName = $"{resource.AssetName}_{Convert.ToHexString(hasher.GetHashAndReset())}"; + string sampleName = $"{resource.AssetName}_{samples[i].SampleID}"; Console.WriteLine($"Writing extracted sample {sampleName}.snr"); - await File.WriteAllBytesAsync(Path.Combine(sampleDirectory, $"{sampleName}.snr"), samples[i]); + await File.WriteAllBytesAsync(Path.Combine(sampleDirectory, $"{sampleName}.snr"), samples[i].Data); if (sxExists) { diff --git a/Volatility/Resources/Resource.cs b/Volatility/Resources/Resource.cs index c64af1b..9c907fe 100644 --- a/Volatility/Resources/Resource.cs +++ b/Volatility/Resources/Resource.cs @@ -94,7 +94,7 @@ public Resource(string path, Endian endianness = Endian.Agnostic) else { // TODO: Add new entry to ResourceDB - ResourceID = Convert.ToUInt64(GetResourceIDFromName(name, importEndianness), 16); + ResourceID = Convert.ToUInt64(ResourceID.FromIDString(name)); AssetName = name; } diff --git a/Volatility/Resources/Splicer/Splicer.cs b/Volatility/Resources/Splicer/Splicer.cs index 91864b1..7b4bc23 100644 --- a/Volatility/Resources/Splicer/Splicer.cs +++ b/Volatility/Resources/Splicer/Splicer.cs @@ -20,7 +20,7 @@ public class Splicer : BinaryResource // This only gets populated when parsing from a stream. // Not sure whether this is a good idea to keep as-is. - private byte[][] _samples; + private List _samples; // Used to make reading parsed files easier. // May remove or keep as generated values @@ -117,14 +117,22 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann SamplePtrOffset = (nint)(reader.BaseStream.Position - DataOffset); - _samples = new byte[numSamples][]; + _samples = new List(numSamples); for (int i = 0; i < numSamples; i++) { reader.BaseStream.Seek(SamplePtrOffset + DataOffset + SamplePtrs[i], SeekOrigin.Begin); int length = (int)((i == (numSamples - 1) ? reader.BaseStream.Length : SamplePtrs[i + 1]) - SamplePtrs[i]); - _samples[i] = reader.ReadBytes(length); + byte[]? data = reader.ReadBytes(length); + + _samples[i] = new Sample + { + SampleID = SnrID.HashFromBytes(data), + Data = data, + }; + + data = null; } } @@ -226,7 +234,7 @@ public void SpliceSamples(EndianAwareBinaryWriter writer, string samplesDir) writer.BaseStream.Seek(tempOffset, SeekOrigin.Begin); } - public byte[][] GetLoadedSamples() + public List GetLoadedSamples() { return _samples; } @@ -267,4 +275,10 @@ public struct SPLICE_SampleRef public byte ERollOffType; public ushort Padding2; } + + public struct Sample + { + public SnrID SampleID; + public byte[] Data; + } } \ No newline at end of file diff --git a/Volatility/StrongID.cs b/Volatility/StrongID.cs new file mode 100644 index 0000000..0edc06a --- /dev/null +++ b/Volatility/StrongID.cs @@ -0,0 +1,102 @@ + +global using ResourceID = StrongID; +global using SnrID = StrongID; + +using System.Text; + +public interface IStrongID +{ + ulong Value { get; } +} +public interface IIDTag +{ + static abstract ulong Hash(string text); + static abstract ulong Hash(ReadOnlySpan data); +} + +public readonly record struct StrongID(ulong Value) where TTag : IIDTag +{ + public static StrongID Default => new(0UL); + public static implicit operator ulong(StrongID id) => id.Value; + public static implicit operator StrongID(ulong v) => new(v); + public static implicit operator StrongID(string s) => HashFromString(s); + public static StrongID HashFromString(string s) => new(TTag.Hash(s)); // For creating IDs from names + public static StrongID HashFromBytes(byte[] b) => new(TTag.Hash(b)); // For creating IDs from data (SnrID) + public static StrongID FromIDString(string s) => new(Convert.ToUInt64(s)); // For strings that are already IDs + public override string ToString() => Value.ToString("X8"); +} + +public readonly struct ResourceTag : IIDTag +{ + public static ulong Hash(string text) => Crc32.Compute(text.ToLower(System.Globalization.CultureInfo.CurrentCulture)); + public static ulong Hash(ReadOnlySpan data) => throw new NotSupportedException("Generating ResourceIDs from direct bytes is not supported. Please convert your input to a string and use the Hash(string text) overload."); +} + +public readonly struct SnrTag : IIDTag +{ + public static ulong Hash(string text) => Crc64.Compute(text.ToLower(System.Globalization.CultureInfo.CurrentCulture)); + public static ulong Hash(ReadOnlySpan data) + { + ReadOnlySpan prefix = "Volatility_"u8; + Span buf = stackalloc byte[prefix.Length + data.Length]; + prefix.CopyTo(buf); + data.CopyTo(buf[prefix.Length..]); + return Crc64.Compute(buf); + } +} + +public static class Crc32 +{ + private static readonly uint[] Table = BuildTable(); + public static uint Compute(string s) + { + var bytes = Encoding.UTF8.GetBytes(s); + uint crc = 0xFFFFFFFFu; + foreach (var b in bytes) + crc = (crc >> 8) ^ Table[(crc ^ b) & 0xFF]; + return ~crc; + } + private static uint[] BuildTable() + { + const uint poly = 0xEDB88320u; + var table = new uint[256]; + for (uint i = 0; i < 256; i++) + { + uint c = i; + for (int j = 0; j < 8; j++) + c = (c & 1) != 0 ? (poly ^ (c >> 1)) : (c >> 1); + table[i] = c; + } + return table; + } +} + +public static class Crc64 +{ + private static readonly ulong[] Table = BuildTable(); + + public static ulong Compute(string s) => + Compute(Encoding.UTF8.GetBytes(s)); + + public static ulong Compute(ReadOnlySpan data) + { + ulong crc = 0xFFFFFFFFFFFFFFFFul; + foreach (var b in data) + crc = Table[(byte)(crc ^ b)] ^ (crc >> 8); + return ~crc; + } + + private static ulong[] BuildTable() + { + const ulong poly = 0xC96C5795D7870F42ul; + var table = new ulong[256]; + for (ulong i = 0; i < 256; i++) + { + ulong c = i; + for (int j = 0; j < 8; j++) + c = (c & 1) != 0 ? (poly ^ (c >> 1)) : (c >> 1); + table[i] = c; + } + return table; + } +} diff --git a/Volatility/Types.cs b/Volatility/Types.cs index 531ae8d..0e4e766 100644 --- a/Volatility/Types.cs +++ b/Volatility/Types.cs @@ -11,19 +11,10 @@ global using ColorRGBA = System.Numerics.Vector4; global using Vector2Literal = System.Numerics.Vector2; + public struct Transform { public Vector3 Location; public Quaternion Rotation; - public Vector3 Scale; -} - -public readonly struct ResourceID -{ - public readonly ulong Value; - public ResourceID(ulong v) => Value = v; - public static implicit operator ulong(ResourceID r) => r.Value; - public static implicit operator ResourceID(ulong v) => new ResourceID(v); - public static readonly ResourceID Default = new ResourceID(0UL); - public override string ToString() => $"{Value:X8}"; + public Vector3 Scale; } \ No newline at end of file diff --git a/Volatility/Utilities/ResourceIDUtilities.cs b/Volatility/Utilities/ResourceIDUtilities.cs index f867432..e2b789e 100644 --- a/Volatility/Utilities/ResourceIDUtilities.cs +++ b/Volatility/Utilities/ResourceIDUtilities.cs @@ -133,21 +133,4 @@ public static string GetNameByResourceID(ResourceID id) return ""; } - - public static string GetResourceIDFromName(string name, Endian endian = Endian.LE) - { - byte[] hash = Crc32.Hash(Encoding.UTF8.GetBytes(name.ToLower())); - - if (endian == Endian.BE) - { - Array.Reverse(hash); - } - - return BitConverter.ToString(hash).Replace("-", "_").ToUpper(); - } - - public static ResourceID GetResourceIDFromName(string name) - { - return BinaryPrimitives.ReadUInt32BigEndian(Crc32.Hash(Encoding.UTF8.GetBytes(name.ToLower()))); - } } From 73928f10d34fb5382290ba8ccd7c9316036bd219 Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Wed, 23 Jul 2025 17:42:59 -0400 Subject: [PATCH 03/15] fix splicer import throwing exception --- .../CLI/Commands/ImportResourceCommand.cs | 90 ++++++++++--------- Volatility/Resources/Splicer/Splicer.cs | 17 ++-- 2 files changed, 58 insertions(+), 49 deletions(-) diff --git a/Volatility/CLI/Commands/ImportResourceCommand.cs b/Volatility/CLI/Commands/ImportResourceCommand.cs index 107be28..c6b06ee 100644 --- a/Volatility/CLI/Commands/ImportResourceCommand.cs +++ b/Volatility/CLI/Commands/ImportResourceCommand.cs @@ -167,49 +167,57 @@ public async Task Execute() string sampleName = $"{resource.AssetName}_{samples[i].SampleID}"; - Console.WriteLine($"Writing extracted sample {sampleName}.snr"); - await File.WriteAllBytesAsync(Path.Combine(sampleDirectory, $"{sampleName}.snr"), samples[i].Data); - if (sxExists) - { - string samplePathName = Path.Combine(sampleDirectory, sampleName); - - string convertedSamplePathName = Path.Combine(sampleDirectory, "_extracted"); - - Directory.CreateDirectory(convertedSamplePathName); - - convertedSamplePathName = Path.Combine(convertedSamplePathName, sampleName); - - ProcessStartInfo start = new ProcessStartInfo - { - FileName = sxPath, - Arguments = $"-wave -s16l_int -v0 \"{samplePathName}.snr\" -=\"{convertedSamplePathName}.wav\"", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - using (Process process = new Process()) - { - process.StartInfo = start; - process.OutputDataReceived += (sender, e) => - { - if (!string.IsNullOrEmpty(e.Data)) Console.WriteLine(e.Data); - }; - - process.ErrorDataReceived += (sender, e) => - { - if (!string.IsNullOrEmpty(e.Data)) Console.WriteLine(e.Data); - }; - - Console.WriteLine($"Converting extracted sample {sampleName}.snr to wave..."); - process.Start(); - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - process.WaitForExit(); - } + string samplePathName = Path.Combine(sampleDirectory, sampleName); + + if (!File.Exists($"{samplePathName}.snr") || Overwrite) + { + Console.WriteLine($"Writing extracted sample {sampleName}.snr"); + await File.WriteAllBytesAsync($"{samplePathName}.snr", samples[i].Data); } + else + { + Console.WriteLine($"Skipping extracted sample {sampleName}.snr"); + } + + if (sxExists) + { + string convertedSamplePathName = Path.Combine(sampleDirectory, "_extracted"); + + Directory.CreateDirectory(convertedSamplePathName); + + convertedSamplePathName = Path.Combine(convertedSamplePathName, sampleName); + + ProcessStartInfo start = new ProcessStartInfo + { + FileName = sxPath, + Arguments = $"-wave -s16l_int -v0 \"{samplePathName}.snr\" -=\"{convertedSamplePathName}.wav\"", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (Process process = new Process()) + { + process.StartInfo = start; + process.OutputDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) Console.WriteLine(e.Data); + }; + + process.ErrorDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) Console.WriteLine(e.Data); + }; + + Console.WriteLine($"Converting extracted sample {sampleName}.snr to wave..."); + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + process.WaitForExit(); + } + } } } diff --git a/Volatility/Resources/Splicer/Splicer.cs b/Volatility/Resources/Splicer/Splicer.cs index 7b4bc23..c58d1b2 100644 --- a/Volatility/Resources/Splicer/Splicer.cs +++ b/Volatility/Resources/Splicer/Splicer.cs @@ -40,7 +40,6 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann if (version != 1) { throw new InvalidDataException("Version mismatch! Version should be 1."); - return; } int pSampleRefTOC = reader.ReadInt32(); @@ -49,7 +48,6 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann if (numSplices <= 0) { throw new InvalidDataException("No splices in Splicer file!"); - return; } int lowestSampleIndex = new int(); @@ -117,7 +115,7 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann SamplePtrOffset = (nint)(reader.BaseStream.Position - DataOffset); - _samples = new List(numSamples); + _samples = new List(); for (int i = 0; i < numSamples; i++) { reader.BaseStream.Seek(SamplePtrOffset + DataOffset + SamplePtrs[i], SeekOrigin.Begin); @@ -126,11 +124,14 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann byte[]? data = reader.ReadBytes(length); - _samples[i] = new Sample - { - SampleID = SnrID.HashFromBytes(data), - Data = data, - }; + _samples.Add + ( + new Sample + { + SampleID = SnrID.HashFromBytes(data), + Data = data, + } + ); data = null; } From 87ca915f2b28d5184475b0a4813e792fe4a3c8d0 Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Wed, 23 Jul 2025 18:22:45 -0400 Subject: [PATCH 04/15] convert to lists, reference samples by SnrID --- .../CLI/Commands/ImportResourceCommand.cs | 5 +- Volatility/Resources/Splicer/Splicer.cs | 99 +++++++++++-------- 2 files changed, 60 insertions(+), 44 deletions(-) diff --git a/Volatility/CLI/Commands/ImportResourceCommand.cs b/Volatility/CLI/Commands/ImportResourceCommand.cs index c6b06ee..8011048 100644 --- a/Volatility/CLI/Commands/ImportResourceCommand.cs +++ b/Volatility/CLI/Commands/ImportResourceCommand.cs @@ -161,12 +161,11 @@ public async Task Execute() List? samples = splicer?.GetLoadedSamples(); for (int i = 0; i < samples?.Count; i++) { - string sampleDirectory = Path.Combine(directoryPath, $"{resource.AssetName}_Samples"); + string sampleDirectory = Path.Combine(directoryPath, "Splicer", "Samples"); Directory.CreateDirectory(sampleDirectory); - string sampleName = $"{resource.AssetName}_{samples[i].SampleID}"; - + string sampleName = $"{samples[i].SampleID}"; string samplePathName = Path.Combine(sampleDirectory, sampleName); diff --git a/Volatility/Resources/Splicer/Splicer.cs b/Volatility/Resources/Splicer/Splicer.cs index c58d1b2..d476f07 100644 --- a/Volatility/Resources/Splicer/Splicer.cs +++ b/Volatility/Resources/Splicer/Splicer.cs @@ -14,8 +14,8 @@ public class Splicer : BinaryResource { public override ResourceType GetResourceType() => ResourceType.Splicer; - public SPLICE_Data[] Splices; - public SPLICE_SampleRef[] SampleRefs; + public List Splices; + public List SampleRefs; public IntPtr[] SamplePtrs; // This only gets populated when parsing from a stream. @@ -51,27 +51,33 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann } int lowestSampleIndex = new int(); + + if (Splices == null) + Splices = new List(numSplices); + // Read Splice Data - Splices = new SPLICE_Data[numSplices]; for (int i = 0; i < numSplices; i++) { // NameHash (null) _ = reader.ReadInt32(); - - Splices[i] = new SPLICE_Data() + + SPLICE_Data spliceData = new SPLICE_Data() { SpliceIndex = reader.ReadUInt16(), ESpliceType = reader.ReadSByte(), Num_SampleRefs = reader.ReadByte(), Volume = reader.ReadSingle(), RND_Pitch = reader.ReadSingle(), - RND_Vol = reader.ReadSingle(), + RND_Vol = reader.ReadSingle(), }; - + // pSampleRefList (null) _ = reader.ReadInt32(); - Splices[i].SampleListIndex = lowestSampleIndex; + spliceData.SampleListIndex = lowestSampleIndex; + + Splices.Add(spliceData); + lowestSampleIndex += Splices[i].Num_SampleRefs; } @@ -79,30 +85,6 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann SampleRefsPtrOffset = (nint)(reader.BaseStream.Position - DataOffset); - // Read SampleRefs - SampleRefs = new SPLICE_SampleRef[numSampleRefs]; - for (int i = 0; i < numSampleRefs; i++) - { - SampleRefs[i] = new SPLICE_SampleRef() - { - SampleIndex = reader.ReadUInt16(), - ESpliceType = reader.ReadSByte(), - Padding = reader.ReadByte(), - Volume = reader.ReadSingle(), - Pitch = reader.ReadSingle(), - Offset = reader.ReadSingle(), - Az = reader.ReadSingle(), - Duration = reader.ReadSingle(), - FadeIn = reader.ReadSingle(), - FadeOut = reader.ReadSingle(), - RND_Vol = reader.ReadSingle(), - RND_Pitch = reader.ReadSingle(), - Priority = reader.ReadByte(), - ERollOffType = reader.ReadByte(), - Padding2 = reader.ReadUInt16(), - }; - } - reader.BaseStream.Seek(pSampleRefTOC + 0xC + DataOffset, SeekOrigin.Begin); int numSamples = reader.ReadInt32(); @@ -115,7 +97,9 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann SamplePtrOffset = (nint)(reader.BaseStream.Position - DataOffset); - _samples = new List(); + if (_samples == null) + _samples = new List(numSamples); + for (int i = 0; i < numSamples; i++) { reader.BaseStream.Seek(SamplePtrOffset + DataOffset + SamplePtrs[i], SeekOrigin.Begin); @@ -135,6 +119,39 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann data = null; } + + reader.BaseStream.Seek(SampleRefsPtrOffset + DataOffset, SeekOrigin.Begin); + + if (SampleRefs == null) + SampleRefs = new List(numSampleRefs); + + // Read SampleRefs + for (int i = 0; i < numSampleRefs; i++) + { + int SampleIndex = reader.ReadUInt16(); + + SPLICE_SampleRef sampleRef = new SPLICE_SampleRef() + { + Sample = _samples[SampleIndex].SampleID, + ESpliceType = reader.ReadSByte(), + Padding = reader.ReadByte(), + Volume = reader.ReadSingle(), + Pitch = reader.ReadSingle(), + Offset = reader.ReadSingle(), + Az = reader.ReadSingle(), + Duration = reader.ReadSingle(), + FadeIn = reader.ReadSingle(), + FadeOut = reader.ReadSingle(), + RND_Vol = reader.ReadSingle(), + RND_Pitch = reader.ReadSingle(), + Priority = reader.ReadByte(), + ERollOffType = reader.ReadByte(), + Padding2 = reader.ReadUInt16(), + }; + + SampleRefs.Add(sampleRef); + } + } public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endianness = Endian.Agnostic) @@ -143,16 +160,16 @@ public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endian writer.BaseStream.Position = DataOffset; - writer.Write((int)1); // version + writer.Write(1); // version int sampleRefTOCPosition = (int)writer.BaseStream.Position; // Saving this for later - writer.Write((int)0); // pSampleRefTOC + writer.Write(0); // pSampleRefTOC - writer.Write((int)Splices.Length); // NumSplices + writer.Write(Splices.Count); // NumSplices // Write Splices - for (int i = 0; i < Splices.Length; i++) + for (int i = 0; i < Splices.Count; i++) { // NameHash, unused by game writer.Write(Encoding.Default.GetBytes("sper")); @@ -169,9 +186,9 @@ public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endian } // Write SampleRefs - for (int i = 0; i < SampleRefs.Length; i++) + for (int i = 0; i < SampleRefs.Count; i++) { - writer.Write(SampleRefs[i].SampleIndex); + writer.Write(_samples.FindIndex(s => s.SampleID.Equals(SampleRefs[i].Sample))); writer.Write(SampleRefs[i].ESpliceType); writer.Write(SampleRefs[i].Padding); writer.Write(SampleRefs[i].Volume); @@ -200,7 +217,7 @@ public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endian public void SpliceSamples(EndianAwareBinaryWriter writer, string samplesDir) { // Enumerate then write Samples - string samplesDirectory = Path.Combine(Path.GetDirectoryName(samplesDir), $"{AssetName}_Samples"); + string samplesDirectory = Path.Combine(Path.GetDirectoryName(samplesDir), "Splicer", "Samples"); string[] paths = Directory.GetFiles(samplesDirectory, "*.snr"); byte[][] samples = Array.Empty(); @@ -260,7 +277,7 @@ public struct SPLICE_Data [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct SPLICE_SampleRef { - public ushort SampleIndex; + public SnrID Sample; public sbyte ESpliceType; public byte Padding; public float Volume; From e8a612b9e34f3844855c2784340385d594a1f012 Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Thu, 24 Jul 2025 10:07:49 -0400 Subject: [PATCH 05/15] rewrite sample writing logic --- .../CLI/Commands/ExportResourceCommand.cs | 4 - Volatility/Resources/Splicer/Splicer.cs | 171 +++++++++++------- 2 files changed, 107 insertions(+), 68 deletions(-) diff --git a/Volatility/CLI/Commands/ExportResourceCommand.cs b/Volatility/CLI/Commands/ExportResourceCommand.cs index a4708dd..a535bbe 100644 --- a/Volatility/CLI/Commands/ExportResourceCommand.cs +++ b/Volatility/CLI/Commands/ExportResourceCommand.cs @@ -116,10 +116,6 @@ public async Task Execute() case TextureBase texture: texture.PushAll(); goto default; - case Splicer splicer: - splicer.WriteToStream(writer); - splicer.SpliceSamples(writer, Path.GetDirectoryName(sourceFile)); - break; default: resource.WriteToStream(writer); break; diff --git a/Volatility/Resources/Splicer/Splicer.cs b/Volatility/Resources/Splicer/Splicer.cs index d476f07..e7ce4ac 100644 --- a/Volatility/Resources/Splicer/Splicer.cs +++ b/Volatility/Resources/Splicer/Splicer.cs @@ -156,100 +156,143 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endianness = Endian.Agnostic) { + LoadDependentSamples(); + base.WriteToStream(writer, endianness); writer.BaseStream.Position = DataOffset; writer.Write(1); // version - int sampleRefTOCPosition = (int)writer.BaseStream.Position; // Saving this for later - - writer.Write(0); // pSampleRefTOC + long pSampleRefTocFieldPos = writer.BaseStream.Position; // Saving this for later + writer.Write(0); // pSampleRefTOC placeholder writer.Write(Splices.Count); // NumSplices - // Write Splices + var spliceSampleRefPtrPatchPos = new List(Splices.Count); + int runningSampleRefIndex = 0; for (int i = 0; i < Splices.Count; i++) { - // NameHash, unused by game - writer.Write(Encoding.Default.GetBytes("sper")); - - writer.Write(Splices[i].SpliceIndex); - writer.Write(Splices[i].ESpliceType); - writer.Write(Splices[i].Num_SampleRefs); - writer.Write(Splices[i].Volume); - writer.Write(Splices[i].RND_Pitch); - writer.Write(Splices[i].RND_Vol); - - // pSampleRefList, filled at runtime - writer.Write(Encoding.Default.GetBytes("dunk")); + var s = Splices[i]; + + writer.Write(s.NameHash); + writer.Write(s.SpliceIndex); + writer.Write(s.ESpliceType); + writer.Write(s.Num_SampleRefs); + writer.Write(s.Volume); + writer.Write(s.RND_Pitch); + writer.Write(s.RND_Vol); + + spliceSampleRefPtrPatchPos.Add(writer.BaseStream.Position); + writer.Write(0); // pSampleRefList placeholder + + s.SampleListIndex = runningSampleRefIndex; + Splices[i] = s; + runningSampleRefIndex += s.Num_SampleRefs; } - // Write SampleRefs + long sampleRefsStart = writer.BaseStream.Position; for (int i = 0; i < SampleRefs.Count; i++) { - writer.Write(_samples.FindIndex(s => s.SampleID.Equals(SampleRefs[i].Sample))); - writer.Write(SampleRefs[i].ESpliceType); - writer.Write(SampleRefs[i].Padding); - writer.Write(SampleRefs[i].Volume); - writer.Write(SampleRefs[i].Pitch); - writer.Write(SampleRefs[i].Offset); - writer.Write(SampleRefs[i].Az); - writer.Write(SampleRefs[i].Duration); - writer.Write(SampleRefs[i].FadeIn); - writer.Write(SampleRefs[i].FadeOut); - writer.Write(SampleRefs[i].RND_Vol); - writer.Write(SampleRefs[i].RND_Pitch); - writer.Write(SampleRefs[i].Priority); - writer.Write(SampleRefs[i].ERollOffType); - writer.Write(SampleRefs[i].Padding2); + var r = SampleRefs[i]; + int sampleIndex = _samples.FindIndex(s => s.SampleID.Equals(r.Sample)); + + writer.Write((ushort)sampleIndex); + writer.Write(r.ESpliceType); + writer.Write(r.Padding); + writer.Write(r.Volume); + writer.Write(r.Pitch); + writer.Write(r.Offset); + writer.Write(r.Az); + writer.Write(r.Duration); + writer.Write(r.FadeIn); + writer.Write(r.FadeOut); + writer.Write(r.RND_Vol); + writer.Write(r.RND_Pitch); + writer.Write(r.Priority); + writer.Write(r.ERollOffType); + writer.Write(r.Padding2); } + long sampleRefsEnd = writer.BaseStream.Position; - int sampleRefTOC = ((int)writer.BaseStream.Position) - (int)DataOffset; // Saving this for later - - writer.Seek(sampleRefTOCPosition, SeekOrigin.Begin); - - writer.Write(sampleRefTOC); - - writer.Seek(sampleRefTOCPosition + (int)DataOffset, SeekOrigin.Begin); - } + // Backpatch each splice's pSampleRefList + for (int i = 0; i < Splices.Count; i++) + { + long patchPos = spliceSampleRefPtrPatchPos[i]; + int offset = (int)(sampleRefsStart - DataOffset + Splices[i].SampleListIndex * Marshal.SizeOf()); + writer.Seek((int)patchPos, SeekOrigin.Begin); + writer.Write(offset); + } + writer.Seek((int)sampleRefsEnd, SeekOrigin.Begin); - public void SpliceSamples(EndianAwareBinaryWriter writer, string samplesDir) - { - // Enumerate then write Samples - string samplesDirectory = Path.Combine(Path.GetDirectoryName(samplesDir), "Splicer", "Samples"); + int numSamples = _samples.Count; + int pSampleRefTOC = (int)(writer.BaseStream.Position - DataOffset); - string[] paths = Directory.GetFiles(samplesDirectory, "*.snr"); - byte[][] samples = Array.Empty(); - int[] lengths = Array.Empty(); + writer.Write(numSamples); - int lowestIndex = 0; + // Reserve space for offsets + long offsetsStart = writer.BaseStream.Position; + for (int i = 0; i < numSamples; i++) writer.Write(0); - for (int i = 0; i < paths.Length; i++) + long dataStart = writer.BaseStream.Position; + int running = 0; + for (int i = 0; i < numSamples; i++) { - samples[i] = File.ReadAllBytes(paths[i]); - lengths[i] = samples[i].Length; + byte[] data = _samples[i].Data; + writer.Write(data); + long curPos = writer.BaseStream.Position; + // backfill this sample's offset + long save = writer.BaseStream.Position; + writer.Seek((int)(offsetsStart + i * 4), SeekOrigin.Begin); + writer.Write(running); + writer.Seek((int)save, SeekOrigin.Begin); + running += data.Length; + } - // Write SamplePtrs - writer.Write(lowestIndex); + // Backfill pSampleRefTOC in header + long endPos = writer.BaseStream.Position; + writer.Seek((int)pSampleRefTocFieldPos, SeekOrigin.Begin); + writer.Write(pSampleRefTOC); + writer.Seek((int)endPos, SeekOrigin.Begin); - lowestIndex += samples[i].Length; - } + // Update DataSize + DataSize = (uint)(writer.BaseStream.Length - 0x10); + long pos = writer.BaseStream.Position; + writer.BaseStream.Seek(0, SeekOrigin.Begin); + writer.Write(DataSize); + writer.BaseStream.Seek(pos, SeekOrigin.Begin); + } + + public void LoadDependentSamples(bool recurse = false) + { + var needed = SampleRefs.Select(r => r.Sample).Distinct().ToList(); + string dir = Path.Combine(AppContext.BaseDirectory, "data", "Resources", "Splicer", "Samples"); + var files = Directory.GetFiles(dir, "*.snr", recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); - for (int i = 0; i < samples.Length; i++) + var map = new Dictionary(needed.Count); + foreach (var f in files) { - writer.Write(samples[i]); + var data = File.ReadAllBytes(f); + var id = SnrID.HashFromBytes(data); + if (!map.ContainsKey(id) && needed.Contains(id)) + map[id] = data; } - long tempOffset = writer.BaseStream.Position; + foreach (var id in needed) + if (!map.ContainsKey(id)) + throw new FileNotFoundException($"Missing sample for {id}"); - // Handle the BinaryResource data size - // Not exactly a fan of how this is hardcoded. - DataSize = (uint)(writer.BaseStream.Length - 0x10); - writer.BaseStream.Seek(0, SeekOrigin.Begin); - writer.Write(DataSize); + _samples = new List(needed.Count); + SamplePtrs = new IntPtr[needed.Count]; - writer.BaseStream.Seek(tempOffset, SeekOrigin.Begin); + int running = 0; + for (int i = 0; i < needed.Count; i++) + { + var data = map[needed[i]]; + _samples.Add(new Sample { SampleID = needed[i], Data = data }); + SamplePtrs[i] = running; + running += data.Length; + } } public List GetLoadedSamples() From ced1041eca204f9db7ca02445cd150c796f9f824 Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Sun, 27 Jul 2025 16:28:24 -0400 Subject: [PATCH 06/15] remove splicer file-wide SamplePtrs & offsets --- Volatility/Resources/Splicer/Splicer.cs | 64 +++++++------------------ 1 file changed, 18 insertions(+), 46 deletions(-) diff --git a/Volatility/Resources/Splicer/Splicer.cs b/Volatility/Resources/Splicer/Splicer.cs index e7ce4ac..109d1b8 100644 --- a/Volatility/Resources/Splicer/Splicer.cs +++ b/Volatility/Resources/Splicer/Splicer.cs @@ -16,22 +16,11 @@ public class Splicer : BinaryResource public List Splices; public List SampleRefs; - public IntPtr[] SamplePtrs; - // This only gets populated when parsing from a stream. - // Not sure whether this is a good idea to keep as-is. + // Only gets populated when parsing from a stream, or when + // loading referenced sample IDs through LoadDependentSamples. private List _samples; - // Used to make reading parsed files easier. - // May remove or keep as generated values - // to make reading & editing easier. - [EditorReadOnly] - public IntPtr SampleRefsPtrOffset; - - [EditorReadOnly] - public IntPtr SamplePtrOffset; - - public override void ParseFromStream(ResourceBinaryReader reader, Endian endianness = Endian.Agnostic) { base.ParseFromStream(reader, endianness); @@ -83,28 +72,28 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann int numSampleRefs = Splices[numSplices - 1].SampleListIndex + Splices[numSplices - 1].Num_SampleRefs; - SampleRefsPtrOffset = (nint)(reader.BaseStream.Position - DataOffset); + long _sampleRefsPtrOffset = reader.BaseStream.Position - DataOffset; reader.BaseStream.Seek(pSampleRefTOC + 0xC + DataOffset, SeekOrigin.Begin); int numSamples = reader.ReadInt32(); - SamplePtrs = new IntPtr[numSamples]; + List _samplePtrs = new List(numSamples); for (int i = 0; i < numSamples; i++) { - SamplePtrs[i] = reader.ReadInt32(); + _samplePtrs.Add(reader.ReadInt32()); } - SamplePtrOffset = (nint)(reader.BaseStream.Position - DataOffset); + long _samplePtrOffset = reader.BaseStream.Position - DataOffset; if (_samples == null) _samples = new List(numSamples); for (int i = 0; i < numSamples; i++) { - reader.BaseStream.Seek(SamplePtrOffset + DataOffset + SamplePtrs[i], SeekOrigin.Begin); + reader.BaseStream.Seek(_samplePtrOffset + DataOffset + _samplePtrs[i], SeekOrigin.Begin); - int length = (int)((i == (numSamples - 1) ? reader.BaseStream.Length : SamplePtrs[i + 1]) - SamplePtrs[i]); + int length = (int)((i == (numSamples - 1) ? reader.BaseStream.Length : _samplePtrs[i + 1]) - _samplePtrs[i]); byte[]? data = reader.ReadBytes(length); @@ -120,7 +109,7 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann data = null; } - reader.BaseStream.Seek(SampleRefsPtrOffset + DataOffset, SeekOrigin.Begin); + reader.BaseStream.Seek(_sampleRefsPtrOffset + DataOffset, SeekOrigin.Begin); if (SampleRefs == null) SampleRefs = new List(numSampleRefs); @@ -192,12 +181,10 @@ public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endian } long sampleRefsStart = writer.BaseStream.Position; - for (int i = 0; i < SampleRefs.Count; i++) + foreach (var r in SampleRefs) { - var r = SampleRefs[i]; - int sampleIndex = _samples.FindIndex(s => s.SampleID.Equals(r.Sample)); - - writer.Write((ushort)sampleIndex); + int idx = _samples.FindIndex(s => s.SampleID.Equals(r.Sample)); + writer.Write((ushort)idx); writer.Write(r.ESpliceType); writer.Write(r.Padding); writer.Write(r.Volume); @@ -213,18 +200,16 @@ public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endian writer.Write(r.ERollOffType); writer.Write(r.Padding2); } - long sampleRefsEnd = writer.BaseStream.Position; + int sampleRefSize = Marshal.SizeOf(); // Backpatch each splice's pSampleRefList for (int i = 0; i < Splices.Count; i++) { - long patchPos = spliceSampleRefPtrPatchPos[i]; - int offset = (int)(sampleRefsStart - DataOffset + Splices[i].SampleListIndex * Marshal.SizeOf()); - writer.Seek((int)patchPos, SeekOrigin.Begin); - writer.Write(offset); + writer.BaseStream.Position = spliceSampleRefPtrPatchPos[i]; + int rel = (int)((sampleRefsStart - DataOffset) + Splices[i].SampleListIndex * sampleRefSize); + writer.Write(rel); } - writer.Seek((int)sampleRefsEnd, SeekOrigin.Begin); - + writer.BaseStream.Position = sampleRefsStart; int numSamples = _samples.Count; int pSampleRefTOC = (int)(writer.BaseStream.Position - DataOffset); @@ -234,13 +219,11 @@ public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endian long offsetsStart = writer.BaseStream.Position; for (int i = 0; i < numSamples; i++) writer.Write(0); - long dataStart = writer.BaseStream.Position; int running = 0; for (int i = 0; i < numSamples; i++) { byte[] data = _samples[i].Data; writer.Write(data); - long curPos = writer.BaseStream.Position; // backfill this sample's offset long save = writer.BaseStream.Position; writer.Seek((int)(offsetsStart + i * 4), SeekOrigin.Begin); @@ -281,18 +264,7 @@ public void LoadDependentSamples(bool recurse = false) foreach (var id in needed) if (!map.ContainsKey(id)) throw new FileNotFoundException($"Missing sample for {id}"); - - _samples = new List(needed.Count); - SamplePtrs = new IntPtr[needed.Count]; - - int running = 0; - for (int i = 0; i < needed.Count; i++) - { - var data = map[needed[i]]; - _samples.Add(new Sample { SampleID = needed[i], Data = data }); - SamplePtrs[i] = running; - running += data.Length; - } + _samples = needed.Select(id => new Sample { SampleID = id, Data = map[id] }).ToList(); } public List GetLoadedSamples() From 210dfcdadd245198e2f7ce13448f8bcdd0d6f14d Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Tue, 29 Jul 2025 14:02:10 -0400 Subject: [PATCH 07/15] remove unnecessary backfills --- Volatility/Resources/Splicer/Splicer.cs | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/Volatility/Resources/Splicer/Splicer.cs b/Volatility/Resources/Splicer/Splicer.cs index 109d1b8..b237869 100644 --- a/Volatility/Resources/Splicer/Splicer.cs +++ b/Volatility/Resources/Splicer/Splicer.cs @@ -152,9 +152,11 @@ public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endian writer.BaseStream.Position = DataOffset; writer.Write(1); // version + + int sizeOfSplices = Splices.Count * 0x18; // Size of Splice_Data + int sizeOfSampleRefs = SampleRefs.Count * 0x2C; // Size of Splice_SampleRef - long pSampleRefTocFieldPos = writer.BaseStream.Position; // Saving this for later - writer.Write(0); // pSampleRefTOC placeholder + writer.Write(sizeOfSplices + sizeOfSampleRefs); // sizedata/pSampleRefTOC writer.Write(Splices.Count); // NumSplices @@ -172,7 +174,6 @@ public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endian writer.Write(s.RND_Pitch); writer.Write(s.RND_Vol); - spliceSampleRefPtrPatchPos.Add(writer.BaseStream.Position); writer.Write(0); // pSampleRefList placeholder s.SampleListIndex = runningSampleRefIndex; @@ -202,13 +203,6 @@ public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endian } int sampleRefSize = Marshal.SizeOf(); - // Backpatch each splice's pSampleRefList - for (int i = 0; i < Splices.Count; i++) - { - writer.BaseStream.Position = spliceSampleRefPtrPatchPos[i]; - int rel = (int)((sampleRefsStart - DataOffset) + Splices[i].SampleListIndex * sampleRefSize); - writer.Write(rel); - } writer.BaseStream.Position = sampleRefsStart; int numSamples = _samples.Count; int pSampleRefTOC = (int)(writer.BaseStream.Position - DataOffset); @@ -232,12 +226,6 @@ public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endian running += data.Length; } - // Backfill pSampleRefTOC in header - long endPos = writer.BaseStream.Position; - writer.Seek((int)pSampleRefTocFieldPos, SeekOrigin.Begin); - writer.Write(pSampleRefTOC); - writer.Seek((int)endPos, SeekOrigin.Begin); - // Update DataSize DataSize = (uint)(writer.BaseStream.Length - 0x10); long pos = writer.BaseStream.Position; From 1d71dbdc222d7bbc4aa793a016e139bb441fe2a0 Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Tue, 29 Jul 2025 14:24:13 -0400 Subject: [PATCH 08/15] remove unused line (whoops) --- Volatility/Resources/Splicer/Splicer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Volatility/Resources/Splicer/Splicer.cs b/Volatility/Resources/Splicer/Splicer.cs index b237869..221479e 100644 --- a/Volatility/Resources/Splicer/Splicer.cs +++ b/Volatility/Resources/Splicer/Splicer.cs @@ -201,7 +201,6 @@ public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endian writer.Write(r.ERollOffType); writer.Write(r.Padding2); } - int sampleRefSize = Marshal.SizeOf(); writer.BaseStream.Position = sampleRefsStart; int numSamples = _samples.Count; From 89a8713dcbce028d5115b021194121416d4c4eca Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Tue, 29 Jul 2025 14:41:03 -0400 Subject: [PATCH 09/15] don't overwrite samplerefs with TOC (exports properly now) --- Volatility/Resources/Splicer/Splicer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Volatility/Resources/Splicer/Splicer.cs b/Volatility/Resources/Splicer/Splicer.cs index 221479e..89259c6 100644 --- a/Volatility/Resources/Splicer/Splicer.cs +++ b/Volatility/Resources/Splicer/Splicer.cs @@ -1,5 +1,4 @@ using System.Runtime.InteropServices; -using System.Text; namespace Volatility.Resources; @@ -155,8 +154,9 @@ public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endian int sizeOfSplices = Splices.Count * 0x18; // Size of Splice_Data int sizeOfSampleRefs = SampleRefs.Count * 0x2C; // Size of Splice_SampleRef + int sizedata = sizeOfSplices + sizeOfSampleRefs; - writer.Write(sizeOfSplices + sizeOfSampleRefs); // sizedata/pSampleRefTOC + writer.Write(sizedata); // sizedata/pSampleRefTOC writer.Write(Splices.Count); // NumSplices @@ -202,7 +202,7 @@ public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endian writer.Write(r.Padding2); } - writer.BaseStream.Position = sampleRefsStart; + writer.BaseStream.Position = DataOffset + 0xC + sizedata; // Header + sizedata int numSamples = _samples.Count; int pSampleRefTOC = (int)(writer.BaseStream.Position - DataOffset); From 6857c79d44988dd4f768f490f17fa3dce8d8c2fc Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Tue, 29 Jul 2025 23:18:37 -0400 Subject: [PATCH 10/15] add SerializeStructure --- Volatility/Utilities/ClassUtilities.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Volatility/Utilities/ClassUtilities.cs b/Volatility/Utilities/ClassUtilities.cs index ba30ddf..5cecd7c 100644 --- a/Volatility/Utilities/ClassUtilities.cs +++ b/Volatility/Utilities/ClassUtilities.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.InteropServices; namespace Volatility.Utilities; @@ -26,4 +27,17 @@ public static Type[] GetDerivedTypes(Type baseType) { return member.GetCustomAttribute(); } + + public static Byte[] SerializeStructure(T msg) where T : struct + { + int objsize = Marshal.SizeOf(); + byte[] ret = new byte[objsize]; + IntPtr buff = Marshal.AllocHGlobal(objsize); + + Marshal.StructureToPtr(msg, buff, true); + Marshal.Copy(buff, ret, 0, objsize); + Marshal.FreeHGlobal(buff); + + return ret; + } } From 8c341bdae09d2fc5be65794af0d3de988dec4cf8 Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Tue, 29 Jul 2025 23:21:10 -0400 Subject: [PATCH 11/15] initial ID-based sampleref system --- .../CLI/Commands/ImportResourceCommand.cs | 83 ++++++++++------ Volatility/Resources/Splicer/Splicer.cs | 95 ++++++++++++++----- 2 files changed, 125 insertions(+), 53 deletions(-) diff --git a/Volatility/CLI/Commands/ImportResourceCommand.cs b/Volatility/CLI/Commands/ImportResourceCommand.cs index 8011048..df3a675 100644 --- a/Volatility/CLI/Commands/ImportResourceCommand.cs +++ b/Volatility/CLI/Commands/ImportResourceCommand.cs @@ -161,7 +161,7 @@ public async Task Execute() List? samples = splicer?.GetLoadedSamples(); for (int i = 0; i < samples?.Count; i++) { - string sampleDirectory = Path.Combine(directoryPath, "Splicer", "Samples"); + string sampleDirectory = Path.Combine("data", "Splicer", "Samples"); Directory.CreateDirectory(sampleDirectory); @@ -185,42 +185,63 @@ public async Task Execute() Directory.CreateDirectory(convertedSamplePathName); - convertedSamplePathName = Path.Combine(convertedSamplePathName, sampleName); + convertedSamplePathName = Path.Combine(convertedSamplePathName, sampleName + ".wav"); - ProcessStartInfo start = new ProcessStartInfo + if (!File.Exists(convertedSamplePathName) || Overwrite) { - FileName = sxPath, - Arguments = $"-wave -s16l_int -v0 \"{samplePathName}.snr\" -=\"{convertedSamplePathName}.wav\"", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - using (Process process = new Process()) + ProcessStartInfo start = new ProcessStartInfo + { + FileName = sxPath, + Arguments = $"-wave -s16l_int -v0 \"{samplePathName}.snr\" -=\"{convertedSamplePathName}\"", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (Process process = new Process()) + { + process.StartInfo = start; + process.OutputDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) Console.WriteLine(e.Data); + }; + + process.ErrorDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) Console.WriteLine(e.Data); + }; + + Console.WriteLine($"Converting extracted sample {sampleName}.snr to wave..."); + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + process.WaitForExit(); + } + } + else { - process.StartInfo = start; - process.OutputDataReceived += (sender, e) => - { - if (!string.IsNullOrEmpty(e.Data)) Console.WriteLine(e.Data); - }; - - process.ErrorDataReceived += (sender, e) => - { - if (!string.IsNullOrEmpty(e.Data)) Console.WriteLine(e.Data); - }; - - Console.WriteLine($"Converting extracted sample {sampleName}.snr to wave..."); - process.Start(); - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - process.WaitForExit(); - } - } + Console.WriteLine($"Converted sample {Path.GetFileName(convertedSamplePathName)} already exists, skipping..."); + } + } } + var _srSerializer = new SerializerBuilder().Build(); + foreach (var kv in splicer.SampleRefs) + { + var path = Path.Combine("data", "Splicer", "SampleRefs"); + + Directory.CreateDirectory(path); + + path = Path.Combine(path, kv.Key + ".yaml"); + + if (!File.Exists(path) || Overwrite) + File.WriteAllText(path, _srSerializer.Serialize(kv.Value)); + else + Console.WriteLine($"SampleRef {kv.Key} already exists, skipping..."); + } } - Console.WriteLine($"Imported {Path.GetFileName(ImportPath)} as {Path.GetFullPath(filePath)}."); + Console.WriteLine($"Imported {Path.GetFileName(sourceFile)} as {Path.GetFullPath(filePath)}."); })); } await Task.WhenAll(tasks); diff --git a/Volatility/Resources/Splicer/Splicer.cs b/Volatility/Resources/Splicer/Splicer.cs index 89259c6..983701f 100644 --- a/Volatility/Resources/Splicer/Splicer.cs +++ b/Volatility/Resources/Splicer/Splicer.cs @@ -1,5 +1,10 @@ using System.Runtime.InteropServices; +using Volatility.Utilities; + +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + namespace Volatility.Resources; // The Splicer resource type contains multiple sound assets and presets for @@ -13,12 +18,12 @@ public class Splicer : BinaryResource { public override ResourceType GetResourceType() => ResourceType.Splicer; - public List Splices; - public List SampleRefs; + public List Splices = new(); + public Dictionary SampleRefs = new(); // Only gets populated when parsing from a stream, or when // loading referenced sample IDs through LoadDependentSamples. - private List _samples; + private List _samples = new(); public override void ParseFromStream(ResourceBinaryReader reader, Endian endianness = Endian.Agnostic) { @@ -91,7 +96,7 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann for (int i = 0; i < numSamples; i++) { reader.BaseStream.Seek(_samplePtrOffset + DataOffset + _samplePtrs[i], SeekOrigin.Begin); - + int length = (int)((i == (numSamples - 1) ? reader.BaseStream.Length : _samplePtrs[i + 1]) - _samplePtrs[i]); byte[]? data = reader.ReadBytes(length); @@ -105,13 +110,15 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann } ); + Console.WriteLine($"Adding sample {i} as {_samples[i].SampleID}"); + data = null; } reader.BaseStream.Seek(_sampleRefsPtrOffset + DataOffset, SeekOrigin.Begin); if (SampleRefs == null) - SampleRefs = new List(numSampleRefs); + SampleRefs = new Dictionary(numSampleRefs); // Read SampleRefs for (int i = 0; i < numSampleRefs; i++) @@ -137,7 +144,13 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann Padding2 = reader.ReadUInt16(), }; - SampleRefs.Add(sampleRef); + SnrID _sampleRefID = new SnrID + { + Value = SnrID.HashFromBytes(ClassUtilities.SerializeStructure(sampleRef)) + }; + + if (!SampleRefs.TryAdd(_sampleRefID, sampleRef)) + Console.WriteLine($"Skipping identical SampleRef {_sampleRefID}"); } } @@ -145,6 +158,7 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endianness = Endian.Agnostic) { LoadDependentSamples(); + LoadDependentSampleRefs(); base.WriteToStream(writer, endianness); @@ -184,22 +198,22 @@ public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endian long sampleRefsStart = writer.BaseStream.Position; foreach (var r in SampleRefs) { - int idx = _samples.FindIndex(s => s.SampleID.Equals(r.Sample)); + int idx = _samples.FindIndex(s => s.SampleID.Equals(r.Value.Sample)); writer.Write((ushort)idx); - writer.Write(r.ESpliceType); - writer.Write(r.Padding); - writer.Write(r.Volume); - writer.Write(r.Pitch); - writer.Write(r.Offset); - writer.Write(r.Az); - writer.Write(r.Duration); - writer.Write(r.FadeIn); - writer.Write(r.FadeOut); - writer.Write(r.RND_Vol); - writer.Write(r.RND_Pitch); - writer.Write(r.Priority); - writer.Write(r.ERollOffType); - writer.Write(r.Padding2); + writer.Write(r.Value.ESpliceType); + writer.Write(r.Value.Padding); + writer.Write(r.Value.Volume); + writer.Write(r.Value.Pitch); + writer.Write(r.Value.Offset); + writer.Write(r.Value.Az); + writer.Write(r.Value.Duration); + writer.Write(r.Value.FadeIn); + writer.Write(r.Value.FadeOut); + writer.Write(r.Value.RND_Vol); + writer.Write(r.Value.RND_Pitch); + writer.Write(r.Value.Priority); + writer.Write(r.Value.ERollOffType); + writer.Write(r.Value.Padding2); } writer.BaseStream.Position = DataOffset + 0xC + sizedata; // Header + sizedata @@ -235,7 +249,7 @@ public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endian public void LoadDependentSamples(bool recurse = false) { - var needed = SampleRefs.Select(r => r.Sample).Distinct().ToList(); + var needed = SampleRefs.Select(r => r.Value.Sample).Distinct().ToList(); string dir = Path.Combine(AppContext.BaseDirectory, "data", "Resources", "Splicer", "Samples"); var files = Directory.GetFiles(dir, "*.snr", recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); @@ -254,6 +268,42 @@ public void LoadDependentSamples(bool recurse = false) _samples = needed.Select(id => new Sample { SampleID = id, Data = map[id] }).ToList(); } + public void LoadDependentSampleRefs() + { + var neededIds = Splices + .SelectMany(s => s.SampleRefIDs) + .Distinct() + .ToList(); + + var deserializer = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .IgnoreUnmatchedProperties() + .Build(); + + if (SampleRefs == null) + SampleRefs = new Dictionary(); + + var baseDir = Path.Combine( + AppContext.BaseDirectory, + "data", "Splicer", "SampleRefs" + ); + + foreach (var id in neededIds) + { + if (SampleRefs.ContainsKey(id)) + continue; + + var yamlFile = Path.Combine(baseDir, $"{id}.yaml"); + if (!File.Exists(yamlFile)) + throw new FileNotFoundException($"Missing SampleRef for {id}"); + + var yaml = File.ReadAllText(yamlFile); + var model = deserializer.Deserialize(yaml); + + SampleRefs[id] = model; + } + } + public List GetLoadedSamples() { return _samples; @@ -274,6 +324,7 @@ public struct SPLICE_Data public float RND_Pitch; public float RND_Vol; public int SampleListIndex; + public List SampleRefIDs { get; set; } } [StructLayout(LayoutKind.Sequential, Pack = 1)] From b6b729f9288bf52b6ea0d87380458dd1766045c4 Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Tue, 29 Jul 2025 23:30:33 -0400 Subject: [PATCH 12/15] update splicer writer to support ID references --- Volatility/Resources/Splicer/Splicer.cs | 49 ++++++++++++++----------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/Volatility/Resources/Splicer/Splicer.cs b/Volatility/Resources/Splicer/Splicer.cs index 983701f..a01d3b3 100644 --- a/Volatility/Resources/Splicer/Splicer.cs +++ b/Volatility/Resources/Splicer/Splicer.cs @@ -165,7 +165,8 @@ public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endian writer.BaseStream.Position = DataOffset; writer.Write(1); // version - + + int totalRefs = Splices.Sum(s => s.SampleRefIDs.Count); int sizeOfSplices = Splices.Count * 0x18; // Size of Splice_Data int sizeOfSampleRefs = SampleRefs.Count * 0x2C; // Size of Splice_SampleRef int sizedata = sizeOfSplices + sizeOfSampleRefs; @@ -174,7 +175,6 @@ public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endian writer.Write(Splices.Count); // NumSplices - var spliceSampleRefPtrPatchPos = new List(Splices.Count); int runningSampleRefIndex = 0; for (int i = 0; i < Splices.Count; i++) { @@ -183,7 +183,7 @@ public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endian writer.Write(s.NameHash); writer.Write(s.SpliceIndex); writer.Write(s.ESpliceType); - writer.Write(s.Num_SampleRefs); + writer.Write((byte)s.SampleRefIDs.Count); writer.Write(s.Volume); writer.Write(s.RND_Pitch); writer.Write(s.RND_Vol); @@ -192,28 +192,33 @@ public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endian s.SampleListIndex = runningSampleRefIndex; Splices[i] = s; - runningSampleRefIndex += s.Num_SampleRefs; + runningSampleRefIndex += s.SampleRefIDs.Count; } long sampleRefsStart = writer.BaseStream.Position; - foreach (var r in SampleRefs) + foreach (var splice in Splices) { - int idx = _samples.FindIndex(s => s.SampleID.Equals(r.Value.Sample)); - writer.Write((ushort)idx); - writer.Write(r.Value.ESpliceType); - writer.Write(r.Value.Padding); - writer.Write(r.Value.Volume); - writer.Write(r.Value.Pitch); - writer.Write(r.Value.Offset); - writer.Write(r.Value.Az); - writer.Write(r.Value.Duration); - writer.Write(r.Value.FadeIn); - writer.Write(r.Value.FadeOut); - writer.Write(r.Value.RND_Vol); - writer.Write(r.Value.RND_Pitch); - writer.Write(r.Value.Priority); - writer.Write(r.Value.ERollOffType); - writer.Write(r.Value.Padding2); + foreach (var refId in splice.SampleRefIDs) + { + var sr = SampleRefs[refId]; + int sampleIdx = _samples.FindIndex(x => x.SampleID.Equals(sr.Sample)); + + writer.Write((ushort)sampleIdx); + writer.Write(sr.ESpliceType); + writer.Write(sr.Padding); + writer.Write(sr.Volume); + writer.Write(sr.Pitch); + writer.Write(sr.Offset); + writer.Write(sr.Az); + writer.Write(sr.Duration); + writer.Write(sr.FadeIn); + writer.Write(sr.FadeOut); + writer.Write(sr.RND_Vol); + writer.Write(sr.RND_Pitch); + writer.Write(sr.Priority); + writer.Write(sr.ERollOffType); + writer.Write(sr.Padding2); + } } writer.BaseStream.Position = DataOffset + 0xC + sizedata; // Header + sizedata @@ -240,7 +245,7 @@ public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endian } // Update DataSize - DataSize = (uint)(writer.BaseStream.Length - 0x10); + DataSize = (uint)(writer.BaseStream.Length - DataOffset); long pos = writer.BaseStream.Position; writer.BaseStream.Seek(0, SeekOrigin.Begin); writer.Write(DataSize); From d5a56f2eefe9366fcae439b593d242f33325df5a Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Tue, 29 Jul 2025 23:31:55 -0400 Subject: [PATCH 13/15] update sample path (whoops) --- Volatility/Resources/Splicer/Splicer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Volatility/Resources/Splicer/Splicer.cs b/Volatility/Resources/Splicer/Splicer.cs index a01d3b3..a53c324 100644 --- a/Volatility/Resources/Splicer/Splicer.cs +++ b/Volatility/Resources/Splicer/Splicer.cs @@ -255,7 +255,7 @@ public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endian public void LoadDependentSamples(bool recurse = false) { var needed = SampleRefs.Select(r => r.Value.Sample).Distinct().ToList(); - string dir = Path.Combine(AppContext.BaseDirectory, "data", "Resources", "Splicer", "Samples"); + string dir = Path.Combine(AppContext.BaseDirectory, "data", "Splicer", "Samples"); var files = Directory.GetFiles(dir, "*.snr", recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); var map = new Dictionary(needed.Count); From 1cd53387a0b8ed7804f370733112d8830948d61d Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Wed, 30 Jul 2025 10:14:17 -0400 Subject: [PATCH 14/15] embed samplerefs in splce instead of storing by ID --- .../CLI/Commands/ImportResourceCommand.cs | 15 -- Volatility/Resources/Splicer/Splicer.cs | 190 +++++++----------- 2 files changed, 69 insertions(+), 136 deletions(-) diff --git a/Volatility/CLI/Commands/ImportResourceCommand.cs b/Volatility/CLI/Commands/ImportResourceCommand.cs index df3a675..9b1bb04 100644 --- a/Volatility/CLI/Commands/ImportResourceCommand.cs +++ b/Volatility/CLI/Commands/ImportResourceCommand.cs @@ -225,21 +225,6 @@ public async Task Execute() } } } - - var _srSerializer = new SerializerBuilder().Build(); - foreach (var kv in splicer.SampleRefs) - { - var path = Path.Combine("data", "Splicer", "SampleRefs"); - - Directory.CreateDirectory(path); - - path = Path.Combine(path, kv.Key + ".yaml"); - - if (!File.Exists(path) || Overwrite) - File.WriteAllText(path, _srSerializer.Serialize(kv.Value)); - else - Console.WriteLine($"SampleRef {kv.Key} already exists, skipping..."); - } } Console.WriteLine($"Imported {Path.GetFileName(sourceFile)} as {Path.GetFullPath(filePath)}."); })); diff --git a/Volatility/Resources/Splicer/Splicer.cs b/Volatility/Resources/Splicer/Splicer.cs index a53c324..b48a8eb 100644 --- a/Volatility/Resources/Splicer/Splicer.cs +++ b/Volatility/Resources/Splicer/Splicer.cs @@ -2,9 +2,6 @@ using Volatility.Utilities; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; - namespace Volatility.Resources; // The Splicer resource type contains multiple sound assets and presets for @@ -19,10 +16,9 @@ public class Splicer : BinaryResource public override ResourceType GetResourceType() => ResourceType.Splicer; public List Splices = new(); - public Dictionary SampleRefs = new(); // Only gets populated when parsing from a stream, or when - // loading referenced sample IDs through LoadDependentSamples. + // loading referenced sample IDs through LoadDependentSamples private List _samples = new(); public override void ParseFromStream(ResourceBinaryReader reader, Endian endianness = Endian.Agnostic) @@ -43,7 +39,7 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann throw new InvalidDataException("No splices in Splicer file!"); } - int lowestSampleIndex = new int(); + var spliceRefCounts = new List(numSplices); if (Splices == null) Splices = new List(numSplices); @@ -51,30 +47,29 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann // Read Splice Data for (int i = 0; i < numSplices; i++) { - // NameHash (null) - _ = reader.ReadInt32(); - - SPLICE_Data spliceData = new SPLICE_Data() + uint nameHash = reader.ReadUInt32(); + ushort spliceIdx = reader.ReadUInt16(); + sbyte eType = reader.ReadSByte(); + byte numRefs = reader.ReadByte(); + float vol = reader.ReadSingle(); + float rpitch = reader.ReadSingle(); + float rvol = reader.ReadSingle(); + _ = reader.ReadInt32(); // pSampleRefList (null) + + spliceRefCounts.Add(numRefs); + Splices.Add(new SPLICE_Data { - SpliceIndex = reader.ReadUInt16(), - ESpliceType = reader.ReadSByte(), - Num_SampleRefs = reader.ReadByte(), - Volume = reader.ReadSingle(), - RND_Pitch = reader.ReadSingle(), - RND_Vol = reader.ReadSingle(), - }; - - // pSampleRefList (null) - _ = reader.ReadInt32(); - - spliceData.SampleListIndex = lowestSampleIndex; - - Splices.Add(spliceData); - - lowestSampleIndex += Splices[i].Num_SampleRefs; + NameHash = nameHash, + SpliceIndex = spliceIdx, + ESpliceType = eType, + Volume = vol, + RND_Pitch = rpitch, + RND_Vol = rvol, + SampleRefs = new List(numRefs) + }); } - int numSampleRefs = Splices[numSplices - 1].SampleListIndex + Splices[numSplices - 1].Num_SampleRefs; + int numSampleRefs = spliceRefCounts.Sum(); long _sampleRefsPtrOffset = reader.BaseStream.Position - DataOffset; @@ -117,48 +112,41 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann reader.BaseStream.Seek(_sampleRefsPtrOffset + DataOffset, SeekOrigin.Begin); - if (SampleRefs == null) - SampleRefs = new Dictionary(numSampleRefs); - // Read SampleRefs - for (int i = 0; i < numSampleRefs; i++) + for (int i = 0; i < Splices.Count; i++) { - int SampleIndex = reader.ReadUInt16(); + int count = spliceRefCounts[i]; + var list = Splices[i].SampleRefs; - SPLICE_SampleRef sampleRef = new SPLICE_SampleRef() + for (int j = 0; j < count; j++) { - Sample = _samples[SampleIndex].SampleID, - ESpliceType = reader.ReadSByte(), - Padding = reader.ReadByte(), - Volume = reader.ReadSingle(), - Pitch = reader.ReadSingle(), - Offset = reader.ReadSingle(), - Az = reader.ReadSingle(), - Duration = reader.ReadSingle(), - FadeIn = reader.ReadSingle(), - FadeOut = reader.ReadSingle(), - RND_Vol = reader.ReadSingle(), - RND_Pitch = reader.ReadSingle(), - Priority = reader.ReadByte(), - ERollOffType = reader.ReadByte(), - Padding2 = reader.ReadUInt16(), - }; - - SnrID _sampleRefID = new SnrID - { - Value = SnrID.HashFromBytes(ClassUtilities.SerializeStructure(sampleRef)) - }; - - if (!SampleRefs.TryAdd(_sampleRefID, sampleRef)) - Console.WriteLine($"Skipping identical SampleRef {_sampleRefID}"); + ushort sampleIdx = reader.ReadUInt16(); + var sr = new SPLICE_SampleRef + { + Sample = _samples[sampleIdx].SampleID, + ESpliceType = reader.ReadSByte(), + Padding = reader.ReadByte(), + Volume = reader.ReadSingle(), + Pitch = reader.ReadSingle(), + Offset = reader.ReadSingle(), + Az = reader.ReadSingle(), + Duration = reader.ReadSingle(), + FadeIn = reader.ReadSingle(), + FadeOut = reader.ReadSingle(), + RND_Vol = reader.ReadSingle(), + RND_Pitch = reader.ReadSingle(), + Priority = reader.ReadByte(), + ERollOffType = reader.ReadByte(), + Padding2 = reader.ReadUInt16() + }; + list.Add(sr); + } } - } public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endianness = Endian.Agnostic) { LoadDependentSamples(); - LoadDependentSampleRefs(); base.WriteToStream(writer, endianness); @@ -166,43 +154,37 @@ public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endian writer.Write(1); // version - int totalRefs = Splices.Sum(s => s.SampleRefIDs.Count); + int totalRefs = Splices.Sum(s => s.SampleRefs.Count); int sizeOfSplices = Splices.Count * 0x18; // Size of Splice_Data - int sizeOfSampleRefs = SampleRefs.Count * 0x2C; // Size of Splice_SampleRef + int sizeOfSampleRefs = totalRefs * 0x2C; // Size of Splice_SampleRef int sizedata = sizeOfSplices + sizeOfSampleRefs; writer.Write(sizedata); // sizedata/pSampleRefTOC writer.Write(Splices.Count); // NumSplices - int runningSampleRefIndex = 0; - for (int i = 0; i < Splices.Count; i++) - { - var s = Splices[i]; + var spliceStartIndices = new List(Splices.Count); + int runningIndex = 0; - writer.Write(s.NameHash); - writer.Write(s.SpliceIndex); - writer.Write(s.ESpliceType); - writer.Write((byte)s.SampleRefIDs.Count); - writer.Write(s.Volume); - writer.Write(s.RND_Pitch); - writer.Write(s.RND_Vol); + foreach (SPLICE_Data splice in Splices) + { + writer.Write(splice.NameHash); + writer.Write(splice.SpliceIndex); + writer.Write(splice.ESpliceType); + writer.Write((byte)splice.SampleRefs.Count); + writer.Write(splice.Volume); + writer.Write(splice.RND_Pitch); + writer.Write(splice.RND_Vol); writer.Write(0); // pSampleRefList placeholder - - s.SampleListIndex = runningSampleRefIndex; - Splices[i] = s; - runningSampleRefIndex += s.SampleRefIDs.Count; } long sampleRefsStart = writer.BaseStream.Position; foreach (var splice in Splices) { - foreach (var refId in splice.SampleRefIDs) + foreach (var sr in splice.SampleRefs) { - var sr = SampleRefs[refId]; - int sampleIdx = _samples.FindIndex(x => x.SampleID.Equals(sr.Sample)); - + int sampleIdx = _samples.FindIndex(x => x.SampleID == sr.Sample); writer.Write((ushort)sampleIdx); writer.Write(sr.ESpliceType); writer.Write(sr.Padding); @@ -254,7 +236,11 @@ public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endian public void LoadDependentSamples(bool recurse = false) { - var needed = SampleRefs.Select(r => r.Value.Sample).Distinct().ToList(); + var needed = Splices + .SelectMany(s => s.SampleRefs.Select(sr => sr.Sample)) + .Distinct() + .ToList(); + string dir = Path.Combine(AppContext.BaseDirectory, "data", "Splicer", "Samples"); var files = Directory.GetFiles(dir, "*.snr", recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); @@ -273,42 +259,6 @@ public void LoadDependentSamples(bool recurse = false) _samples = needed.Select(id => new Sample { SampleID = id, Data = map[id] }).ToList(); } - public void LoadDependentSampleRefs() - { - var neededIds = Splices - .SelectMany(s => s.SampleRefIDs) - .Distinct() - .ToList(); - - var deserializer = new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .IgnoreUnmatchedProperties() - .Build(); - - if (SampleRefs == null) - SampleRefs = new Dictionary(); - - var baseDir = Path.Combine( - AppContext.BaseDirectory, - "data", "Splicer", "SampleRefs" - ); - - foreach (var id in neededIds) - { - if (SampleRefs.ContainsKey(id)) - continue; - - var yamlFile = Path.Combine(baseDir, $"{id}.yaml"); - if (!File.Exists(yamlFile)) - throw new FileNotFoundException($"Missing SampleRef for {id}"); - - var yaml = File.ReadAllText(yamlFile); - var model = deserializer.Deserialize(yaml); - - SampleRefs[id] = model; - } - } - public List GetLoadedSamples() { return _samples; @@ -319,17 +269,15 @@ public Splicer() : base() { } public Splicer(string path, Endian endianness = Endian.Agnostic) : base(path, endianness) { } [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct SPLICE_Data + public class SPLICE_Data { public uint NameHash; public ushort SpliceIndex; public sbyte ESpliceType; - public byte Num_SampleRefs; public float Volume; public float RND_Pitch; public float RND_Vol; - public int SampleListIndex; - public List SampleRefIDs { get; set; } + public List SampleRefs { get; set; } } [StructLayout(LayoutKind.Sequential, Pack = 1)] From 8a6db69bfd484aea89156b5cac9222dfa0abc80e Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Wed, 30 Jul 2025 10:58:33 -0400 Subject: [PATCH 15/15] fix StrongID yaml support --- .../CLI/Commands/ImportResourceCommand.cs | 2 +- .../YAML/ResourceIDYamlTypeConverter.cs | 33 -------------- .../YAML/ResourceYamlDeserializer.cs | 2 +- .../YAML/StrongIDYamlTypeConverter.cs | 44 +++++++++++++++++++ 4 files changed, 46 insertions(+), 35 deletions(-) delete mode 100644 Volatility/Utilities/YAML/ResourceIDYamlTypeConverter.cs create mode 100644 Volatility/Utilities/YAML/StrongIDYamlTypeConverter.cs diff --git a/Volatility/CLI/Commands/ImportResourceCommand.cs b/Volatility/CLI/Commands/ImportResourceCommand.cs index 9b1bb04..0a47620 100644 --- a/Volatility/CLI/Commands/ImportResourceCommand.cs +++ b/Volatility/CLI/Commands/ImportResourceCommand.cs @@ -74,7 +74,7 @@ public async Task Execute() .DisableAliases() .WithTypeInspector(inner => new IncludeFieldsTypeInspector(inner)) .WithTypeConverter(new ResourceYamlTypeConverter()) - .WithTypeConverter(new ResourceIDYamlTypeConverter()) + .WithTypeConverter(new StrongIDYamlTypeConverter()) .WithTypeConverter(new StringEnumYamlTypeConverter()) .Build(); diff --git a/Volatility/Utilities/YAML/ResourceIDYamlTypeConverter.cs b/Volatility/Utilities/YAML/ResourceIDYamlTypeConverter.cs deleted file mode 100644 index 18edd7e..0000000 --- a/Volatility/Utilities/YAML/ResourceIDYamlTypeConverter.cs +++ /dev/null @@ -1,33 +0,0 @@ -using YamlDotNet.Core; -using YamlDotNet.Core.Events; -using YamlDotNet.Serialization; - -namespace Volatility.Utilities; - -public class ResourceIDYamlTypeConverter : IYamlTypeConverter -{ - public bool Accepts(Type type) => type == typeof(ResourceID); - - public object ReadYaml(IParser parser, Type type, ObjectDeserializer nestedObjectDeserializer) - { - var scalar = parser.Consume().Value; - var hex = scalar.StartsWith("0x", StringComparison.OrdinalIgnoreCase) - ? scalar.Substring(2) - : scalar; - var ul = Convert.ToUInt64(hex, 16); - return new ResourceID(ul); - } - - public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer nestedObjectSerializer) - { - var text = value.ToString(); - emitter.Emit(new Scalar( - anchor: null, - tag: null, - value: text, - style: ScalarStyle.Plain, - isPlainImplicit: true, - isQuotedImplicit: false - )); - } -} \ No newline at end of file diff --git a/Volatility/Utilities/YAML/ResourceYamlDeserializer.cs b/Volatility/Utilities/YAML/ResourceYamlDeserializer.cs index 4c8a58b..8e7a59d 100644 --- a/Volatility/Utilities/YAML/ResourceYamlDeserializer.cs +++ b/Volatility/Utilities/YAML/ResourceYamlDeserializer.cs @@ -43,7 +43,7 @@ public static object DeserializeResource(Type resourceClass, string yaml) var finalDeserializer = new DeserializerBuilder() .IgnoreUnmatchedProperties() - .WithTypeConverter(new ResourceIDYamlTypeConverter()) + .WithTypeConverter(new StrongIDYamlTypeConverter()) .Build(); using (var reader = new StringReader(mergedYaml)) { diff --git a/Volatility/Utilities/YAML/StrongIDYamlTypeConverter.cs b/Volatility/Utilities/YAML/StrongIDYamlTypeConverter.cs new file mode 100644 index 0000000..9e771b7 --- /dev/null +++ b/Volatility/Utilities/YAML/StrongIDYamlTypeConverter.cs @@ -0,0 +1,44 @@ +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace Volatility.Utilities +{ + public class StrongIDYamlTypeConverter : IYamlTypeConverter + { + public bool Accepts(Type type) + { + return type.IsValueType + && type.IsGenericType + && type.GetGenericTypeDefinition() == typeof(StrongID<>); + } + + public object ReadYaml(IParser parser, Type type, ObjectDeserializer nestedObjectDeserializer) + { + var scalar = parser.Consume().Value!; + var hex = scalar.StartsWith("0x", StringComparison.OrdinalIgnoreCase) + ? scalar.Substring(2) + : scalar; + var value = Convert.ToUInt64(hex, 16); + + var ctor = type.GetConstructor(new[] { typeof(ulong) }) + ?? throw new InvalidOperationException( + $"No ctor(ulong) found on {type.FullName}" + ); + return ctor.Invoke(new object[] { value })!; + } + + public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer nestedObjectSerializer) + { + var text = value.ToString()!; + emitter.Emit(new Scalar( + anchor: null, + tag: null, + value: text, + style: ScalarStyle.Plain, + isPlainImplicit: true, + isQuotedImplicit: false + )); + } + } +}