コード例 #1
0
ファイル: imximage.py プロジェクト: boringhexi/gitarootools
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)
コード例 #2
0
def read_subwav16(file):
    """read a Subsong from a WAV file (16-bit signed linear PCM)

    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 probably be right after the wav file, but
      I can't guarantee it since I didn't write the built-in `wave` module
    - the caller is responsible for closing the file afterwards
    raises: SubsongError if wav file's sample format is not 16-bit
    """
    with wave.open(file, "rb") as wavfile:
        if wavfile.getsampwidth() != 2:  # i.e. not 16-bit
            raise SubsongError(
                "Only 16-bit wav files are supported, this wav file is "
                f"{wavfile.getsampwidth() * 8}-bit.")

        num_channels = wavfile.getnchannels()
        sample_rate = wavfile.getframerate()
        num_frames = wavfile.getnframes()

        # deinterleave samples
        allsamples = struct.unpack(f"<{num_channels * num_frames}h",
                                   wavfile.readframes(num_frames))
        # a wav frame is a single sample at the same time from all channels
        frames = chunks(allsamples, num_channels)
        del allsamples
        channels_samples = zip(*frames)

    channels = list()
    for ch_samples in channels_samples:
        ch = Pcm16Channel(ch_samples)
        channels.append(ch)
    return Subsong(channels, sample_rate)
コード例 #3
0
    def get_imcdata(self, entire=False):
        """interleaves and returns subsong's PS-ADPCM data, including header

        PS-ADPCM data is returned with a certain number of frames per block (fpb) and
        blocks per channel (bpc). There are 3 possibilities, in this order of priority:
        - use self.original_block_layout's saved fpb/bpc if it has them
        - if entire==True, use large fpb + just enough bpc to hold the data
        - otherwise, use 768 fpb + just enough bpc to hold the data
        "entire" is for sound effects but not music, "otherwise" is for both (sfx/music)
        """
        # 1. decide block layout
        if self.original_block_layout is not None:
            frames_per_block, blocks_per_channel = self.original_block_layout
        elif entire:
            # some arbitrary maximum number of frames we'll allow in a single block:
            max_frames_per_block = 32767  # (higher values may work too, haven't tested)
            # if too many frames for a single block, we'll divide them evenly to fit
            divisor = ceil(self.num_frames / max_frames_per_block)
            frames_per_block, blocks_per_channel = ceil(self.num_frames /
                                                        divisor), None

        else:
            frames_per_block, blocks_per_channel = 768, None

        # 2. create PS-ADPCM header
        num_blocks = self.num_channels * (
            blocks_per_channel if blocks_per_channel is not None else ceil(
                self.num_frames / frames_per_block))
        psadpcm_header = struct.pack("<4I", self.num_channels,
                                     self.sample_rate, frames_per_block,
                                     num_blocks)

        # 3. create PS-ADPCM data (divide channels into blocks & interleave them)
        bytes_per_block = PSFRAME_NUMBYTES * frames_per_block
        # a. start with each channel
        #   [ch1data, ch2data]
        channel_datas = (ch.get_psadpcm() for ch in self.channels)
        # if necessary, pad each channel to reach blocks_per_channel
        if blocks_per_channel is not None:
            bytes_per_channel = bytes_per_block * blocks_per_channel
            channel_datas = (chdata + b"\0" * (bytes_per_channel - len(chdata))
                             for chdata in channel_datas)
        # b. chunk each channel into into a list of blocks:
        #   [(ch1block, ch1block, ...), (ch2block, ch2block, ...)]
        #   last block will be zero-padded to a full block
        channel_groups = (chunks(channel_data, bytes_per_block, fillseq=b"\0")
                          for channel_data in channel_datas)
        # c. rearrange the lists of blocks into lists of interleaved blocks:
        #   [(ch1block, ch2block), (ch1block, ch2block), ...]
        interleaved_groups = zip(*channel_groups)
        # d. flatten the lists of interleaved blocks into 1 list of interleaved_blocks:
        #   [ch1block, ch2block, ch1block, ch2block ...]
        interleaved_blocks = chain.from_iterable(interleaved_groups)
        # e. join the interleaved blocks into psadpcm_data:
        #   ch1block-ch2block-ch1block-ch2block-...
        psadpcm_data = b"".join(interleaved_blocks)

        return psadpcm_header + psadpcm_data
