Skip to content

Instantly share code, notes, and snippets.

@barncastle
Last active December 13, 2024 20:20
Show Gist options
  • Save barncastle/5748b602751c0de2649f47f4e079dd59 to your computer and use it in GitHub Desktop.
Save barncastle/5748b602751c0de2649f47f4e079dd59 to your computer and use it in GitHub Desktop.
Code for reading Shin'en GAX Sound Engine GHX2 files
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
namespace ConsoleApp2
{
internal class GHX2
{
struct GHX2_Header
{
public uint HeaderMagic; // "GHXC"
public int DecompressedDataLen;
public uint CompressedDataChecksum; // Fletcher32(CompressedData)
public uint LZODMagic; // "LZOD"
public int CompressedDataLen;
// byte CompressedData[CompressedDataLen]
}
public static unsafe void DecompressAndExport(string filepath)
{
const int CompressedDataOffset = 20;
Span<byte> buffer = File.ReadAllBytes(filepath);
// read header and compressed data
GHX2_Header header = Unsafe.As<byte, GHX2_Header>(ref buffer[0]);
Span<byte> compressedData = buffer[CompressedDataOffset..];
Debug.Assert(header.HeaderMagic == 0x43584847); // "GHXC"
Debug.Assert(header.LZODMagic == 0x444F5A4C); // "LZOD"
Debug.Assert(header.CompressedDataLen == buffer.Length - CompressedDataOffset); // to EOF
// validate checksum
uint checksum = Fletcher32(compressedData, header.CompressedDataLen);
Debug.Assert(checksum == header.CompressedDataChecksum);
// decompress the data
byte[] output = Decompress(compressedData, header.DecompressedDataLen);
// write to a new file
File.WriteAllBytes(Path.ChangeExtension(filepath, ".bin"), output);
}
public static uint Fletcher32(ReadOnlySpan<byte> data, int size)
{
uint sum1 = 0, sum2 = 0;
for (int i = 0; i < size;)
{
int blockSize = Math.Min(size - i, 0x15B0);
for (int j = 0; j < blockSize; j++)
{
sum1 += data[i++];
sum2 += sum1;
}
sum1 %= 0xFFF1;
sum2 %= 0xFFF1;
}
return (sum2 << 0x10) | (sum1 & 0xFFFF);
}
public static byte[] Decompress(ReadOnlySpan<byte> input, int outLen)
{
int copyLen;
int delta;
int backRefIndex;
byte[] output = new byte[outLen];
// i = input index, o = output index
for (int i = 0, o = 0; o < outLen;)
{
byte val = input[i++];
if (val >= 0x40) // short distance back ref
{
copyLen = (val >> 5) + 1;
delta = input[i++];
backRefIndex = o - (((val >> 2) & 7) + 1 + (delta << 3));
CopyBlockUnaligned(output, ref o, output, ref backRefIndex, copyLen);
}
else if (val >= 0x20) // med distance back ref with dword copy
{
copyLen = ReadVarInt(input, ref i, val, 0x1F) + 2;
delta = input[i++] | (input[i++] << 8); // read uint16
backRefIndex = o - ((delta >> 2) + 1);
// note: dword copy not implemented
CopyBlockUnaligned(output, ref o, output, ref backRefIndex, copyLen);
}
else if (val >= 0x10) // long distance back ref
{
copyLen = ReadVarInt(input, ref i, val, 0x7) + 2;
delta = input[i++] | (input[i++] << 8); // read uint16
backRefIndex = o - (((val & 8) << 11) + (delta >> 2) + 0x4000);
if (backRefIndex == o)
continue;
CopyBlockUnaligned(output, ref o, output, ref backRefIndex, copyLen);
}
else // literal copy
{
copyLen = ReadVarInt(input, ref i, val, 0xF) + 3;
CopyBlockUnaligned(output, ref o, input, ref i, copyLen);
}
// back refs can incur an extra literal copy
if (val >= 0x10)
{
copyLen = input[i - 2] & 3;
if (copyLen != 0)
CopyBlockUnaligned(output, ref o, input, ref i, copyLen);
}
}
return output;
}
private static int ReadVarInt(ReadOnlySpan<byte> buffer, ref int index, int val, int mask)
{
int value = val & mask;
if (value == 0)
{
while (buffer[index] == 0)
{
value += 0xFF;
index++;
}
value += buffer[index++] + mask;
}
return value;
}
private static void CopyBlockUnaligned(Span<byte> dest, ref int destIndex, ReadOnlySpan<byte> src, ref int srcIndex, int size)
{
for (int i = 0; i < size; i++)
dest[destIndex++] = src[srcIndex++];
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment