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.
Files changed (48) hide show
  1. bumble/_version.py +2 -2
  2. bumble/a2dp.py +502 -202
  3. bumble/apps/controller_info.py +60 -0
  4. bumble/apps/player/player.py +608 -0
  5. bumble/apps/speaker/speaker.py +25 -27
  6. bumble/att.py +2 -2
  7. bumble/avc.py +1 -2
  8. bumble/avdtp.py +54 -97
  9. bumble/avrcp.py +48 -29
  10. bumble/codecs.py +214 -68
  11. bumble/device.py +19 -11
  12. bumble/hci.py +31 -5
  13. bumble/hfp.py +52 -48
  14. bumble/host.py +12 -0
  15. bumble/profiles/hap.py +27 -18
  16. bumble/rtp.py +110 -0
  17. bumble/transport/android_netsim.py +31 -11
  18. bumble/transport/grpc_protobuf/netsim/__init__.py +0 -0
  19. bumble/transport/grpc_protobuf/{common_pb2.py → netsim/common_pb2.py} +9 -8
  20. bumble/transport/grpc_protobuf/{common_pb2.pyi → netsim/common_pb2.pyi} +11 -5
  21. bumble/transport/grpc_protobuf/netsim/hci_packet_pb2.py +29 -0
  22. bumble/transport/grpc_protobuf/{hci_packet_pb2.pyi → netsim/hci_packet_pb2.pyi} +13 -7
  23. bumble/transport/grpc_protobuf/netsim/model_pb2.py +63 -0
  24. bumble/transport/grpc_protobuf/netsim/model_pb2.pyi +238 -0
  25. bumble/transport/grpc_protobuf/netsim/packet_streamer_pb2.py +32 -0
  26. bumble/transport/grpc_protobuf/{packet_streamer_pb2.pyi → netsim/packet_streamer_pb2.pyi} +6 -6
  27. bumble/transport/grpc_protobuf/{packet_streamer_pb2_grpc.py → netsim/packet_streamer_pb2_grpc.py} +7 -7
  28. bumble/transport/grpc_protobuf/netsim/startup_pb2.py +41 -0
  29. bumble/transport/grpc_protobuf/netsim/startup_pb2.pyi +76 -0
  30. bumble/transport/grpc_protobuf/netsim/startup_pb2_grpc.py +4 -0
  31. bumble/transport/grpc_protobuf/rootcanal/__init__.py +0 -0
  32. bumble/transport/grpc_protobuf/rootcanal/configuration_pb2.py +39 -0
  33. bumble/transport/grpc_protobuf/rootcanal/configuration_pb2.pyi +78 -0
  34. bumble/transport/grpc_protobuf/rootcanal/configuration_pb2_grpc.py +4 -0
  35. bumble/transport/pyusb.py +2 -1
  36. {bumble-0.0.199.dist-info → bumble-0.0.201.dist-info}/METADATA +2 -2
  37. {bumble-0.0.199.dist-info → bumble-0.0.201.dist-info}/RECORD +44 -34
  38. {bumble-0.0.199.dist-info → bumble-0.0.201.dist-info}/WHEEL +1 -1
  39. {bumble-0.0.199.dist-info → bumble-0.0.201.dist-info}/entry_points.txt +1 -0
  40. bumble/transport/grpc_protobuf/hci_packet_pb2.py +0 -28
  41. bumble/transport/grpc_protobuf/packet_streamer_pb2.py +0 -31
  42. bumble/transport/grpc_protobuf/startup_pb2.py +0 -32
  43. bumble/transport/grpc_protobuf/startup_pb2.pyi +0 -46
  44. /bumble/transport/grpc_protobuf/{common_pb2_grpc.py → netsim/common_pb2_grpc.py} +0 -0
  45. /bumble/transport/grpc_protobuf/{hci_packet_pb2_grpc.py → netsim/hci_packet_pb2_grpc.py} +0 -0
  46. /bumble/transport/grpc_protobuf/{startup_pb2_grpc.py → netsim/model_pb2_grpc.py} +0 -0
  47. {bumble-0.0.199.dist-info → bumble-0.0.201.dist-info}/LICENSE +0 -0
  48. {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 struct
22
+ import enum
22
23
  import logging
23
- from collections.abc import AsyncGenerator
24
- from typing import List, Callable, Awaitable
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: int
261
- channel_mode: int
262
- block_length: int
263
- subbands: int
264
- allocation_method: int
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
- SAMPLING_FREQUENCY_BITS = {16000: 1 << 3, 32000: 1 << 2, 44100: 1 << 1, 48000: 1}
269
- CHANNEL_MODE_BITS = {
270
- SBC_MONO_CHANNEL_MODE: 1 << 3,
271
- SBC_DUAL_CHANNEL_MODE: 1 << 2,
272
- SBC_STEREO_CHANNEL_MODE: 1 << 1,
273
- SBC_JOINT_STEREO_CHANNEL_MODE: 1,
274
- }
275
- BLOCK_LENGTH_BITS = {4: 1 << 3, 8: 1 << 2, 12: 1 << 1, 16: 1}
276
- SUBBANDS_BITS = {4: 1 << 1, 8: 1}
277
- ALLOCATION_METHOD_BITS = {
278
- SBC_SNR_ALLOCATION_METHOD: 1 << 1,
279
- SBC_LOUDNESS_ALLOCATION_METHOD: 1,
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
- @staticmethod
283
- def from_bytes(data: bytes) -> SbcMediaCodecInformation:
284
- sampling_frequency = (data[0] >> 4) & 0x0F
285
- channel_mode = (data[0] >> 0) & 0x0F
286
- block_length = (data[1] >> 4) & 0x0F
287
- subbands = (data[1] >> 2) & 0x03
288
- allocation_method = (data[1] >> 0) & 0x03
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 SbcMediaCodecInformation(
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: int
385
- sampling_frequency: int
386
- channels: int
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
- OBJECT_TYPE_BITS = {
392
- MPEG_2_AAC_LC_OBJECT_TYPE: 1 << 7,
393
- MPEG_4_AAC_LC_OBJECT_TYPE: 1 << 6,
394
- MPEG_4_AAC_LTP_OBJECT_TYPE: 1 << 5,
395
- MPEG_4_AAC_SCALABLE_OBJECT_TYPE: 1 << 4,
396
- }
397
- SAMPLING_FREQUENCY_BITS = {
398
- 8000: 1 << 11,
399
- 11025: 1 << 10,
400
- 12000: 1 << 9,
401
- 16000: 1 << 8,
402
- 22050: 1 << 7,
403
- 24000: 1 << 6,
404
- 32000: 1 << 5,
405
- 44100: 1 << 4,
406
- 48000: 1 << 3,
407
- 64000: 1 << 2,
408
- 88200: 1 << 1,
409
- 96000: 1,
410
- }
411
- CHANNELS_BITS = {1: 1 << 1, 2: 1}
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
- @staticmethod
414
- def from_bytes(data: bytes) -> AacMediaCodecInformation:
415
- object_type = data[0]
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 from_discrete_values(
427
- cls,
428
- object_type: int,
429
- sampling_frequency: int,
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
- @classmethod
444
- def from_lists(
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=sum(cls.OBJECT_TYPE_BITS[x] for x in object_types),
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, self.value)
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, blocks, channel_mode, subbands, payload
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
- timestamp = 0
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) == 16
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 = timestamp / frame.sampling_frequency
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
- timestamp += sum((frame.sample_count for frame in frames))
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
+ }