コード例 #4
0
    def get_pcm16(self):
        """return 16-bit signed linear PCM samples, converted from PS-ADPCM data

        returns: list of 16-bit signed ints (i.e. in the range -32768, 32767)
        raises: SubsongError on encountering a PS-ADPCM data frame with an invalid
          coef_idx, shift_factor, or flag
        """
        decodedsamples = []
        hist1 = hist2 = 0

        for frame_idx, frame in enumerate(
                chunks(self._psadpcm_data, PSFRAME_NUMBYTES)):
            shift_factor, coef_idx = from_nibbles(frame[0])
            flag = frame[1]

            # check for invalid values
            errors = []
            if not coef_idx <= 0x4:
                errors.append(f"invalid coef_idx {coef_idx:#x}")
            if not shift_factor <= 0xC:
                errors.append(f"invalid shift_factor {shift_factor:#x}")
            if not flag <= 0x7:
                errors.append(f"invalid flag {flag:#x}")
            if errors:
                raise SubsongError(f"Frame {frame_idx} has " +
                                   " and ".join(errors))

            for nibble in from_nibbles(frame[2:], signed=True):
                # To turn a nibble into a sample:
                # 1. multiply nibble by a biggish value
                sample = nibble * 2**(12 - shift_factor)
                # 2. adjust a little based on the previous sample
                sample += ps_adpcm_coefs[coef_idx][0] * hist1
                # 3. and adjust again based on the sample before that
                sample += ps_adpcm_coefs[coef_idx][1] * hist2
                # 4 clamp to 16-bit signed int
                if sample <= -32768:
                    sample = -32768
                elif sample >= 32767:
                    sample = 32767
                else:
                    sample = int(sample)

                decodedsamples.append(sample)
                hist2 = hist1
                hist1 = sample

        return decodedsamples
コード例 #5
0
    def __init__(self, psadpcm_data):
        """psadpcm_data: bytes representing one entire channel of PS-ADPCM frames

        Note that data returned by get_psadpcm() may differ from original psadpcm_data:
        - Padding frames will be removed. This means all frames after the end frame
          (the first frame having flag 0x7, which means "End + don't play this frame")
          or if that doesn't exist, all-zero padding frames at the end.
        - It will ensure the last two frames respectively have flag 0x1 ("End marker
        [last frame]") and flag 0x7, ("End + don't play this frame")
        - If the data didn't already have an end flag 0x7 frame, it will be given one.

        raises: SubsongError if the length of psadpcm_data does not represent a whole
          number of PS-ADPCM frames
        """
        super().__init__()

        # verify length of psadpcm_data
        extrabytes = len(psadpcm_data) % PSFRAME_NUMBYTES
        if extrabytes:
            raise SubsongError(
                "psadpcm_data is not a whole number of frames, "
                f"last frame is only {extrabytes} bytes out of {PSFRAME_NUMBYTES}"
            )

        # remove end/padding frames from the end of get_psadpcm
        discard_frame_idx = 0
        for i, frame in enumerate(chunks(psadpcm_data, PSFRAME_NUMBYTES)):
            if frame[1] == 0x7:
                # discard the first frame having flag=0x7 and all frames after it
                # flag 0x7 means end + don't play this frame
                discard_frame_idx = i
                endframe = frame  # save end frame to re-add later
                break
            if any(frame):
                # Or if no flag 0x7 frame, discard all after the last non-zero frame
                discard_frame_idx = i + 1
        else:
            endframe = PSFRAME_ENDBLANK  # no end frame, so let's create our own
        psadpcm_data = psadpcm_data[:discard_frame_idx * PSFRAME_NUMBYTES]

        # ensure the last audible frame has flag 0x1, meaning "End marker (last frame)"
        if psadpcm_data:
            psadpcm_data = psadpcm_data[:-15] + b"\x01" + psadpcm_data[-14:]

        self._psadpcm_data = psadpcm_data + endframe
