1# Copyright 2021-2023 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# ----------------------------------------------------------------------------- 18from __future__ import annotations 19import enum 20import logging 21from typing import Dict, List 22 23from bumble.core import BT_BR_EDR_TRANSPORT, CommandTimeoutError 24from bumble.device import Device, DeviceConfiguration 25from bumble.pairing import PairingConfig 26from bumble.sdp import ServiceAttribute 27from bumble.avdtp import ( 28 AVDTP_AUDIO_MEDIA_TYPE, 29 Listener, 30 MediaCodecCapabilities, 31 MediaPacket, 32 Protocol, 33) 34from bumble.a2dp import ( 35 make_audio_sink_service_sdp_records, 36 MPEG_2_AAC_LC_OBJECT_TYPE, 37 A2DP_SBC_CODEC_TYPE, 38 A2DP_MPEG_2_4_AAC_CODEC_TYPE, 39 SBC_MONO_CHANNEL_MODE, 40 SBC_DUAL_CHANNEL_MODE, 41 SBC_SNR_ALLOCATION_METHOD, 42 SBC_LOUDNESS_ALLOCATION_METHOD, 43 SBC_STEREO_CHANNEL_MODE, 44 SBC_JOINT_STEREO_CHANNEL_MODE, 45 SbcMediaCodecInformation, 46 AacMediaCodecInformation, 47) 48from bumble.utils import AsyncRunner 49from bumble.codecs import AacAudioRtpPacket 50from bumble.hci import HCI_Reset_Command 51 52 53# ----------------------------------------------------------------------------- 54# Logging 55# ----------------------------------------------------------------------------- 56logger = logging.getLogger(__name__) 57 58 59# ----------------------------------------------------------------------------- 60class AudioExtractor: 61 @staticmethod 62 def create(codec: str): 63 if codec == 'aac': 64 return AacAudioExtractor() 65 if codec == 'sbc': 66 return SbcAudioExtractor() 67 68 def extract_audio(self, packet: MediaPacket) -> bytes: 69 raise NotImplementedError() 70 71 72# ----------------------------------------------------------------------------- 73class AacAudioExtractor: 74 def extract_audio(self, packet: MediaPacket) -> bytes: 75 return AacAudioRtpPacket(packet.payload).to_adts() 76 77 78# ----------------------------------------------------------------------------- 79class SbcAudioExtractor: 80 def extract_audio(self, packet: MediaPacket) -> bytes: 81 # header = packet.payload[0] 82 # fragmented = header >> 7 83 # start = (header >> 6) & 0x01 84 # last = (header >> 5) & 0x01 85 # number_of_frames = header & 0x0F 86 87 # TODO: support fragmented payloads 88 return packet.payload[1:] 89 90 91# ----------------------------------------------------------------------------- 92class Speaker: 93 class StreamState(enum.Enum): 94 IDLE = 0 95 STOPPED = 1 96 STARTED = 2 97 SUSPENDED = 3 98 99 def __init__(self, hci_source, hci_sink, codec): 100 self.hci_source = hci_source 101 self.hci_sink = hci_sink 102 self.js_listeners = {} 103 self.codec = codec 104 self.device = None 105 self.connection = None 106 self.avdtp_listener = None 107 self.packets_received = 0 108 self.bytes_received = 0 109 self.stream_state = Speaker.StreamState.IDLE 110 self.audio_extractor = AudioExtractor.create(codec) 111 112 def sdp_records(self) -> Dict[int, List[ServiceAttribute]]: 113 service_record_handle = 0x00010001 114 return { 115 service_record_handle: make_audio_sink_service_sdp_records( 116 service_record_handle 117 ) 118 } 119 120 def codec_capabilities(self) -> MediaCodecCapabilities: 121 if self.codec == 'aac': 122 return self.aac_codec_capabilities() 123 124 if self.codec == 'sbc': 125 return self.sbc_codec_capabilities() 126 127 raise RuntimeError('unsupported codec') 128 129 def aac_codec_capabilities(self) -> MediaCodecCapabilities: 130 return MediaCodecCapabilities( 131 media_type=AVDTP_AUDIO_MEDIA_TYPE, 132 media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE, 133 media_codec_information=AacMediaCodecInformation.from_lists( 134 object_types=[MPEG_2_AAC_LC_OBJECT_TYPE], 135 sampling_frequencies=[48000, 44100], 136 channels=[1, 2], 137 vbr=1, 138 bitrate=256000, 139 ), 140 ) 141 142 def sbc_codec_capabilities(self) -> MediaCodecCapabilities: 143 return MediaCodecCapabilities( 144 media_type=AVDTP_AUDIO_MEDIA_TYPE, 145 media_codec_type=A2DP_SBC_CODEC_TYPE, 146 media_codec_information=SbcMediaCodecInformation.from_lists( 147 sampling_frequencies=[48000, 44100, 32000, 16000], 148 channel_modes=[ 149 SBC_MONO_CHANNEL_MODE, 150 SBC_DUAL_CHANNEL_MODE, 151 SBC_STEREO_CHANNEL_MODE, 152 SBC_JOINT_STEREO_CHANNEL_MODE, 153 ], 154 block_lengths=[4, 8, 12, 16], 155 subbands=[4, 8], 156 allocation_methods=[ 157 SBC_LOUDNESS_ALLOCATION_METHOD, 158 SBC_SNR_ALLOCATION_METHOD, 159 ], 160 minimum_bitpool_value=2, 161 maximum_bitpool_value=53, 162 ), 163 ) 164 165 def on_key_store_update(self): 166 print("Key Store updated") 167 self.emit('key_store_update') 168 169 def on_bluetooth_connection(self, connection): 170 print(f'Connection: {connection}') 171 self.connection = connection 172 connection.on('disconnection', self.on_bluetooth_disconnection) 173 peer_name = '' if connection.peer_name is None else connection.peer_name 174 peer_address = connection.peer_address.to_string(False) 175 self.emit('connection', {'peer_name': peer_name, 'peer_address': peer_address}) 176 177 def on_bluetooth_disconnection(self, reason): 178 print(f'Disconnection ({reason})') 179 self.connection = None 180 self.emit('disconnection', None) 181 182 def on_avdtp_connection(self, protocol): 183 print('Audio Stream Open') 184 185 # Add a sink endpoint to the server 186 sink = protocol.add_sink(self.codec_capabilities()) 187 sink.on('start', self.on_sink_start) 188 sink.on('stop', self.on_sink_stop) 189 sink.on('suspend', self.on_sink_suspend) 190 sink.on('configuration', lambda: self.on_sink_configuration(sink.configuration)) 191 sink.on('rtp_packet', self.on_rtp_packet) 192 sink.on('rtp_channel_open', self.on_rtp_channel_open) 193 sink.on('rtp_channel_close', self.on_rtp_channel_close) 194 195 # Listen for close events 196 protocol.on('close', self.on_avdtp_close) 197 198 def on_avdtp_close(self): 199 print("Audio Stream Closed") 200 201 def on_sink_start(self): 202 print("Sink Started") 203 self.stream_state = self.StreamState.STARTED 204 self.emit('start', None) 205 206 def on_sink_stop(self): 207 print("Sink Stopped") 208 self.stream_state = self.StreamState.STOPPED 209 self.emit('stop', None) 210 211 def on_sink_suspend(self): 212 print("Sink Suspended") 213 self.stream_state = self.StreamState.SUSPENDED 214 self.emit('suspend', None) 215 216 def on_sink_configuration(self, config): 217 print("Sink Configuration:") 218 print('\n'.join([" " + str(capability) for capability in config])) 219 220 def on_rtp_channel_open(self): 221 print("RTP Channel Open") 222 223 def on_rtp_channel_close(self): 224 print("RTP Channel Closed") 225 self.stream_state = self.StreamState.IDLE 226 227 def on_rtp_packet(self, packet): 228 self.packets_received += 1 229 self.bytes_received += len(packet.payload) 230 self.emit("audio", self.audio_extractor.extract_audio(packet)) 231 232 async def connect(self, address): 233 # Connect to the source 234 print(f'=== Connecting to {address}...') 235 connection = await self.device.connect(address, transport=BT_BR_EDR_TRANSPORT) 236 print(f'=== Connected to {connection.peer_address}') 237 238 # Request authentication 239 print('*** Authenticating...') 240 await connection.authenticate() 241 print('*** Authenticated') 242 243 # Enable encryption 244 print('*** Enabling encryption...') 245 await connection.encrypt() 246 print('*** Encryption on') 247 248 protocol = await Protocol.connect(connection) 249 self.avdtp_listener.set_server(connection, protocol) 250 self.on_avdtp_connection(protocol) 251 252 async def discover_remote_endpoints(self, protocol): 253 endpoints = await protocol.discover_remote_endpoints() 254 print(f'@@@ Found {len(endpoints)} endpoints') 255 for endpoint in endpoints: 256 print('@@@', endpoint) 257 258 def on(self, event_name, listener): 259 self.js_listeners[event_name] = listener 260 261 def emit(self, event_name, event=None): 262 if listener := self.js_listeners.get(event_name): 263 listener(event) 264 265 async def run(self, connect_address): 266 # Create a device 267 device_config = DeviceConfiguration() 268 device_config.name = "Bumble Speaker" 269 device_config.class_of_device = 0x240414 270 device_config.keystore = "JsonKeyStore:/bumble/keystore.json" 271 device_config.classic_enabled = True 272 device_config.le_enabled = False 273 self.device = Device.from_config_with_hci( 274 device_config, self.hci_source, self.hci_sink 275 ) 276 277 # Setup the SDP to expose the sink service 278 self.device.sdp_service_records = self.sdp_records() 279 280 # Don't require MITM when pairing. 281 self.device.pairing_config_factory = lambda connection: PairingConfig( 282 mitm=False 283 ) 284 285 # Start the controller 286 await self.device.power_on() 287 288 # Listen for Bluetooth connections 289 self.device.on('connection', self.on_bluetooth_connection) 290 291 # Listen for changes to the key store 292 self.device.on('key_store_update', self.on_key_store_update) 293 294 # Create a listener to wait for AVDTP connections 295 self.avdtp_listener = Listener.for_device(self.device) 296 self.avdtp_listener.on('connection', self.on_avdtp_connection) 297 298 print(f'Speaker ready to play, codec={self.codec}') 299 300 if connect_address: 301 # Connect to the source 302 try: 303 await self.connect(connect_address) 304 except CommandTimeoutError: 305 print("Connection timed out") 306 return 307 else: 308 # We'll wait for a connection 309 print("Waiting for connection...") 310 311 async def start(self): 312 await self.run(None) 313 314 async def stop(self): 315 # TODO: replace this once a proper reset is implemented in the lib. 316 await self.device.host.send_command(HCI_Reset_Command()) 317 await self.device.power_off() 318 print('Speaker stopped') 319 320 321# ----------------------------------------------------------------------------- 322def main(hci_source, hci_sink): 323 return Speaker(hci_source, hci_sink, "aac") 324