bumble 0.0.198__py3-none-any.whl → 0.0.200__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,608 @@
1
+ # Copyright 2024 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # -----------------------------------------------------------------------------
16
+ # Imports
17
+ # -----------------------------------------------------------------------------
18
+ from __future__ import annotations
19
+ import asyncio
20
+ import asyncio.subprocess
21
+ import os
22
+ import logging
23
+ from typing import Optional, Union
24
+
25
+ import click
26
+
27
+ from bumble.a2dp import (
28
+ make_audio_source_service_sdp_records,
29
+ A2DP_SBC_CODEC_TYPE,
30
+ A2DP_MPEG_2_4_AAC_CODEC_TYPE,
31
+ A2DP_NON_A2DP_CODEC_TYPE,
32
+ AacFrame,
33
+ AacParser,
34
+ AacPacketSource,
35
+ AacMediaCodecInformation,
36
+ SbcFrame,
37
+ SbcParser,
38
+ SbcPacketSource,
39
+ SbcMediaCodecInformation,
40
+ OpusPacket,
41
+ OpusParser,
42
+ OpusPacketSource,
43
+ OpusMediaCodecInformation,
44
+ )
45
+ from bumble.avrcp import Protocol as AvrcpProtocol
46
+ from bumble.avdtp import (
47
+ find_avdtp_service_with_connection,
48
+ AVDTP_AUDIO_MEDIA_TYPE,
49
+ AVDTP_DELAY_REPORTING_SERVICE_CATEGORY,
50
+ MediaCodecCapabilities,
51
+ MediaPacketPump,
52
+ Protocol as AvdtpProtocol,
53
+ )
54
+ from bumble.colors import color
55
+ from bumble.core import (
56
+ AdvertisingData,
57
+ ConnectionError as BumbleConnectionError,
58
+ DeviceClass,
59
+ BT_BR_EDR_TRANSPORT,
60
+ )
61
+ from bumble.device import Connection, Device, DeviceConfiguration
62
+ from bumble.hci import Address, HCI_CONNECTION_ALREADY_EXISTS_ERROR, HCI_Constant
63
+ from bumble.pairing import PairingConfig
64
+ from bumble.transport import open_transport
65
+ from bumble.utils import AsyncRunner
66
+
67
+
68
+ # -----------------------------------------------------------------------------
69
+ # Logging
70
+ # -----------------------------------------------------------------------------
71
+ logger = logging.getLogger(__name__)
72
+
73
+
74
+ # -----------------------------------------------------------------------------
75
+ def a2dp_source_sdp_records():
76
+ service_record_handle = 0x00010001
77
+ return {
78
+ service_record_handle: make_audio_source_service_sdp_records(
79
+ service_record_handle
80
+ )
81
+ }
82
+
83
+
84
+ # -----------------------------------------------------------------------------
85
+ async def sbc_codec_capabilities(read_function) -> MediaCodecCapabilities:
86
+ sbc_parser = SbcParser(read_function)
87
+ sbc_frame: SbcFrame
88
+ async for sbc_frame in sbc_parser.frames:
89
+ # We only need the first frame
90
+ print(color(f"SBC format: {sbc_frame}", "cyan"))
91
+ break
92
+
93
+ channel_mode = [
94
+ SbcMediaCodecInformation.ChannelMode.MONO,
95
+ SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL,
96
+ SbcMediaCodecInformation.ChannelMode.STEREO,
97
+ SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
98
+ ][sbc_frame.channel_mode]
99
+ block_length = {
100
+ 4: SbcMediaCodecInformation.BlockLength.BL_4,
101
+ 8: SbcMediaCodecInformation.BlockLength.BL_8,
102
+ 12: SbcMediaCodecInformation.BlockLength.BL_12,
103
+ 16: SbcMediaCodecInformation.BlockLength.BL_16,
104
+ }[sbc_frame.block_count]
105
+ subbands = {
106
+ 4: SbcMediaCodecInformation.Subbands.S_4,
107
+ 8: SbcMediaCodecInformation.Subbands.S_8,
108
+ }[sbc_frame.subband_count]
109
+ allocation_method = [
110
+ SbcMediaCodecInformation.AllocationMethod.LOUDNESS,
111
+ SbcMediaCodecInformation.AllocationMethod.SNR,
112
+ ][sbc_frame.allocation_method]
113
+ return MediaCodecCapabilities(
114
+ media_type=AVDTP_AUDIO_MEDIA_TYPE,
115
+ media_codec_type=A2DP_SBC_CODEC_TYPE,
116
+ media_codec_information=SbcMediaCodecInformation(
117
+ sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.from_int(
118
+ sbc_frame.sampling_frequency
119
+ ),
120
+ channel_mode=channel_mode,
121
+ block_length=block_length,
122
+ subbands=subbands,
123
+ allocation_method=allocation_method,
124
+ minimum_bitpool_value=2,
125
+ maximum_bitpool_value=40,
126
+ ),
127
+ )
128
+
129
+
130
+ # -----------------------------------------------------------------------------
131
+ async def aac_codec_capabilities(read_function) -> MediaCodecCapabilities:
132
+ aac_parser = AacParser(read_function)
133
+ aac_frame: AacFrame
134
+ async for aac_frame in aac_parser.frames:
135
+ # We only need the first frame
136
+ print(color(f"AAC format: {aac_frame}", "cyan"))
137
+ break
138
+
139
+ sampling_frequency = AacMediaCodecInformation.SamplingFrequency.from_int(
140
+ aac_frame.sampling_frequency
141
+ )
142
+ channels = (
143
+ AacMediaCodecInformation.Channels.MONO
144
+ if aac_frame.channel_configuration == 1
145
+ else AacMediaCodecInformation.Channels.STEREO
146
+ )
147
+
148
+ return MediaCodecCapabilities(
149
+ media_type=AVDTP_AUDIO_MEDIA_TYPE,
150
+ media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE,
151
+ media_codec_information=AacMediaCodecInformation(
152
+ object_type=AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC,
153
+ sampling_frequency=sampling_frequency,
154
+ channels=channels,
155
+ vbr=1,
156
+ bitrate=128000,
157
+ ),
158
+ )
159
+
160
+
161
+ # -----------------------------------------------------------------------------
162
+ async def opus_codec_capabilities(read_function) -> MediaCodecCapabilities:
163
+ opus_parser = OpusParser(read_function)
164
+ opus_packet: OpusPacket
165
+ async for opus_packet in opus_parser.packets:
166
+ # We only need the first packet
167
+ print(color(f"Opus format: {opus_packet}", "cyan"))
168
+ break
169
+
170
+ if opus_packet.channel_mode == OpusPacket.ChannelMode.MONO:
171
+ channel_mode = OpusMediaCodecInformation.ChannelMode.MONO
172
+ elif opus_packet.channel_mode == OpusPacket.ChannelMode.STEREO:
173
+ channel_mode = OpusMediaCodecInformation.ChannelMode.STEREO
174
+ else:
175
+ channel_mode = OpusMediaCodecInformation.ChannelMode.DUAL_MONO
176
+
177
+ if opus_packet.duration == 10:
178
+ frame_size = OpusMediaCodecInformation.FrameSize.FS_10MS
179
+ else:
180
+ frame_size = OpusMediaCodecInformation.FrameSize.FS_20MS
181
+
182
+ return MediaCodecCapabilities(
183
+ media_type=AVDTP_AUDIO_MEDIA_TYPE,
184
+ media_codec_type=A2DP_NON_A2DP_CODEC_TYPE,
185
+ media_codec_information=OpusMediaCodecInformation(
186
+ channel_mode=channel_mode,
187
+ sampling_frequency=OpusMediaCodecInformation.SamplingFrequency.SF_48000,
188
+ frame_size=frame_size,
189
+ ),
190
+ )
191
+
192
+
193
+ # -----------------------------------------------------------------------------
194
+ class Player:
195
+ def __init__(
196
+ self,
197
+ transport: str,
198
+ device_config: Optional[str],
199
+ authenticate: bool,
200
+ encrypt: bool,
201
+ ) -> None:
202
+ self.transport = transport
203
+ self.device_config = device_config
204
+ self.authenticate = authenticate
205
+ self.encrypt = encrypt
206
+ self.avrcp_protocol: Optional[AvrcpProtocol] = None
207
+ self.done: Optional[asyncio.Event]
208
+
209
+ async def run(self, workload) -> None:
210
+ self.done = asyncio.Event()
211
+ try:
212
+ await self._run(workload)
213
+ except Exception as error:
214
+ print(color(f"!!! ERROR: {error}", "red"))
215
+
216
+ async def _run(self, workload) -> None:
217
+ async with await open_transport(self.transport) as (hci_source, hci_sink):
218
+ # Create a device
219
+ device_config = DeviceConfiguration()
220
+ if self.device_config:
221
+ device_config.load_from_file(self.device_config)
222
+ else:
223
+ device_config.name = "Bumble Player"
224
+ device_config.class_of_device = DeviceClass.pack_class_of_device(
225
+ DeviceClass.AUDIO_SERVICE_CLASS,
226
+ DeviceClass.AUDIO_VIDEO_MAJOR_DEVICE_CLASS,
227
+ DeviceClass.AUDIO_VIDEO_UNCATEGORIZED_MINOR_DEVICE_CLASS,
228
+ )
229
+ device_config.keystore = "JsonKeyStore"
230
+
231
+ device_config.classic_enabled = True
232
+ device_config.le_enabled = False
233
+ device_config.le_simultaneous_enabled = False
234
+ device_config.classic_sc_enabled = False
235
+ device_config.classic_smp_enabled = False
236
+ device = Device.from_config_with_hci(device_config, hci_source, hci_sink)
237
+
238
+ # Setup the SDP records to expose the SRC service
239
+ device.sdp_service_records = a2dp_source_sdp_records()
240
+
241
+ # Setup AVRCP
242
+ self.avrcp_protocol = AvrcpProtocol()
243
+ self.avrcp_protocol.listen(device)
244
+
245
+ # Don't require MITM when pairing.
246
+ device.pairing_config_factory = lambda connection: PairingConfig(mitm=False)
247
+
248
+ # Start the controller
249
+ await device.power_on()
250
+
251
+ # Print some of the config/properties
252
+ print(
253
+ "Player Bluetooth Address:",
254
+ color(
255
+ device.public_address.to_string(with_type_qualifier=False),
256
+ "yellow",
257
+ ),
258
+ )
259
+
260
+ # Listen for connections
261
+ device.on("connection", self.on_bluetooth_connection)
262
+
263
+ # Run the workload
264
+ try:
265
+ await workload(device)
266
+ except BumbleConnectionError as error:
267
+ if error.error_code == HCI_CONNECTION_ALREADY_EXISTS_ERROR:
268
+ print(color("Connection already established", "blue"))
269
+ else:
270
+ print(color(f"Failed to connect: {error}", "red"))
271
+
272
+ # Wait until it is time to exit
273
+ assert self.done is not None
274
+ await asyncio.wait(
275
+ [hci_source.terminated, asyncio.ensure_future(self.done.wait())],
276
+ return_when=asyncio.FIRST_COMPLETED,
277
+ )
278
+
279
+ def on_bluetooth_connection(self, connection: Connection) -> None:
280
+ print(color(f"--- Connected: {connection}", "cyan"))
281
+ connection.on("disconnection", self.on_bluetooth_disconnection)
282
+
283
+ def on_bluetooth_disconnection(self, reason) -> None:
284
+ print(color(f"--- Disconnected: {HCI_Constant.error_name(reason)}", "cyan"))
285
+ self.set_done()
286
+
287
+ async def connect(self, device: Device, address: str) -> Connection:
288
+ print(color(f"Connecting to {address}...", "green"))
289
+ connection = await device.connect(address, transport=BT_BR_EDR_TRANSPORT)
290
+
291
+ # Request authentication
292
+ if self.authenticate:
293
+ print(color("*** Authenticating...", "blue"))
294
+ await connection.authenticate()
295
+ print(color("*** Authenticated", "blue"))
296
+
297
+ # Enable encryption
298
+ if self.encrypt:
299
+ print(color("*** Enabling encryption...", "blue"))
300
+ await connection.encrypt()
301
+ print(color("*** Encryption on", "blue"))
302
+
303
+ return connection
304
+
305
+ async def create_avdtp_protocol(self, connection: Connection) -> AvdtpProtocol:
306
+ # Look for an A2DP service
307
+ avdtp_version = await find_avdtp_service_with_connection(connection)
308
+ if not avdtp_version:
309
+ raise RuntimeError("no A2DP service found")
310
+
311
+ print(color(f"AVDTP Version: {avdtp_version}"))
312
+
313
+ # Create a client to interact with the remote device
314
+ return await AvdtpProtocol.connect(connection, avdtp_version)
315
+
316
+ async def stream_packets(
317
+ self,
318
+ protocol: AvdtpProtocol,
319
+ codec_type: int,
320
+ vendor_id: int,
321
+ codec_id: int,
322
+ packet_source: Union[SbcPacketSource, AacPacketSource, OpusPacketSource],
323
+ codec_capabilities: MediaCodecCapabilities,
324
+ ):
325
+ # Discover all endpoints on the remote device
326
+ endpoints = await protocol.discover_remote_endpoints()
327
+ for endpoint in endpoints:
328
+ print('@@@', endpoint)
329
+
330
+ # Select a sink
331
+ sink = protocol.find_remote_sink_by_codec(
332
+ AVDTP_AUDIO_MEDIA_TYPE, codec_type, vendor_id, codec_id
333
+ )
334
+ if sink is None:
335
+ print(color('!!! no compatible sink found', 'red'))
336
+ return
337
+ print(f'### Selected sink: {sink.seid}')
338
+
339
+ # Check if the sink supports delay reporting
340
+ delay_reporting = False
341
+ for capability in sink.capabilities:
342
+ if capability.service_category == AVDTP_DELAY_REPORTING_SERVICE_CATEGORY:
343
+ delay_reporting = True
344
+ break
345
+
346
+ def on_delay_report(delay: int):
347
+ print(color(f"*** DELAY REPORT: {delay}", "blue"))
348
+
349
+ # Adjust the codec capabilities for certain codecs
350
+ for capability in sink.capabilities:
351
+ if isinstance(capability, MediaCodecCapabilities):
352
+ if isinstance(
353
+ codec_capabilities.media_codec_information, SbcMediaCodecInformation
354
+ ) and isinstance(
355
+ capability.media_codec_information, SbcMediaCodecInformation
356
+ ):
357
+ codec_capabilities.media_codec_information.minimum_bitpool_value = (
358
+ capability.media_codec_information.minimum_bitpool_value
359
+ )
360
+ codec_capabilities.media_codec_information.maximum_bitpool_value = (
361
+ capability.media_codec_information.maximum_bitpool_value
362
+ )
363
+ print(color("Source media codec:", "green"), codec_capabilities)
364
+
365
+ # Stream the packets
366
+ packet_pump = MediaPacketPump(packet_source.packets)
367
+ source = protocol.add_source(codec_capabilities, packet_pump, delay_reporting)
368
+ source.on("delay_report", on_delay_report)
369
+ stream = await protocol.create_stream(source, sink)
370
+ await stream.start()
371
+
372
+ await packet_pump.wait_for_completion()
373
+
374
+ async def discover(self, device: Device) -> None:
375
+ @device.listens_to("inquiry_result")
376
+ def on_inquiry_result(
377
+ address: Address, class_of_device: int, data: AdvertisingData, rssi: int
378
+ ) -> None:
379
+ (
380
+ service_classes,
381
+ major_device_class,
382
+ minor_device_class,
383
+ ) = DeviceClass.split_class_of_device(class_of_device)
384
+ separator = "\n "
385
+ print(f">>> {color(address.to_string(False), 'yellow')}:")
386
+ print(f" Device Class (raw): {class_of_device:06X}")
387
+ major_class_name = DeviceClass.major_device_class_name(major_device_class)
388
+ print(" Device Major Class: " f"{major_class_name}")
389
+ minor_class_name = DeviceClass.minor_device_class_name(
390
+ major_device_class, minor_device_class
391
+ )
392
+ print(" Device Minor Class: " f"{minor_class_name}")
393
+ print(
394
+ " Device Services: "
395
+ f"{', '.join(DeviceClass.service_class_labels(service_classes))}"
396
+ )
397
+ print(f" RSSI: {rssi}")
398
+ if data.ad_structures:
399
+ print(f" {data.to_string(separator)}")
400
+
401
+ await device.start_discovery()
402
+
403
+ async def pair(self, device: Device, address: str) -> None:
404
+ print(color(f"Connecting to {address}...", "green"))
405
+ connection = await device.connect(address, transport=BT_BR_EDR_TRANSPORT)
406
+
407
+ print(color("Pairing...", "magenta"))
408
+ await connection.authenticate()
409
+ print(color("Pairing completed", "magenta"))
410
+ self.set_done()
411
+
412
+ async def inquire(self, device: Device, address: str) -> None:
413
+ connection = await self.connect(device, address)
414
+ avdtp_protocol = await self.create_avdtp_protocol(connection)
415
+
416
+ # Discover the remote endpoints
417
+ endpoints = await avdtp_protocol.discover_remote_endpoints()
418
+ print(f'@@@ Found {len(list(endpoints))} endpoints')
419
+ for endpoint in endpoints:
420
+ print('@@@', endpoint)
421
+
422
+ self.set_done()
423
+
424
+ async def play(
425
+ self,
426
+ device: Device,
427
+ address: Optional[str],
428
+ audio_format: str,
429
+ audio_file: str,
430
+ ) -> None:
431
+ if audio_format == "auto":
432
+ if audio_file.endswith(".sbc"):
433
+ audio_format = "sbc"
434
+ elif audio_file.endswith(".aac") or audio_file.endswith(".adts"):
435
+ audio_format = "aac"
436
+ elif audio_file.endswith(".ogg"):
437
+ audio_format = "opus"
438
+ else:
439
+ raise ValueError("Unable to determine audio format from file extension")
440
+
441
+ device.on(
442
+ "connection",
443
+ lambda connection: AsyncRunner.spawn(on_connection(connection)),
444
+ )
445
+
446
+ async def on_connection(connection: Connection):
447
+ avdtp_protocol = await self.create_avdtp_protocol(connection)
448
+
449
+ with open(audio_file, 'rb') as input_file:
450
+ # NOTE: this should be using asyncio file reading, but blocking reads
451
+ # are good enough for this command line app.
452
+ async def read_audio_data(byte_count):
453
+ return input_file.read(byte_count)
454
+
455
+ # Obtain the codec capabilities from the stream
456
+ packet_source: Union[SbcPacketSource, AacPacketSource, OpusPacketSource]
457
+ vendor_id = 0
458
+ codec_id = 0
459
+ if audio_format == "sbc":
460
+ codec_type = A2DP_SBC_CODEC_TYPE
461
+ codec_capabilities = await sbc_codec_capabilities(read_audio_data)
462
+ packet_source = SbcPacketSource(
463
+ read_audio_data,
464
+ avdtp_protocol.l2cap_channel.peer_mtu,
465
+ )
466
+ elif audio_format == "aac":
467
+ codec_type = A2DP_MPEG_2_4_AAC_CODEC_TYPE
468
+ codec_capabilities = await aac_codec_capabilities(read_audio_data)
469
+ packet_source = AacPacketSource(
470
+ read_audio_data,
471
+ avdtp_protocol.l2cap_channel.peer_mtu,
472
+ )
473
+ else:
474
+ codec_type = A2DP_NON_A2DP_CODEC_TYPE
475
+ vendor_id = OpusMediaCodecInformation.VENDOR_ID
476
+ codec_id = OpusMediaCodecInformation.CODEC_ID
477
+ codec_capabilities = await opus_codec_capabilities(read_audio_data)
478
+ packet_source = OpusPacketSource(
479
+ read_audio_data,
480
+ avdtp_protocol.l2cap_channel.peer_mtu,
481
+ )
482
+
483
+ # Rewind to the start
484
+ input_file.seek(0)
485
+
486
+ try:
487
+ await self.stream_packets(
488
+ avdtp_protocol,
489
+ codec_type,
490
+ vendor_id,
491
+ codec_id,
492
+ packet_source,
493
+ codec_capabilities,
494
+ )
495
+ except Exception as error:
496
+ print(color(f"!!! Error while streaming: {error}", "red"))
497
+
498
+ self.set_done()
499
+
500
+ if address:
501
+ await self.connect(device, address)
502
+ else:
503
+ print(color("Waiting for an incoming connection...", "magenta"))
504
+
505
+ def set_done(self) -> None:
506
+ if self.done:
507
+ self.done.set()
508
+
509
+
510
+ # -----------------------------------------------------------------------------
511
+ def create_player(context) -> Player:
512
+ return Player(
513
+ transport=context.obj["hci_transport"],
514
+ device_config=context.obj["device_config"],
515
+ authenticate=context.obj["authenticate"],
516
+ encrypt=context.obj["encrypt"],
517
+ )
518
+
519
+
520
+ # -----------------------------------------------------------------------------
521
+ @click.group()
522
+ @click.pass_context
523
+ @click.option("--hci-transport", metavar="TRANSPORT", required=True)
524
+ @click.option("--device-config", metavar="FILENAME", help="Device configuration file")
525
+ @click.option(
526
+ "--authenticate",
527
+ is_flag=True,
528
+ help="Request authentication when connecting",
529
+ default=False,
530
+ )
531
+ @click.option(
532
+ "--encrypt", is_flag=True, help="Request encryption when connecting", default=True
533
+ )
534
+ def player_cli(ctx, hci_transport, device_config, authenticate, encrypt):
535
+ ctx.ensure_object(dict)
536
+ ctx.obj["hci_transport"] = hci_transport
537
+ ctx.obj["device_config"] = device_config
538
+ ctx.obj["authenticate"] = authenticate
539
+ ctx.obj["encrypt"] = encrypt
540
+
541
+
542
+ @player_cli.command("discover")
543
+ @click.pass_context
544
+ def discover(context):
545
+ """Discover speakers or headphones"""
546
+ player = create_player(context)
547
+ asyncio.run(player.run(player.discover))
548
+
549
+
550
+ @player_cli.command("inquire")
551
+ @click.pass_context
552
+ @click.argument(
553
+ "address",
554
+ metavar="ADDRESS",
555
+ )
556
+ def inquire(context, address):
557
+ """Connect to a speaker or headphone and inquire about their capabilities"""
558
+ player = create_player(context)
559
+ asyncio.run(player.run(lambda device: player.inquire(device, address)))
560
+
561
+
562
+ @player_cli.command("pair")
563
+ @click.pass_context
564
+ @click.argument(
565
+ "address",
566
+ metavar="ADDRESS",
567
+ )
568
+ def pair(context, address):
569
+ """Pair with a speaker or headphone"""
570
+ player = create_player(context)
571
+ asyncio.run(player.run(lambda device: player.pair(device, address)))
572
+
573
+
574
+ @player_cli.command("play")
575
+ @click.pass_context
576
+ @click.option(
577
+ "--connect",
578
+ "address",
579
+ metavar="ADDRESS",
580
+ help="Address or name to connect to",
581
+ )
582
+ @click.option(
583
+ "-f",
584
+ "--audio-format",
585
+ type=click.Choice(["auto", "sbc", "aac", "opus"]),
586
+ help="Audio file format (use 'auto' to infer the format from the file extension)",
587
+ default="auto",
588
+ )
589
+ @click.argument("audio_file")
590
+ def play(context, address, audio_format, audio_file):
591
+ """Play and audio file"""
592
+ player = create_player(context)
593
+ asyncio.run(
594
+ player.run(
595
+ lambda device: player.play(device, address, audio_format, audio_file)
596
+ )
597
+ )
598
+
599
+
600
+ # -----------------------------------------------------------------------------
601
+ def main():
602
+ logging.basicConfig(level=os.environ.get("BUMBLE_LOGLEVEL", "WARNING").upper())
603
+ player_cli()
604
+
605
+
606
+ # -----------------------------------------------------------------------------
607
+ if __name__ == "__main__":
608
+ main() # pylint: disable=no-value-for-parameter
@@ -44,25 +44,18 @@ from bumble.avdtp import (
44
44
  AVDTP_AUDIO_MEDIA_TYPE,
45
45
  Listener,
46
46
  MediaCodecCapabilities,
47
- MediaPacket,
48
47
  Protocol,
49
48
  )
