TonieToolbox 0.1.0__py3-none-any.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.
- TonieToolbox/__init__.py +5 -0
- TonieToolbox/__main__.py +145 -0
- TonieToolbox/audio_conversion.py +194 -0
- TonieToolbox/constants.py +14 -0
- TonieToolbox/dependency_manager.py +378 -0
- TonieToolbox/filename_generator.py +94 -0
- TonieToolbox/logger.py +57 -0
- TonieToolbox/ogg_page.py +588 -0
- TonieToolbox/opus_packet.py +219 -0
- TonieToolbox/tonie_analysis.py +522 -0
- TonieToolbox/tonie_file.py +411 -0
- TonieToolbox/tonie_header.proto +11 -0
- TonieToolbox/tonie_header_pb2.py +99 -0
- tonietoolbox-0.1.0.dist-info/METADATA +301 -0
- tonietoolbox-0.1.0.dist-info/RECORD +19 -0
- tonietoolbox-0.1.0.dist-info/WHEEL +5 -0
- tonietoolbox-0.1.0.dist-info/entry_points.txt +2 -0
- tonietoolbox-0.1.0.dist-info/licenses/LICENSE.md +674 -0
- tonietoolbox-0.1.0.dist-info/top_level.txt +1 -0
TonieToolbox/ogg_page.py
ADDED
@@ -0,0 +1,588 @@
|
|
1
|
+
"""
|
2
|
+
Classes and functions for handling OGG container pages
|
3
|
+
"""
|
4
|
+
|
5
|
+
import struct
|
6
|
+
import math
|
7
|
+
|
8
|
+
from .opus_packet import OpusPacket
|
9
|
+
from .constants import (
|
10
|
+
ONLY_CONVERT_FRAMEPACKING,
|
11
|
+
OTHER_PACKET_NEEDED,
|
12
|
+
DO_NOTHING,
|
13
|
+
TOO_MANY_SEGMENTS
|
14
|
+
)
|
15
|
+
from .logger import get_logger
|
16
|
+
|
17
|
+
# Setup logging
|
18
|
+
logger = get_logger('ogg_page')
|
19
|
+
|
20
|
+
|
21
|
+
def create_crc_table():
|
22
|
+
"""
|
23
|
+
Create a CRC lookup table for OGG page checksums.
|
24
|
+
|
25
|
+
Returns:
|
26
|
+
list: CRC32 lookup table for OGG pages
|
27
|
+
"""
|
28
|
+
logger.debug("Creating CRC table for OGG page checksums")
|
29
|
+
table = []
|
30
|
+
for i in range(256):
|
31
|
+
k = i << 24
|
32
|
+
for _ in range(8):
|
33
|
+
k = (k << 1) ^ 0x04c11db7 if k & 0x80000000 else k << 1
|
34
|
+
table.append(k & 0xffffffff)
|
35
|
+
return table
|
36
|
+
|
37
|
+
|
38
|
+
# Global CRC table for OGG checksums
|
39
|
+
CRC_TABLE = create_crc_table()
|
40
|
+
|
41
|
+
|
42
|
+
def crc32(bytestream):
|
43
|
+
"""
|
44
|
+
Calculate a CRC32 checksum for the given bytestream.
|
45
|
+
|
46
|
+
Args:
|
47
|
+
bytestream: Bytes to calculate the CRC for
|
48
|
+
|
49
|
+
Returns:
|
50
|
+
int: CRC32 checksum
|
51
|
+
"""
|
52
|
+
crc = 0
|
53
|
+
for byte in bytestream:
|
54
|
+
lookup_index = ((crc >> 24) ^ byte) & 0xff
|
55
|
+
crc = ((crc & 0xffffff) << 8) ^ CRC_TABLE[lookup_index]
|
56
|
+
return crc
|
57
|
+
|
58
|
+
|
59
|
+
class OggPage:
|
60
|
+
"""
|
61
|
+
Represents an OGG container page.
|
62
|
+
|
63
|
+
This class provides methods to parse, modify, and write OGG pages,
|
64
|
+
with particular focus on features needed for Tonie compatibility.
|
65
|
+
"""
|
66
|
+
|
67
|
+
def __init__(self, filehandle):
|
68
|
+
"""
|
69
|
+
Initialize a new OggPage.
|
70
|
+
|
71
|
+
Args:
|
72
|
+
filehandle: File handle to read the page data from, or None to create an empty page
|
73
|
+
"""
|
74
|
+
self.version = None
|
75
|
+
self.page_type = None
|
76
|
+
self.granule_position = None
|
77
|
+
self.serial_no = None
|
78
|
+
self.page_no = None
|
79
|
+
self.checksum = None
|
80
|
+
self.segment_count = None
|
81
|
+
self.segments = None
|
82
|
+
|
83
|
+
if filehandle is None:
|
84
|
+
logger.trace("Creating empty OggPage")
|
85
|
+
return
|
86
|
+
|
87
|
+
logger.trace("Initializing OggPage from file handle")
|
88
|
+
self.parse_header(filehandle)
|
89
|
+
self.parse_segments(filehandle)
|
90
|
+
|
91
|
+
def parse_header(self, filehandle):
|
92
|
+
"""
|
93
|
+
Parse the OGG page header.
|
94
|
+
|
95
|
+
Args:
|
96
|
+
filehandle: File handle to read the header from
|
97
|
+
"""
|
98
|
+
header = filehandle.read(27)
|
99
|
+
unpacked = struct.unpack("<BBQLLLB", header[4:27])
|
100
|
+
self.version = unpacked[0]
|
101
|
+
self.page_type = unpacked[1]
|
102
|
+
self.granule_position = unpacked[2]
|
103
|
+
self.serial_no = unpacked[3]
|
104
|
+
self.page_no = unpacked[4]
|
105
|
+
self.checksum = unpacked[5]
|
106
|
+
self.segment_count = unpacked[6]
|
107
|
+
|
108
|
+
logger.trace("Parsed OGG header - Page #%d, Type: %d, Granule: %d, Serial: %d, Segments: %d",
|
109
|
+
self.page_no, self.page_type, self.granule_position, self.serial_no, self.segment_count)
|
110
|
+
|
111
|
+
def parse_segments(self, filehandle):
|
112
|
+
"""
|
113
|
+
Parse the segments in this OGG page.
|
114
|
+
|
115
|
+
Args:
|
116
|
+
filehandle: File handle to read the segments from
|
117
|
+
|
118
|
+
Raises:
|
119
|
+
RuntimeError: If an opus packet spans multiple OGG pages
|
120
|
+
"""
|
121
|
+
logger.trace("Parsing %d segments in OGG page #%d", self.segment_count, self.page_no)
|
122
|
+
table = filehandle.read(self.segment_count)
|
123
|
+
self.segments = []
|
124
|
+
last_length = -1
|
125
|
+
dont_parse_info = (self.page_no == 0) or (self.page_no == 1)
|
126
|
+
|
127
|
+
for length in table:
|
128
|
+
segment = OpusPacket(filehandle, length, last_length, dont_parse_info)
|
129
|
+
last_length = length
|
130
|
+
self.segments.append(segment)
|
131
|
+
|
132
|
+
if self.segments and self.segments[len(self.segments) - 1].spanning_packet:
|
133
|
+
logger.error("Found an opus packet spanning OGG pages, which is not supported")
|
134
|
+
raise RuntimeError("Found an opus packet spanning ogg pages. This is not supported yet.")
|
135
|
+
|
136
|
+
def correct_values(self, last_granule):
|
137
|
+
"""
|
138
|
+
Correct the granule position and checksum for this page.
|
139
|
+
|
140
|
+
Args:
|
141
|
+
last_granule: Last granule position
|
142
|
+
|
143
|
+
Raises:
|
144
|
+
RuntimeError: If there are too many segments in the page
|
145
|
+
"""
|
146
|
+
if len(self.segments) > 255:
|
147
|
+
logger.error("Too many segments in page: %d (max 255 allowed)", len(self.segments))
|
148
|
+
raise RuntimeError(f"Too many segments: {len(self.segments)} - max 255 allowed")
|
149
|
+
|
150
|
+
granule = 0
|
151
|
+
if not (self.page_no == 0) and not (self.page_no == 1):
|
152
|
+
for segment in self.segments:
|
153
|
+
if segment.first_packet:
|
154
|
+
granule = granule + segment.granule
|
155
|
+
|
156
|
+
self.granule_position = last_granule + granule
|
157
|
+
self.segment_count = len(self.segments)
|
158
|
+
self.checksum = self.calc_checksum()
|
159
|
+
|
160
|
+
logger.trace("Corrected OGG page values: Page #%d, Segments: %d, Granule: %d",
|
161
|
+
self.page_no, self.segment_count, self.granule_position)
|
162
|
+
|
163
|
+
def calc_checksum(self):
|
164
|
+
"""
|
165
|
+
Calculate the checksum for this page.
|
166
|
+
|
167
|
+
Returns:
|
168
|
+
int: CRC32 checksum
|
169
|
+
"""
|
170
|
+
data = b"OggS" + struct.pack("<BBQLLLB", self.version, self.page_type, self.granule_position, self.serial_no,
|
171
|
+
self.page_no, 0, self.segment_count)
|
172
|
+
for segment in self.segments:
|
173
|
+
data = data + struct.pack("<B", segment.size)
|
174
|
+
for segment in self.segments:
|
175
|
+
data = data + segment.data
|
176
|
+
|
177
|
+
checksum = crc32(data)
|
178
|
+
logger.trace("Calculated checksum for page #%d: 0x%X", self.page_no, checksum)
|
179
|
+
return checksum
|
180
|
+
|
181
|
+
def get_page_size(self):
|
182
|
+
"""
|
183
|
+
Get the total size of this page in bytes.
|
184
|
+
|
185
|
+
Returns:
|
186
|
+
int: Page size in bytes
|
187
|
+
"""
|
188
|
+
size = 27 + len(self.segments)
|
189
|
+
for segment in self.segments:
|
190
|
+
size = size + len(segment.data)
|
191
|
+
return size
|
192
|
+
|
193
|
+
def get_size_of_first_opus_packet(self):
|
194
|
+
"""
|
195
|
+
Get the size of the first opus packet in bytes.
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
int: Size of first opus packet in bytes
|
199
|
+
"""
|
200
|
+
if not len(self.segments):
|
201
|
+
return 0
|
202
|
+
segment_size = self.segments[0].size
|
203
|
+
size = segment_size
|
204
|
+
i = 1
|
205
|
+
while (segment_size == 255) and (i < len(self.segments)):
|
206
|
+
segment_size = self.segments[i].size
|
207
|
+
size = size + segment_size
|
208
|
+
i = i + 1
|
209
|
+
return size
|
210
|
+
|
211
|
+
def get_segment_count_of_first_opus_packet(self):
|
212
|
+
"""
|
213
|
+
Get the number of segments in the first opus packet.
|
214
|
+
|
215
|
+
Returns:
|
216
|
+
int: Number of segments
|
217
|
+
"""
|
218
|
+
if not len(self.segments):
|
219
|
+
return 0
|
220
|
+
segment_size = self.segments[0].size
|
221
|
+
count = 1
|
222
|
+
while (segment_size == 255) and (count < len(self.segments)):
|
223
|
+
segment_size = self.segments[count].size
|
224
|
+
count = count + 1
|
225
|
+
return count
|
226
|
+
|
227
|
+
def insert_empty_segment(self, index_after, spanning_packet=False, first_packet=False):
|
228
|
+
"""
|
229
|
+
Insert an empty segment after the specified index.
|
230
|
+
|
231
|
+
Args:
|
232
|
+
index_after: Index to insert the segment after
|
233
|
+
spanning_packet: Whether this segment belongs to a packet that spans pages
|
234
|
+
first_packet: Whether this is the first segment of a packet
|
235
|
+
"""
|
236
|
+
logger.trace("Inserting empty segment after index %d (spanning: %s, first: %s)",
|
237
|
+
index_after, spanning_packet, first_packet)
|
238
|
+
segment = OpusPacket(None)
|
239
|
+
segment.first_packet = first_packet
|
240
|
+
segment.spanning_packet = spanning_packet
|
241
|
+
segment.size = 0
|
242
|
+
segment.data = bytes()
|
243
|
+
self.segments.insert(index_after + 1, segment)
|
244
|
+
|
245
|
+
def get_opus_packet_size(self, seg_start):
|
246
|
+
"""
|
247
|
+
Get the size of the opus packet starting at the specified segment index.
|
248
|
+
|
249
|
+
Args:
|
250
|
+
seg_start: Starting segment index
|
251
|
+
|
252
|
+
Returns:
|
253
|
+
int: Size of the opus packet in bytes
|
254
|
+
"""
|
255
|
+
size = len(self.segments[seg_start].data)
|
256
|
+
seg_start = seg_start + 1
|
257
|
+
while (seg_start < len(self.segments)) and not self.segments[seg_start].first_packet:
|
258
|
+
size = size + self.segments[seg_start].size
|
259
|
+
seg_start = seg_start + 1
|
260
|
+
return size
|
261
|
+
|
262
|
+
def get_segment_count_of_packet_at(self, seg_start):
|
263
|
+
"""
|
264
|
+
Get the number of segments in the packet starting at the specified segment index.
|
265
|
+
|
266
|
+
Args:
|
267
|
+
seg_start: Starting segment index
|
268
|
+
|
269
|
+
Returns:
|
270
|
+
int: Number of segments
|
271
|
+
"""
|
272
|
+
seg_end = seg_start + 1
|
273
|
+
while (seg_end < len(self.segments)) and not self.segments[seg_end].first_packet:
|
274
|
+
seg_end = seg_end + 1
|
275
|
+
return seg_end - seg_start
|
276
|
+
|
277
|
+
def redistribute_packet_data_at(self, seg_start, pad_count):
|
278
|
+
"""
|
279
|
+
Redistribute packet data starting at the specified segment index.
|
280
|
+
|
281
|
+
Args:
|
282
|
+
seg_start: Starting segment index
|
283
|
+
pad_count: Number of padding bytes to add
|
284
|
+
"""
|
285
|
+
logger.trace("Redistributing packet data at segment %d with %d padding bytes",
|
286
|
+
seg_start, pad_count)
|
287
|
+
seg_count = self.get_segment_count_of_packet_at(seg_start)
|
288
|
+
full_data = bytes()
|
289
|
+
for i in range(0, seg_count):
|
290
|
+
full_data = full_data + self.segments[seg_start + i].data
|
291
|
+
full_data = full_data + bytes(pad_count)
|
292
|
+
size = len(full_data)
|
293
|
+
|
294
|
+
if size < 255:
|
295
|
+
self.segments[seg_start].size = size
|
296
|
+
self.segments[seg_start].data = full_data
|
297
|
+
logger.trace("Data fits in a single segment (size: %d)", size)
|
298
|
+
return
|
299
|
+
|
300
|
+
needed_seg_count = math.ceil(size / 255)
|
301
|
+
if (size % 255) == 0:
|
302
|
+
needed_seg_count = needed_seg_count + 1
|
303
|
+
segments_to_create = needed_seg_count - seg_count
|
304
|
+
|
305
|
+
if segments_to_create > 0:
|
306
|
+
logger.trace("Need to create %d new segments", segments_to_create)
|
307
|
+
for i in range(0, segments_to_create):
|
308
|
+
self.insert_empty_segment(seg_start + seg_count + i, i != (segments_to_create - 1))
|
309
|
+
seg_count = needed_seg_count
|
310
|
+
|
311
|
+
for i in range(0, seg_count):
|
312
|
+
self.segments[seg_start + i].data = full_data[:255]
|
313
|
+
self.segments[seg_start + i].size = len(self.segments[seg_start + i].data)
|
314
|
+
full_data = full_data[255:]
|
315
|
+
|
316
|
+
logger.trace("Redistribution complete, %d segments used", seg_count)
|
317
|
+
assert len(full_data) == 0
|
318
|
+
|
319
|
+
def convert_packet_to_framepacking_three_and_pad(self, seg_start, pad=False, count=0):
|
320
|
+
"""
|
321
|
+
Convert the packet to framepacking three mode and add padding if required.
|
322
|
+
|
323
|
+
Args:
|
324
|
+
seg_start: Starting segment index
|
325
|
+
pad: Whether to add padding
|
326
|
+
count: Number of padding bytes to add
|
327
|
+
|
328
|
+
Raises:
|
329
|
+
AssertionError: If the segment is not the first packet
|
330
|
+
"""
|
331
|
+
logger.trace("Converting packet at segment %d to framepacking three (pad: %s, count: %d)",
|
332
|
+
seg_start, pad, count)
|
333
|
+
assert self.segments[seg_start].first_packet is True
|
334
|
+
self.segments[seg_start].convert_to_framepacking_three()
|
335
|
+
if pad:
|
336
|
+
self.segments[seg_start].set_pad_count(count)
|
337
|
+
self.redistribute_packet_data_at(seg_start, count)
|
338
|
+
|
339
|
+
def calc_actual_padding_value(self, seg_start, bytes_needed):
|
340
|
+
"""
|
341
|
+
Calculate the actual padding value needed for the packet.
|
342
|
+
|
343
|
+
Args:
|
344
|
+
seg_start: Starting segment index
|
345
|
+
bytes_needed: Number of bytes needed for padding
|
346
|
+
|
347
|
+
Returns:
|
348
|
+
int: Actual padding value or a special return code
|
349
|
+
|
350
|
+
Raises:
|
351
|
+
AssertionError: If bytes_needed is negative
|
352
|
+
"""
|
353
|
+
if bytes_needed < 0:
|
354
|
+
logger.error("Page is already too large! Something went wrong. Bytes needed: %d",
|
355
|
+
bytes_needed)
|
356
|
+
assert bytes_needed >= 0, "Page is already too large! Something went wrong."
|
357
|
+
|
358
|
+
seg_end = seg_start + self.get_segment_count_of_packet_at(seg_start)
|
359
|
+
size_of_last_segment = self.segments[seg_end - 1].size
|
360
|
+
convert_framepacking_needed = self.segments[seg_start].framepacking != 3
|
361
|
+
|
362
|
+
logger.trace("Calculating padding for segment %d, bytes needed: %d, last segment size: %d",
|
363
|
+
seg_start, bytes_needed, size_of_last_segment)
|
364
|
+
|
365
|
+
if bytes_needed == 0:
|
366
|
+
logger.trace("No padding needed")
|
367
|
+
return DO_NOTHING
|
368
|
+
|
369
|
+
if (bytes_needed + size_of_last_segment) % 255 == 0:
|
370
|
+
logger.trace("Need another packet (would end exactly on segment boundary)")
|
371
|
+
return OTHER_PACKET_NEEDED
|
372
|
+
|
373
|
+
if bytes_needed == 1:
|
374
|
+
if convert_framepacking_needed:
|
375
|
+
logger.trace("Only need to convert framepacking")
|
376
|
+
return ONLY_CONVERT_FRAMEPACKING
|
377
|
+
else:
|
378
|
+
logger.trace("Already using framepacking three, can pad with 0")
|
379
|
+
return 0
|
380
|
+
|
381
|
+
new_segments_needed = 0
|
382
|
+
if bytes_needed + size_of_last_segment >= 255:
|
383
|
+
tmp_count = bytes_needed + size_of_last_segment - 255
|
384
|
+
while tmp_count >= 0:
|
385
|
+
tmp_count = tmp_count - 255 - 1
|
386
|
+
new_segments_needed = new_segments_needed + 1
|
387
|
+
logger.trace("Need %d new segments", new_segments_needed)
|
388
|
+
|
389
|
+
if new_segments_needed + len(self.segments) > 255:
|
390
|
+
logger.warning("Too many segments would be needed: %d",
|
391
|
+
new_segments_needed + len(self.segments))
|
392
|
+
return TOO_MANY_SEGMENTS
|
393
|
+
|
394
|
+
if (bytes_needed + size_of_last_segment) % 255 == (new_segments_needed - 1):
|
395
|
+
logger.trace("Need another packet (would end with empty segment)")
|
396
|
+
return OTHER_PACKET_NEEDED
|
397
|
+
|
398
|
+
packet_bytes_needed = bytes_needed - new_segments_needed
|
399
|
+
logger.trace("Packet bytes needed: %d", packet_bytes_needed)
|
400
|
+
|
401
|
+
if packet_bytes_needed == 1:
|
402
|
+
if convert_framepacking_needed:
|
403
|
+
logger.trace("Need to convert framepacking only")
|
404
|
+
return ONLY_CONVERT_FRAMEPACKING
|
405
|
+
else:
|
406
|
+
logger.trace("Already using framepacking three, can pad with 0")
|
407
|
+
return 0
|
408
|
+
|
409
|
+
if convert_framepacking_needed:
|
410
|
+
packet_bytes_needed = packet_bytes_needed - 1 # frame_count_byte
|
411
|
+
logger.trace("Need to convert framepacking, adjusted bytes needed: %d",
|
412
|
+
packet_bytes_needed)
|
413
|
+
|
414
|
+
packet_bytes_needed = packet_bytes_needed - 1 # padding_count_data is at least 1 byte
|
415
|
+
size_of_padding_count_data = max(1, math.ceil(packet_bytes_needed / 254))
|
416
|
+
check_size = math.ceil((packet_bytes_needed - size_of_padding_count_data + 1) / 254)
|
417
|
+
|
418
|
+
logger.trace("Padding size check: needed=%d, check_size=%d",
|
419
|
+
size_of_padding_count_data, check_size)
|
420
|
+
|
421
|
+
if check_size != size_of_padding_count_data:
|
422
|
+
logger.trace("Need another packet (padding size calculation mismatch)")
|
423
|
+
return OTHER_PACKET_NEEDED
|
424
|
+
else:
|
425
|
+
result = packet_bytes_needed - size_of_padding_count_data + 1
|
426
|
+
logger.trace("Calculated actual padding value: %d", result)
|
427
|
+
return result
|
428
|
+
|
429
|
+
def pad(self, pad_to, idx_offset=-1):
|
430
|
+
"""
|
431
|
+
Pad the page to the specified size.
|
432
|
+
|
433
|
+
Args:
|
434
|
+
pad_to: Target size to pad to
|
435
|
+
idx_offset: Index offset to start from, defaults to last segment
|
436
|
+
|
437
|
+
Raises:
|
438
|
+
RuntimeError: If beginning of last packet cannot be found
|
439
|
+
AssertionError: If the actual page size after padding does not match the target
|
440
|
+
"""
|
441
|
+
logger.debug("Padding page #%d to size %d (current size: %d)",
|
442
|
+
self.page_no, pad_to, self.get_page_size())
|
443
|
+
|
444
|
+
if idx_offset == -1:
|
445
|
+
idx = len(self.segments) - 1
|
446
|
+
else:
|
447
|
+
idx = idx_offset
|
448
|
+
|
449
|
+
logger.trace("Starting from segment index %d", idx)
|
450
|
+
|
451
|
+
while not self.segments[idx].first_packet:
|
452
|
+
idx = idx - 1
|
453
|
+
if idx < 0:
|
454
|
+
logger.error("Could not find beginning of last packet")
|
455
|
+
raise RuntimeError("Could not find begin of last packet!")
|
456
|
+
|
457
|
+
logger.trace("Found beginning of packet at segment index %d", idx)
|
458
|
+
|
459
|
+
pad_count = pad_to - self.get_page_size()
|
460
|
+
logger.trace("Need to add %d bytes of padding", pad_count)
|
461
|
+
|
462
|
+
actual_padding = self.calc_actual_padding_value(idx, pad_count)
|
463
|
+
logger.trace("Actual padding value: %d", actual_padding)
|
464
|
+
|
465
|
+
if actual_padding == DO_NOTHING:
|
466
|
+
logger.debug("No padding needed")
|
467
|
+
return
|
468
|
+
if actual_padding == ONLY_CONVERT_FRAMEPACKING:
|
469
|
+
logger.debug("Only need to convert framepacking")
|
470
|
+
self.convert_packet_to_framepacking_three_and_pad(idx)
|
471
|
+
return
|
472
|
+
if actual_padding == OTHER_PACKET_NEEDED:
|
473
|
+
logger.debug("Padding with one byte first, then recalculating")
|
474
|
+
self.pad_one_byte()
|
475
|
+
self.pad(pad_to)
|
476
|
+
return
|
477
|
+
if actual_padding == TOO_MANY_SEGMENTS:
|
478
|
+
logger.debug("Too many segments would be needed, padding previous packet first")
|
479
|
+
self.pad(pad_to - (pad_count // 2), idx - 1)
|
480
|
+
self.pad(pad_to)
|
481
|
+
return
|
482
|
+
|
483
|
+
logger.debug("Converting packet to framepacking three and adding %d bytes of padding",
|
484
|
+
actual_padding)
|
485
|
+
self.convert_packet_to_framepacking_three_and_pad(idx, True, actual_padding)
|
486
|
+
|
487
|
+
final_size = self.get_page_size()
|
488
|
+
if final_size != pad_to:
|
489
|
+
logger.error("Page size after padding (%d) doesn't match target size (%d)",
|
490
|
+
final_size, pad_to)
|
491
|
+
assert final_size == pad_to
|
492
|
+
|
493
|
+
def pad_one_byte(self):
|
494
|
+
"""
|
495
|
+
Add one byte of padding to the page.
|
496
|
+
|
497
|
+
Raises:
|
498
|
+
RuntimeError: If the page seems impossible to pad correctly
|
499
|
+
"""
|
500
|
+
logger.debug("Adding one byte of padding to page #%d", self.page_no)
|
501
|
+
i = 0
|
502
|
+
while not (self.segments[i].first_packet and not self.segments[i].padding
|
503
|
+
and self.get_opus_packet_size(i) % 255 < 254):
|
504
|
+
i = i + 1
|
505
|
+
if i >= len(self.segments):
|
506
|
+
logger.error("Page seems impossible to pad correctly")
|
507
|
+
raise RuntimeError("Page seems impossible to pad correctly")
|
508
|
+
|
509
|
+
logger.trace("Found suitable packet at segment index %d", i)
|
510
|
+
|
511
|
+
if self.segments[i].framepacking == 3:
|
512
|
+
logger.trace("Packet already has framepacking 3, adding 0 bytes of padding")
|
513
|
+
self.convert_packet_to_framepacking_three_and_pad(i, True, 0)
|
514
|
+
else:
|
515
|
+
logger.trace("Converting packet to framepacking 3")
|
516
|
+
self.convert_packet_to_framepacking_three_and_pad(i)
|
517
|
+
|
518
|
+
def write_page(self, filehandle, sha1=None):
|
519
|
+
"""
|
520
|
+
Write the page to a file handle.
|
521
|
+
|
522
|
+
Args:
|
523
|
+
filehandle: File handle to write to
|
524
|
+
sha1: Optional SHA1 hash object to update with the written data
|
525
|
+
"""
|
526
|
+
logger.trace("Writing OGG page #%d to file (segments: %d)", self.page_no, len(self.segments))
|
527
|
+
data = b"OggS" + struct.pack("<BBQLLLB", self.version, self.page_type, self.granule_position, self.serial_no,
|
528
|
+
self.page_no, self.checksum, self.segment_count)
|
529
|
+
for segment in self.segments:
|
530
|
+
data = data + struct.pack("<B", segment.size)
|
531
|
+
if sha1 is not None:
|
532
|
+
sha1.update(data)
|
533
|
+
filehandle.write(data)
|
534
|
+
for segment in self.segments:
|
535
|
+
if sha1 is not None:
|
536
|
+
sha1.update(segment.data)
|
537
|
+
segment.write(filehandle)
|
538
|
+
|
539
|
+
@staticmethod
|
540
|
+
def from_page(other_page):
|
541
|
+
"""
|
542
|
+
Create a new OggPage based on another page.
|
543
|
+
|
544
|
+
Args:
|
545
|
+
other_page: Source page to copy from
|
546
|
+
|
547
|
+
Returns:
|
548
|
+
OggPage: New page with copied properties
|
549
|
+
"""
|
550
|
+
logger.trace("Creating new OGG page from existing page #%d", other_page.page_no)
|
551
|
+
new_page = OggPage(None)
|
552
|
+
new_page.version = other_page.version
|
553
|
+
new_page.page_type = other_page.page_type
|
554
|
+
new_page.granule_position = other_page.granule_position
|
555
|
+
new_page.serial_no = other_page.serial_no
|
556
|
+
new_page.page_no = other_page.page_no
|
557
|
+
new_page.checksum = 0
|
558
|
+
new_page.segment_count = 0
|
559
|
+
new_page.segments = []
|
560
|
+
return new_page
|
561
|
+
|
562
|
+
@staticmethod
|
563
|
+
def seek_to_page_header(filehandle):
|
564
|
+
"""
|
565
|
+
Seek to the next OGG page header in a file.
|
566
|
+
|
567
|
+
Args:
|
568
|
+
filehandle: File handle to seek in
|
569
|
+
|
570
|
+
Returns:
|
571
|
+
bool: True if a page header was found, False otherwise
|
572
|
+
"""
|
573
|
+
logger.trace("Seeking to next OGG page header in file")
|
574
|
+
current_pos = filehandle.tell()
|
575
|
+
filehandle.seek(0, 2)
|
576
|
+
size = filehandle.tell()
|
577
|
+
filehandle.seek(current_pos, 0)
|
578
|
+
five_bytes = filehandle.read(5)
|
579
|
+
while five_bytes and (filehandle.tell() + 5 < size):
|
580
|
+
if five_bytes == b"OggS\x00":
|
581
|
+
filehandle.seek(-5, 1)
|
582
|
+
logger.trace("Found OGG page header at position %d", filehandle.tell())
|
583
|
+
return True
|
584
|
+
filehandle.seek(-4, 1)
|
585
|
+
five_bytes = filehandle.read(5)
|
586
|
+
|
587
|
+
logger.trace("No OGG page header found")
|
588
|
+
return False
|