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.
@@ -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