PyCriCodecsEx 0.0.1__cp313-cp313-win_amd64.whl → 0.0.2__cp313-cp313-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of PyCriCodecsEx might be problematic. Click here for more details.

Binary file
PyCriCodecsEx/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.0.1"
1
+ __version__ = "0.0.2"
PyCriCodecsEx/acb.py CHANGED
@@ -1,18 +1,16 @@
1
- from struct import iter_unpack
2
- from typing import BinaryIO, List
3
- from io import BytesIO
4
- from PyCriCodecsEx.chunk import *
5
- from PyCriCodecsEx.utf import UTF, UTFBuilder, UTFViewer
6
- from PyCriCodecsEx.awb import AWB, AWBBuilder
7
- from PyCriCodecsEx.hca import HCA
8
- from copy import deepcopy
9
- import os
10
-
11
1
  # Credit:
12
2
  # - github.com/vgmstream/vgmstream which is why this is possible at all
13
3
  # - Original work by https://github.com/Youjose/PyCriCodecs
14
4
  # See Research/ACBSchema.py for more details.
15
5
 
6
+ from typing import Generator, List, Tuple
7
+ from PyCriCodecsEx.chunk import *
8
+ from PyCriCodecsEx.utf import UTF, UTFBuilder, UTFViewer
9
+ from PyCriCodecsEx.usm import HCACodec, ADXCodec
10
+ from PyCriCodecsEx.awb import AWB, AWBBuilder
11
+ from dataclasses import dataclass
12
+ from copy import deepcopy
13
+
16
14
  class CueNameTable(UTFViewer):
17
15
  CueIndex: int
18
16
  CueName: str
@@ -20,11 +18,13 @@ class CueNameTable(UTFViewer):
20
18
 
21
19
  class CueTable(UTFViewer):
22
20
  CueId: int
21
+ Length: int
23
22
  ReferenceIndex: int
24
23
  ReferenceType: int
25
24
 
26
25
 
27
26
  class SequenceTable(UTFViewer):
27
+ NumTracks : int
28
28
  TrackIndex: bytes
29
29
  Type: int
30
30
 
@@ -65,6 +65,75 @@ class ACBTable(UTFViewer):
65
65
  TrackTable: List[TrackTable]
66
66
  WaveformTable: List[WaveformTable]
67
67
 
68
+ @staticmethod
69
+ def decode_tlv(data : bytes):
70
+ pos = 0
71
+ while pos < len(data):
72
+ tag = data[pos : pos + 2]
73
+ length = data[pos + 3]
74
+ value = data[pos + 4 : pos + 4 + length]
75
+ pos += 3 + length
76
+ yield (tag, value)
77
+
78
+ def waveform_of_track(self, index: int):
79
+ tlv = self.decode_tlv(self.TrackEventTable[index])
80
+ def noteOn(data: bytes):
81
+ # Handle note on event
82
+ tlv_type, tlv_index = AcbTrackCommandNoteOnStruct.unpack(data[:AcbTrackCommandNoteOnStruct.size])
83
+ match tlv_type:
84
+ case 0x02: # Synth
85
+ yield from self.waveform_of_synth(tlv_index)
86
+ case 0x03: # Sequence
87
+ yield from self.waveform_of_sequence(tlv_index)
88
+ # Ignore others silently
89
+ for code, data in tlv:
90
+ match code:
91
+ case 2000:
92
+ yield from noteOn(data)
93
+ case 2003:
94
+ yield from noteOn(data)
95
+
96
+ def waveform_of_sequence(self, index : int):
97
+ seq = self.SequenceTable[index]
98
+ for i in range(seq.NumTracks):
99
+ track_index = int.from_bytes(seq.TrackIndex[i*2:i*2+2], 'big')
100
+ yield self.WaveformTable[track_index]
101
+
102
+ def waveform_of_synth(self, index: int):
103
+ item_type, item_index = AcbSynthReferenceStruct.unpack(self.SynthTable[index].ReferenceItems)
104
+ match item_type:
105
+ case 0x00: # No audio
106
+ return
107
+ case 0x01: # Waveform
108
+ yield self.WaveformTable[item_index]
109
+ case 0x02: # Yet another synth...
110
+ yield from self.waveform_of_synth(item_index)
111
+ case 0x03: # Sequence
112
+ yield from self.waveform_of_sequence(item_index)
113
+ case _:
114
+ raise NotImplementedError(f"Unknown synth reference type: {item_type} at index {index}")
115
+
116
+ def waveform_of(self, index : int) -> List["WaveformTable"]:
117
+ cue = next(filter(lambda c: c.CueId == index, self.CueTable), None)
118
+ assert cue, "cue of index %d not found" % index
119
+ match cue.ReferenceType:
120
+ case 0x01:
121
+ return [self.WaveformTable[index]]
122
+ case 0x02:
123
+ return list(self.waveform_of_synth(index))
124
+ case 0x03:
125
+ return list(self.waveform_of_sequence(index))
126
+ case 0x08:
127
+ raise NotImplementedError("BlockSequence type not implemented yet")
128
+ case _:
129
+ raise NotImplementedError(f"Unknown cue reference type: {cue.ReferenceType}")
130
+
131
+ @dataclass(frozen=True)
132
+ class CueItem:
133
+ CueId: int
134
+ CueName: str
135
+ Length: float
136
+ Waveforms: list[int] # List of waveform IDs
68
137
 
69
138
  class ACB(UTF):
70
139
  """An ACB is basically a giant @UTF table. Use this class to extract any ACB, and potentially modifiy it in place."""
@@ -73,21 +142,117 @@ class ACB(UTF):
73
142
 
74
143
  @property
75
144
  def payload(self) -> dict:
76
- """Retrives the only top-level UTF table dict within the ACB file."""
145
+ """Retrives the only UTF table dict within the ACB file."""
77
146
  return self.dictarray[0]
78
147
 
79
148
  @property
80
149
  def view(self) -> ACBTable:
81
- """Returns a view of the ACB file, with all known tables mapped to their respective classes."""
150
+ """Returns a view of the ACB file, with all known tables mapped to their respective classes.
151
+
152
+ * Use this to interact with the ACB payload instead of `payload` for helper functions, etc"""
82
153
  return ACBTable(self.payload)
83
154
 