50
49
  from bumble.a2dp import (
51
- MPEG_2_AAC_LC_OBJECT_TYPE,
52
50
  make_audio_sink_service_sdp_records,
53
51
  A2DP_SBC_CODEC_TYPE,
54
52
  A2DP_MPEG_2_4_AAC_CODEC_TYPE,
55
- SBC_MONO_CHANNEL_MODE,
56
- SBC_DUAL_CHANNEL_MODE,
57
- SBC_SNR_ALLOCATION_METHOD,
58
- SBC_LOUDNESS_ALLOCATION_METHOD,
59
- SBC_STEREO_CHANNEL_MODE,
60
- SBC_JOINT_STEREO_CHANNEL_MODE,
61
53
  SbcMediaCodecInformation,
62
54
  AacMediaCodecInformation,
63
55
  )
64
56
  from bumble.utils import AsyncRunner
65
57
  from bumble.codecs import AacAudioRtpPacket
58
+ from bumble.rtp import MediaPacket
66
59
 
67
60
 
68
61
  # -----------------------------------------------------------------------------
@@ -93,7 +86,7 @@ class AudioExtractor:
93
86
  # -----------------------------------------------------------------------------
94
87
  class AacAudioExtractor:
95
88
  def extract_audio(self, packet: MediaPacket) -> bytes:
