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.
- bumble/_version.py +9 -4
- bumble/apps/auracast.py +631 -98
- bumble/apps/bench.py +238 -157
- bumble/apps/console.py +19 -12
- bumble/apps/controller_info.py +23 -7
- bumble/apps/device_info.py +50 -4
- bumble/apps/gg_bridge.py +1 -1
- bumble/apps/lea_unicast/app.py +61 -201
- bumble/att.py +51 -37
- bumble/audio/__init__.py +17 -0
- bumble/audio/io.py +553 -0
- bumble/controller.py +24 -9
- bumble/core.py +305 -156
- bumble/device.py +1090 -99
- bumble/gatt.py +36 -226
- bumble/gatt_adapters.py +374 -0
- bumble/gatt_client.py +52 -33
- bumble/gatt_server.py +5 -5
- bumble/hci.py +812 -14
- bumble/host.py +367 -65
- bumble/l2cap.py +3 -16
- bumble/pairing.py +5 -5
- bumble/pandora/host.py +7 -12
- bumble/profiles/aics.py +48 -57
- bumble/profiles/ascs.py +8 -19
- bumble/profiles/asha.py +16 -14
- bumble/profiles/bass.py +16 -22
- bumble/profiles/battery_service.py +13 -3
- bumble/profiles/device_information_service.py +16 -14
- bumble/profiles/gap.py +12 -8
- bumble/profiles/gatt_service.py +167 -0
- bumble/profiles/gmap.py +198 -0
- bumble/profiles/hap.py +8 -6
- bumble/profiles/heart_rate_service.py +20 -4
- bumble/profiles/le_audio.py +87 -4
- bumble/profiles/mcp.py +11 -9
- bumble/profiles/pacs.py +61 -16
- bumble/profiles/tmap.py +8 -12
- bumble/profiles/{vcp.py → vcs.py} +35 -29
- bumble/profiles/vocs.py +62 -85
- bumble/sdp.py +223 -93
- bumble/smp.py +1 -1
- bumble/utils.py +12 -2
- bumble/vendor/android/hci.py +1 -1
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/METADATA +13 -11
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/RECORD +50 -46
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/WHEEL +1 -1
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/entry_points.txt +1 -0
- bumble/apps/lea_unicast/liblc3.wasm +0 -0
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/LICENSE +0 -0
- {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
|
|
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
|
|
342
|
+
if self.connection_phy is not None:
|
|
336
343
|
phy_state = (
|
|
337
|
-
f' RX={le_phy_name(
|
|
338
|
-
f'TX={le_phy_name(
|
|
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
|
|
842
|
-
f'TX={HCI_Constant.le_phy_name(phy
|
|
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.
|
|
1081
|
-
|
|
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(
|
bumble/apps/controller_info.py
CHANGED
|
@@ -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('
|
|
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
|
|
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(
|
|
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.
|
|
170
|
-
f'packets of size {response.return_parameters.
|
|
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
|
|
278
|
-
await
|
|
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)
|
bumble/apps/device_info.py
CHANGED
|
@@ -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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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,
|
bumble/apps/lea_unicast/app.py
CHANGED
|
@@ -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
|
|
29
|
+
import wave
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
import
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
+
cis_link: CisLink,
|
|
258
114
|
) -> None:
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
541
|
-
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
codec_config.frame_duration.us,
|
|
549
|
-
codec_config.
|
|
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', '
|
|
448
|
+
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
|
589
449
|
speaker()
|
|
590
450
|
|
|
591
451
|
|