84
- # TODO: Extraction routines
85
- # See Research/ACBSchema.py. vgmstream presented 4 possible permutations of subsong retrieval.
155
+ @property
156
+ def name(self) -> str:
157
+ """Returns the name of the ACB file."""
158
+ return self.view.Name
159
+
160
+ @property
161
+ def awb(self) -> AWB:
162
+ """Returns the AWB object associated with the ACB."""
163
+ return AWB(self.view.AwbFile)
164
+
165
+ def get_waveforms(self) -> List[HCACodec | ADXCodec | Tuple[AcbEncodeTypes, int, int, int, bytes]]:
166
+ """Returns a list of decoded waveforms.
167
+
168
+ Item may be a codec (if known), or a tuple of (Codec ID, Channel Count, Sample Count, Sample Rate, Raw data).
169
+ """
170
+ CODEC_TABLE = {
171
+ AcbEncodeTypes.ADX: ADXCodec,
172
+ AcbEncodeTypes.HCA: HCACodec,
173
+ AcbEncodeTypes.HCAMX: HCACodec,
174
+ }
175
+ awb = self.awb
176
+ wavs = []
177
+ for wav in self.view.WaveformTable:
178
+ encode = AcbEncodeTypes(wav.EncodeType)
179
+ codec = (CODEC_TABLE.get(encode, None))
180
+ if codec:
181
+ wavs.append(codec(awb.get_file_at(wav.MemoryAwbId)))
182
+ else:
183
+ wavs.append((encode, wav.NumChannels, wav.NumSamples, wav.SamplingRate, awb.get_file_at(wav.MemoryAwbId)))
184
+ return wavs
185
+
186
+ def set_waveforms(self, value: List[HCACodec | ADXCodec | Tuple[AcbEncodeTypes, int, int, int, bytes]]):
187
+ """Sets the waveform data.
188
+
189
+ Input item may be a codec (if known), or a tuple of (Codec ID, Channel Count, Sample Count, Sample Rate, Raw data).
190
+
191
+ NOTE: Cue duration is not set. You need to change that manually.
192
+ """
193
+ WAVEFORM = self.view.WaveformTable[0]._payload.copy()
194
+ encoded = []
195
+ tables = self.view.WaveformTable
196
+ tables.clear()
197
+ for i, codec in enumerate(value):
198
+ if type(codec) == HCACodec:
199
+ encoded.append(codec.get_encoded())
200
+ tables.append(WaveformTable(WAVEFORM.copy()))
201
+ entry = tables[-1]
202
+ entry.EncodeType = AcbEncodeTypes.HCA.value
203
+ entry.NumChannels = codec.chnls
204
+ entry.NumSamples = codec.total_samples
205
+ entry.SamplingRate = codec.sampling_rate
206
+ elif type(codec) == ADXCodec:
207
+ encoded.append(codec.get_encoded())
208
+ tables.append(WaveformTable(WAVEFORM.copy()))
209
+ entry = tables[-1]
210
+ entry.EncodeType = AcbEncodeTypes.ADX.value
211
+ entry.NumChannels = codec.chnls
212
+ entry.NumSamples = codec.total_samples
213
+ entry.SamplingRate = codec.sampling_rate
214
+ elif isinstance(codec, tuple):
215
+ e_type, e_channels, e_samples, e_rate, e_data = codec
216
+ encoded.append(e_data)
217
+ tables.append(WaveformTable(WAVEFORM.copy()))
218
+ entry = tables[-1]
219
+ entry.EncodeType = e_type.value
220
+ entry.NumChannels = e_channels
221
+ entry.NumSamples = e_samples
222
+ entry.SamplingRate = e_rate
223
+ else:
224
+ raise TypeError(f"Unsupported codec type: {type(codec)}")
225
+ tables[-1].MemoryAwbId = i
226
+ awb = self.awb
227
+ self.view.AwbFile = AWBBuilder(encoded, awb.subkey, awb.version, align=awb.align).build()
228
+ pass
229
+
230
+ @property
231
+ def cues(self) -> Generator[CueItem, None, None]:
232
+ """Returns a generator of **read-only** Cues.
233
+
234
+ Cues reference waveform bytes by their AWB IDs, which can be accessed via `waveforms`.
235
+ To modify cues, use the `view` property instead.
236
+ """
237
+ for name, cue in zip(self.view.CueNameTable, self.view.CueTable):
238
+ waveforms = self.view.waveform_of(cue.CueId)
239
+ yield CueItem(cue.CueId, name.CueName, cue.Length / 1000.0, [waveform.MemoryAwbId for waveform in waveforms])
86
240
 
87
241
  class ACBBuilder:
88
242
  acb: ACB
89
243
 
90
244
  def __init__(self, acb: ACB) -> None:
245
+ """Initializes the ACBBuilder with an existing ACB object.
246
+
247
+ Building ACB from scratch isn't planned for now since:
248
+ * We don't know how SeqCommandTable TLVs work. This is the biggest issue.
249
+ * Many fields are unknown or not well understood
250
+ - Games may expect AcfReferenceTable, Asiac stuff etc to be present for their own assets in conjunction
251
+ with their own ACF table. Missing these is not a fun debugging experience.
252
+ * ACB tables differ a LOT from game to game (e.g. Lipsync info), contary to USM formats.
253
+
254
+ Maybe one day I'll get around to this. But otherwise starting from nothing is a WONTFIX for now.
255
+ """
91
256
  self.acb = acb
92
257
 
93
258
  def build(self) -> bytes:
@@ -95,6 +260,9 @@ class ACBBuilder:
95
260
 
96
261
  The object may be modified in place before building, which will be reflected in the output binary.
