bumble 0.0.199__py3-none-any.whl → 0.0.201__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.
- bumble/_version.py +2 -2
- bumble/a2dp.py +502 -202
- bumble/apps/controller_info.py +60 -0
- bumble/apps/player/player.py +608 -0
- bumble/apps/speaker/speaker.py +25 -27
- bumble/att.py +2 -2
- bumble/avc.py +1 -2
- bumble/avdtp.py +54 -97
- bumble/avrcp.py +48 -29
- bumble/codecs.py +214 -68
- bumble/device.py +19 -11
- bumble/hci.py +31 -5
- bumble/hfp.py +52 -48
- bumble/host.py +12 -0
- bumble/profiles/hap.py +27 -18
- bumble/rtp.py +110 -0
- bumble/transport/android_netsim.py +31 -11
- bumble/transport/grpc_protobuf/netsim/__init__.py +0 -0
- bumble/transport/grpc_protobuf/{common_pb2.py → netsim/common_pb2.py} +9 -8
- bumble/transport/grpc_protobuf/{common_pb2.pyi → netsim/common_pb2.pyi} +11 -5
- bumble/transport/grpc_protobuf/netsim/hci_packet_pb2.py +29 -0
- bumble/transport/grpc_protobuf/{hci_packet_pb2.pyi → netsim/hci_packet_pb2.pyi} +13 -7
- bumble/transport/grpc_protobuf/netsim/model_pb2.py +63 -0
- bumble/transport/grpc_protobuf/netsim/model_pb2.pyi +238 -0
- bumble/transport/grpc_protobuf/netsim/packet_streamer_pb2.py +32 -0
- bumble/transport/grpc_protobuf/{packet_streamer_pb2.pyi → netsim/packet_streamer_pb2.pyi} +6 -6
- bumble/transport/grpc_protobuf/{packet_streamer_pb2_grpc.py → netsim/packet_streamer_pb2_grpc.py} +7 -7
- bumble/transport/grpc_protobuf/netsim/startup_pb2.py +41 -0
- bumble/transport/grpc_protobuf/netsim/startup_pb2.pyi +76 -0
- bumble/transport/grpc_protobuf/netsim/startup_pb2_grpc.py +4 -0
- bumble/transport/grpc_protobuf/rootcanal/__init__.py +0 -0
- bumble/transport/grpc_protobuf/rootcanal/configuration_pb2.py +39 -0
- bumble/transport/grpc_protobuf/rootcanal/configuration_pb2.pyi +78 -0
- bumble/transport/grpc_protobuf/rootcanal/configuration_pb2_grpc.py +4 -0
- bumble/transport/pyusb.py +2 -1
- {bumble-0.0.199.dist-info → bumble-0.0.201.dist-info}/METADATA +2 -2
- {bumble-0.0.199.dist-info → bumble-0.0.201.dist-info}/RECORD +44 -34
- {bumble-0.0.199.dist-info → bumble-0.0.201.dist-info}/WHEEL +1 -1
- {bumble-0.0.199.dist-info → bumble-0.0.201.dist-info}/entry_points.txt +1 -0
- bumble/transport/grpc_protobuf/hci_packet_pb2.py +0 -28
- bumble/transport/grpc_protobuf/packet_streamer_pb2.py +0 -31
- bumble/transport/grpc_protobuf/startup_pb2.py +0 -32
- bumble/transport/grpc_protobuf/startup_pb2.pyi +0 -46
- /bumble/transport/grpc_protobuf/{common_pb2_grpc.py → netsim/common_pb2_grpc.py} +0 -0
- /bumble/transport/grpc_protobuf/{hci_packet_pb2_grpc.py → netsim/hci_packet_pb2_grpc.py} +0 -0
- /bumble/transport/grpc_protobuf/{startup_pb2_grpc.py → netsim/model_pb2_grpc.py} +0 -0
- {bumble-0.0.199.dist-info → bumble-0.0.201.dist-info}/LICENSE +0 -0
- {bumble-0.0.199.dist-info → bumble-0.0.201.dist-info}/top_level.txt +0 -0
bumble/a2dp.py
CHANGED
|
@@ -17,12 +17,16 @@
|
|
|
17
17
|
# -----------------------------------------------------------------------------
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
+
from collections.abc import AsyncGenerator
|
|
20
21
|
import dataclasses
|
|
21
|
-
import
|
|
22
|
+
import enum
|
|
22
23
|
import logging
|
|
23
|
-
|
|
24
|
-
from typing import
|
|
24
|
+
import struct
|
|
25
|
+
from typing import Awaitable, Callable
|
|
26
|
+
from typing_extensions import ClassVar, Self
|
|
27
|
+
|
|
25
28
|
|
|
29
|
+
from .codecs import AacAudioRtpPacket
|
|
26
30
|
from .company_ids import COMPANY_IDENTIFIERS
|
|
27
31
|
from .sdp import (
|
|
28
32
|
DataElement,
|
|
@@ -42,6 +46,7 @@ from .core import (
|
|
|
42
46
|
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
|
43
47
|
name_or_number,
|
|
44
48
|
)
|
|
49
|
+
from .rtp import MediaPacket
|
|
45
50
|
|
|
46
51
|
|
|
47
52
|
# -----------------------------------------------------------------------------
|
|
@@ -103,6 +108,8 @@ SBC_ALLOCATION_METHOD_NAMES = {
|
|
|
103
108
|
SBC_LOUDNESS_ALLOCATION_METHOD: 'SBC_LOUDNESS_ALLOCATION_METHOD'
|
|
104
109
|
}
|
|
105
110
|
|
|
111
|
+
SBC_MAX_FRAMES_IN_RTP_PAYLOAD = 15
|
|
112
|
+
|
|
106
113
|
MPEG_2_4_AAC_SAMPLING_FREQUENCIES = [
|
|
107
114
|
8000,
|
|
108
115
|
11025,
|
|
@@ -130,6 +137,9 @@ MPEG_2_4_OBJECT_TYPE_NAMES = {
|
|
|
130
137
|
MPEG_4_AAC_SCALABLE_OBJECT_TYPE: 'MPEG_4_AAC_SCALABLE_OBJECT_TYPE'
|
|
131
138
|
}
|
|
132
139
|
|
|
140
|
+
|
|
141
|
+
OPUS_MAX_FRAMES_IN_RTP_PAYLOAD = 15
|
|
142
|
+
|
|
133
143
|
# fmt: on
|
|
134
144
|
|
|
135
145
|
|
|
@@ -257,38 +267,61 @@ class SbcMediaCodecInformation:
|
|
|
257
267
|
A2DP spec - 4.3.2 Codec Specific Information Elements
|
|
258
268
|
'''
|
|
259
269
|
|
|
260
|
-
sampling_frequency:
|
|
261
|
-
channel_mode:
|
|
262
|
-
block_length:
|
|
263
|
-
subbands:
|
|
264
|
-
allocation_method:
|
|
270
|
+
sampling_frequency: SamplingFrequency
|
|
271
|
+
channel_mode: ChannelMode
|
|
272
|
+
block_length: BlockLength
|
|
273
|
+
subbands: Subbands
|
|
274
|
+
allocation_method: AllocationMethod
|
|
265
275
|
minimum_bitpool_value: int
|
|
266
276
|
maximum_bitpool_value: int
|
|
267
277
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
278
|
+
class SamplingFrequency(enum.IntFlag):
|
|
279
|
+
SF_16000 = 1 << 3
|
|
280
|
+
SF_32000 = 1 << 2
|
|
281
|
+
SF_44100 = 1 << 1
|
|
282
|
+
SF_48000 = 1 << 0
|
|
283
|
+
|
|
284
|
+
@classmethod
|
|
285
|
+
def from_int(cls, sampling_frequency: int) -> Self:
|
|
286
|
+
sampling_frequencies = [
|
|
287
|
+
16000,
|
|
288
|
+
32000,
|
|
289
|
+
44100,
|
|
290
|
+
48000,
|
|
291
|
+
]
|
|
292
|
+
index = sampling_frequencies.index(sampling_frequency)
|
|
293
|
+
return cls(1 << (len(sampling_frequencies) - index - 1))
|
|
281
294
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
295
|
+
class ChannelMode(enum.IntFlag):
|
|
296
|
+
MONO = 1 << 3
|
|
297
|
+
DUAL_CHANNEL = 1 << 2
|
|
298
|
+
STEREO = 1 << 1
|
|
299
|
+
JOINT_STEREO = 1 << 0
|
|
300
|
+
|
|
301
|
+
class BlockLength(enum.IntFlag):
|
|
302
|
+
BL_4 = 1 << 3
|
|
303
|
+
BL_8 = 1 << 2
|
|
304
|
+
BL_12 = 1 << 1
|
|
305
|
+
BL_16 = 1 << 0
|
|
306
|
+
|
|
307
|
+
class Subbands(enum.IntFlag):
|
|
308
|
+
S_4 = 1 << 1
|
|
309
|
+
S_8 = 1 << 0
|
|
310
|
+
|
|
311
|
+
class AllocationMethod(enum.IntFlag):
|
|
312
|
+
SNR = 1 << 1
|
|
313
|
+
LOUDNESS = 1 << 0
|
|
314
|
+
|
|
315
|
+
@classmethod
|
|
316
|
+
def from_bytes(cls, data: bytes) -> Self:
|
|
317
|
+
sampling_frequency = cls.SamplingFrequency((data[0] >> 4) & 0x0F)
|
|
318
|
+
channel_mode = cls.ChannelMode((data[0] >> 0) & 0x0F)
|
|
319
|
+
block_length = cls.BlockLength((data[1] >> 4) & 0x0F)
|
|
320
|
+
subbands = cls.Subbands((data[1] >> 2) & 0x03)
|
|
321
|
+
allocation_method = cls.AllocationMethod((data[1] >> 0) & 0x03)
|
|
289
322
|
minimum_bitpool_value = (data[2] >> 0) & 0xFF
|
|
290
323
|
maximum_bitpool_value = (data[3] >> 0) & 0xFF
|
|
291
|
-
return
|
|
324
|
+
return cls(
|
|
292
325
|
sampling_frequency,
|
|
293
326
|
channel_mode,
|
|
294
327
|
block_length,
|
|
@@ -298,52 +331,6 @@ class SbcMediaCodecInformation:
|
|
|
298
331
|
maximum_bitpool_value,
|
|
299
332
|
)
|
|
300
333
|
|
|
301
|
-
@classmethod
|
|
302
|
-
def from_discrete_values(
|
|
303
|
-
cls,
|
|
304
|
-
sampling_frequency: int,
|
|
305
|
-
channel_mode: int,
|
|
306
|
-
block_length: int,
|
|
307
|
-
subbands: int,
|
|
308
|
-
allocation_method: int,
|
|
309
|
-
minimum_bitpool_value: int,
|
|
310
|
-
maximum_bitpool_value: int,
|
|
311
|
-
) -> SbcMediaCodecInformation:
|
|
312
|
-
return SbcMediaCodecInformation(
|
|
313
|
-
sampling_frequency=cls.SAMPLING_FREQUENCY_BITS[sampling_frequency],
|
|
314
|
-
channel_mode=cls.CHANNEL_MODE_BITS[channel_mode],
|
|
315
|
-
block_length=cls.BLOCK_LENGTH_BITS[block_length],
|
|
316
|
-
subbands=cls.SUBBANDS_BITS[subbands],
|
|
317
|
-
allocation_method=cls.ALLOCATION_METHOD_BITS[allocation_method],
|
|
318
|
-
minimum_bitpool_value=minimum_bitpool_value,
|
|
319
|
-
maximum_bitpool_value=maximum_bitpool_value,
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
@classmethod
|
|
323
|
-
def from_lists(
|
|
324
|
-
cls,
|
|
325
|
-
sampling_frequencies: List[int],
|
|
326
|
-
channel_modes: List[int],
|
|
327
|
-
block_lengths: List[int],
|
|
328
|
-
subbands: List[int],
|
|
329
|
-
allocation_methods: List[int],
|
|
330
|
-
minimum_bitpool_value: int,
|
|
331
|
-
maximum_bitpool_value: int,
|
|
332
|
-
) -> SbcMediaCodecInformation:
|
|
333
|
-
return SbcMediaCodecInformation(
|
|
334
|
-
sampling_frequency=sum(
|
|
335
|
-
cls.SAMPLING_FREQUENCY_BITS[x] for x in sampling_frequencies
|
|
336
|
-
),
|
|
337
|
-
channel_mode=sum(cls.CHANNEL_MODE_BITS[x] for x in channel_modes),
|
|
338
|
-
block_length=sum(cls.BLOCK_LENGTH_BITS[x] for x in block_lengths),
|
|
339
|
-
subbands=sum(cls.SUBBANDS_BITS[x] for x in subbands),
|
|
340
|
-
allocation_method=sum(
|
|
341
|
-
cls.ALLOCATION_METHOD_BITS[x] for x in allocation_methods
|
|
342
|
-
),
|
|
343
|
-
minimum_bitpool_value=minimum_bitpool_value,
|
|
344
|
-
maximum_bitpool_value=maximum_bitpool_value,
|
|
345
|
-
)
|
|
346
|
-
|
|
347
334
|
def __bytes__(self) -> bytes:
|
|
348
335
|
return bytes(
|
|
349
336
|
[
|
|
@@ -356,23 +343,6 @@ class SbcMediaCodecInformation:
|
|
|
356
343
|
]
|
|
357
344
|
)
|
|
358
345
|
|
|
359
|
-
def __str__(self) -> str:
|
|
360
|
-
channel_modes = ['MONO', 'DUAL_CHANNEL', 'STEREO', 'JOINT_STEREO']
|
|
361
|
-
allocation_methods = ['SNR', 'Loudness']
|
|
362
|
-
return '\n'.join(
|
|
363
|
-
# pylint: disable=line-too-long
|
|
364
|
-
[
|
|
365
|
-
'SbcMediaCodecInformation(',
|
|
366
|
-
f' sampling_frequency: {",".join([str(x) for x in flags_to_list(self.sampling_frequency, SBC_SAMPLING_FREQUENCIES)])}',
|
|
367
|
-
f' channel_mode: {",".join([str(x) for x in flags_to_list(self.channel_mode, channel_modes)])}',
|
|
368
|
-
f' block_length: {",".join([str(x) for x in flags_to_list(self.block_length, SBC_BLOCK_LENGTHS)])}',
|
|
369
|
-
f' subbands: {",".join([str(x) for x in flags_to_list(self.subbands, SBC_SUBBANDS)])}',
|
|
370
|
-
f' allocation_method: {",".join([str(x) for x in flags_to_list(self.allocation_method, allocation_methods)])}',
|
|
371
|
-
f' minimum_bitpool_value: {self.minimum_bitpool_value}',
|
|
372
|
-
f' maximum_bitpool_value: {self.maximum_bitpool_value}' ')',
|
|
373
|
-
]
|
|
374
|
-
)
|
|
375
|
-
|
|
376
346
|
|
|
377
347
|
# -----------------------------------------------------------------------------
|
|
378
348
|
@dataclasses.dataclass
|
|
@@ -381,83 +351,66 @@ class AacMediaCodecInformation:
|
|
|
381
351
|
A2DP spec - 4.5.2 Codec Specific Information Elements
|
|
382
352
|
'''
|
|
383
353
|
|
|
384
|
-
object_type:
|
|
385
|
-
sampling_frequency:
|
|
386
|
-
channels:
|
|
387
|
-
rfa: int
|
|
354
|
+
object_type: ObjectType
|
|
355
|
+
sampling_frequency: SamplingFrequency
|
|
356
|
+
channels: Channels
|
|
388
357
|
vbr: int
|
|
389
358
|
bitrate: int
|
|
390
359
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
360
|
+
class ObjectType(enum.IntFlag):
|
|
361
|
+
MPEG_2_AAC_LC = 1 << 7
|
|
362
|
+
MPEG_4_AAC_LC = 1 << 6
|
|
363
|
+
MPEG_4_AAC_LTP = 1 << 5
|
|
364
|
+
MPEG_4_AAC_SCALABLE = 1 << 4
|
|
365
|
+
|
|
366
|
+
class SamplingFrequency(enum.IntFlag):
|
|
367
|
+
SF_8000 = 1 << 11
|
|
368
|
+
SF_11025 = 1 << 10
|
|
369
|
+
SF_12000 = 1 << 9
|
|
370
|
+
SF_16000 = 1 << 8
|
|
371
|
+
SF_22050 = 1 << 7
|
|
372
|
+
SF_24000 = 1 << 6
|
|
373
|
+
SF_32000 = 1 << 5
|
|
374
|
+
SF_44100 = 1 << 4
|
|
375
|
+
SF_48000 = 1 << 3
|
|
376
|
+
SF_64000 = 1 << 2
|
|
377
|
+
SF_88200 = 1 << 1
|
|
378
|
+
SF_96000 = 1 << 0
|
|
379
|
+
|
|
380
|
+
@classmethod
|
|
381
|
+
def from_int(cls, sampling_frequency: int) -> Self:
|
|
382
|
+
sampling_frequencies = [
|
|
383
|
+
8000,
|
|
384
|
+
11025,
|
|
385
|
+
12000,
|
|
386
|
+
16000,
|
|
387
|
+
22050,
|
|
388
|
+
24000,
|
|
389
|
+
32000,
|
|
390
|
+
44100,
|
|
391
|
+
48000,
|
|
392
|
+
64000,
|
|
393
|
+
88200,
|
|
394
|
+
96000,
|
|
395
|
+
]
|
|
396
|
+
index = sampling_frequencies.index(sampling_frequency)
|
|
397
|
+
return cls(1 << (len(sampling_frequencies) - index - 1))
|
|
412
398
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
sampling_frequency = (data[1] << 4) | ((data[2] >> 4) & 0x0F)
|
|
417
|
-
channels = (data[2] >> 2) & 0x03
|
|
418
|
-
rfa = 0
|
|
419
|
-
vbr = (data[3] >> 7) & 0x01
|
|
420
|
-
bitrate = ((data[3] & 0x7F) << 16) | (data[4] << 8) | data[5]
|
|
421
|
-
return AacMediaCodecInformation(
|
|
422
|
-
object_type, sampling_frequency, channels, rfa, vbr, bitrate
|
|
423
|
-
)
|
|
399
|
+
class Channels(enum.IntFlag):
|
|
400
|
+
MONO = 1 << 1
|
|
401
|
+
STEREO = 1 << 0
|
|
424
402
|
|
|
425
403
|
@classmethod
|
|
426
|
-
def
|
|
427
|
-
cls
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
channels: int,
|
|
431
|
-
vbr: int,
|
|
432
|
-
bitrate: int,
|
|
433
|
-
) -> AacMediaCodecInformation:
|
|
434
|
-
return AacMediaCodecInformation(
|
|
435
|
-
object_type=cls.OBJECT_TYPE_BITS[object_type],
|
|
436
|
-
sampling_frequency=cls.SAMPLING_FREQUENCY_BITS[sampling_frequency],
|
|
437
|
-
channels=cls.CHANNELS_BITS[channels],
|
|
438
|
-
rfa=0,
|
|
439
|
-
vbr=vbr,
|
|
440
|
-
bitrate=bitrate,
|
|
404
|
+
def from_bytes(cls, data: bytes) -> AacMediaCodecInformation:
|
|
405
|
+
object_type = cls.ObjectType(data[0])
|
|
406
|
+
sampling_frequency = cls.SamplingFrequency(
|
|
407
|
+
(data[1] << 4) | ((data[2] >> 4) & 0x0F)
|
|
441
408
|
)
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
cls,
|
|
446
|
-
object_types: List[int],
|
|
447
|
-
sampling_frequencies: List[int],
|
|
448
|
-
channels: List[int],
|
|
449
|
-
vbr: int,
|
|
450
|
-
bitrate: int,
|
|
451
|
-
) -> AacMediaCodecInformation:
|
|
409
|
+
channels = cls.Channels((data[2] >> 2) & 0x03)
|
|
410
|
+
vbr = (data[3] >> 7) & 0x01
|
|
411
|
+
bitrate = ((data[3] & 0x7F) << 16) | (data[4] << 8) | data[5]
|
|
452
412
|
return AacMediaCodecInformation(
|
|
453
|
-
object_type
|
|
454
|
-
sampling_frequency=sum(
|
|
455
|
-
cls.SAMPLING_FREQUENCY_BITS[x] for x in sampling_frequencies
|
|
456
|
-
),
|
|
457
|
-
channels=sum(cls.CHANNELS_BITS[x] for x in channels),
|
|
458
|
-
rfa=0,
|
|
459
|
-
vbr=vbr,
|
|
460
|
-
bitrate=bitrate,
|
|
413
|
+
object_type, sampling_frequency, channels, vbr, bitrate
|
|
461
414
|
)
|
|
462
415
|
|
|
463
416
|
def __bytes__(self) -> bytes:
|
|
@@ -472,30 +425,6 @@ class AacMediaCodecInformation:
|
|
|
472
425
|
]
|
|
473
426
|
)
|
|
474
427
|
|
|
475
|
-
def __str__(self) -> str:
|
|
476
|
-
object_types = [
|
|
477
|
-
'MPEG_2_AAC_LC',
|
|
478
|
-
'MPEG_4_AAC_LC',
|
|
479
|
-
'MPEG_4_AAC_LTP',
|
|
480
|
-
'MPEG_4_AAC_SCALABLE',
|
|
481
|
-
'[4]',
|
|
482
|
-
'[5]',
|
|
483
|
-
'[6]',
|
|
484
|
-
'[7]',
|
|
485
|
-
]
|
|
486
|
-
channels = [1, 2]
|
|
487
|
-
# pylint: disable=line-too-long
|
|
488
|
-
return '\n'.join(
|
|
489
|
-
[
|
|
490
|
-
'AacMediaCodecInformation(',
|
|
491
|
-
f' object_type: {",".join([str(x) for x in flags_to_list(self.object_type, object_types)])}',
|
|
492
|
-
f' sampling_frequency: {",".join([str(x) for x in flags_to_list(self.sampling_frequency, MPEG_2_4_AAC_SAMPLING_FREQUENCIES)])}',
|
|
493
|
-
f' channels: {",".join([str(x) for x in flags_to_list(self.channels, channels)])}',
|
|
494
|
-
f' vbr: {self.vbr}',
|
|
495
|
-
f' bitrate: {self.bitrate}' ')',
|
|
496
|
-
]
|
|
497
|
-
)
|
|
498
|
-
|
|
499
428
|
|
|
500
429
|
@dataclasses.dataclass
|
|
501
430
|
# -----------------------------------------------------------------------------
|
|
@@ -514,7 +443,7 @@ class VendorSpecificMediaCodecInformation:
|
|
|
514
443
|
return VendorSpecificMediaCodecInformation(vendor_id, codec_id, data[6:])
|
|
515
444
|
|
|
516
445
|
def __bytes__(self) -> bytes:
|
|
517
|
-
return struct.pack('<IH', self.vendor_id, self.codec_id
|
|
446
|
+
return struct.pack('<IH', self.vendor_id, self.codec_id) + self.value
|
|
518
447
|
|
|
519
448
|
def __str__(self) -> str:
|
|
520
449
|
# pylint: disable=line-too-long
|
|
@@ -528,13 +457,69 @@ class VendorSpecificMediaCodecInformation:
|
|
|
528
457
|
)
|
|
529
458
|
|
|
530
459
|
|
|
460
|
+
# -----------------------------------------------------------------------------
|
|
461
|
+
@dataclasses.dataclass
|
|
462
|
+
class OpusMediaCodecInformation(VendorSpecificMediaCodecInformation):
|
|
463
|
+
vendor_id: int = dataclasses.field(init=False, repr=False)
|
|
464
|
+
codec_id: int = dataclasses.field(init=False, repr=False)
|
|
465
|
+
value: bytes = dataclasses.field(init=False, repr=False)
|
|
466
|
+
channel_mode: ChannelMode
|
|
467
|
+
frame_size: FrameSize
|
|
468
|
+
sampling_frequency: SamplingFrequency
|
|
469
|
+
|
|
470
|
+
class ChannelMode(enum.IntFlag):
|
|
471
|
+
MONO = 1 << 0
|
|
472
|
+
STEREO = 1 << 1
|
|
473
|
+
DUAL_MONO = 1 << 2
|
|
474
|
+
|
|
475
|
+
class FrameSize(enum.IntFlag):
|
|
476
|
+
FS_10MS = 1 << 0
|
|
477
|
+
FS_20MS = 1 << 1
|
|
478
|
+
|
|
479
|
+
class SamplingFrequency(enum.IntFlag):
|
|
480
|
+
SF_48000 = 1 << 0
|
|
481
|
+
|
|
482
|
+
VENDOR_ID: ClassVar[int] = 0x000000E0
|
|
483
|
+
CODEC_ID: ClassVar[int] = 0x0001
|
|
484
|
+
|
|
485
|
+
def __post_init__(self) -> None:
|
|
486
|
+
self.vendor_id = self.VENDOR_ID
|
|
487
|
+
self.codec_id = self.CODEC_ID
|
|
488
|
+
self.value = bytes(
|
|
489
|
+
[
|
|
490
|
+
self.channel_mode
|
|
491
|
+
| (self.frame_size << 3)
|
|
492
|
+
| (self.sampling_frequency << 7)
|
|
493
|
+
]
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
@classmethod
|
|
497
|
+
def from_bytes(cls, data: bytes) -> Self:
|
|
498
|
+
"""Create a new instance from the `value` part of the data, not including
|
|
499
|
+
the vendor id and codec id"""
|
|
500
|
+
channel_mode = cls.ChannelMode(data[0] & 0x07)
|
|
501
|
+
frame_size = cls.FrameSize((data[0] >> 3) & 0x03)
|
|
502
|
+
sampling_frequency = cls.SamplingFrequency((data[0] >> 7) & 0x01)
|
|
503
|
+
|
|
504
|
+
return cls(
|
|
505
|
+
channel_mode,
|
|
506
|
+
frame_size,
|
|
507
|
+
sampling_frequency,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
def __str__(self) -> str:
|
|
511
|
+
return repr(self)
|
|
512
|
+
|
|
513
|
+
|
|
531
514
|
# -----------------------------------------------------------------------------
|
|
532
515
|
@dataclasses.dataclass
|
|
533
516
|
class SbcFrame:
|
|
534
517
|
sampling_frequency: int
|
|
535
518
|
block_count: int
|
|
536
519
|
channel_mode: int
|
|
520
|
+
allocation_method: int
|
|
537
521
|
subband_count: int
|
|
522
|
+
bitpool: int
|
|
538
523
|
payload: bytes
|
|
539
524
|
|
|
540
525
|
@property
|
|
@@ -553,8 +538,10 @@ class SbcFrame:
|
|
|
553
538
|
return (
|
|
554
539
|
f'SBC(sf={self.sampling_frequency},'
|
|
555
540
|
f'cm={self.channel_mode},'
|
|
541
|
+
f'am={self.allocation_method},'
|
|
556
542
|
f'br={self.bitrate},'
|
|
557
543
|
f'sc={self.sample_count},'
|
|
544
|
+
f'bp={self.bitpool},'
|
|
558
545
|
f'size={len(self.payload)})'
|
|
559
546
|
)
|
|
560
547
|
|
|
@@ -583,6 +570,7 @@ class SbcParser:
|
|
|
583
570
|
blocks = 4 * (1 + ((header[1] >> 4) & 3))
|
|
584
571
|
channel_mode = (header[1] >> 2) & 3
|
|
585
572
|
channels = 1 if channel_mode == SBC_MONO_CHANNEL_MODE else 2
|
|
573
|
+
allocation_method = (header[1] >> 1) & 1
|
|
586
574
|
subbands = 8 if ((header[1]) & 1) else 4
|
|
587
575
|
bitpool = header[2]
|
|
588
576
|
|
|
@@ -602,7 +590,13 @@ class SbcParser:
|
|
|
602
590
|
|
|
603
591
|
# Emit the next frame
|
|
604
592
|
yield SbcFrame(
|
|
605
|
-
sampling_frequency,
|
|
593
|
+
sampling_frequency,
|
|
594
|
+
blocks,
|
|
595
|
+
channel_mode,
|
|
596
|
+
allocation_method,
|
|
597
|
+
subbands,
|
|
598
|
+
bitpool,
|
|
599
|
+
payload,
|
|
606
600
|
)
|
|
607
601
|
|
|
608
602
|
return generate_frames()
|
|
@@ -610,21 +604,15 @@ class SbcParser:
|
|
|
610
604
|
|
|
611
605
|
# -----------------------------------------------------------------------------
|
|
612
606
|
class SbcPacketSource:
|
|
613
|
-
def __init__(
|
|
614
|
-
self, read: Callable[[int], Awaitable[bytes]], mtu: int, codec_capabilities
|
|
615
|
-
) -> None:
|
|
607
|
+
def __init__(self, read: Callable[[int], Awaitable[bytes]], mtu: int) -> None:
|
|
616
608
|
self.read = read
|
|
617
609
|
self.mtu = mtu
|
|
618
|
-
self.codec_capabilities = codec_capabilities
|
|
619
610
|
|
|
620
611
|
@property
|
|
621
612
|
def packets(self):
|
|
622
613
|
async def generate_packets():
|
|
623
|
-
# pylint: disable=import-outside-toplevel
|
|
624
|
-
from .avdtp import MediaPacket # Import here to avoid a circular reference
|
|
625
|
-
|
|
626
614
|
sequence_number = 0
|
|
627
|
-
|
|
615
|
+
sample_count = 0
|
|
628
616
|
frames = []
|
|
629
617
|
frames_size = 0
|
|
630
618
|
max_rtp_payload = self.mtu - 12 - 1
|
|
@@ -632,29 +620,29 @@ class SbcPacketSource:
|
|
|
632
620
|
# NOTE: this doesn't support frame fragments
|
|
633
621
|
sbc_parser = SbcParser(self.read)
|
|
634
622
|
async for frame in sbc_parser.frames:
|
|
635
|
-
print(frame)
|
|
636
|
-
|
|
637
623
|
if (
|
|
638
624
|
frames_size + len(frame.payload) > max_rtp_payload
|
|
639
|
-
or len(frames) ==
|
|
625
|
+
or len(frames) == SBC_MAX_FRAMES_IN_RTP_PAYLOAD
|
|
640
626
|
):
|
|
641
627
|
# Need to flush what has been accumulated so far
|
|
628
|
+
logger.debug(f"yielding {len(frames)} frames")
|
|
642
629
|
|
|
643
630
|
# Emit a packet
|
|
644
|
-
sbc_payload = bytes([len(frames)]) + b''.join(
|
|
631
|
+
sbc_payload = bytes([len(frames) & 0x0F]) + b''.join(
|
|
645
632
|
[frame.payload for frame in frames]
|
|
646
633
|
)
|
|
634
|
+
timestamp_seconds = sample_count / frame.sampling_frequency
|
|
635
|
+
timestamp = int(1000 * timestamp_seconds)
|
|
647
636
|
packet = MediaPacket(
|
|
648
637
|
2, 0, 0, 0, sequence_number, timestamp, 0, [], 96, sbc_payload
|
|
649
638
|
)
|
|
650
|
-
packet.timestamp_seconds =
|
|
639
|
+
packet.timestamp_seconds = timestamp_seconds
|
|
651
640
|
yield packet
|
|
652
641
|
|
|
653
642
|
# Prepare for next packets
|
|
654
643
|
sequence_number += 1
|
|
655
644
|
sequence_number &= 0xFFFF
|
|
656
|
-
|
|
657
|
-
timestamp &= 0xFFFFFFFF
|
|
645
|
+
sample_count += sum((frame.sample_count for frame in frames))
|
|
658
646
|
frames = [frame]
|
|
659
647
|
frames_size = len(frame.payload)
|
|
660
648
|
else:
|
|
@@ -663,3 +651,315 @@ class SbcPacketSource:
|
|
|
663
651
|
frames_size += len(frame.payload)
|
|
664
652
|
|
|
665
653
|
return generate_packets()
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
# -----------------------------------------------------------------------------
|
|
657
|
+
@dataclasses.dataclass
|
|
658
|
+
class AacFrame:
|
|
659
|
+
class Profile(enum.IntEnum):
|
|
660
|
+
MAIN = 0
|
|
661
|
+
LC = 1
|
|
662
|
+
SSR = 2
|
|
663
|
+
LTP = 3
|
|
664
|
+
|
|
665
|
+
profile: Profile
|
|
666
|
+
sampling_frequency: int
|
|
667
|
+
channel_configuration: int
|
|
668
|
+
payload: bytes
|
|
669
|
+
|
|
670
|
+
@property
|
|
671
|
+
def sample_count(self) -> int:
|
|
672
|
+
return 1024
|
|
673
|
+
|
|
674
|
+
@property
|
|
675
|
+
def duration(self) -> float:
|
|
676
|
+
return self.sample_count / self.sampling_frequency
|
|
677
|
+
|
|
678
|
+
def __str__(self) -> str:
|
|
679
|
+
return (
|
|
680
|
+
f'AAC(sf={self.sampling_frequency},'
|
|
681
|
+
f'ch={self.channel_configuration},'
|
|
682
|
+
f'size={len(self.payload)})'
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
# -----------------------------------------------------------------------------
|
|
687
|
+
ADTS_AAC_SAMPLING_FREQUENCIES = [
|
|
688
|
+
96000,
|
|
689
|
+
88200,
|
|
690
|
+
64000,
|
|
691
|
+
48000,
|
|
692
|
+
44100,
|
|
693
|
+
32000,
|
|
694
|
+
24000,
|
|
695
|
+
22050,
|
|
696
|
+
16000,
|
|
697
|
+
12000,
|
|
698
|
+
11025,
|
|
699
|
+
8000,
|
|
700
|
+
7350,
|
|
701
|
+
0,
|
|
702
|
+
0,
|
|
703
|
+
0,
|
|
704
|
+
]
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
# -----------------------------------------------------------------------------
|
|
708
|
+
class AacParser:
|
|
709
|
+
"""Parser for AAC frames in an ADTS stream"""
|
|
710
|
+
|
|
711
|
+
def __init__(self, read: Callable[[int], Awaitable[bytes]]) -> None:
|
|
712
|
+
self.read = read
|
|
713
|
+
|
|
714
|
+
@property
|
|
715
|
+
def frames(self) -> AsyncGenerator[AacFrame, None]:
|
|
716
|
+
async def generate_frames() -> AsyncGenerator[AacFrame, None]:
|
|
717
|
+
while True:
|
|
718
|
+
header = await self.read(7)
|
|
719
|
+
if not header:
|
|
720
|
+
return
|
|
721
|
+
|
|
722
|
+
sync_word = (header[0] << 4) | (header[1] >> 4)
|
|
723
|
+
if sync_word != 0b111111111111:
|
|
724
|
+
raise ValueError(f"invalid sync word ({sync_word:06x})")
|
|
725
|
+
layer = (header[1] >> 1) & 0b11
|
|
726
|
+
profile = AacFrame.Profile((header[2] >> 6) & 0b11)
|
|
727
|
+
sampling_frequency = ADTS_AAC_SAMPLING_FREQUENCIES[
|
|
728
|
+
(header[2] >> 2) & 0b1111
|
|
729
|
+
]
|
|
730
|
+
channel_configuration = ((header[2] & 0b1) << 2) | (header[3] >> 6)
|
|
731
|
+
frame_length = (
|
|
732
|
+
((header[3] & 0b11) << 11) | (header[4] << 3) | (header[5] >> 5)
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
if layer != 0:
|
|
736
|
+
raise ValueError("layer must be 0")
|
|
737
|
+
|
|
738
|
+
payload = await self.read(frame_length - 7)
|
|
739
|
+
if payload:
|
|
740
|
+
yield AacFrame(
|
|
741
|
+
profile, sampling_frequency, channel_configuration, payload
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
return generate_frames()
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
# -----------------------------------------------------------------------------
|
|
748
|
+
class AacPacketSource:
|
|
749
|
+
def __init__(self, read: Callable[[int], Awaitable[bytes]], mtu: int) -> None:
|
|
750
|
+
self.read = read
|
|
751
|
+
self.mtu = mtu
|
|
752
|
+
|
|
753
|
+
@property
|
|
754
|
+
def packets(self):
|
|
755
|
+
async def generate_packets():
|
|
756
|
+
sequence_number = 0
|
|
757
|
+
sample_count = 0
|
|
758
|
+
|
|
759
|
+
aac_parser = AacParser(self.read)
|
|
760
|
+
async for frame in aac_parser.frames:
|
|
761
|
+
logger.debug("yielding one AAC frame")
|
|
762
|
+
|
|
763
|
+
# Emit a packet
|
|
764
|
+
aac_payload = bytes(
|
|
765
|
+
AacAudioRtpPacket.for_simple_aac(
|
|
766
|
+
frame.sampling_frequency,
|
|
767
|
+
frame.channel_configuration,
|
|
768
|
+
frame.payload,
|
|
769
|
+
)
|
|
770
|
+
)
|
|
771
|
+
timestamp_seconds = sample_count / frame.sampling_frequency
|
|
772
|
+
timestamp = int(1000 * timestamp_seconds)
|
|
773
|
+
packet = MediaPacket(
|
|
774
|
+
2, 0, 0, 0, sequence_number, timestamp, 0, [], 96, aac_payload
|
|
775
|
+
)
|
|
776
|
+
packet.timestamp_seconds = timestamp_seconds
|
|
777
|
+
yield packet
|
|
778
|
+
|
|
779
|
+
# Prepare for next packets
|
|
780
|
+
sequence_number += 1
|
|
781
|
+
sequence_number &= 0xFFFF
|
|
782
|
+
sample_count += frame.sample_count
|
|
783
|
+
|
|
784
|
+
return generate_packets()
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
# -----------------------------------------------------------------------------
|
|
788
|
+
@dataclasses.dataclass
|
|
789
|
+
class OpusPacket:
|
|
790
|
+
class ChannelMode(enum.IntEnum):
|
|
791
|
+
MONO = 0
|
|
792
|
+
STEREO = 1
|
|
793
|
+
DUAL_MONO = 2
|
|
794
|
+
|
|
795
|
+
channel_mode: ChannelMode
|
|
796
|
+
duration: int # Duration in ms.
|
|
797
|
+
sampling_frequency: int
|
|
798
|
+
payload: bytes
|
|
799
|
+
|
|
800
|
+
def __str__(self) -> str:
|
|
801
|
+
return (
|
|
802
|
+
f'Opus(ch={self.channel_mode.name}, '
|
|
803
|
+
f'd={self.duration}ms, '
|
|
804
|
+
f'size={len(self.payload)})'
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
# -----------------------------------------------------------------------------
|
|
809
|
+
class OpusParser:
|
|
810
|
+
"""
|
|
811
|
+
Parser for Opus packets in an Ogg stream
|
|
812
|
+
|
|
813
|
+
See RFC 3533
|
|
814
|
+
|
|
815
|
+
NOTE: this parser only supports bitstreams with a single logical stream.
|
|
816
|
+
"""
|
|
817
|
+
|
|
818
|
+
CAPTURE_PATTERN = b'OggS'
|
|
819
|
+
|
|
820
|
+
class HeaderType(enum.IntFlag):
|
|
821
|
+
CONTINUED = 0x01
|
|
822
|
+
FIRST = 0x02
|
|
823
|
+
LAST = 0x04
|
|
824
|
+
|
|
825
|
+
def __init__(self, read: Callable[[int], Awaitable[bytes]]) -> None:
|
|
826
|
+
self.read = read
|
|
827
|
+
|
|
828
|
+
@property
|
|
829
|
+
def packets(self) -> AsyncGenerator[OpusPacket, None]:
|
|
830
|
+
async def generate_frames() -> AsyncGenerator[OpusPacket, None]:
|
|
831
|
+
packet = b''
|
|
832
|
+
packet_count = 0
|
|
833
|
+
expected_bitstream_serial_number = None
|
|
834
|
+
expected_page_sequence_number = 0
|
|
835
|
+
channel_mode = OpusPacket.ChannelMode.STEREO
|
|
836
|
+
|
|
837
|
+
while True:
|
|
838
|
+
# Parse the page header
|
|
839
|
+
header = await self.read(27)
|
|
840
|
+
if len(header) != 27:
|
|
841
|
+
logger.debug("end of stream")
|
|
842
|
+
break
|
|
843
|
+
|
|
844
|
+
capture_pattern = header[:4]
|
|
845
|
+
if capture_pattern != self.CAPTURE_PATTERN:
|
|
846
|
+
print(capture_pattern.hex())
|
|
847
|
+
raise ValueError("invalid capture pattern at start of page")
|
|
848
|
+
|
|
849
|
+
version = header[4]
|
|
850
|
+
if version != 0:
|
|
851
|
+
raise ValueError(f"version {version} not supported")
|
|
852
|
+
|
|
853
|
+
header_type = self.HeaderType(header[5])
|
|
854
|
+
(
|
|
855
|
+
granule_position,
|
|
856
|
+
bitstream_serial_number,
|
|
857
|
+
page_sequence_number,
|
|
858
|
+
crc_checksum,
|
|
859
|
+
page_segments,
|
|
860
|
+
) = struct.unpack_from("<QIIIB", header, 6)
|
|
861
|
+
segment_table = await self.read(page_segments)
|
|
862
|
+
|
|
863
|
+
if header_type & self.HeaderType.FIRST:
|
|
864
|
+
if expected_bitstream_serial_number is None:
|
|
865
|
+
# We will only accept pages for the first encountered stream
|
|
866
|
+
logger.debug("BOS")
|
|
867
|
+
expected_bitstream_serial_number = bitstream_serial_number
|
|
868
|
+
expected_page_sequence_number = page_sequence_number
|
|
869
|
+
|
|
870
|
+
if (
|
|
871
|
+
expected_bitstream_serial_number is None
|
|
872
|
+
or expected_bitstream_serial_number != bitstream_serial_number
|
|
873
|
+
):
|
|
874
|
+
logger.debug("skipping page (not the first logical bitstream)")
|
|
875
|
+
for lacing_value in segment_table:
|
|
876
|
+
if lacing_value:
|
|
877
|
+
await self.read(lacing_value)
|
|
878
|
+
continue
|
|
879
|
+
|
|
880
|
+
if expected_page_sequence_number != page_sequence_number:
|
|
881
|
+
raise ValueError(
|
|
882
|
+
f"expected page sequence number {expected_page_sequence_number}"
|
|
883
|
+
f" but got {page_sequence_number}"
|
|
884
|
+
)
|
|
885
|
+
expected_page_sequence_number = page_sequence_number + 1
|
|
886
|
+
|
|
887
|
+
# Assemble the page
|
|
888
|
+
if not header_type & self.HeaderType.CONTINUED:
|
|
889
|
+
packet = b''
|
|
890
|
+
for lacing_value in segment_table:
|
|
891
|
+
if lacing_value:
|
|
892
|
+
packet += await self.read(lacing_value)
|
|
893
|
+
if lacing_value < 255:
|
|
894
|
+
# End of packet
|
|
895
|
+
packet_count += 1
|
|
896
|
+
|
|
897
|
+
if packet_count == 1:
|
|
898
|
+
# The first packet contains the identification header
|
|
899
|
+
logger.debug("first packet (header)")
|
|
900
|
+
if packet[:8] != b"OpusHead":
|
|
901
|
+
raise ValueError("first packet is not OpusHead")
|
|
902
|
+
packet_count = (
|
|
903
|
+
OpusPacket.ChannelMode.MONO
|
|
904
|
+
if packet[9] == 1
|
|
905
|
+
else OpusPacket.ChannelMode.STEREO
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
elif packet_count == 2:
|
|
909
|
+
# The second packet contains the comment header
|
|
910
|
+
logger.debug("second packet (tags)")
|
|
911
|
+
if packet[:8] != b"OpusTags":
|
|
912
|
+
logger.warning("second packet is not OpusTags")
|
|
913
|
+
else:
|
|
914
|
+
yield OpusPacket(channel_mode, 20, 48000, packet)
|
|
915
|
+
|
|
916
|
+
packet = b''
|
|
917
|
+
|
|
918
|
+
if header_type & self.HeaderType.LAST:
|
|
919
|
+
logger.debug("EOS")
|
|
920
|
+
|
|
921
|
+
return generate_frames()
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
# -----------------------------------------------------------------------------
|
|
925
|
+
class OpusPacketSource:
|
|
926
|
+
def __init__(self, read: Callable[[int], Awaitable[bytes]], mtu: int) -> None:
|
|
927
|
+
self.read = read
|
|
928
|
+
self.mtu = mtu
|
|
929
|
+
|
|
930
|
+
@property
|
|
931
|
+
def packets(self):
|
|
932
|
+
async def generate_packets():
|
|
933
|
+
sequence_number = 0
|
|
934
|
+
elapsed_ms = 0
|
|
935
|
+
|
|
936
|
+
opus_parser = OpusParser(self.read)
|
|
937
|
+
async for opus_packet in opus_parser.packets:
|
|
938
|
+
# We only support sending one Opus frame per RTP packet
|
|
939
|
+
# TODO: check the spec for the first byte value here
|
|
940
|
+
opus_payload = bytes([1]) + opus_packet.payload
|
|
941
|
+
elapsed_s = elapsed_ms / 1000
|
|
942
|
+
timestamp = int(elapsed_s * opus_packet.sampling_frequency)
|
|
943
|
+
rtp_packet = MediaPacket(
|
|
944
|
+
2, 0, 0, 0, sequence_number, timestamp, 0, [], 96, opus_payload
|
|
945
|
+
)
|
|
946
|
+
rtp_packet.timestamp_seconds = elapsed_s
|
|
947
|
+
yield rtp_packet
|
|
948
|
+
|
|
949
|
+
# Prepare for next packets
|
|
950
|
+
sequence_number += 1
|
|
951
|
+
sequence_number &= 0xFFFF
|
|
952
|
+
elapsed_ms += opus_packet.duration
|
|
953
|
+
|
|
954
|
+
return generate_packets()
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
# -----------------------------------------------------------------------------
|
|
958
|
+
# This map should be left at the end of the file so it can refer to the classes
|
|
959
|
+
# above
|
|
960
|
+
# -----------------------------------------------------------------------------
|
|
961
|
+
A2DP_VENDOR_MEDIA_CODEC_INFORMATION_CLASSES = {
|
|
962
|
+
OpusMediaCodecInformation.VENDOR_ID: {
|
|
963
|
+
OpusMediaCodecInformation.CODEC_ID: OpusMediaCodecInformation
|
|
964
|
+
}
|
|
965
|
+
}
|