示例#1
0
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)
示例#2
0
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)
示例#3
0
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())
示例#4
0
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)
示例#5
0
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)
示例#6
0
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)
示例#7
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)
示例#8
0
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)