97
262
  """
98
- payload = deepcopy(self.acb.dictarray)
99
- binary = UTFBuilder(payload, encoding=self.acb.encoding, table_name=self.acb.table_name)
263
+ # Check whether all AWB indices are valid
264
+ assert all(
265
+ waveform.MemoryAwbId < self.acb.awb.numfiles for waveform in self.acb.view.WaveformTable
266
+ ), "one or more AWB indices are out of range"
267
+ binary = UTFBuilder(self.acb.dictarray, encoding=self.acb.encoding, table_name=self.acb.table_name)
100
268
  return binary.bytes()
PyCriCodecsEx/awb.py CHANGED
@@ -19,16 +19,21 @@ class AWB:
19
19
  headersize: int
20
20
  id_alignment: int
21
21
 
22
- def __init__(self, stream) -> None:
22
+ def __init__(self, stream : str | BinaryIO) -> None:
23
+ """Initializes the AWB object
24
+
25
+ Args:
26
+ stream (str | BinaryIO): Source file path or binary stream
27
+ """
23
28
  if type(stream) == str:
24
29
  self.stream = FileIO(stream)
25
30
  self.filename = stream
26
31
  else:
27
32
  self.stream = BytesIO(stream)
28
33
  self.filename = ""
29
- self.readheader()
34
+ self._readheader()
30
35
 
31
- def readheader(self):
36
+ def _readheader(self):
32
37
  # Reads header.
33
38
  magic, self.version, offset_intsize, self.id_intsize, self.numfiles, self.align, self.subkey = AWBChunkHeader.unpack(
34
39
  self.stream.read(AWBChunkHeader.size)
@@ -39,9 +44,9 @@ class AWB:
39
44
  # Reads data in the header.
40
45
  self.ids = list()
41
46
  self.ofs = list()
42
- for i in iter_unpack(f"<{self.stringtypes(self.id_intsize)}", self.stream.read(self.id_intsize*self.numfiles)):
47
+ for i in iter_unpack(f"<{self._stringtypes(self.id_intsize)}", self.stream.read(self.id_intsize*self.numfiles)):
43
48
  self.ids.append(i[0])
44
- for i in iter_unpack(f"<{self.stringtypes(offset_intsize)}", self.stream.read(offset_intsize*(self.numfiles+1))):
49
+ for i in iter_unpack(f"<{self._stringtypes(offset_intsize)}", self.stream.read(offset_intsize*(self.numfiles+1))):
45
50
  self.ofs.append(i[0] if i[0] % self.align == 0 else (i[0] + (self.align - (i[0] % self.align))))
46
51
 
47
52
  # Seeks to files offset.
@@ -51,21 +56,20 @@ class AWB:
51
56
  self.stream.seek(self.headersize, 0)
52
57
 
53
58
  def get_files(self):
54
- """ Generator function to yield all data blobs from an AWB. """
59
+ """Generator function to yield all data blobs from an AWB. """
60
+ self.stream.seek(self.headersize, 0)
55
61
  for i in range(1, len(self.ofs)):
56
62
  data = self.stream.read((self.ofs[i]-self.ofs[i-1]))
57
63
  self.stream.seek(self.ofs[i], 0)
58
64
  yield data
59
65
 
60
- def get_file_at(self, index):
61
- """ Gets you a file at specific index. """
62
- index += 1
66
+ def get_file_at(self, index) -> bytes:
67
+ """Gets you a file at specific index. """
63
68
  self.stream.seek(self.ofs[index], 0)
64
- data = self.stream.read(self.ofs[index]-self.ofs[index-1])
65
- self.stream.seek(self.headersize, 0) # Seeks back to headersize for getfiles.
69
+ data = self.stream.read(self.ofs[index + 1]-self.ofs[index])
66
70
  return data
67
71
 
68
- def stringtypes(self, intsize: int) -> str:
72
+ def _stringtypes(self, intsize: int) -> str:
69
73
  if intsize == 1:
70
74
  return "B" # Probably impossible.
71
75
  elif intsize == 2:
@@ -79,6 +83,15 @@ class AWB:
79
83
 
80
84
  class AWBBuilder:
81
85
  def __init__(self, infiles: list[bytes], subkey: int = 0, version: int = 2, id_intsize = 0x2, align: int = 0x20) -> None:
86
+ """Initializes the AWB builder.
87
+
88
+ Args:
89
+ infiles (list[bytes]): List of bytes to be included in the AWB file.
90
+ subkey (int, optional): AWB subkey. Defaults to 0.
91
+ version (int, optional): AWB version. Defaults to 2.
92
+ id_intsize (hexadecimal, optional): Integer size (in bytes) for string lengths. Defaults to 0x2.
93
+ align (int, optional): Alignment. Defaults to 0x20.
94
+ """
82
95
  if version == 1 and subkey != 0:
83
96
  raise ValueError("Cannot have a subkey with AWB version of 1.")
84
97
  elif id_intsize not in [0x2, 0x4, 0x8]:
@@ -89,7 +102,7 @@ class AWBBuilder:
89
102
  self.subkey = subkey
90
103
  self.id_intsize = id_intsize
91
104
 
92
- def stringtypes(self, intsize: int) -> str:
105
+ def _stringtypes(self, intsize: int) -> str:
93
106
  if intsize == 1:
94
107
  return "B" # Probably impossible.
95
108
  elif intsize == 2:
@@ -102,6 +115,7 @@ class AWBBuilder:
102
115
  raise ValueError("Unknown int size.")
103
116
 
104
117
  def build(self) -> bytes:
118
+ """Builds the AWB file from the provided infiles bytes."""
105
119
  size = 0
106
120
  ofs = []
107
121
  numfiles = 0
@@ -122,7 +136,7 @@ class AWBBuilder:
122
136
  b'AFS2', self.version, intsize, self.id_intsize, numfiles, self.align, self.subkey
123
137
  )
124
138
 
125
- id_strsize = f"<{self.stringtypes(self.id_intsize)}"
139
+ id_strsize = f"<{self._stringtypes(self.id_intsize)}"
126
140
  for i in range(numfiles):
127
141
  header += pack(id_strsize, i)
128
142
 
PyCriCodecsEx/chunk.py CHANGED
@@ -12,7 +12,8 @@ WavNoteHeaderStruct = Struct("<4sII")
12
12
  WavDataHeaderStruct = Struct("<4sI")
13
13
  AdxHeaderStruct = Struct(">HHBBBBIIHBB")
14
14
  AdxLoopHeaderStruct = Struct(">HHHHIIII")
15
-
15
+ AcbSynthReferenceStruct = Struct(">HH")
16
+ AcbTrackCommandNoteOnStruct = Struct(">HH")
16
17
  class USMChunckHeaderType(Enum):
17
18
  CRID = b"CRID" # Header.
18
19
  SFSH = b"SFSH" # SofDec1 Header?
@@ -72,4 +73,25 @@ class CriHcaQuality(Enum):
72
73
  High = 1
73
74
  Middle = 2
74
75
  Low = 3
75
- Lowest = 5
76
+ Lowest = 5
77
+
78
+ class AcbEncodeTypes(Enum):
79
+ ADX = 0
80
+ PCM = 1
81
+ HCA = 2
82
+ ADX_ALT = 3
83
+ WiiDSP = 4
84
+ NDSDSP = 5
85
+ HCAMX = 6
86
+ VAG = 7
87
+ ATRAC3 = 8
88
+ CWAV = 9
89
+ HEVAG = 10
90
+ ATRAC9 = 11
91
+ X360XMA = 12
92
+ DSP = 13
93
+ CWACDSP = 14
94
+ PS4HEVAG = 15
95
+ PS4ATRAC9 = 16
96
+ AACM4A = 17
97
+ SwitchOpus = 18
PyCriCodecsEx/cpk.py CHANGED
@@ -8,7 +8,7 @@ from concurrent.futures import ProcessPoolExecutor, as_completed
8
8
  from tempfile import NamedTemporaryFile
9
9
  import CriCodecsEx
10
10
 
11
- def worker_do_compression(src : str, dst: str):
11
+ def _worker_do_compression(src : str, dst: str):
12
12
  with open(src, "rb") as fsrc, open(dst, "wb") as fdst:
13
13
  data = fsrc.read()
14
14
  compressed = CriCodecsEx.CriLaylaCompress(data)
@@ -55,7 +55,12 @@ class CPK:
55
55
  stream: BinaryIO
56
56
  tables: dict
57
57
  filename: str
58
- def __init__(self, filename) -> None:
58
+ def __init__(self, filename : str | BinaryIO) -> None:
59
+ """Loads a CPK archive's table-of-content and ready for file reading.
60
+
61
+ Args:
62
+ filename (str | BinaryIO): The path to the CPK file or a BinaryIO stream containing the CPK data.
63
+ """
59
64
  if type(filename) == str:
60
65
  self.filename = filename
61
66
  self.stream = FileIO(filename)
@@ -271,7 +276,7 @@ class CPKBuilder:
271
276
  futures = []
272
277
  for (src, _, _), (dst, compress) in zip(self.in_files,self.os_files):
273
278
  if compress:
274
- futures.append(exec.submit(worker_do_compression, src, dst))
279
+ futures.append(exec.submit(_worker_do_compression, src, dst))
275
280
  for i, fut in as_completed(futures):
276
281
  try:
277
282
  fut.result()
@@ -281,7 +286,7 @@ class CPKBuilder:
281
286
  else:
282
287
  for i, ((src, _, _), (dst, compress)) in enumerate(zip(self.in_files,self.os_files)):
283
288
  if compress:
284
- worker_do_compression(src, dst)
289
+ _worker_do_compression(src, dst)
285
290
  self.progress_cb("Compress %s" % os.path.basename(src), i + 1, len(self.in_files))
286
291
  for (src, filename, _) , (dst, _) in zip(self.in_files,self.os_files):
287
292
  file_size = os.stat(src).st_size
PyCriCodecsEx/hca.py CHANGED
@@ -46,7 +46,14 @@ class HCA:
46
46
  table: array
47
47
  looping: bool
48
48
 
49
- def __init__(self, stream: BinaryIO, key: int = 0, subkey: int = 0) -> None:
49
+ def __init__(self, stream: str | BinaryIO, key: int = 0, subkey: int = 0) -> None:
50
+ """Initializes the HCA encoder/decoder
51
+
52
+ Args:
53
+ stream (str | BinaryIO): Path to the HCA or WAV file, or a BinaryIO stream.
54
+ key (int, optional): HCA key. Defaults to 0.
55
+ subkey (int, optional): HCA subkey. Defaults to 0.
56
+ """
50
57
  if type(stream) == str:
51
58
  self.stream = FileIO(stream)
52
59
  self.hcastream = FileIO(stream)
@@ -66,10 +73,10 @@ class HCA:
66
73
  self.hcabytes: bytearray = b''
67
74
  self.enc_table: array = b''
68
75
  self.table: array = b''
69
- self.Pyparse_header()
76
+ self._Pyparse_header()
70
77
 
71
78
 
72
- def Pyparse_header(self) -> None:
79
+ def _Pyparse_header(self) -> None:
73
80
  self.HcaSig, self.version, self.header_size = HcaHeaderStruct.unpack(
74
81
  self.hcastream.read(HcaHeaderStruct.size)
75
82
  )
@@ -230,7 +237,7 @@ class HCA:
230
237
  self.hcastream.seek(0)
231
238
 
232
239
  def info(self) -> dict:
233
- """ Returns info related to the input file. """
240
+ """Returns info related to the input file. """
234
241
  if self.filetype == "hca":
235
242
  return self.hca
236
243
  elif self.filetype == "wav":
@@ -238,6 +245,7 @@ class HCA:
238
245
  return wav
239
246
 
240
247
  def decode(self) -> bytes:
248
+ """Decodes the HCA or WAV file to WAV bytes. """
241
249
  if self.filetype == "wav":
242
250
  raise ValueError("Input type for decoding must be an HCA file.")
243
251
  self.hcastream.seek(0)
@@ -247,6 +255,7 @@ class HCA:
247
255
  return bytes(self.wavbytes)
248
256
 
249
257
  def encode(self, force_not_looping: bool = False, encrypt: bool = False, keyless: bool = False, quality_level: CriHcaQuality = CriHcaQuality.High) -> bytes:
258
+ """Encodes the WAV file to HCA bytes."""
250
259
  if self.filetype == "hca":
251
260
  raise ValueError("Input type for encoding must be a WAV file.")
252
261
  if force_not_looping == False:
@@ -260,21 +269,21 @@ class HCA:
260
269
  self.stream.seek(0)
261
270
  self.hcabytes = CriCodecsEx.HcaEncode(self.stream.read(), force_not_looping, quality_level.value)
262
271
  self.hcastream = BytesIO(self.hcabytes)
263
- self.Pyparse_header()
272
+ self._Pyparse_header()
264
273
  if encrypt:
265
274
  if self.key == 0 and not keyless:
266
275
  self.key = 0xCF222F1FE0748978 # Default key.
267
- self.encrypt(self.key, keyless)
276
+ self._encrypt(self.key, keyless)
268
277
  return self.get_hca()
269
278
 
270
- def encrypt(self, keycode: int, subkey: int = 0, keyless: bool = False) -> None:
279
+ def _encrypt(self, keycode: int, subkey: int = 0, keyless: bool = False) -> None:
271
280
  if(self.encrypted):
272
281
  raise ValueError("HCA is already encrypted.")
273
282
  self.encrypted = True
274
283
  enc = CriCodecsEx.HcaCrypt(self.get_hca(), 1, self.header_size, (1 if keyless else 56), keycode, subkey)
275
284
  self.hcastream = BytesIO(enc)
276
285
 
277
- def decrypt(self, keycode: int, subkey: int = 0) -> None:
286
+ def _decrypt(self, keycode: int, subkey: int = 0) -> None:
278
287
  if(not self.encrypted):
279
288
  raise ValueError("HCA is already decrypted.")
280
289
  self.encrypted = False
@@ -282,20 +291,20 @@ class HCA:
282
291
  self.hcastream = BytesIO(dec)
283
292
 
284
293
  def get_hca(self) -> bytes:
285
- """ Use this function to get the HCA file bytes after encrypting or decrypting. """
294
+ """Get the HCA file bytes after encrypting or decrypting. """
286
295
  self.hcastream.seek(0)
287
296
  fl: bytes = self.hcastream.read()
288
297
  self.hcastream.seek(0)
289
298
  return fl
290
299
 
291
300
  def get_frames(self):
292
- """ Generator function to yield Frame number, and Frame data. """
301
+ """Generator function to yield Frame number, and Frame data. """
293
302
  self.hcastream.seek(self.header_size, 0)
294
303
  for i in range(self.hca['FrameCount']):
295
304
  yield (i, self.hcastream.read(self.hca['FrameSize']))
296
305
 
297
306
  def get_header(self) -> bytes:
298
- """ Use this function to retrieve the HCA Header. """
307
+ """Get the HCA Header. """
299
308
  self.hcastream.seek(0)
300
309
  header = self.hcastream.read(self.header_size)
301
310
  self.hcastream.seek(0)
PyCriCodecsEx/usm.py CHANGED
@@ -332,7 +332,7 @@ class HCACodec(HCA):
332
332
 
333
333
  filesize: int
334
334
 
335
- def __init__(self, stream: str | bytes, filename: str, quality: CriHcaQuality = CriHcaQuality.High, key=0, subkey=0, **kwargs):
335
+ def __init__(self, stream: str | bytes, filename: str = "default.hca", quality: CriHcaQuality = CriHcaQuality.High, key=0, subkey=0, **kwargs):
336
336
  self.filename = filename
337
337
  super().__init__(stream, key, subkey)
338
338
  if self.filetype == "wav":
@@ -433,6 +433,13 @@ class HCACodec(HCA):
433
433
  p.strings = b"<NULL>\x00" + p.strings
434
434
  return p.bytes()
435
435
 
436
+ def get_encoded(self):
437
+ """Gets the encoded HCA audio data."""
438
+ self.hcastream.seek(0)
439
+ res = self.hcastream.read()
440
+ self.hcastream.seek(0)
441
+ return res
442
+
436
443
  def save(self, filepath: str):
437
444
  """Saves the decoded WAV audio to filepath"""
438
445
  with open(filepath, "wb") as f:
@@ -468,7 +475,7 @@ class ADXCodec(ADX):
468
475
  total_samples: int
469
476
  avbps: int
470
477
 
471
- def __init__(self, stream: str | bytes, filename: str, bitdepth: int = 4, **kwargs):
478
+ def __init__(self, stream: str | bytes, filename: str = "default.adx", bitdepth: int = 4, **kwargs):
472
479
  if type(stream) == str:
473
480
  self.adx = open(stream, "rb").read()
474
481
  else:
@@ -481,8 +488,8 @@ class ADXCodec(ADX):
481
488
  self.sfaStream = BytesIO(self.adx)
482
489
  header = AdxHeaderStruct.unpack(self.sfaStream.read(AdxHeaderStruct.size))
483
490
  FourCC, self.AdxDataOffset, self.AdxEncoding, self.AdxBlocksize, self.AdxSampleBitdepth, self.AdxChannelCount, self.AdxSamplingRate, self.AdxSampleCount, self.AdxHighpassFrequency, self.AdxVersion, self.AdxFlags = header
484
- assert FourCC == 0x8000, "Either ADX or WAV is supported"
485
- assert self.AdxVersion in {3,4}, "Unsupported ADX version"
491
+ assert FourCC == 0x8000, "either ADX or WAV is supported"
492
+ assert self.AdxVersion in {3,4}, "unsupported ADX version"
486
493
  if self.AdxVersion == 4:
487
494
  self.sfaStream.seek(4 + 4 * self.AdxChannelCount, 1) # Padding + Hist values, they always seem to be 0.
488
495
  self.sfaStream.seek(0)
@@ -552,6 +559,10 @@ class ADXCodec(ADX):
552
559
  def get_metadata(self):
553
560
  return None
554
561
 
562
+ def get_encoded(self):
563
+ """Gets the encoded ADX audio data."""
564
+ return self.adx
565
+
555
566
  def save(self, filepath: str):
556
567
  """Saves the encoded ADX audio to filepath"""
557
568
  with open(filepath, "wb") as f:
@@ -729,7 +740,7 @@ class USM(USMCrypt):
729
740
  stream.filename = sfname
730
741
  return stream
731
742
 
732
- def get_audios(self) -> List[HCACodec]:
743
+ def get_audios(self) -> List[ADXCodec | HCACodec]:
733
744
  """Create a list of audio codecs from the available streams."""
734
745
  match self.audio_codec:
735
746
  case ADXCodec.AUDIO_CODEC:
@@ -742,113 +753,71 @@ class USM(USMCrypt):
742
753
  class USMBuilder(USMCrypt):
743
754
  """USM class for building USM files."""
744
755
  video_stream: VP9Codec | H264Codec | MPEG1Codec
745
-
746
- enable_audio: bool
747
756
  audio_streams: List[HCACodec | ADXCodec]
748
757
 
749
- key: int
750
- encrypt: bool
751
- encrypt_audio: bool
758
+ key: int = None
759
+ encrypt: bool = False
760
+ encrypt_audio: bool = False
752
761
 
753
- audio_codec: int
754
- # !!: TODO Quality settings
755
762
  def __init__(
756
763
  self,
757
- video: str,
758
- audio: List[str] | str = None,
759
764
  key = None,
760
- audio_codec=HCACodec.AUDIO_CODEC,
761
- encrypt_audio: bool = False,
765
+ encrypt_audio = False
762
766
  ) -> None:
763
767
  """Initialize the USMBuilder from set source files.
