1+ namespace Cryogenic . AdgPlayer . Services ;
2+
3+ using Serilog ;
4+
5+ /// <summary>
6+ /// HSQ decompression for ADG/ADP music files from DUNE.DAT.
7+ /// Many files are HSQ-compressed (LZ77 variant with a 6-byte header).
8+ /// Ported from OpenRakis/tools/dune-ds/source/unhsq.c via the MT32 player.
9+ /// Checksum: (byte0 + byte1 + byte2 + byte3 + byte4 + byte5) & 0xFF == 0xAB
10+ /// </summary>
11+ public sealed partial class DuneAdgPlayerEngine {
12+ private static readonly ILogger HsqLogger = Log . ForContext ( "Subsystem" , "HSQ" ) ;
13+
14+ /// <summary>
15+ /// Attempts HSQ decompression; returns null if the header is not a valid HSQ signature.
16+ /// </summary>
17+ private static byte [ ] ? TryDecompressHsq ( byte [ ] source ) {
18+ if ( source . Length < 6 ) {
19+ return null ;
20+ }
21+
22+ int uncompressedSize = source [ 0 ] | ( source [ 1 ] << 8 ) ;
23+ if ( uncompressedSize == 0 || uncompressedSize > 0x100000 ) {
24+ return null ;
25+ }
26+
27+ int checksum = ( source [ 0 ] + source [ 1 ] + source [ 2 ] + source [ 3 ] + source [ 4 ] + source [ 5 ] ) & 0xFF ;
28+ if ( checksum != 0xAB ) {
29+ return null ;
30+ }
31+
32+ try {
33+ byte [ ] result = DecompressHsq ( source , uncompressedSize ) ;
34+ HsqLogger . Information ( "HSQ decompressed: {SrcLen} → {DstLen} bytes" , source . Length , result . Length ) ;
35+ return result ;
36+ } catch {
37+ HsqLogger . Warning ( "HSQ decompression failed, using raw data" ) ;
38+ return null ;
39+ }
40+ }
41+
42+ /// <summary>
43+ /// Core HSQ decompression. Faithful port of unhsq_unpack2 from OpenRakis.
44+ /// </summary>
45+ private static byte [ ] DecompressHsq ( byte [ ] source , int uncompressedSize ) {
46+ byte [ ] output = new byte [ uncompressedSize ] ;
47+ int srcPos = 6 ;
48+ int dstPos = 0 ;
49+ int bitBuffer = 1 ;
50+
51+ while ( dstPos < uncompressedSize && srcPos < source . Length ) {
52+ if ( HsqGetBit ( source , ref srcPos , ref bitBuffer ) ) {
53+ if ( srcPos >= source . Length ) {
54+ break ;
55+ }
56+ output [ dstPos ++ ] = source [ srcPos ++ ] ;
57+ } else {
58+ int count ;
59+ int offset ;
60+
61+ if ( HsqGetBit ( source , ref srcPos , ref bitBuffer ) ) {
62+ if ( srcPos + 1 >= source . Length ) {
63+ break ;
64+ }
65+ byte first = source [ srcPos ++ ] ;
66+ byte second = source [ srcPos ++ ] ;
67+
68+ count = first & 7 ;
69+ int word = first | ( second << 8 ) ;
70+ int offsetBits = word >> 3 ;
71+ offset = offsetBits - 8192 ;
72+
73+ if ( count == 0 ) {
74+ if ( srcPos >= source . Length ) {
75+ break ;
76+ }
77+ count = source [ srcPos ++ ] ;
78+ if ( count == 0 ) {
79+ break ;
80+ }
81+ }
82+ } else {
83+ count = HsqGetBit ( source , ref srcPos , ref bitBuffer ) ? 2 : 0 ;
84+ count |= HsqGetBit ( source , ref srcPos , ref bitBuffer ) ? 1 : 0 ;
85+
86+ if ( srcPos >= source . Length ) {
87+ break ;
88+ }
89+ offset = source [ srcPos ++ ] - 256 ;
90+ }
91+
92+ count += 2 ;
93+ int copyFrom = dstPos + offset ;
94+ if ( copyFrom < 0 ) {
95+ break ;
96+ }
97+
98+ for ( int i = 0 ; i < count && dstPos < uncompressedSize ; i ++ ) {
99+ output [ dstPos ++ ] = output [ copyFrom + i ] ;
100+ }
101+ }
102+ }
103+
104+ return output ;
105+ }
106+
107+ /// <summary>
108+ /// Reads one bit from the HSQ bit stream using the sentinel approach.
109+ /// </summary>
110+ private static bool HsqGetBit ( byte [ ] source , ref int srcPos , ref int bitBuffer ) {
111+ if ( bitBuffer == 1 ) {
112+ if ( srcPos + 1 >= source . Length ) {
113+ return false ;
114+ }
115+ bitBuffer = 0x10000 | source [ srcPos ] | ( source [ srcPos + 1 ] << 8 ) ;
116+ srcPos += 2 ;
117+ }
118+
119+ bool bit = ( bitBuffer & 1 ) != 0 ;
120+ bitBuffer >>= 1 ;
121+ return bit ;
122+ }
123+ }
0 commit comments