コード例 #6
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)
コード例 #7
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)
コード例 #8
0
    def get_psadpcm(self):
        """return PS-ADPCM data (bytes), converted from 16-bit PCM samples

        Note that, following the format of Gitaroo Man audio:
        - the last audible frame will have flag 0x1 (meaning "End marker (last frame)")
        - audible frames will be followed by a single blank frame with flag 0x7 (meaning
          "End + don't play this frame")
        """
        num_audible_frames = self.num_psadpcm_frames - 1
        if num_audible_frames <= 0:
            return PSFRAME_ENDBLANK

        # set up the stuff we'll be iterating through
        frames_samples = chunks(self._pcm_samples,
                                PSFRAME_NUMSAMPLES,
                                fillseq=[0])
        # TODO if changing to numpy array..
        flags = chain(repeat(0x0, num_audible_frames - 1), (0x1, ))
        # normal frames have flag 0x0, final audible frame has flag 0x1 i.e. "End marker
        #   (last frame)"

        # Now, let's encode each frame one at a time and accumulate the encoded nibble
        #   values in psadpcm_nibblevals
        prev_shift_factor, prev_coef_idx = 0, 0
        psadpcm_nibblevals = []
        hist1 = hist2 = 0

        # for each frame (group of 28 samples):
        for frame_samples, flag in zip(frames_samples, flags):

            if not any(frame_samples):
                # a frame with all 0 samples is easy to encode
                frame_nibblevals = (0, 0, flag, 0) + (0, ) * PSFRAME_NUMSAMPLES
                psadpcm_nibblevals.extend(frame_nibblevals)
                hist1 = hist2 = 0
                continue

            # Summary of the nested loop below: We make multiple attempts to encode
            # this frame and keep the best one. For the first attempt, since are no
            # previous attempts to compare to, we encode the whole frame and keep it.
            # For subsequent attempts, we encode each sample to a nibble, re-decode
            # it, and compare the re-decoded sample to the original. Depending on how
            # accurate it is compared to previous attempts, we may encode the whole
            # frame this way and keep it (replacing previous best attempts),
            # or we may give up partway through and move on to the next attempt.

            # shift_factors and coefs: For this frame, we'll test every combination
            # of shift_factor/coef_idx and see which one works the best (i.e. encodes
            # the frame most accurately). We could just use range(13) and range(5),
            # but it's much faster to test in an order based on the previous frame's
            # shift_factor/coef_idx.
            shift_factors = shift_factors_order[prev_shift_factor]
            coefs = coefs_order[prev_coef_idx]
            # bestdiff: an encoded frame's accuracy is measured by its one worst
            # sample (how much it differs from the original sample). This is the best
            # such value we've managed to get so far for this frame.
            bestdiff = None
            best_framevals = None
            best_hists = None

            for shift_factor in shift_factors:
                shiftmul = 2**(12 - shift_factor)
                for coef_idx, (coef1, coef2) in coefs:
                    attempt_hist1, attempt_hist2 = hist1, hist2
                    attempt_worstdiff = None
                    attempt_nibbles = []

                    # Let's encode the samples in this frame.
                    for sample in frame_samples:
                        coefval = (attempt_hist1 * coef1) + (attempt_hist2 *
                                                             coef2)

                        # encode to nibble using these parameters (I may need to add
                        # 0.5 to compensate for integer truncation during decoding,
                        # but I'm not sure)
                        nibble = (sample + 0.5 - coefval) / shiftmul
                        # clamp to nearest 4-bit signed int
                        if nibble <= -8:
                            nibble = -8
                        elif nibble >= 7:
                            nibble = 7
                        else:
                            nibble = round(nibble)

                        # re-decode nibble
                        desample = nibble * shiftmul + coefval
                        # clamp to 16-bit signed int
                        if desample <= -32768:
                            desample = -32768
                        elif desample >= 32767:
                            desample = 32767
                        else:
                            desample = int(desample)

                        # difference between original sample and the result of decoding
                        # the encoded sample
                        diff = abs(sample - desample)
                        if bestdiff is not None and diff >= bestdiff:
                            # diff is too big for these parameters to be an improvement
                            break  # give up on these parameters and move on to next
                        if attempt_worstdiff is None or diff > attempt_worstdiff:
                            attempt_worstdiff = diff

                        attempt_nibbles.append(nibble)
                        attempt_hist2 = attempt_hist1
                        attempt_hist1 = desample

                    else:
                        # We didn't hit the `break` and give up partway through
                        # frame_samples, which means we now have an improvement over
                        # previous attempts. (Note that due to bestdiff's initial
                        # value, it will never give up partway through the first
                        # attempt)
                        bestdiff = attempt_worstdiff
                        best_framevals = (
                            shift_factor,
                            coef_idx,
                            flag,
                            0,
                            *attempt_nibbles,
                        )
                        best_hists = (attempt_hist1, attempt_hist2)

            # At this point, we've tried every shift_factor/coef_idx, so now
            # best_framevals has what we want
            psadpcm_nibblevals.extend(best_framevals)
            hist1, hist2 = best_hists
            prev_shift_factor, prev_coef_idx = best_framevals[:2]

        # At this point, psadpcm_nibblevals contains all our nibbles values,
        # they need to be turned into bytes
        return to_nibbles(*psadpcm_nibblevals) + PSFRAME_ENDBLANK