764
768
 
765
769
  Args:
766
- video (str): The path to the video file. The video source format will be used to map accordingly to the ones Sofdec use.
767
- - MPEG1 (with M1V container): MPEG1 Codec (Sofdec Prime)
768
- - H264 (with H264 container): H264 Codec
769
- - VP9 (with IVF container): VP9 Codec
770
- audio (List[str] | str, optional): The path(s) to the audio file(s). Defaults to None.
771
770
  key (str | int, optional): The encryption key. Either int64 or a hex string. Defaults to None.
772
- audio_codec (int, optional): The audio codec to use. Defaults to HCACodec.AUDIO_CODEC.
773
- encrypt_audio (bool, optional): Whether to encrypt the audio. Defaults to False.
771
+ encrypt_audio (bool, optional): Whether to also encrypt the audio. Defaults to False.
774
772
  """
775
- self.audio_codec = audio_codec
776
- self.encrypt = False
777
- self.enable_audio = False
778
- self.encrypt_audio = encrypt_audio
779
- self.key = 0
780
- if encrypt_audio and not key:
781
- raise ValueError("Cannot encrypt Audio without key.")
782
773
  if key:
783
774
  self.init_key(key)
784
775
  self.encrypt = True
785
- self.load_video(video)
776
+ self.encrypt_audio = encrypt_audio
786
777
  self.audio_streams = []
787
- if audio:
788
- self.load_audio(audio)
789
- self.enable_audio = True
790
-
791
- def load_video(self, video):
792
- temp_stream = FFmpegCodec(video)
793
- self.video_stream = None
794
- match temp_stream.stream["codec_name"]:
795
- case "h264":
796
- self.video_stream = H264Codec(video)
797
- case "vp9":
798
- self.video_stream = VP9Codec(video)
799
- case "mpeg1video":
800
- self.video_stream = MPEG1Codec(video)
801
- assert self.video_stream, (
802
- "fail to match suitable video codec. Codec=%s"
803
- % temp_stream.stream["codec_name"]
804
- )
805
778
 
806
- def load_audio(self, audio):
807
- self.audio_filenames = []
808
- if type(audio) == list:
809
- count = 0
810
- for track in audio:
811
- if type(track) == str:
812
- self.audio_filenames.append(os.path.basename(track))
813
- else:
814
- self.audio_filenames.append("{:02d}.sfa".format(count))
815
- count += 1
816
- else:
817
- if type(audio) == str:
818
- self.audio_filenames.append(os.path.basename(audio))
819
- else:
820
- self.audio_filenames.append("00.sfa")
779
+ def add_video(self, video : str | H264Codec | VP9Codec | MPEG1Codec):
780
+ """Sets the video stream from the specified video file.
821
781
 
