• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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