def save(self, filename=None, compress=False): if self.file_platform not in XNB_PLATFORMS: raise ReaderError("bad platform: '{!r}'".format(self.file_platform)) if self.file_version not in XNB_VERSIONS: raise ReaderError("bad version: {}".format(self.file_version)) attribs = 0 if self.file_version >= VERSION_40: if self.graphics_profile not in XNB_PROFILES: raise ReaderError("bad profile: {}".format(self.graphics_profile)) attribs |= self.graphics_profile & _PROFILE_MASK do_compress = False if self.file_version >= VERSION_30: if compress: do_compress = True attribs |= _COMPRESS_MASK stream = BinaryStream() if do_compress: raise ReaderError("Recompression not supported") else: data = self.getvalue() size = len(data) + stream.calc_size(_XNB_HEADER) stream.pack(_XNB_HEADER, XNB_SIGNATURE, self.file_platform, self.file_version, attribs, size) stream.write(data) if filename is not None: filename = os.path.normpath(filename) dirname = os.path.dirname(filename) if not os.path.isdir(dirname): os.makedirs(dirname) if not filename.endswith(XNB_EXTENSION): filename += XNB_EXTENSION stream.write_file(filename) else: return stream.getvalue()
def write(self, filename): h_s = BinaryStream() h_s.pack(self._waveformatex, self.h_format_tag, self.h_channels, self.h_samples_per_sec, self.h_avg_bytes_per_sec, self.h_block_align, self.h_bits_per_sample) if self.h_size is not None: h_s.write_uint16(self.h_size) if self.h_format_tag == WAVE_FORMAT_XMA2: # hack so mono sounds end up center rather than left if self.h_channels == 1 and self.hx_channel_mask == 1: hx_channel_mask = 0 else: hx_channel_mask = self.hx_channel_mask h_s.pack(self._waveformat_xma2, self.hx_num_streams, hx_channel_mask, self.hx_samples_encoded, self.hx_bytes_per_block, self.hx_play_begin, self.hx_play_length, self.hx_loop_begin, self.hx_loop_length, self.hx_loop_count, self.hx_encoder_version, self.hx_block_count) elif self.h_format_tag == WAVE_FORMAT_EXTENSIBLE: h_s.pack(self._waveformat_extensible, self.he_valid_bits_per_sample, self.he_channel_mask, self.he_subformat.bytes_le) if self.he_remainder: h_s.write(self.he_remainder) if self.h_remainder: h_s.write(self.h_remainder) header_raw = h_s.getvalue() if self.dpds_raw: dpds_size = len(self.dpds_raw) else: dpds_size = None if self.seek_raw: seek_size = len(self.seek_raw) else: seek_size = None o_s = BinaryStream() if self.h_format_tag == WAVE_FORMAT_WMAUDIO2 or self.h_format_tag == WAVE_FORMAT_WMAUDIO3: riff_type = b'XWMA' else: riff_type = b'WAVE' self.write_header(o_s, riff_type, len(header_raw), len(self.data_raw), dpds_size, seek_size) self.write_chunk(o_s, b'fmt ', header_raw) if self.dpds_raw: self.write_chunk(o_s, b'dpds', self.dpds_raw) if self.seek_raw: self.write_chunk(o_s, b'seek', self.seek_raw) self.write_chunk(o_s, b'data', self.data_raw) if self.h_format_tag == WAVE_FORMAT_XMA2: full_filename = filename + '.xma' elif self.h_format_tag == WAVE_FORMAT_WMAUDIO2 or self.h_format_tag == WAVE_FORMAT_WMAUDIO2: full_filename = filename + '.xwma' else: full_filename = filename + '.wav' o_s.write_file(full_filename)
def save(self, filename=None, compress=False): if self.file_platform not in XNB_PLATFORMS: raise ReaderError("bad platform: '{!r}'".format( self.file_platform)) if self.file_version not in XNB_VERSIONS: raise ReaderError("bad version: {}".format(self.file_version)) attribs = 0 if self.file_version >= VERSION_40: if self.graphics_profile not in XNB_PROFILES: raise ReaderError("bad profile: {}".format( self.graphics_profile)) attribs |= self.graphics_profile & _PROFILE_MASK do_compress = False if self.file_version >= VERSION_30: if compress: do_compress = True attribs |= _COMPRESS_MASK stream = BinaryStream() if do_compress: raise ReaderError("Recompression not supported") else: data = self.getvalue() size = len(data) + stream.calc_size(_XNB_HEADER) stream.pack(_XNB_HEADER, XNB_SIGNATURE, self.file_platform, self.file_version, attribs, size) stream.write(data) if filename is not None: filename = os.path.normpath(filename) dirname = os.path.dirname(filename) if not os.path.isdir(dirname): os.makedirs(dirname) if not filename.endswith(XNB_EXTENSION): filename += XNB_EXTENSION stream.write_file(filename) else: return stream.getvalue()
def __init__(self, data=None, filename=None, audio_engine=None): self.audio_engine = audio_engine # open in little endian initially stream = BinaryStream(data=data, filename=filename) del data # check sig to find actual endianess h_sig = stream.peek(len(WB_L_SIGNATURE)) if h_sig == WB_L_SIGNATURE: big_endian = False elif h_sig == WB_B_SIGNATURE: big_endian = True else: raise ValueError("bad sig: {!r}".format(h_sig)) # switch stream to correct endianess stream.set_endian(big_endian) (h_sig, self.h_version, self.h_header_version) = stream.unpack(_WB_HEADER) regions = {k: XWBRegion._make(stream.unpack(_WB_REGION)) for k in _REGIONS} # pylint: disable-msg=W0212 # check if we have a valid BANKDATA region and parse it bankdata_size = stream.calc_size(_WB_DATA) if regions['BANKDATA'].length != bankdata_size: raise ReaderError("Invalid BANKDATA size: {} != {}".format(regions['BANKDATA'].length, bankdata_size)) stream.seek(regions['BANKDATA'].offset) (self.flags, h_entry_count, h_bank_name_raw, h_entry_metadata_element_size, h_entry_name_element_size, self.alignment, h_compact_format, buildtime_raw_low, buildtime_raw_high) = stream.unpack(_WB_DATA) self.bank_name = h_bank_name_raw.rstrip(b'\x00').decode('iso8859-1') del h_bank_name_raw self.buildtime = filetime_to_datetime(buildtime_raw_low, buildtime_raw_high) if self.flags & ~(WB_TYPE_MASK | WB_FLAGS_MASK): raise ReaderError("Unknown flags in WAVEBANK") # check what type of ENTRYMETADATA we have and parse it if self.has_compact: raise ReaderError("Compact format not supported") bankentry_size = stream.calc_size(_WB_ENTRY) if bankentry_size != h_entry_metadata_element_size: raise ReaderError("Unknown EntryMetaDataElementSize: {} != {}".format(bankentry_size, h_entry_metadata_element_size)) if regions['ENTRYMETADATA'].length != bankentry_size * h_entry_count: raise ReaderError("Invalid ENTRYMETADATA size: {} != {}".format(regions['ENTRYMETADATA'].length, bankentry_size * h_entry_count)) stream.seek(regions['ENTRYMETADATA'].offset) entry_metadata = [XWBEntry._make(stream.unpack(_WB_ENTRY)) # pylint: disable-msg=W0212,E1101 for _ in range(h_entry_count)] # read ENTRYNAMES if present entry_names = [] if self.has_entry_names and regions['ENTRYNAMES'].offset and regions['ENTRYNAMES'].length: if regions['ENTRYNAMES'].length != h_entry_name_element_size * h_entry_count: raise ReaderError("Invalid ENTRYNAMES region size: {} != {}".format( regions['ENTRYNAMES'].length, h_entry_name_element_size * h_entry_count)) stream.seek(regions['ENTRYNAMES'].offset) entry_names = [stream.read(h_entry_name_element_size).rstrip(b'\x00').decode('iso8859-1') for _ in range(h_entry_count)] # read SEEKTABLES if present entry_seektables = [] if self.has_seek_tables and regions['SEEKTABLES'].offset and regions['SEEKTABLES'].length: stream.seek(regions['SEEKTABLES'].offset) seek_offsets = [] for _ in range(h_entry_count): seek_offsets.append(stream.read_int32()) seek_data_offset = stream.tell() for cur_offset in seek_offsets: if cur_offset >= 0: stream.seek(seek_data_offset + cur_offset) packet_count = stream.read_uint32() cur_seek_data = BinaryStream() for _ in range(packet_count): cur_seek_data.write_uint32(stream.read_uint32()) entry_seektables.append(cur_seek_data.getvalue()) else: entry_seektables.append(None) self.entries = [] for i, cur_meta in enumerate(entry_metadata): c_entry_flags = cur_meta.flags_duration & WB_ENTRY_FLAGS_MASK c_duration = (cur_meta.flags_duration & WB_ENTRY_DURATION_MASK) >> 4 c_format_tag = cur_meta.format & WB_FORMAT_TAG_MASK c_channels = (cur_meta.format & WB_FORMAT_CHANNELS) >> 2 c_samples_per_sec = (cur_meta.format & WB_FORMAT_SAMPLES_PER_SEC) >> 5 c_block_align = (cur_meta.format & WB_FORMAT_BLOCK_ALIGN) >> 23 c_bits_per_sample = (cur_meta.format & WB_FORMAT_BITS_PER_SAMPLE) >> 31 entry_name = None if entry_names: entry_name = entry_names[i] entry_dpds = None entry_seek = None extra_header = bytes() # build format specific header and seek data if c_format_tag == WB_FORMAT_TAG_PCM: c_format_tag = WAVE_FORMAT_PCM if c_bits_per_sample == 1: c_bits_per_sample = 16 else: c_bits_per_sample = 8 c_avg_bytes_per_sec = c_samples_per_sec * c_block_align elif c_format_tag == WB_FORMAT_TAG_ADPCM: c_format_tag = WAVE_FORMAT_ADPCM c_bits_per_sample = 4 c_block_align = (c_block_align + ADPCM_BLOCK_ALIGN_OFFSET) * c_channels cx_samples_per_block = ((c_block_align - (7 * c_channels)) * 8) // (c_bits_per_sample * c_channels) + 2 c_avg_bytes_per_sec = (c_samples_per_sec // cx_samples_per_block) * c_block_align cx_num_coef = len(ADPCM_COEF) extra_header = _ADPCM_WAVEFORMAT.pack(cx_samples_per_block, cx_num_coef) for coef in ADPCM_COEF: extra_header += _ADPCM_WAVEFORMAT_COEF.pack(coef[0], coef[1]) elif c_format_tag == WB_FORMAT_TAG_WMA: if c_bits_per_sample == 1: c_format_tag = WAVE_FORMAT_WMAUDIO3 else: c_format_tag = WAVE_FORMAT_WMAUDIO2 c_bits_per_sample = 16 c_avg_bytes_per_sec = WMA_AVG_BYTES_PER_SEC[c_block_align >> 5] c_block_align = WMA_BLOCK_ALIGN[c_block_align & 0x1f] if entry_seektables: entry_dpds = entry_seektables[i] else: raise ReaderError("No SEEKTABLES found for xWMA format") elif c_format_tag == WB_FORMAT_TAG_XMA: # lots of placeholders in here but seems to decode ok c_format_tag = WAVE_FORMAT_XMA2 c_bits_per_sample = 16 c_avg_bytes_per_sec = 0 cx_num_streams = 1 if c_channels == 2: cx_channel_mask = 3 else: cx_channel_mask = 0 cx_samples_encoded = 0 cx_bytes_per_block = 0 cx_play_begin = 0 cx_play_length = 0 cx_loop_begin = 0 cx_loop_length = 0 cx_loop_count = 0 cx_encoder_version = 4 cx_block_count = 1 extra_header = _XMA_WAVEFORMAT.pack(cx_num_streams, cx_channel_mask, cx_samples_encoded, cx_bytes_per_block, cx_play_begin, cx_play_length, cx_loop_begin, cx_loop_length, cx_loop_count, cx_encoder_version, cx_block_count) if entry_seektables: entry_seek = entry_seektables[i] else: raise ReaderError("No SEEKTABLES found for XMA2 format") else: raise ReaderError("Unhandled entry format: {}".format(c_format_tag)) cx_size = len(extra_header) entry_header = _WAVEFORMATEX.pack(c_format_tag, c_channels, c_samples_per_sec, c_avg_bytes_per_sec, c_block_align, c_bits_per_sample, cx_size) entry_header += extra_header # read entry wave data stream.seek(regions['ENTRYWAVEDATA'].offset + cur_meta.play_offset) # manually swap PCM data if needed entry_data = stream.read(cur_meta.play_length) if big_endian and c_format_tag == WAVE_FORMAT_PCM and c_bits_per_sample == 16: entry_data = bytearray(entry_data) entry_data[1::2], entry_data[0::2] = entry_data[0::2], entry_data[1::2] self.entries.append(Entry(entry_name, entry_header, entry_data, entry_dpds, entry_seek))
def __init__(self, data=None, filename=None, audio_engine=None): self.audio_engine = audio_engine # open in little endian initially stream = BinaryStream(data=data, filename=filename) del data # check sig to find actual endianess h_sig = stream.peek(len(WB_L_SIGNATURE)) if h_sig == WB_L_SIGNATURE: big_endian = False elif h_sig == WB_B_SIGNATURE: big_endian = True else: raise ValueError("bad sig: {!r}".format(h_sig)) # switch stream to correct endianess stream.set_endian(big_endian) (h_sig, self.h_version, self.h_header_version) = stream.unpack(_WB_HEADER) regions = { k: XWBRegion._make(stream.unpack(_WB_REGION)) for k in _REGIONS } # pylint: disable-msg=W0212 # check if we have a valid BANKDATA region and parse it bankdata_size = stream.calc_size(_WB_DATA) if regions['BANKDATA'].length != bankdata_size: raise ReaderError("Invalid BANKDATA size: {} != {}".format( regions['BANKDATA'].length, bankdata_size)) stream.seek(regions['BANKDATA'].offset) (self.flags, h_entry_count, h_bank_name_raw, h_entry_metadata_element_size, h_entry_name_element_size, self.alignment, h_compact_format, buildtime_raw_low, buildtime_raw_high) = stream.unpack(_WB_DATA) self.bank_name = h_bank_name_raw.rstrip(b'\x00').decode('iso8859-1') del h_bank_name_raw self.buildtime = filetime_to_datetime(buildtime_raw_low, buildtime_raw_high) if self.flags & ~(WB_TYPE_MASK | WB_FLAGS_MASK): raise ReaderError("Unknown flags in WAVEBANK") # check what type of ENTRYMETADATA we have and parse it if self.has_compact: raise ReaderError("Compact format not supported") bankentry_size = stream.calc_size(_WB_ENTRY) if bankentry_size != h_entry_metadata_element_size: raise ReaderError( "Unknown EntryMetaDataElementSize: {} != {}".format( bankentry_size, h_entry_metadata_element_size)) if regions['ENTRYMETADATA'].length != bankentry_size * h_entry_count: raise ReaderError("Invalid ENTRYMETADATA size: {} != {}".format( regions['ENTRYMETADATA'].length, bankentry_size * h_entry_count)) stream.seek(regions['ENTRYMETADATA'].offset) entry_metadata = [ XWBEntry._make( stream.unpack(_WB_ENTRY)) # pylint: disable-msg=W0212,E1101 for _ in range(h_entry_count) ] # read ENTRYNAMES if present entry_names = [] if self.has_entry_names and regions['ENTRYNAMES'].offset and regions[ 'ENTRYNAMES'].length: if regions[ 'ENTRYNAMES'].length != h_entry_name_element_size * h_entry_count: raise ReaderError( "Invalid ENTRYNAMES region size: {} != {}".format( regions['ENTRYNAMES'].length, h_entry_name_element_size * h_entry_count)) stream.seek(regions['ENTRYNAMES'].offset) entry_names = [ stream.read(h_entry_name_element_size).rstrip(b'\x00').decode( 'iso8859-1') for _ in range(h_entry_count) ] # read SEEKTABLES if present entry_seektables = [] if self.has_seek_tables and regions['SEEKTABLES'].offset and regions[ 'SEEKTABLES'].length: stream.seek(regions['SEEKTABLES'].offset) seek_offsets = [] for _ in range(h_entry_count): seek_offsets.append(stream.read_int32()) seek_data_offset = stream.tell() for cur_offset in seek_offsets: if cur_offset >= 0: stream.seek(seek_data_offset + cur_offset) packet_count = stream.read_uint32() cur_seek_data = BinaryStream() for _ in range(packet_count): cur_seek_data.write_uint32(stream.read_uint32()) entry_seektables.append(cur_seek_data.getvalue()) else: entry_seektables.append(None) self.entries = [] for i, cur_meta in enumerate(entry_metadata): c_entry_flags = cur_meta.flags_duration & WB_ENTRY_FLAGS_MASK c_duration = (cur_meta.flags_duration & WB_ENTRY_DURATION_MASK) >> 4 c_format_tag = cur_meta.format & WB_FORMAT_TAG_MASK c_channels = (cur_meta.format & WB_FORMAT_CHANNELS) >> 2 c_samples_per_sec = (cur_meta.format & WB_FORMAT_SAMPLES_PER_SEC) >> 5 c_block_align = (cur_meta.format & WB_FORMAT_BLOCK_ALIGN) >> 23 c_bits_per_sample = (cur_meta.format & WB_FORMAT_BITS_PER_SAMPLE) >> 31 entry_name = None if entry_names: entry_name = entry_names[i] entry_dpds = None entry_seek = None extra_header = bytes() # build format specific header and seek data if c_format_tag == WB_FORMAT_TAG_PCM: c_format_tag = WAVE_FORMAT_PCM if c_bits_per_sample == 1: c_bits_per_sample = 16 else: c_bits_per_sample = 8 c_avg_bytes_per_sec = c_samples_per_sec * c_block_align elif c_format_tag == WB_FORMAT_TAG_ADPCM: c_format_tag = WAVE_FORMAT_ADPCM c_bits_per_sample = 4 c_block_align = (c_block_align + ADPCM_BLOCK_ALIGN_OFFSET) * c_channels cx_samples_per_block = ( (c_block_align - (7 * c_channels)) * 8) // (c_bits_per_sample * c_channels) + 2 c_avg_bytes_per_sec = (c_samples_per_sec // cx_samples_per_block) * c_block_align cx_num_coef = len(ADPCM_COEF) extra_header = _ADPCM_WAVEFORMAT.pack(cx_samples_per_block, cx_num_coef) for coef in ADPCM_COEF: extra_header += _ADPCM_WAVEFORMAT_COEF.pack( coef[0], coef[1]) elif c_format_tag == WB_FORMAT_TAG_WMA: if c_bits_per_sample == 1: c_format_tag = WAVE_FORMAT_WMAUDIO3 else: c_format_tag = WAVE_FORMAT_WMAUDIO2 c_bits_per_sample = 16 c_avg_bytes_per_sec = WMA_AVG_BYTES_PER_SEC[c_block_align >> 5] c_block_align = WMA_BLOCK_ALIGN[c_block_align & 0x1f] if entry_seektables: entry_dpds = entry_seektables[i] else: raise ReaderError("No SEEKTABLES found for xWMA format") elif c_format_tag == WB_FORMAT_TAG_XMA: # lots of placeholders in here but seems to decode ok c_format_tag = WAVE_FORMAT_XMA2 c_bits_per_sample = 16 c_avg_bytes_per_sec = 0 cx_num_streams = 1 if c_channels == 2: cx_channel_mask = 3 else: cx_channel_mask = 0 cx_samples_encoded = 0 cx_bytes_per_block = 0 cx_play_begin = 0 cx_play_length = 0 cx_loop_begin = 0 cx_loop_length = 0 cx_loop_count = 0 cx_encoder_version = 4 cx_block_count = 1 extra_header = _XMA_WAVEFORMAT.pack( cx_num_streams, cx_channel_mask, cx_samples_encoded, cx_bytes_per_block, cx_play_begin, cx_play_length, cx_loop_begin, cx_loop_length, cx_loop_count, cx_encoder_version, cx_block_count) if entry_seektables: entry_seek = entry_seektables[i] else: raise ReaderError("No SEEKTABLES found for XMA2 format") else: raise ReaderError( "Unhandled entry format: {}".format(c_format_tag)) cx_size = len(extra_header) entry_header = _WAVEFORMATEX.pack(c_format_tag, c_channels, c_samples_per_sec, c_avg_bytes_per_sec, c_block_align, c_bits_per_sample, cx_size) entry_header += extra_header # read entry wave data stream.seek(regions['ENTRYWAVEDATA'].offset + cur_meta.play_offset) # manually swap PCM data if needed entry_data = stream.read(cur_meta.play_length) if big_endian and c_format_tag == WAVE_FORMAT_PCM and c_bits_per_sample == 16: entry_data = bytearray(entry_data) entry_data[1::2], entry_data[0::2] = entry_data[ 0::2], entry_data[1::2] self.entries.append( Entry(entry_name, entry_header, entry_data, entry_dpds, entry_seek))