822
- self.audio_streams = []
823
- codec = None
824
- match self.audio_codec:
825
- case HCACodec.AUDIO_CODEC:
826
- codec = HCACodec
827
- case ADXCodec.AUDIO_CODEC:
828
- codec = ADXCodec
829
- assert codec, (
830
- "fail to match suitable audio codec given option: %s" % self.audio_codec
831
- )
832
- if type(audio) == list:
833
- for track in audio:
834
- if type(track) == str:
835
- fn = os.path.basename(track)
836
- else:
837
- fn = "{:02d}.sfa".format(count)
838
- hcaObj = codec(track, fn, key=self.key)
839
- self.audio_streams.append(hcaObj)
782
+ USMs only support one video stream. Consecutive calls to this method will replace the existing video stream.
783
+
784
+ When `video` is str - it will be treated as a file path. The video source format will be used to map accordingly to the ones Sofdec use.
785
+ - MPEG1 (with M1V container): MPEG1 Codec (Sofdec Prime)
786
+ - H264 (with H264 container): H264 Codec
787
+ - VP9 (with IVF container): VP9 Codec
788
+
789
+ Args:
790
+ video (str | FFmpegCodec): The path to the video file or an FFmpegCodec instance.
791
+ """
792
+ if isinstance(video, str):
793
+ temp_stream = FFmpegCodec(video)
794
+ self.video_stream = None
795
+ match temp_stream.stream["codec_name"]:
796
+ case "h264":
797
+ self.video_stream = H264Codec(video)
798
+ case "vp9":
799
+ self.video_stream = VP9Codec(video)
800
+ case "mpeg1video":
801
+ self.video_stream = MPEG1Codec(video)
802
+ assert self.video_stream, (
803
+ "fail to match suitable video codec. Codec=%s"
804
+ % temp_stream.stream["codec_name"]
805
+ )
840
806
  else:
841
- if type(audio) == str:
842
- fn = os.path.basename(audio)
843
- else:
844
- fn = "00.sfa"
845
- hcaObj = codec(audio, fn, key=self.key)
846
- self.audio_streams.append(hcaObj)
807
+ self.video_stream = video
847
808
 
809
+ def add_audio(self, audio : ADXCodec | HCACodec):
810
+ """Append the audio stream(s) from the specified audio file(s).
811
+
812
+ Args:
813
+ audio (ADXCodec | HCACodec): The path(s) to the audio file(s).
814
+ """
815
+ self.audio_streams.append(audio)
848
816
 
849
817
  def build(self) -> bytes:
818
+ """Build the USM payload"""
850
819
  SFV_list = self.video_stream.generate_SFV(self)
851
- if self.enable_audio:
820
+ if self.audio_streams:
852
821
  SFA_chunks = [s.generate_SFA(i, self) for i, s in enumerate(self.audio_streams) ]
853
822
  else:
854
823
  SFA_chunks = []
@@ -928,7 +897,7 @@ class USMBuilder(USMCrypt):
928
897
  )
929
898
  CRIUSF_DIR_STREAM.append(video_dict)
930
899
 
931
- if self.enable_audio:
900
+ if self.audio_streams:
932
901
  chno = 0
933
902
  for stream in self.audio_streams:
934
903
  avbps = stream.avbps
@@ -936,7 +905,7 @@ class USMBuilder(USMCrypt):
936
905
  minbuf += 27860
937
906
  audio_dict = dict(
938
907
  fmtver=(UTFTypeValues.uint, 0),
939
- filename=(UTFTypeValues.string, self.audio_filenames[chno]),
908
+ filename=(UTFTypeValues.string, stream.filename),
940
909
  filesize=(UTFTypeValues.uint, stream.filesize),
941
910
  datasize=(UTFTypeValues.uint, 0),
942
911
  stmid=(
@@ -1018,7 +987,7 @@ class USMBuilder(USMCrypt):
1018
987
 
1019
988
  audio_metadata = []
1020
989
  audio_headers = []
1021
- if self.enable_audio:
990
+ if self.audio_streams:
1022
991
  chno = 0
1023
992
  for stream in self.audio_streams:
1024
993
  metadata = stream.get_metadata()
@@ -1141,7 +1110,7 @@ class USMBuilder(USMCrypt):
1141
1110
  VIDEO_HDRINFO = gen_video_hdr_info(len(VIDEO_SEEKINFO))
1142
1111
 
1143
1112
  total_len = sum([len(x) for x in SFV_list]) + first_chk_ofs
1144
- if self.enable_audio:
1113
+ if self.audio_streams:
1145
1114
  sum_len = 0
1146
1115
  for stream in SFA_chunks:
1147
1116
  for x in stream:
@@ -1181,7 +1150,7 @@ class USMBuilder(USMCrypt):
1181
1150
 
1182
1151
  # Header chunks
1183
1152
  header += VIDEO_HDRINFO
1184
- if self.enable_audio:
1153
+ if self.audio_streams:
1185
1154
  header += b''.join(audio_headers)
1186
1155
  SFV_END = USMChunkHeader.pack(
1187
1156
  USMChunckHeaderType.SFV.value,
@@ -1202,7 +1171,7 @@ class USMBuilder(USMCrypt):
1202
1171
  header += SFV_END
1203
1172
 
1204
1173
  SFA_chk_END = b'' # Maybe reused
1205
- if self.enable_audio:
1174
+ if self.audio_streams:
1206
1175
  SFA_chk_END = b''.join([
1207
1176
  USMChunkHeader.pack(
1208
1177
  USMChunckHeaderType.SFA.value,
@@ -1223,7 +1192,7 @@ class USMBuilder(USMCrypt):
1223
1192
  header += SFA_chk_END # Ends audio_headers
1224
1193
  header += VIDEO_SEEKINFO
1225
1194
 
1226
- if self.enable_audio:
1195
+ if self.audio_streams:
1227
1196
  header += b''.join(audio_metadata)
1228
1197
  SFV_END = USMChunkHeader.pack(
1229
1198
  USMChunckHeaderType.SFV.value,
PyCriCodecsEx/utf.py CHANGED
@@ -1,6 +1,8 @@
1
1
  from typing import BinaryIO, TypeVar, Type, List
2
+ from copy import deepcopy
2
3
 
3
4
  T = TypeVar("T")
5
+ Ty = TypeVar("Ty", bound="UTFViewer")
4
6
  from io import BytesIO, FileIO
5
7
  from struct import unpack, calcsize, pack
6
8
  from PyCriCodecsEx.chunk import *
@@ -302,7 +304,7 @@ class UTFBuilder:
302
304
 
303
305
  def __init__(
304
306
  self,
305
- dictarray: list[dict],
307
+ dictarray_src: list[dict],
306
308
  encrypt: bool = False,
307
309
  encoding: str = "utf-8",
308
310
  table_name: str = "PyCriCodecs_table",
@@ -311,14 +313,14 @@ class UTFBuilder:
311
313
  """Packs UTF payload back into their binary form