96
- return AacAudioRtpPacket(packet.payload).to_adts()
89
+ return AacAudioRtpPacket.from_bytes(packet.payload).to_adts()
97
90
 
98
91
 
99
92
  # -----------------------------------------------------------------------------
@@ -451,10 +444,12 @@ class Speaker:
451
444
  return MediaCodecCapabilities(
452
445
  media_type=AVDTP_AUDIO_MEDIA_TYPE,
453
446
  media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE,
454
- media_codec_information=AacMediaCodecInformation.from_lists(
455
- object_types=[MPEG_2_AAC_LC_OBJECT_TYPE],
456
- sampling_frequencies=[48000, 44100],
457
- channels=[1, 2],
447
+ media_codec_information=AacMediaCodecInformation(
448
+ object_type=AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC,
449
+ sampling_frequency=AacMediaCodecInformation.SamplingFrequency.SF_48000
450
+ | AacMediaCodecInformation.SamplingFrequency.SF_44100,
451
+ channels=AacMediaCodecInformation.Channels.MONO
452
+ | AacMediaCodecInformation.Channels.STEREO,
458
453
  vbr=1,
459
454
  bitrate=256000,
460
455
  ),
@@ -464,20 +459,23 @@ class Speaker:
464
459
  return MediaCodecCapabilities(
465
460
  media_type=AVDTP_AUDIO_MEDIA_TYPE,
466
461
  media_codec_type=A2DP_SBC_CODEC_TYPE,
467
- media_codec_information=SbcMediaCodecInformation.from_lists(
468
- sampling_frequencies=[48000, 44100, 32000, 16000],
469
- channel_modes=[
470
- SBC_MONO_CHANNEL_MODE,
471
- SBC_DUAL_CHANNEL_MODE,
472
- SBC_STEREO_CHANNEL_MODE,
473
- SBC_JOINT_STEREO_CHANNEL_MODE,
474
- ],
475
- block_lengths=[4, 8, 12, 16],
476
- subbands=[4, 8],
477
- allocation_methods=[
478
- SBC_LOUDNESS_ALLOCATION_METHOD,
479
- SBC_SNR_ALLOCATION_METHOD,
480
- ],
462
+ media_codec_information=SbcMediaCodecInformation(
463
+ sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_48000
464
+ | SbcMediaCodecInformation.SamplingFrequency.SF_44100
465
+ | SbcMediaCodecInformation.SamplingFrequency.SF_32000
466
+ | SbcMediaCodecInformation.SamplingFrequency.SF_16000,
467
+ channel_mode=SbcMediaCodecInformation.ChannelMode.MONO
468
+ | SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
469
+ | SbcMediaCodecInformation.ChannelMode.STEREO
470
+ | SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
471
+ block_length=SbcMediaCodecInformation.BlockLength.BL_4
472
+ | SbcMediaCodecInformation.BlockLength.BL_8
473
+ | SbcMediaCodecInformation.BlockLength.BL_12
474
+ | SbcMediaCodecInformation.BlockLength.BL_16,
475
+ subbands=SbcMediaCodecInformation.Subbands.S_4
476
+ | SbcMediaCodecInformation.Subbands.S_8,
477
+ allocation_method=SbcMediaCodecInformation.AllocationMethod.LOUDNESS
478
+ | SbcMediaCodecInformation.AllocationMethod.SNR,
481
479
  minimum_bitpool_value=2,
482
480
  maximum_bitpool_value=53,
483
481
  ),