bumble 0.0.204__py3-none-any.whl → 0.0.208__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 (51) hide show
  1. bumble/_version.py +9 -4
  2. bumble/apps/auracast.py +631 -98
  3. bumble/apps/bench.py +238 -157
  4. bumble/apps/console.py +19 -12
  5. bumble/apps/controller_info.py +23 -7
  6. bumble/apps/device_info.py +50 -4
  7. bumble/apps/gg_bridge.py +1 -1
  8. bumble/apps/lea_unicast/app.py +61 -201
  9. bumble/att.py +51 -37
  10. bumble/audio/__init__.py +17 -0
  11. bumble/audio/io.py +553 -0
  12. bumble/controller.py +24 -9
  13. bumble/core.py +305 -156
  14. bumble/device.py +1090 -99
  15. bumble/gatt.py +36 -226
  16. bumble/gatt_adapters.py +374 -0
  17. bumble/gatt_client.py +52 -33
  18. bumble/gatt_server.py +5 -5
  19. bumble/hci.py +812 -14
  20. bumble/host.py +367 -65
  21. bumble/l2cap.py +3 -16
  22. bumble/pairing.py +5 -5
  23. bumble/pandora/host.py +7 -12
  24. bumble/profiles/aics.py +48 -57
  25. bumble/profiles/ascs.py +8 -19
  26. bumble/profiles/asha.py +16 -14
  27. bumble/profiles/bass.py +16 -22
  28. bumble/profiles/battery_service.py +13 -3
  29. bumble/profiles/device_information_service.py +16 -14
  30. bumble/profiles/gap.py +12 -8
  31. bumble/profiles/gatt_service.py +167 -0
  32. bumble/profiles/gmap.py +198 -0
  33. bumble/profiles/hap.py +8 -6
  34. bumble/profiles/heart_rate_service.py +20 -4
  35. bumble/profiles/le_audio.py +87 -4
  36. bumble/profiles/mcp.py +11 -9
  37. bumble/profiles/pacs.py +61 -16
  38. bumble/profiles/tmap.py +8 -12
  39. bumble/profiles/{vcp.py → vcs.py} +35 -29
  40. bumble/profiles/vocs.py +62 -85
  41. bumble/sdp.py +223 -93
  42. bumble/smp.py +1 -1
  43. bumble/utils.py +12 -2
  44. bumble/vendor/android/hci.py +1 -1
  45. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/METADATA +13 -11
  46. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/RECORD +50 -46
  47. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/WHEEL +1 -1
  48. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/entry_points.txt +1 -0
  49. bumble/apps/lea_unicast/liblc3.wasm +0 -0
  50. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/LICENSE +0 -0
  51. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/top_level.txt +0 -0
bumble/apps/console.py CHANGED
@@ -22,7 +22,6 @@
22
22
  import asyncio
23
23
  import logging
24
24
  import os
25
- import random
26
25
  import re
27
26
  import humanize
28
27
  from typing import Optional, Union
@@ -57,7 +56,13 @@ from bumble import __version__
57
56
  import bumble.core
58
57
  from bumble import colors
59
58
  from bumble.core import UUID, AdvertisingData, BT_LE_TRANSPORT
60
- from bumble.device import ConnectionParametersPreferences, Device, Connection, Peer
59
+ from bumble.device import (
60
+ ConnectionParametersPreferences,
61
+ ConnectionPHY,
62
+ Device,
63
+ Connection,
64
+ Peer,
65
+ )
61
66
  from bumble.utils import AsyncRunner
62
67
  from bumble.transport import open_transport_or_link
63
68
  from bumble.gatt import Characteristic, Service, CharacteristicDeclaration, Descriptor
@@ -125,6 +130,7 @@ def parse_phys(phys):
125
130
  # -----------------------------------------------------------------------------
126
131
  class ConsoleApp:
127
132
  connected_peer: Optional[Peer]
133
+ connection_phy: Optional[ConnectionPHY]
128
134
 
129
135
  def __init__(self):
130
136
  self.known_addresses = set()
@@ -132,6 +138,7 @@ class ConsoleApp:
132
138
  self.known_local_attributes = []
133
139
  self.device = None
134
140
  self.connected_peer = None
141
+ self.connection_phy = None
135
142
  self.top_tab = 'device'
136
143
  self.monitor_rssi = False
137
144
  self.connection_rssi = None