312
314
 
313
315
  Args:
314
- dictarray: A list of dictionaries representing the UTF table.
316
+ dictarray_src: list[dict]: A list of dictionaries representing the UTF table.
315
317
  encrypt: Whether to encrypt the table (default: False).
316
318
  encoding: The character encoding to use (default: "utf-8").
317
319
  table_name: The name of the table (default: "PyCriCodecs_table").
318
320
  ignore_recursion: Whether to ignore recursion when packing (default: False).
319
321
  """
320
- assert type(dictarray) == list, "dictarray must be a list of dictionaries (see UTF.dictarray)."
321
-
322
+ assert type(dictarray_src) == list, "dictarray must be a list of dictionaries (see UTF.dictarray)."
323
+ dictarray = deepcopy(dictarray_src)
322
324
  # Preprocess for nested dictarray types
323
325
  def dfs(payload: list[dict], name: str) -> None:
324
326
  for dict in range(len(payload)):
@@ -618,7 +620,7 @@ class UTFViewer:
618
620
  See __new__ for the actual constructor.
619
621
  ```
620
622
  """
621
- assert isinstance(payload, dict), "Payload must be a dictionary."
623
+ assert isinstance(payload, dict), "payload must be a dictionary."
622
624
  super().__setattr__("_payload", payload)
