import os
import tempfile
import pytest
pytest.importorskip("cridecoder")
import cridecoder
class TestHcaRoundtrip:
def test_encode_decode_roundtrip(self):
wav_data = create_test_wav(sample_rate=44100, channels=2, duration_sec=0.5)
hca_data = cridecoder.encode_hca_bytes(wav_data, bitrate=256000)
assert hca_data[:4] == b"HCA\x00", "Should have HCA magic"
decoded_wav = cridecoder.decode_hca_bytes(hca_data)
assert decoded_wav[:4] == b"RIFF", "Should have RIFF magic"
assert decoded_wav[8:12] == b"WAVE", "Should have WAVE format"
def test_encode_with_encryption(self):
wav_data = create_test_wav(sample_rate=44100, channels=1, duration_sec=0.2)
hca_data = cridecoder.encode_hca_bytes(
wav_data,
bitrate=128000,
encryption_key=0x1234567890ABCDEF
)
assert hca_data[0] == 0xC8, "Should have masked HCA magic"
def test_encode_mono(self):
wav_data = create_test_wav(sample_rate=22050, channels=1, duration_sec=0.3)
hca_data = cridecoder.encode_hca_bytes(wav_data, bitrate=128000)
assert hca_data[:4] == b"HCA\x00"
decoded = cridecoder.decode_hca_bytes(hca_data)
assert decoded[:4] == b"RIFF"
def test_encode_stereo(self):
wav_data = create_test_wav(sample_rate=48000, channels=2, duration_sec=0.3)
hca_data = cridecoder.encode_hca_bytes(wav_data, bitrate=320000)
assert hca_data[:4] == b"HCA\x00"
decoded = cridecoder.decode_hca_bytes(hca_data)
assert decoded[:4] == b"RIFF"
class TestContainerBuilding:
def test_build_acb_structure(self):
wav_data = create_test_wav(sample_rate=44100, channels=2, duration_sec=0.1)
hca_data = cridecoder.encode_hca_bytes(wav_data, bitrate=256000)
tracks = [("test_track", 0, hca_data)]
acb_data = cridecoder.build_acb_bytes(tracks)
assert acb_data[:4] == b"@UTF", "Should have UTF magic"
assert len(acb_data) > 100, "Should have substantial content"
def test_extract_acb_bytes(self):
wav_data = create_test_wav(sample_rate=44100, channels=1, duration_sec=0.1)
hca_data = cridecoder.encode_hca_bytes(wav_data, bitrate=128000)
acb_data = cridecoder.build_acb_bytes([("memory_track", 0, hca_data)])
tracks = cridecoder.extract_acb_bytes(acb_data)
assert len(tracks) == 1
assert tracks[0]["name"] == "memory_track"
assert tracks[0]["extension"] == "hca"
assert tracks[0]["filename"] == "memory_track.hca"
assert tracks[0]["data"][:4] == b"HCA\x00"
def test_build_acb_multiple_tracks(self):
tracks = []
for i in range(3):
wav_data = create_test_wav(sample_rate=44100, channels=1, duration_sec=0.1)
hca_data = cridecoder.encode_hca_bytes(wav_data, bitrate=128000)
tracks.append((f"track_{i}", i, hca_data))
acb_data = cridecoder.build_acb_bytes(tracks)
assert acb_data[:4] == b"@UTF"
def test_build_usm_structure(self):
video_data = bytes([
0x00, 0x00, 0x01, 0xB3, 0x14, 0x00, 0xF0, 0x24, 0xFF, 0xFF, 0xE0, 0x00, ])
usm_data = cridecoder.build_usm_bytes("test_video", video_data)
assert usm_data[:4] == b"CRID", "Should have CRID magic"
assert len(usm_data) > 100, "Should have substantial content"
class TestRealFileOperations:
@pytest.fixture
def acb_path(self):
path = "se_0126_01.acb"
if not os.path.exists(path):
pytest.skip("Test fixture se_0126_01.acb not found")
return path
@pytest.fixture
def usm_path(self):
path = "0703.usm"
if not os.path.exists(path):
pytest.skip("Test fixture 0703.usm not found")
return path
def test_extract_acb(self, acb_path):
with tempfile.TemporaryDirectory() as tmpdir:
extracted = cridecoder.extract_acb(acb_path, tmpdir)
assert extracted is not None
assert len(extracted) > 0, "Should extract tracks"
for path in extracted:
assert os.path.exists(path), f"Extracted file should exist: {path}"
def test_extract_usm(self, usm_path):
with tempfile.TemporaryDirectory() as tmpdir:
extracted = cridecoder.extract_usm(usm_path, tmpdir)
assert len(extracted) > 0, "Should extract files"
m2v_files = [p for p in extracted if p.endswith(".m2v")]
assert len(m2v_files) > 0, "Should extract M2V video"
def test_extract_usm_bytes(self, usm_path):
with open(usm_path, "rb") as f:
usm_data = f.read()
streams = cridecoder.extract_usm_bytes(usm_data, fallback_name=os.path.basename(usm_path))
assert len(streams) > 0
m2v_streams = [stream for stream in streams if stream["extension"] == "m2v"]
assert len(m2v_streams) > 0, "Should extract M2V video"
assert m2v_streams[0]["data"].startswith(b"\x00\x00\x01")
def test_hca_decode_and_reencode(self, acb_path):
with tempfile.TemporaryDirectory() as tmpdir:
extracted = cridecoder.extract_acb(acb_path, tmpdir)
assert extracted is not None and len(extracted) > 0
hca_path = extracted[0]
wav_path = os.path.join(tmpdir, "decoded.wav")
info = cridecoder.decode_hca(hca_path, wav_path)
assert info["sample_rate"] > 0
assert info["channels"] > 0
with open(wav_path, "rb") as f:
wav_data = f.read()
new_hca_data = cridecoder.encode_hca_bytes(
wav_data,
sample_rate=info["sample_rate"],
channels=info["channels"],
bitrate=256000
)
assert new_hca_data[:4] == b"HCA\x00", "Should produce valid HCA"
decoded_again = cridecoder.decode_hca_bytes(new_hca_data)
assert decoded_again[:4] == b"RIFF", "Re-encoded HCA should decode"
def test_usm_extract_and_rebuild_video(self, usm_path):
with tempfile.TemporaryDirectory() as tmpdir:
extracted = cridecoder.extract_usm(usm_path, tmpdir)
m2v_path = None
for path in extracted:
if path.endswith(".m2v"):
m2v_path = path
break
assert m2v_path is not None, "Should have M2V"
with open(m2v_path, "rb") as f:
m2v_data = f.read()
rebuilt_path = os.path.join(tmpdir, "rebuilt.usm")
cridecoder.build_usm("rebuilt", m2v_data, rebuilt_path)
assert os.path.exists(rebuilt_path)
with open(rebuilt_path, "rb") as f:
header = f.read(4)
assert header == b"CRID", "Rebuilt USM should have CRID magic"
def test_full_acb_pipeline(self, acb_path):
with tempfile.TemporaryDirectory() as tmpdir:
extract_dir = os.path.join(tmpdir, "extract")
os.makedirs(extract_dir)
extracted = cridecoder.extract_acb(acb_path, extract_dir)
assert extracted and len(extracted) > 0
new_tracks = []
for i, hca_path in enumerate(extracted[:2]): wav_path = os.path.join(tmpdir, f"track{i}.wav")
info = cridecoder.decode_hca(hca_path, wav_path)
with open(wav_path, "rb") as f:
wav_data = f.read()
new_hca = cridecoder.encode_hca_bytes(
wav_data,
sample_rate=info["sample_rate"],
channels=info["channels"],
bitrate=256000
)
new_tracks.append((f"track_{i}", i, new_hca))
rebuilt_path = os.path.join(tmpdir, "rebuilt.acb")
cridecoder.build_acb(new_tracks, rebuilt_path)
assert os.path.exists(rebuilt_path)
with open(rebuilt_path, "rb") as f:
header = f.read(4)
assert header == b"@UTF", "Rebuilt ACB should have UTF magic"
size = os.path.getsize(rebuilt_path)
assert size > 1000, f"ACB should have substantial size, got {size}"
def create_test_wav(sample_rate: int, channels: int, duration_sec: float) -> bytes:
import struct
import math
num_samples = int(sample_rate * duration_sec)
bits_per_sample = 16
byte_rate = sample_rate * channels * bits_per_sample // 8
block_align = channels * bits_per_sample // 8
data_size = num_samples * channels * bits_per_sample // 8
frequency = 440.0 samples = []
for i in range(num_samples):
t = i / sample_rate
value = int(32767 * 0.5 * math.sin(2 * math.pi * frequency * t))
for _ in range(channels):
samples.append(value)
wav = bytearray()
wav.extend(b"RIFF")
wav.extend(struct.pack("<I", 36 + data_size))
wav.extend(b"WAVE")
wav.extend(b"fmt ")
wav.extend(struct.pack("<I", 16)) wav.extend(struct.pack("<H", 1)) wav.extend(struct.pack("<H", channels))
wav.extend(struct.pack("<I", sample_rate))
wav.extend(struct.pack("<I", byte_rate))
wav.extend(struct.pack("<H", block_align))
wav.extend(struct.pack("<H", bits_per_sample))
wav.extend(b"data")
wav.extend(struct.pack("<I", data_size))
for sample in samples:
wav.extend(struct.pack("<h", sample))
return bytes(wav)
if __name__ == "__main__":
pytest.main([__file__, "-v"])