@@ -332,10 +339,10 @@ class ConsoleApp:
332
339
  f'{connection.parameters.peripheral_latency}/'
333
340
  f'{connection.parameters.supervision_timeout}'
334
341
  )
335
- if connection.transport == BT_LE_TRANSPORT:
342
+ if self.connection_phy is not None:
336
343
  phy_state = (
337
- f' RX={le_phy_name(connection.phy.rx_phy)}/'
338
- f'TX={le_phy_name(connection.phy.tx_phy)}'
344
+ f' RX={le_phy_name(self.connection_phy.rx_phy)}/'
345
+ f'TX={le_phy_name(self.connection_phy.tx_phy)}'
339
346
  )
340
347
  else:
341
348
  phy_state = ''
@@ -654,11 +661,12 @@ class ConsoleApp:
654
661
  self.append_to_output('connecting...')
655
662
 
656
663
  try:
657
- await self.device.connect(
664
+ connection = await self.device.connect(
658
665
  params[0],
659
666
  connection_parameters_preferences=connection_parameters_preferences,
660
667
  timeout=DEFAULT_CONNECTION_TIMEOUT,
661
668
  )
669
+ self.connection_phy = await connection.get_phy()
662
670
  self.top_tab = 'services'
663
671
  except bumble.core.TimeoutError:
664
672
  self.show_error('connection timed out')
@@ -838,8 +846,8 @@ class ConsoleApp:
838
846
 
839
847
  phy = await self.connected_peer.connection.get_phy()
840
848
  self.append_to_output(
841
- f'PHY: RX={HCI_Constant.le_phy_name(phy[0])}, '
842
- f'TX={HCI_Constant.le_phy_name(phy[1])}'
849
+ f'PHY: RX={HCI_Constant.le_phy_name(phy.rx_phy)}, '
850
+ f'TX={HCI_Constant.le_phy_name(phy.tx_phy)}'
843
851
  )
844
852
 
845
853
  async def do_request_mtu(self, params):
@@ -1076,10 +1084,9 @@ class DeviceListener(Device.Listener, Connection.Listener):
1076
1084
  f'{self.app.connected_peer.connection.parameters}'
1077
1085
  )
1078
1086
 
1079
- def on_connection_phy_update(self):
1080
- self.app.append_to_output(
1081
- f'connection phy update: {self.app.connected_peer.connection.phy}'
1082
- )
1087
+ def on_connection_phy_update(self, phy):
1088
+ self.app.connection_phy = phy
1089
+ self.app.append_to_output(f'connection phy update: {phy}')
1083
1090
 
1084
1091
  def on_connection_att_mtu_update(self):
