diff --git a/it2taud.py b/it2taud.py index e70c1fd..10ce7c2 100644 --- a/it2taud.py +++ b/it2taud.py @@ -294,11 +294,11 @@ def _it214_decompress_block(payload: bytes, num_samples: int, return out -def it214_decompress(blob: bytes, smp_offset: int, num_samples: int, - is_16bit: bool, is_it215: bool) -> bytes: - """Decode IT2.14/IT2.15 compressed sample data. Returns raw PCM bytes (signed).""" +def _it214_decompress_channel(blob: bytes, pos: int, num_samples: int, + is_16bit: bool, is_it215: bool) -> tuple: + """Decode one channel of IT2.14/IT2.15 compressed data. Returns + (raw PCM bytes, next position after consumed blocks).""" block_size = 0x4000 if is_16bit else 0x8000 - pos = smp_offset out_samples = [] while len(out_samples) < num_samples: @@ -318,9 +318,24 @@ def it214_decompress(blob: bytes, smp_offset: int, num_samples: int, result = bytearray(len(out_samples) * 2) for i, s in enumerate(out_samples): struct.pack_into(' bytes: + """Decode IT2.14/IT2.15 compressed sample data. Returns raw PCM bytes + (signed). For stereo samples, returns the left channel block followed + by the right channel block (matching IT's on-disk SF_SS layout).""" + left, pos = _it214_decompress_channel(blob, smp_offset, num_samples, + is_16bit, is_it215) + if not is_stereo: + return left + right, _ = _it214_decompress_channel(blob, pos, num_samples, + is_16bit, is_it215) + return left + right # ── IT sample parser ────────────────────────────────────────────────────────── @@ -384,7 +399,7 @@ def parse_samples(data: bytes, h: ITHeader, decompress: bool) -> list: try: is_it215 = bool(s.cvt & 0x04) raw = it214_decompress(data, s.smp_point, s.length, - s.is_16bit, is_it215) + s.is_16bit, is_it215, s.is_stereo) s.sample_data = normalise_sample(raw, True, s.is_16bit, s.is_stereo, s.name) s.length = len(s.sample_data) diff --git a/mod2taud.py b/mod2taud.py index 31060d8..08a2e3a 100644 --- a/mod2taud.py +++ b/mod2taud.py @@ -592,7 +592,7 @@ def build_sample_inst_bin(samples: list) -> tuple: # PT hard-pans channels in LRRL order: 0=L 1=R 2=R 3=L (and tile for >4). def _default_channel_pan(ch_idx: int) -> int: side = (ch_idx % 4) - return 16 if side in (0, 3) else 47 + return 8 if side in (0, 3) else 55 def build_pattern(grid: list, ch_idx: int, default_pan: int, diff --git a/taud_common.py b/taud_common.py index f29c4e7..25d97ee 100644 --- a/taud_common.py +++ b/taud_common.py @@ -614,31 +614,44 @@ def build_project_data(*, project_name: str = '', def normalise_sample(raw: bytes, signed: bool, is_16bit: bool, is_stereo: bool, name: str) -> bytes: - """Return unsigned 8-bit mono sample bytes, downmixing/depthing as needed.""" + """Return unsigned 8-bit mono sample bytes, downmixing/depthing as needed. + + Stereo samples are stored as a split (non-interleaved) layout — the full + left channel block followed by the full right channel block — matching the + on-disk format used by IT, S3M, and XM (Schism's SF_SS). + """ out = [] - stride = (2 if is_16bit else 1) * (2 if is_stereo else 1) - i = 0 - while i + stride <= len(raw): + bps = 2 if is_16bit else 1 + chans = 2 if is_stereo else 1 + n_frames = len(raw) // (bps * chans) + chan_bytes = n_frames * bps + + for i in range(n_frames): if is_16bit: if is_stereo: - l16 = struct.unpack_from('> 1 else: - s = struct.unpack_from('> 8) + 128 else: if is_stereo: - l8 = raw[i]; r8 = raw[i+1] - raw_s = (l8 + r8) // 2 + l8 = raw[i] + r8 = raw[chan_bytes + i] + if signed: + l_s = l8 - 256 if l8 >= 0x80 else l8 + r_s = r8 - 256 if r8 >= 0x80 else r8 + v = ((l_s + r_s) >> 1) + 128 + else: + v = (l8 + r8) >> 1 else: raw_s = raw[i] - if signed: - v = (raw_s ^ 0x80) & 0xFF - else: - v = raw_s + if signed: + v = (raw_s ^ 0x80) & 0xFF + else: + v = raw_s out.append(v & 0xFF) - i += stride if is_16bit or is_stereo: vprint(f" info: '{name}' converted to unsigned 8-bit mono ({len(out)} samples)") return bytes(out)