623
625
 
624
626
  def __getattr__(self, item):
@@ -643,7 +645,9 @@ class UTFViewer:
643
645
  def __setattr__(self, item, value):
644
646
  payload = super().__getattribute__("_payload")
645
647
  if item not in payload:
646
- raise AttributeError(f"{item} not in payload")
648
+ raise AttributeError(f"{item} not in payload. UTFViewer should not store extra states")
649
+ if isinstance(value, dict) or isinstance(value, list):
650
+ raise AttributeError(f"Dict or list assignment is not allowed as this may potentially change the table layout. Access by elements and use list APIs instead")
647
651
  typeof, _ = payload[item]
648
652
  payload[item] = (typeof, value)
649
653
 
@@ -659,9 +663,9 @@ class UTFViewer:
659
663
 
660
664
  class ListView(list):
661
665
  _payload : List[dict]
662
- def __init__(self, payload: list[T]):
666
+ def __init__(self, clazz : Type[Ty], payload: list[Ty]):
663
667
  self._payload = payload
664
- super().__init__([UTFViewer(item) for item in payload])
668
+ super().__init__([clazz(item) for item in payload])
665
669
 
666
670
  def pop(self, index = -1):
667
671
  self._payload.pop(index)
@@ -669,7 +673,7 @@ class UTFViewer:
669
673
 
670
674
  def append(self, o : "UTFViewer"):
671
675
  if len(self):
672
- assert type(self[0]) == type(o), "all items in the list must be of the same type."
676
+ assert isinstance(o, UTFViewer) and type(self[0]) == type(o), "all items in the list must be of the same type, and must be an instance of UTFViewer."
673
677
  self._payload.append(o._payload)
674
678
  return super().append(o)
675
679
 
@@ -677,9 +681,9 @@ class UTFViewer:
677
681
  for item in iterable:
678
682
  self.append(item)
679
683
 
680
- def insert(self, index, o : "UTFViewer"):
684
+ def insert(self, index, o : "UTFViewer"):
681
685
  if len(self):
682
- assert type(self[0]) == type(o), "all items in the list must be of the same type."
686
+ assert isinstance(o, UTFViewer) and type(self[0]) == type(o), "all items in the list must be of the same type, and must be an instance of UTFViewer."
683
687
  self._payload.insert(index, o._payload)
684
688
  return super().insert(index, o)
685
689
 
@@ -698,7 +702,7 @@ class UTFViewer:
698
702
  self._payload[:] = [self._payload[i] for x,i in p]
699
703
  self[:] = [x for x,i in p]
700
704
 
701
- def __new__(cls: Type[T], payload: list | dict, **args) -> T | List[T]:
705
+ def __new__(cls: Type[Ty], payload: list | dict, **args) -> Ty | List[Ty]:
702
706
  if isinstance(payload, list):
703
- return UTFViewer.ListView(payload)
707
+ return UTFViewer.ListView(cls, payload)
704
708
  return super().__new__(cls)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyCriCodecsEx
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: Criware formats library for Python
5
5
  Home-page: https://github.com/mos9527/PyCriCodecsEx
6
6
  Classifier: Programming Language :: Python :: 3
@@ -33,8 +33,13 @@ For USM features, you need `ffmpeg` installed and available in your PATH. See al
33
33
  ## Features
34
34
  If not otherwise mentioned, all features marked with [x] are considered working, and has been verified with official tools.