1085
1092
  self.app.append_to_output(
@@ -37,6 +37,8 @@ from bumble.hci import (
37
37
  HCI_Command_Status_Event,
38
38
  HCI_READ_BUFFER_SIZE_COMMAND,
39
39
  HCI_Read_Buffer_Size_Command,
40
+ HCI_LE_READ_BUFFER_SIZE_V2_COMMAND,
41
+ HCI_LE_Read_Buffer_Size_V2_Command,
40
42
  HCI_READ_BD_ADDR_COMMAND,
41
43
  HCI_Read_BD_ADDR_Command,
42
44
  HCI_READ_LOCAL_NAME_COMMAND,
@@ -75,7 +77,7 @@ async def get_classic_info(host: Host) -> None:
75
77
  if command_succeeded(response):
76
78
  print()
77
79
  print(
78
- color('Classic Address:', 'yellow'),
80
+ color('Public Address:', 'yellow'),
79
81
  response.return_parameters.bd_addr.to_string(False),
80
82
  )
81
83
 
@@ -147,7 +149,7 @@ async def get_le_info(host: Host) -> None:
147
149
 
148
150
 
149
151
  # -----------------------------------------------------------------------------
150
- async def get_acl_flow_control_info(host: Host) -> None:
152
+ async def get_flow_control_info(host: Host) -> None:
151
153
  print()
152
154
 
153
155
  if host.supports_command(HCI_READ_BUFFER_SIZE_COMMAND):
@@ -160,14 +162,28 @@ async def get_acl_flow_control_info(host: Host) -> None:
160
162
  f'packets of size {response.return_parameters.hc_acl_data_packet_length}',
161
163
  )
162
164
 
163
- if host.supports_command(HCI_LE_READ_BUFFER_SIZE_COMMAND):
165
+ if host.supports_command(HCI_LE_READ_BUFFER_SIZE_V2_COMMAND):
166
+ response = await host.send_command(
167
+ HCI_LE_Read_Buffer_Size_V2_Command(), check_result=True
168
+ )
169
+ print(
170
+ color('LE ACL Flow Control:', 'yellow'),
171
+ f'{response.return_parameters.total_num_le_acl_data_packets} '
172
+ f'packets of size {response.return_parameters.le_acl_data_packet_length}',
173
+ )
174
+ print(
175
+ color('LE ISO Flow Control:', 'yellow'),
176
+ f'{response.return_parameters.total_num_iso_data_packets} '
177
+ f'packets of size {response.return_parameters.iso_data_packet_length}',
178
+ )
179
+ elif host.supports_command(HCI_LE_READ_BUFFER_SIZE_COMMAND):
164
180
  response = await host.send_command(
165
181
  HCI_LE_Read_Buffer_Size_Command(), check_result=True
166
182
  )
167
183
  print(
168
184
  color('LE ACL Flow Control:', 'yellow'),
169
- f'{response.return_parameters.hc_total_num_le_acl_data_packets} '
170
- f'packets of size {response.return_parameters.hc_le_acl_data_packet_length}',
185
+ f'{response.return_parameters.total_num_le_acl_data_packets} '
186
+ f'packets of size {response.return_parameters.le_acl_data_packet_length}',
171
187
  )
172
188
 
173
189
 
@@ -274,8 +290,8 @@ async def async_main(latency_probes, transport):
274
290
  # Get the LE info
275
291
  await get_le_info(host)
276
292
 
277
- # Print the ACL flow control info
278
- await get_acl_flow_control_info(host)
293
+ # Print the flow control info
294
+ await get_flow_control_info(host)
279
295
 
280
296
  # Get codec info
281
297
  await get_codecs_info(host)
@@ -29,7 +29,9 @@ from bumble.gatt import Service
29
29
  from bumble.profiles.device_information_service import DeviceInformationServiceProxy
30
30
  from bumble.profiles.battery_service import BatteryServiceProxy
31
31
  from bumble.profiles.gap import GenericAccessServiceProxy
32
+ from bumble.profiles.pacs import PublishedAudioCapabilitiesServiceProxy
32
33
  from bumble.profiles.tmap import TelephonyAndMediaAudioServiceProxy
34
+ from bumble.profiles.vcs import VolumeControlServiceProxy
33
35
  from bumble.transport import open_transport_or_link
34
36
 
35
37
 
@@ -126,14 +128,52 @@ async def show_tmas(
126
128
  print(color('### Telephony And Media Audio Service', 'yellow'))
127
129
 
128
130
  if tmas.role:
129
- print(
130
- color(' Role:', 'green'),
131
- await tmas.role.read_value(),
132
- )
131
+ role = await tmas.role.read_value()
132
+ print(color(' Role:', 'green'), role)
133
+
134
+ print()
135
+
136
+
137
+ # -----------------------------------------------------------------------------
138
+ async def show_pacs(pacs: PublishedAudioCapabilitiesServiceProxy) -> None:
139
+ print(color('### Published Audio Capabilities Service', 'yellow'))
140
+
141
+ contexts = await pacs.available_audio_contexts.read_value()
142
+ print(color(' Available Audio Contexts:', 'green'), contexts)
143
+
144
+ contexts = await pacs.supported_audio_contexts.read_value()
145
+ print(color(' Supported Audio Contexts:', 'green'), contexts)
146
+
147
+ if pacs.sink_pac:
148
+ pac = await pacs.sink_pac.read_value()
149
+ print(color(' Sink PAC: ', 'green'), pac)
150
+
151
+ if pacs.sink_audio_locations:
152
+ audio_locations = await pacs.sink_audio_locations.read_value()
153
+ print(color(' Sink Audio Locations: ', 'green'), audio_locations)
154
+
155
+ if pacs.source_pac:
156
+ pac = await pacs.source_pac.read_value()
157
+ print(color(' Source PAC: ', 'green'), pac)
158
+
159
+ if pacs.source_audio_locations:
160
+ audio_locations = await pacs.source_audio_locations.read_value()
161
+ print(color(' Source Audio Locations: ', 'green'), audio_locations)
133
162
 
134
163
  print()
135
164
 
136
165
 
166
+ # -----------------------------------------------------------------------------
167
+ async def show_vcs(vcs: VolumeControlServiceProxy) -> None:
168
+ print(color('### Volume Control Service', 'yellow'))
169
+
170
+ volume_state = await vcs.volume_state.read_value()
171
+ print(color(' Volume State:', 'green'), volume_state)
172
+
173
+ volume_flags = await vcs.volume_flags.read_value()
174
+ print(color(' Volume Flags:', 'green'), volume_flags)
175
+
176
+
137
177
  # -----------------------------------------------------------------------------
138
178
  async def show_device_info(peer, done: Optional[asyncio.Future]) -> None:
139
179
  try:
@@ -161,6 +201,12 @@ async def show_device_info(peer, done: Optional[asyncio.Future]) -> None:
161
201
  if tmas := peer.create_service_proxy(TelephonyAndMediaAudioServiceProxy):
162
202
  await try_show(show_tmas, tmas)
163
203
 
204
+ if pacs := peer.create_service_proxy(PublishedAudioCapabilitiesServiceProxy):
205
+ await try_show(show_pacs, pacs)
206
+
207
+ if vcs := peer.create_service_proxy(VolumeControlServiceProxy):
208
+ await try_show(show_vcs, vcs)
209
+
164
210
  if done is not None:
165
211
  done.set_result(None)
166
212
  except asyncio.CancelledError:
bumble/apps/gg_bridge.py CHANGED
@@ -234,7 +234,7 @@ class GattlinkNodeBridge(GattlinkL2capEndpoint, Device.Listener):
234
234
  Characteristic.WRITEABLE,
235
235
  CharacteristicValue(write=self.on_rx_write),
236
236
  )
237
- self.tx_characteristic = Characteristic(
237
+ self.tx_characteristic: Characteristic[bytes] = Characteristic(
238
238
  GG_GATTLINK_TX_CHARACTERISTIC_UUID,
239
239
  Characteristic.Properties.NOTIFY,
240
240
  Characteristic.READABLE,
@@ -16,23 +16,22 @@
16
16
  # Imports
17
17
  # -----------------------------------------------------------------------------
18
18
  from __future__ import annotations
19
+
19
20
  import asyncio
20
21
  import datetime
21
- import enum
22
22
  import functools
23
23
  from importlib import resources
24
24
  import json
25
25
  import os
26
26
  import logging
27
27
  import pathlib
28
- from typing import Optional, List, cast
29
28
  import weakref
30
- import struct
29
+ import wave
31
30
 
32
- import ctypes
33
- import wasmtime
34
- import wasmtime.loader
35
- import liblc3 # type: ignore
31
+ try:
32
+ import lc3 # type: ignore # pylint: disable=E0401
33
+ except ImportError as e:
34
+ raise ImportError("Try `python -m pip install \".[lc3]\"`.") from e
36
35
 
37
36
  import click
38
37
  import aiohttp.web
@@ -40,11 +39,12 @@ import aiohttp.web
40
39
  import bumble
41
40
  from bumble.core import AdvertisingData
42
41
  from bumble.colors import color
43
- from bumble.device import Device, DeviceConfiguration, AdvertisingParameters
42
+ from bumble.device import Device, DeviceConfiguration, AdvertisingParameters, CisLink
44
43
  from bumble.transport import open_transport
45
44
  from bumble.profiles import ascs, bap, pacs
46
45
  from bumble.hci import Address, CodecID, CodingFormat, HCI_IsoDataPacket
47
46
 
47
+
48
48
  # -----------------------------------------------------------------------------
49
49
  # Logging
50
50
  # -----------------------------------------------------------------------------
@@ -54,6 +54,7 @@ logger = logging.getLogger(__name__)
54
54
  # Constants
55
55
  # -----------------------------------------------------------------------------
56
56
  DEFAULT_UI_PORT = 7654
57
+ DEFAULT_PCM_BYTES_PER_SAMPLE = 2
57
58
 
58
59
 
59
60
  def _sink_pac_record() -> pacs.PacRecord:
@@ -100,153 +101,8 @@ def _source_pac_record() -> pacs.PacRecord:
100
101
  )
101
102
 
102
103
 
103
- # -----------------------------------------------------------------------------
104
- # WASM - liblc3
105
- # -----------------------------------------------------------------------------
106
- store = wasmtime.loader.store
107
- _memory = cast(wasmtime.Memory, liblc3.memory)
108
- STACK_POINTER = _memory.data_len(store)
109
- _memory.grow(store, 1)
110
- # Mapping wasmtime memory to linear address
111
- memory = (ctypes.c_ubyte * _memory.data_len(store)).from_address(
112
- ctypes.addressof(_memory.data_ptr(store).contents) # type: ignore
113
- )
114
-
115
-
116
- class Liblc3PcmFormat(enum.IntEnum):
117
- S16 = 0
118
- S24 = 1
119
- S24_3LE = 2
120
- FLOAT = 3
121
-
122
-
123
- MAX_DECODER_SIZE = liblc3.lc3_decoder_size(10000, 48000)
124
- MAX_ENCODER_SIZE = liblc3.lc3_encoder_size(10000, 48000)
125
-
126
- DECODER_STACK_POINTER = STACK_POINTER
127
- ENCODER_STACK_POINTER = DECODER_STACK_POINTER + MAX_DECODER_SIZE * 2
128
- DECODE_BUFFER_STACK_POINTER = ENCODER_STACK_POINTER + MAX_ENCODER_SIZE * 2
129
- ENCODE_BUFFER_STACK_POINTER = DECODE_BUFFER_STACK_POINTER + 8192
130
- DEFAULT_PCM_SAMPLE_RATE = 48000
131
- DEFAULT_PCM_FORMAT = Liblc3PcmFormat.S16
132
- DEFAULT_PCM_BYTES_PER_SAMPLE = 2
133
-
134
-
135
- encoders: List[int] = []
136
- decoders: List[int] = []
137
-
138
-
139
- def setup_encoders(
140
- sample_rate_hz: int, frame_duration_us: int, num_channels: int
141
- ) -> None:
142
- logger.info(
143
- f"setup_encoders {sample_rate_hz}Hz {frame_duration_us}us {num_channels}channels"
144
- )
145
- encoders[:num_channels] = [
146
- liblc3.lc3_setup_encoder(
147
- frame_duration_us,
148
- sample_rate_hz,
149
- DEFAULT_PCM_SAMPLE_RATE, # Input sample rate
150
- ENCODER_STACK_POINTER + MAX_ENCODER_SIZE * i,
151
- )
152
- for i in range(num_channels)
153
- ]
154
-
155
-
156
- def setup_decoders(
157
- sample_rate_hz: int, frame_duration_us: int, num_channels: int
158
- ) -> None:
159
- logger.info(
160
- f"setup_decoders {sample_rate_hz}Hz {frame_duration_us}us {num_channels}channels"
161
- )
162
- decoders[:num_channels] = [
163
- liblc3.lc3_setup_decoder(
164
- frame_duration_us,
165
- sample_rate_hz,
166
- DEFAULT_PCM_SAMPLE_RATE, # Output sample rate
167
- DECODER_STACK_POINTER + MAX_DECODER_SIZE * i,
168
- )
169
- for i in range(num_channels)
170
- ]
171
-
172
-
173
- def decode(
174
- frame_duration_us: int,
175
- num_channels: int,
176
- input_bytes: bytes,
177
- ) -> bytes:
178
- if not input_bytes:
179
- return b''
180
-
181
- input_buffer_offset = DECODE_BUFFER_STACK_POINTER
182
- input_buffer_size = len(input_bytes)
183
- input_bytes_per_frame = input_buffer_size // num_channels
184
-
185
- # Copy into wasm
186
- memory[input_buffer_offset : input_buffer_offset + input_buffer_size] = input_bytes # type: ignore
187
-
188
- output_buffer_offset = input_buffer_offset + input_buffer_size
189
- output_buffer_size = (
190
- liblc3.lc3_frame_samples(frame_duration_us, DEFAULT_PCM_SAMPLE_RATE)
191
- * DEFAULT_PCM_BYTES_PER_SAMPLE
192
- * num_channels
193
- )
194
-
195
- for i in range(num_channels):
196
- res = liblc3.lc3_decode(
197
- decoders[i],
198
- input_buffer_offset + input_bytes_per_frame * i,
199
- input_bytes_per_frame,
200
- DEFAULT_PCM_FORMAT,
201
- output_buffer_offset + i * DEFAULT_PCM_BYTES_PER_SAMPLE,
202
- num_channels, # Stride
203
- )
204
-
205
- if res != 0:
206
- logging.error(f"Parsing failed, res={res}")
207
-
208
- # Extract decoded data from the output buffer
209
- return bytes(
210
- memory[output_buffer_offset : output_buffer_offset + output_buffer_size]
211
- )
212
-
213
-
214
- def encode(
215
- sdu_length: int,
216
- num_channels: int,
217
- stride: int,
218
- input_bytes: bytes,
219
- ) -> bytes:
220
- if not input_bytes:
221
- return b''
222
-
223
- input_buffer_offset = ENCODE_BUFFER_STACK_POINTER
224
- input_buffer_size = len(input_bytes)
225
-
226
- # Copy into wasm
227
- memory[input_buffer_offset : input_buffer_offset + input_buffer_size] = input_bytes # type: ignore
228
-
229
- output_buffer_offset = input_buffer_offset + input_buffer_size
230
- output_buffer_size = sdu_length
231
- output_frame_size = output_buffer_size // num_channels
232
-
233
- for i in range(num_channels):
234
- res = liblc3.lc3_encode(
235
- encoders[i],
236
- DEFAULT_PCM_FORMAT,
237
- input_buffer_offset + DEFAULT_PCM_BYTES_PER_SAMPLE * i,
238
- stride,
239
- output_frame_size,
240
- output_buffer_offset + output_frame_size * i,
241
- )
242
-
243
- if res != 0:
244
- logging.error(f"Parsing failed, res={res}")
245
-
246
- # Extract decoded data from the output buffer
247
- return bytes(
248
- memory[output_buffer_offset : output_buffer_offset + output_buffer_size]
249
- )
104
+ decoder: lc3.Decoder | None = None
105
+ encoding_config: bap.CodecSpecificConfiguration | None = None
250
106
 
251
107
 
252
108
  async def lc3_source_task(
@@ -254,44 +110,49 @@ async def lc3_source_task(
254
110
  sdu_length: int,
255
111
  frame_duration_us: int,
256
112
  device: Device,
257
- cis_handle: int,
113
+ cis_link: CisLink,
258
114
  ) -> None:
259
- with open(filename, 'rb') as f:
260
- header = f.read(44)
261
- assert header[8:12] == b'WAVE'
262
-
263
- pcm_num_channel, pcm_sample_rate, _byte_rate, _block_align, bits_per_sample = (
264
- struct.unpack("<HIIHH", header[22:36])
265
- )
266
- assert pcm_sample_rate == DEFAULT_PCM_SAMPLE_RATE
267
- assert bits_per_sample == DEFAULT_PCM_BYTES_PER_SAMPLE * 8
115
+ logger.info(
116
+ "lc3_source_task filename=%s, sdu_length=%d, frame_duration=%.1f",
117
+ filename,
118
+ sdu_length,
119
+ frame_duration_us / 1000,
120
+ )
121
+ with wave.open(filename, 'rb') as wav:
122
+ bits_per_sample = wav.getsampwidth() * 8
268
123
 
269
- frame_bytes = (
270
- liblc3.lc3_frame_samples(frame_duration_us, DEFAULT_PCM_SAMPLE_RATE)
271
- * DEFAULT_PCM_BYTES_PER_SAMPLE
272
- )
273
- packet_sequence_number = 0
124
+ encoder: lc3.Encoder | None = None
274
125
 
275
126
  while True:
276
127
  next_round = datetime.datetime.now() + datetime.timedelta(
277
128
  microseconds=frame_duration_us
278
129
  )
279
- pcm_data = f.read(frame_bytes)
280
- sdu = encode(sdu_length, pcm_num_channel, pcm_num_channel, pcm_data)
281
-
282
- iso_packet = HCI_IsoDataPacket(
283
- connection_handle=cis_handle,
284
- data_total_length=sdu_length + 4,
285
- packet_sequence_number=packet_sequence_number,
286
- pb_flag=0b10,
287
- packet_status_flag=0,
288
- iso_sdu_length=sdu_length,
289
- iso_sdu_fragment=sdu,
290
- )
291
- device.host.send_hci_packet(iso_packet)
292
- packet_sequence_number += 1
130
+ if not encoder:
131
+ if (
132
+ encoding_config
133
+ and (frame_duration := encoding_config.frame_duration)
134
+ and (sampling_frequency := encoding_config.sampling_frequency)
135
+ and (
136
+ audio_channel_allocation := encoding_config.audio_channel_allocation
137
+ )
138
+ ):
139
+ logger.info("Use %s", encoding_config)
140
+ encoder = lc3.Encoder(
141
+ frame_duration_us=frame_duration.us,
142
+ sample_rate_hz=sampling_frequency.hz,
143
+ num_channels=audio_channel_allocation.channel_count,
144
+ input_sample_rate_hz=wav.getframerate(),
145
+ )
146
+ else:
147
+ sdu = encoder.encode(
148
+ pcm=wav.readframes(encoder.get_frame_samples()),
149
+ num_bytes=sdu_length,
150
+ bit_depth=bits_per_sample,
151
+ )
152
+ cis_link.write(sdu)
153
+
293
154
  sleep_time = next_round - datetime.datetime.now()
294
- await asyncio.sleep(sleep_time.total_seconds())
155
+ await asyncio.sleep(sleep_time.total_seconds() * 0.9)
295
156
 
296
157
 
297
158
  # -----------------------------------------------------------------------------
@@ -410,7 +271,7 @@ class Speaker:
410
271
 
411
272
  def __init__(
412
273
  self,
413
- device_config_path: Optional[str],
274
+ device_config_path: str | None,
414
275
  ui_port: int,
415
276
  transport: str,
416
277
  lc3_input_file_path: str,
@@ -437,6 +298,7 @@ class Speaker:
437
298
  advertising_interval_min=25,
438
299
  advertising_interval_max=25,
439
300
  address=Address('F1:F2:F3:F4:F5:F6'),
301
+ identity_address_type=Address.RANDOM_DEVICE_ADDRESS,
440
302
  )
441
303
 
442
304
  device_config.le_enabled = True
@@ -490,12 +352,12 @@ class Speaker:
490
352
  not isinstance(codec_config, bap.CodecSpecificConfiguration)
491
353
  or codec_config.frame_duration is None
492
354
  or codec_config.audio_channel_allocation is None
355
+ or decoder is None
356
+ or not pdu.iso_sdu_fragment
493
357
  ):
494
358
  return
495
- pcm = decode(
496
- codec_config.frame_duration.us,
497
- codec_config.audio_channel_allocation.channel_count,
498
- pdu.iso_sdu_fragment,
359
+ pcm = decoder.decode(
360
+ pdu.iso_sdu_fragment, bit_depth=DEFAULT_PCM_BYTES_PER_SAMPLE * 8
499
361
  )
500
362
  self.device.abort_on('disconnection', self.ui_server.send_audio(pcm))
501
363
 
@@ -521,7 +383,7 @@ class Speaker:
521
383
  ),
522
384
  frame_duration_us=codec_config.frame_duration.us,
523
385
  device=self.device,
524
- cis_handle=ase.cis_link.handle,
386
+ cis_link=ase.cis_link,
525
387
  ),
526
388
  )
