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,219 @@
1
+ """
2
+ Classes and functions for handling Opus audio packets
3
+ """
4
+
5
+ import struct
6
+ from .constants import SAMPLE_RATE_KHZ
7
+ from .logger import get_logger
8
+
9
+ # Setup logging
10
+ logger = get_logger('opus_packet')
11
+
12
+
13
+ class OpusPacket:
14
+ """
15
+ Represents an Opus audio packet in an Ogg container.
16
+
17
+ This class provides methods to parse, modify, and write Opus packets,
18
+ with particular focus on features needed for Tonie compatibility.
19
+ """
20
+
21
+ def __init__(self, filehandle, size=-1, last_size=-1, dont_parse_info=False):
22
+ """
23
+ Initialize a new OpusPacket.
24
+
25
+ Args:
26
+ filehandle: File handle to read the packet data from, or None to create an empty packet
27
+ size: Size of the packet in bytes
28
+ last_size: Size of the previous packet in bytes
29
+ dont_parse_info: If True, don't parse the packet information even if this is a first packet
30
+ """
31
+ self.config_value = None
32
+ self.stereo = None
33
+ self.framepacking = None
34
+ self.padding = None
35
+ self.frame_count = None
36
+ self.frame_size = None
37
+ self.granule = None
38
+
39
+ if filehandle is None:
40
+ logger.trace("Creating empty opus packet")
41
+ return
42
+
43
+ self.size = size
44
+ self.data = filehandle.read(self.size)
45
+ self.spanning_packet = size == 255
46
+ self.first_packet = last_size != 255
47
+
48
+ logger.trace("Created opus packet: size=%d, spanning=%s, first=%s",
49
+ self.size, self.spanning_packet, self.first_packet)
50
+
51
+ if self.first_packet and not dont_parse_info:
52
+ self.parse_segment_info()
53
+
54
+ def get_frame_count(self):
55
+ """
56
+ Get the number of frames in this packet based on its framepacking.
57
+
58
+ Returns:
59
+ int: Number of frames in the packet
60
+ """
61
+ if self.framepacking == 0:
62
+ return 1
63
+ elif self.framepacking == 1:
64
+ return 2
65
+ elif self.framepacking == 2:
66
+ return 2
67
+ elif self.framepacking == 3:
68
+ unpacked = struct.unpack("<B", self.data[1:2])
69
+ return unpacked[0] & 63
70
+
71
+ def get_padding(self):
72
+ """
73
+ Get the padding count for this packet.
74
+
75
+ Returns:
76
+ int: Number of padding bytes
77
+ """
78
+ if self.framepacking != 3:
79
+ return 0
80
+
81
+ unpacked = struct.unpack("<BB", self.data[1:3])
82
+ is_padded = (unpacked[0] >> 6) & 1
83
+ if not is_padded:
84
+ return 0
85
+
86
+ padding = unpacked[1]
87
+ total_padding = padding
88
+ i = 3
89
+ while padding == 255:
90
+ padding = struct.unpack("<B", self.data[i:i + 1])
91
+ total_padding = total_padding + padding[0] - 1
92
+ i = i + 1
93
+
94
+ logger.trace("Packet has %d bytes of padding", total_padding)
95
+ return total_padding
96
+
97
+ def get_frame_size(self):
98
+ """
99
+ Get the frame size in milliseconds based on the config value.
100
+
101
+ Returns:
102
+ float: Frame size in milliseconds
103
+
104
+ Raises:
105
+ RuntimeError: If the config value indicates a non-CELT encoding
106
+ """
107
+ if self.config_value in [16, 20, 24, 28]:
108
+ return 2.5
109
+ elif self.config_value in [17, 21, 25, 29]:
110
+ return 5
111
+ elif self.config_value in [18, 22, 26, 30]:
112
+ return 10
113
+ elif self.config_value in [19, 23, 27, 31]:
114
+ return 20
115
+ else:
116
+ error_msg = (
117
+ "Found config value {} in opus packet, but CELT-only encodings (16-31) are required by the box.\n"
118
+ "Please encode your input files accordingly or fix your encoding pipeline to do so.\n"
119
+ "Did you built libopus with custom modes support?".format(self.config_value)
120
+ )
121
+ logger.error(error_msg)
122
+ raise RuntimeError(error_msg)
123
+
124
+ def calc_granule(self):
125
+ """
126
+ Calculate the granule position for this packet.
127
+
128
+ Returns:
129
+ float: Granule position
130
+ """
131
+ granule = self.frame_size * self.frame_count * SAMPLE_RATE_KHZ
132
+ logger.trace("Calculated granule: %f (frame_size=%f, frame_count=%d)",
133
+ granule, self.frame_size, self.frame_count)
134
+ return granule
135
+
136
+ def parse_segment_info(self):
137
+ """Parse the segment information from the packet data."""
138
+ logger.trace("Parsing segment info from packet data")
139
+ byte = struct.unpack("<B", self.data[0:1])[0]
140
+ self.config_value = byte >> 3
141
+ self.stereo = (byte & 4) >> 2
142
+ self.framepacking = byte & 3
143
+ self.padding = self.get_padding()
144
+ self.frame_count = self.get_frame_count()
145
+ self.frame_size = self.get_frame_size()
146
+ self.granule = self.calc_granule()
147
+
148
+ logger.trace("Packet info: config=%d, stereo=%d, framepacking=%d, frame_count=%d, frame_size=%f",
149
+ self.config_value, self.stereo, self.framepacking, self.frame_count, self.frame_size)
150
+
151
+ def write(self, filehandle):
152
+ """
153
+ Write the packet data to a file.
154
+
155
+ Args:
156
+ filehandle: File handle to write the data to
157
+ """
158
+ if len(self.data):
159
+ logger.trace("Writing %d bytes of packet data to file", len(self.data))
160
+ filehandle.write(self.data)
161
+
162
+ def convert_to_framepacking_three(self):
163
+ """
164
+ Convert the packet to use framepacking mode 3.
165
+
166
+ This is needed for proper padding in Tonie files.
167
+ """
168
+ if self.framepacking == 3:
169
+ logger.trace("Packet already uses framepacking mode 3, no conversion needed")
170
+ return
171
+
172
+ logger.debug("Converting packet from framepacking mode %d to mode 3", self.framepacking)
173
+ toc_byte = struct.unpack("<B", self.data[0:1])[0]
174
+ toc_byte = toc_byte | 0b11
175
+
176
+ frame_count_byte = self.frame_count
177
+ if self.framepacking == 2:
178
+ frame_count_byte = frame_count_byte | 0b10000000 # vbr
179
+ logger.trace("Setting VBR flag in frame count byte")
180
+
181
+ self.data = struct.pack("<BB", toc_byte, frame_count_byte) + self.data[1:]
182
+ self.framepacking = 3
183
+ logger.debug("Packet successfully converted to framepacking mode 3")
184
+
185
+ def set_pad_count(self, count):
186
+ """
187
+ Set the padding count for this packet.
188
+
189
+ Args:
190
+ count: Number of padding bytes to add
191
+
192
+ Raises:
193
+ AssertionError: If the packet is not using framepacking mode 3 or is already padded
194
+ """
195
+ logger.debug("Setting padding count to %d bytes", count)
196
+
197
+ if self.framepacking != 3:
198
+ logger.error("Cannot set padding on a packet with framepacking mode %d", self.framepacking)
199
+ assert self.framepacking == 3, "Only code 3 packets can contain padding!"
200
+
201
+ if self.padding != 0:
202
+ logger.error("Packet already has %d bytes of padding", self.padding)
203
+ assert self.padding == 0, "Packet already padded. Not supported yet!"
204
+
205
+ frame_count_byte = struct.unpack("<B", self.data[1:2])[0]
206
+ frame_count_byte = frame_count_byte | 0b01000000
207
+ logger.trace("Setting padding flag in frame count byte")
208
+
209
+ pad_count_data = bytes()
210
+ val = count
211
+ while val > 254:
212
+ pad_count_data = pad_count_data + b"\xFF"
213
+ val = val - 254
214
+ logger.trace("Added padding byte 0xFF (254 bytes)")
215
+ pad_count_data = pad_count_data + struct.pack("<B", val)
216
+ logger.trace("Added final padding byte %d", val)
217
+
218
+ self.data = self.data[0:1] + struct.pack("<B", frame_count_byte) + pad_count_data + self.data[2:]
219
+ logger.debug("Padding count set successfully, new data length: %d bytes", len(self.data))