35
35
 
36
+ Examples are available in [Tests](https://github.com/mos9527/PyCriCodecsEx/tree/main/Tests)
37
+
36
38
  ### ACB Cue sheets (also AWB)
37
- - [x] Editing & Saving (Scripting APIs. Helper functions TODO. see examples in [Tests](https://github.com/mos9527/PyCriCodecsEx/tree/main/Tests))
39
+ - [x] Cue extraction support for most ACBs
40
+ - [x] Cue waveform(s) encoding with ADX/HCA support
41
+ - [x] Comprehensive Cue metadata editing support (via Python API)
42
+
38
43
  ### USM Sofdec2 (Encode & Decode)
39
44
  #### Audio Stream
40
45
  For audio to be muxed in, you need a PCM WAV sample with NO metadata, which can be produced with e.g.:
@@ -63,7 +68,7 @@ Decoding and Encoded format can be the following:
63
68
  - [x] Packing
64
69
 
65
70
  ## Roadmap
66
- - [ ] ACB Extraction (Massive TODO. see also https://github.com/mos9527/PyCriCodecsEx/blob/main/Research/ACBSchema.py)
71
+ - [x] ACB Extraction (Massive TODO. see also https://github.com/mos9527/PyCriCodecsEx/blob/main/Research/ACBSchema.py)
67
72
  - [ ] Interface for encode tasks (CLI then maybe GUI?)
68
73
  - [ ] Documentation
69
74
  - [ ] C/C++ port + FFI
@@ -0,0 +1,15 @@
1
+ CriCodecsEx.cp313-win_amd64.pyd,sha256=rA5VO1X7Iwm0BehS5jnvj4ifrgdsdVUYHhhwG2ktVPo,89088
2
+ PyCriCodecsEx/__init__.py,sha256=j7895T8JfsWQAOvxm8SAdESs5iFmIuAqKpyfjSAfKZ4,23
3
+ PyCriCodecsEx/acb.py,sha256=vNPiGW1S0pkdWjcegTWId4C0suUCeX-kfFXUWuRnB-w,10352
4
+ PyCriCodecsEx/adx.py,sha256=KnL4wy1ccoc6uHYZVino7cJQB2pMn4BbRhpH3r527TY,777
5
+ PyCriCodecsEx/awb.py,sha256=sWUo5ITERYG504YVe9ejDYMM-bpLB31Ny4X-7rmmWlQ,6296
6
+ PyCriCodecsEx/chunk.py,sha256=quk9VicOfp-_PgmcjyLL9rCIZQG8DhophXQZWpqfEUw,2925
7
+ PyCriCodecsEx/cpk.py,sha256=sLFUGtG1yuwNC4puyp_TOgkhv1qnRGxVQu4ctlqXeUQ,37932
8
+ PyCriCodecsEx/hca.py,sha256=ArZAaHogejBRE2rWEkTt8lS993Wf4SkjkjhNTo5Anqs,14550
9
+ PyCriCodecsEx/usm.py,sha256=t2V98eno_beVuvU1tbuaFe4p7OohwROiUerMDHhPhEw,45107
10
+ PyCriCodecsEx/utf.py,sha256=YWvgpgLy1tdhVWvRe4pbiJZFxnBRHdtQiR6xEetLsGk,28819
11
+ pycricodecsex-0.0.2.dist-info/licenses/LICENSE,sha256=B47Qr_82CmMyuL-wNFAH_P6PNJWaonv5dxo9MfgjEXw,1083
12
+ pycricodecsex-0.0.2.dist-info/METADATA,sha256=CAVs16QnoaoIrn8_PJH2w8wM64DTiIXNfxMUvCb-ZN0,3438
13
+ pycricodecsex-0.0.2.dist-info/WHEEL,sha256=qV0EIPljj1XC_vuSatRWjn02nZIz3N1t8jsZz7HBr2U,101
14
+ pycricodecsex-0.0.2.dist-info/top_level.txt,sha256=mSrrEse9hT0s6nl-sWAQWAhNRuZ6jo98pbFoN3L2MXk,26
15
+ pycricodecsex-0.0.2.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- CriCodecsEx.cp313-win_amd64.pyd,sha256=bW3k3WYfeg9DMTVWSylRNGQGwLcdeHFjZqU77F9oO90,89088
2
- PyCriCodecsEx/__init__.py,sha256=ry5O5SW74ugwOUMKSV2_LbWjYa8B0IEvUm7S-lHPA2c,23
3
- PyCriCodecsEx/acb.py,sha256=v0CxjveQewxUWuila1O0qDGcAfvOxqnzYqAL4AHneqU,2706
4
- PyCriCodecsEx/adx.py,sha256=KnL4wy1ccoc6uHYZVino7cJQB2pMn4BbRhpH3r527TY,777
5
- PyCriCodecsEx/awb.py,sha256=p68tVqZOorOE1Js-w1Sqy4GMvu9gxzlBgUL2LAUNEqU,5657
6
- PyCriCodecsEx/chunk.py,sha256=nrRUANDFZ1LDszHEGDDnQDuNnAB_kGCzbpC7bsgtjqc,2504
7
- PyCriCodecsEx/cpk.py,sha256=AKShN6P7p70i3GQfKKS-VH3mkbexOLfHEJrjuxS0SQQ,37692
8
- PyCriCodecsEx/hca.py,sha256=lSslLyv1D1zM_kHFyOlKpbM7qzghmJDYRd-isP8qcyg,14192
9
- PyCriCodecsEx/usm.py,sha256=X4mv-3ksiX10C6EDACpu-dwRn9eSy3wmY7of2aO7eiA,46216
10
- PyCriCodecsEx/utf.py,sha256=Yp1Le3AS7UC4So_sGO39N5X5D23X7MsWugHo52npwR0,28256
11
- pycricodecsex-0.0.1.dist-info/licenses/LICENSE,sha256=B47Qr_82CmMyuL-wNFAH_P6PNJWaonv5dxo9MfgjEXw,1083
12
- pycricodecsex-0.0.1.dist-info/METADATA,sha256=Z4KAoITGoFoNhiiJ_YXSnrBE--W51_Q-pXt3lerFMX8,3324
13
- pycricodecsex-0.0.1.dist-info/WHEEL,sha256=qV0EIPljj1XC_vuSatRWjn02nZIz3N1t8jsZz7HBr2U,101
14
- pycricodecsex-0.0.1.dist-info/top_level.txt,sha256=mSrrEse9hT0s6nl-sWAQWAhNRuZ6jo98pbFoN3L2MXk,26
15
- pycricodecsex-0.0.1.dist-info/RECORD,,