527
389
  else:
@@ -537,16 +399,14 @@ class Speaker:
537
399
  ):
538
400
  return
539
401
  if ase.role == ascs.AudioRole.SOURCE:
540
- setup_encoders(
541
- codec_config.sampling_frequency.hz,
542
- codec_config.frame_duration.us,
543
- codec_config.audio_channel_allocation.channel_count,
544
- )
402
+ global encoding_config
403
+ encoding_config = codec_config
545
404
  else:
546
- setup_decoders(
547
- codec_config.sampling_frequency.hz,
548
- codec_config.frame_duration.us,
549
- codec_config.audio_channel_allocation.channel_count,
405
+ global decoder
406
+ decoder = lc3.Decoder(
407
+ frame_duration_us=codec_config.frame_duration.us,
408
+ sample_rate_hz=codec_config.sampling_frequency.hz,
409
+ num_channels=codec_config.audio_channel_allocation.channel_count,
550
410
  )
551
411
 
552
412
  for ase in ascs_service.ase_state_machines.values():
@@ -585,7 +445,7 @@ def speaker(ui_port: int, device_config: str, transport: str, lc3_file: str) ->
585
445
 
586
446
  # -----------------------------------------------------------------------------
587
447
  def main():
588
- logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
448
+ logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
589
449
  speaker()
590
450
 
591
451