def write_xgm(xgm: XgmContainer, file: Union[AnyStr, BinaryIO], progressfunc: Callable = None) -> None: """write an XgmContainer to file :param xgm: XgmContainer object :param file: A file path. Or it can be an already-opened file, in which case: * data will be written starting from the current file position * after returning, file position is at the end of the last item's data * the caller is responsible for closing the file afterwards :param progressfunc: function to run whenever an item of the XgmContainer is about to be processed. It must accept three arguments: an int item index, an int total number of items, and an xgmitem.XgmImageItem/XgmModelItem instance """ with open_maybe(file, "wb") as file: num_imageitems, num_modelitems = len(xgm.imageitems), len( xgm.modelitems) file.write(struct.pack("<II", num_imageitems, num_modelitems)) for i, item in enumerate(xgm.imageitems): if progressfunc is not None: progressfunc(i, num_imageitems, item) write_imageitem(item, file) for item in xgm.modelitems: if progressfunc is not None: progressfunc(i, num_modelitems, item) write_modelitem(item, file)
def read_imx(fp: BinaryFp) -> ImxImage: """read from an IMX image file and return an ImxImage :param fp: A file path. Or it can be an already-opened file, in which case: - it will read starting from the current file position - after returning, file position will be right after the IMX file data - the caller is responsible for closing the file afterwards :raises ImxImageError if IMX header is invalid :raises EndOfImxImageError if end of IMX data is reached unexpectedly :return: ImxImage instance """ with open_maybe(fp, "rb") as file: try: magic = readdata(file, 4) if magic != b"IMX\0": raise ImxImageError(f"Not an IMX file, unknown magic {magic!r}") width, height, pixfmtdata = readstruct(file, "<16x2I8s") pixfmt = _pixfmtdata_pixfmt.get(pixfmtdata) if pixfmt is None: raise ImxImageError(f"Unknown IMX pixel format data {pixfmtdata!r}") if width % 2: raise ImxImageError( "indexed-4 pixel format requires width be a multiple of 2, " f"but width={width}" ) if pixfmt in ("i4", "i8"): palette_size = readstruct(file, "<I") struct_fmt = f"{palette_size}B" palette = tuple(chunks(readstruct(file, struct_fmt), 4)) file.seek(4, SEEK_CUR) # always 2? else: palette = None pixels_size = readstruct(file, "<I") if pixfmt == "i4": struct_fmt = f"{pixels_size}B" pixels = tuple(from_nibbles(readstruct(file, struct_fmt))) elif pixfmt == "i8": struct_fmt = f"{pixels_size}B" pixels = readstruct(file, struct_fmt) elif pixfmt == "rgb24": struct_fmt = f"{pixels_size}B" pixels = tuple(chunks(readstruct(file, struct_fmt), 3)) elif pixfmt == "rgba32": struct_fmt = f"{pixels_size}B" pixels = tuple(chunks(readstruct(file, struct_fmt), 4)) # footer file.seek(8, SEEK_CUR) # always uint32s (3,0)? except EOFError as e: raise EndOfImxImageError(str(e)) return ImxImage(width, height, pixels, palette, pixfmt=pixfmt, alpha128=True)
def write_subimc(subsong, file): """write a Subsong to IMC subsong file subsong: a Subsong instance file: A file path. Or it can be an already-opened file, in which case: - it will write starting from the current file position. After returning, the file position will be at the end of the IMC subsong data it just wrote. - the caller is responsible for closing the file afterwards """ with open_maybe(file, "wb") as file: file.write(subsong.get_imcdata())
def read_xgm(file: Union[AnyStr, BinaryIO]) -> XgmContainer: """read from file and return an XgmContainer :param file: A file path. Or it can be an already-opened file, in which case: * data will be read starting from the current file position * after returning, file position is at the end of the last item's data * the caller is responsible for closing the file afterwards :return: XgmContainer instance """ with open_maybe(file, "rb") as file: num_imageitems, num_modelitems = readstruct(file, "<II") imageitems = [read_imageitem(file) for _ in range(num_imageitems)] modelitems = [read_modelitem(file) for _ in range(num_modelitems)] return XgmContainer(imageitems, modelitems)
def read_imc(file): """ read from a IMC audio container file and return an ImcContainer file: A file path. Or it can be an already-opened file, in which case: - it will read starting from the current file position, with no guarantee of file position after returning - the caller is responsible for closing the file afterwards raises: EOFError if end of file is reached unexpectedly """ with open_maybe(file, "rb") as file: start_offset = file.tell() # read number of subsongs num_subsongs = readstruct(file, "<I") # read raw ssinfo fmt, items_per_ssinfo_entry = ("<" + "16s4I" * num_subsongs), 5 raw_ssinfos = tuple( chunks(readstruct(file, fmt), items_per_ssinfo_entry)) next_ssoffsets = (x[1] for x in raw_ssinfos[1:]) # read subsongs, convert to ContainerSubsongs csubsongs = [] for raw_ssinfo, next_ssoffset in zip_longest(raw_ssinfos, next_ssoffsets, fillvalue=None): rawname, ssoffset, unk1, unk2, loadmode_raw = raw_ssinfo name = rawname.split(b"\0", 1)[0].decode(encoding="ascii") if next_ssoffset is not None: ss_knownsize = next_ssoffset - ssoffset else: ss_knownsize = None # read Subsong from within IMC container file file.seek(start_offset + ssoffset) subsong_ = subsong.read_subimc(file, knownsize=ss_knownsize) csubsong = ContainerSubsong(subsong_, name, loadmode_raw, rawname, unk1, unk2) csubsongs.append(csubsong) return ImcContainer(csubsongs)
def write_imx(imximage: ImxImage, fp: BinaryFp) -> None: """write imximage to file :param imximage: an ImxImage instance :param fp: A file path. Or it can be an already-opened file, in which case: - it will write starting from the current file position - after returning, file position will be right after the written IMX data - the caller is responsible for closing the file afterwards """ with open_maybe(fp, "wb") as file: # header file.write(b"IMX\0") pixfmtdata = _pixfmt_pixfmtdata[imximage.pixfmt] writestruct(file, "<16x2I8s", *imximage.size, pixfmtdata) # palette if imximage.pixfmt in ("i4", "i8"): palette = imximage.palette128 palette_num_bytes = len(palette) * 4 writestruct(file, "<I", palette_num_bytes) writestruct(file, f"{palette_num_bytes}B", *chain.from_iterable(palette)) writestruct(file, "<I", 2) # pixels pixels = imximage.pixels128 if imximage.pixfmt == "i8": pixels_flat = pixels # already flat elif imximage.pixfmt == "i4": pixels_flat = tuple(to_nibbles(*pixels)) else: pixels_flat = tuple(chain.from_iterable(pixels)) # flatten (r,g,b) stuff pixels_num_bytes = len(pixels_flat) writestruct(file, "<I", pixels_num_bytes) writestruct(file, f"{pixels_num_bytes}B", *pixels_flat) # footer writestruct(file, "<2I", 3, 0)
def write_imc(imccontainer, file, progressfunc=None): """write an ImcContainer to an IMC audio container file imccontainer: an ImcContainer object file: A file path. Or it can be an already-opened file, in which case: - it will write starting from the current file position, with no guarantee of file position after returning - the caller is responsible for closing the file afterwards progressfunc: function to run whenever a subsong of the ImcContainer is about to be processed. It must accept three arguments: an int subsong index, an int total number of subsongs, and an imccontainer.ContainerSubsong instance """ with open_maybe(file, "wb") as file: start_offset = file.tell( ) # in case we're reading from inside an ISO file, etc # write num_subsongs num_subsongs = imccontainer.num_subsongs file.write(struct.pack("<I", num_subsongs)) if not num_subsongs: return # true offsets for subsong info entries (contained in the IMC container header) true_ssinfoentry_offsets = (start_offset + 4 + i * 0x20 for i in range(num_subsongs)) file.seek(start_offset + 4 + num_subsongs * 0x20) # after the last ssinfo entry for ssidx, true_ssinfoentry_offset, csubsong in zip( count(), true_ssinfoentry_offsets, imccontainer.csubsongs): if progressfunc is not None: progressfunc(ssidx, imccontainer.num_subsongs, csubsong) # current pos is where we should write the subsong, but we won't just yet true_subsong_offset = file.tell() subsong_offset = true_subsong_offset - start_offset # prepare subsong info entry # uses patch-friendly info if present: rawname, unk1, unk2 if csubsong.rawname is not None: # use csubsong.name pasted on top of csubsong.rawname rawname = csubsong.name.encode(encoding="ascii") if len(rawname) < 16: rawname += b"\0" rawname += csubsong.rawname[len(rawname):] else: # just zero-pad csubsong.name rawname = csubsong.name.encode(encoding="ascii") rawname += (16 - len(rawname)) * b"\0" unk1 = 0 if csubsong.unk1 is None else csubsong.unk1 unk2 = 0 if csubsong.unk2 is None else csubsong.unk2 ss_infoentry = struct.pack("<16s4I", rawname, subsong_offset, unk1, unk2, csubsong.loadmode_raw) # write subsong info entry into IMC container header file.seek(true_ssinfoentry_offset) file.write(ss_infoentry) # write subsong data file.seek(true_subsong_offset) subsong.write_subimc(csubsong, file)
def read_subimc(file, knownsize=None): """read from a IMC subsong file and return a Subsong file: A file path. Or it can be an already-opened file, in which case: - it will read starting from the current file position - after returning, file position will be right after the subsong file data - the caller is responsible for closing the file afterwards knownsize: Optional size of the IMC subsong file known in advance. It's only used for a quick sanity check (to anticipate reading past end of the subsong). Without this, it assumes this subsong ends when the file ends, so it's recommended to pass this if reading this subsong from inside a larger container file. raises: - SubsongError if IMC subsong header is invalid - EndOfSubsongError if end of subsong is reached unexpectedly """ with open_maybe(file, "rb") as file: # Read imc subsong header header = file.read(16) if len(header) != 16: raise EndOfSubsongError( "Expected 16 bytes for IMC subsong header, only got" f"{len(header)} bytes") num_channels, sample_rate, frames_per_block, num_blocks = struct.unpack( "<4I", header) # sanity checks, since the format is so simple if not 1 <= num_channels <= 8: raise SubsongError(f"invalid number of channels {num_channels}") if not 8000 <= sample_rate <= 48000: raise SubsongError(f"invalid sample rate {sample_rate}") if frames_per_block == 0: raise SubsongError("frames per block should not be 0") if num_blocks == 0: raise SubsongError("number of blocks should not be 0") if not num_blocks % num_channels == 0: raise SubsongError( f"number of channels ({num_channels}) does not divide evenly into" f"number of blocks ({num_blocks})") # quick check for premature end of file if knownsize is None: # assume subsong ends when the file ends oldtell = file.tell() file.seek(0, SEEK_END) real_datasize = file.tell() - oldtell file.seek(oldtell) else: real_datasize = knownsize - 0x10 # size - header predicted_datasize = PSFRAME_NUMBYTES * frames_per_block * num_blocks if predicted_datasize > real_datasize: raise EndOfSubsongError( f"IMC subsong header predicts {predicted_datasize} bytes of data, " f"but only {real_datasize} bytes remain in file") bytes_per_block = PSFRAME_NUMBYTES * frames_per_block # Deinterleave file data, separating the blocks into channels: # starting with one big lump of interleaved_data: # ch1block-ch2block-ch1block-ch2block-... interleaved_data = file.read(bytes_per_block * num_blocks) # chunk interleaved data into interleaved_blocks: # [ch1block, ch2block, ch1block, ch2block ...] interleaved_blocks = tuple(chunks(interleaved_data, bytes_per_block)) del interleaved_data # now in interleaved_blocks, no point in keeping # group interleaved blocks into interleaved_groups: # [(ch1block, ch2block), (ch1block, ch2block), ...] interleaved_groups = chunks(interleaved_blocks, num_channels) # deinterleave interleaved_groups into channel_groups: # [(ch1block, ch1block, ...), (ch2block, ch2block, ...)] channel_groups = zip(*interleaved_groups) # join each channel group into a one big lump of channel data: # [ch1block-ch1block-..., ch2block-ch2block-...] channel_datas = (b"".join(channel_group) for channel_group in channel_groups) # create Subsong from each channel_data channelobjs = [] for channel_data in channel_datas: chobj = PsAdpcmChannel(channel_data) channelobjs.append(chobj) blocks_per_channel = int(num_blocks / num_channels) return Subsong(channelobjs, sample_rate, frames_per_block, blocks_per_channel)