Created
February 12, 2026 13:50
-
-
Save rvanlaar/8bcb776bb0760919db2f93fc1ca41a9d to your computer and use it in GitHub Desktop.
paco_extract.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| import struct | |
| import sys | |
| from dataclasses import dataclass, field | |
| @dataclass | |
| class PACoHeader: | |
| # _ stands for unkown usage | |
| head: int # 0x0001 | |
| head2: int # 0x0026 | |
| width: int | |
| height:int | |
| fps: int # negative is indicative of no audio, but not definitive | |
| flags: int # flags is 0x100: audio | |
| max_chunk_size: int | |
| _zero: int # always zero? | |
| _audio_related: int # unkown, audio related | |
| frames: int # number of chunks, i.e. frames | |
| frames2: int # number of chunks repeated | |
| _eight: int # always 8? | |
| _six: int # always 0x600 | |
| _flags2: int # some flags | |
| _zero2: int # always 0? | |
| has_audio: bool = field(init=False) | |
| framerate: int = field(init=False) | |
| def __post_init__(self): | |
| self.has_audio = (self.flags & 0x100) == 0x100 | |
| self.framerate = abs(self.fps) | |
| def check(self): | |
| assert self.head == 0x1, f"head is not 0x1: {self.head}" | |
| assert self.head2 == 0x26, f"head2 is not 0x26: {self.head2}" | |
| assert self._zero == 0, f"_zero is not 0: {self._zero}" | |
| assert self._eight == 8, f"_eight is not 8: {self._eight}" | |
| assert self._six == 0x600, f"_six is not 0x600: {self._six}" | |
| assert self._zero2 == 0, f"_zero2 is not 0: {self._zero2}" | |
| def main(): | |
| filename = sys.argv[1] | |
| print(f"opening: {filename}") | |
| f = open(filename, "rb") | |
| audio_out = open(f"{filename}.pcm", "wb") | |
| get8 = lambda: struct.unpack(">B", f.read(1))[0] | |
| get16 = lambda: struct.unpack(">H", f.read(2))[0] | |
| get16_s = lambda: struct.unpack(">h", f.read(2))[0] | |
| get24 = lambda: struct.unpack(">I", b'\x00' + f.read(3))[0] | |
| get32 = lambda: struct.unpack(">I", f.read(4))[0] | |
| header = PACoHeader(get16(), get16(), get16(), get16(), get16_s(), get16(), get32(), get32(), get32(), get16(), get16(), get16(), get16(), get32(), get16()) | |
| header.check() | |
| print(header) | |
| frame_sizes = [get32() for i in range(header.frames)] | |
| for i in range(header.frames): | |
| next_frame = f.tell() + frame_sizes[i] | |
| print("new frame") | |
| while (f.tell() < next_frame): | |
| chunk_type = get8() | |
| chunk_size = get24() | |
| if chunk_type in CHUNK_TYPES: | |
| chunk_name = CHUNK_TYPES[chunk_type] | |
| else: | |
| chunk_name = f"{chunk_type}: unknown" | |
| print(f"chunk type: {chunk_name} size: {chunk_size}") | |
| if chunk_type == 2: | |
| palette_data = f.read(chunk_size-4) | |
| quicktime_palette = b'\x30\x00\x00\x00\x00\x00\x00\x00' | |
| custom_palette = b'\x10\x00\x00\x00\x00\x00\x00\x00' | |
| if palette_data.startswith(quicktime_palette): | |
| print("QuicktimePalette") | |
| elif palette_data.startswith(custom_palette): | |
| print("Defines custom palette") | |
| else: | |
| print("unkown palette") | |
| elif chunk_type == 4: | |
| # 8 bit unsigned pcm | |
| sampling_rates = [5563, 7418, 11127, 22254] | |
| header = get16() | |
| get16() | |
| index = (header >> 10) & 7 | |
| print(f"\t sampling rate: {sampling_rates[index]}") | |
| audio_out.write(f.read(chunk_size-8)) | |
| else: | |
| f.seek(f.tell() + chunk_size - 4) | |
| audio_out.close() | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment