• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2021-2022 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# -----------------------------------------------------------------------------
18import asyncio
19import sys
20import os
21import logging
22
23from colors import color
24from bumble.device import Device
25from bumble.transport import open_transport_or_link
26from bumble.core import BT_BR_EDR_TRANSPORT
27from bumble.avdtp import (
28    AVDTP_AUDIO_MEDIA_TYPE,
29    Protocol,
30    Listener,
31    MediaCodecCapabilities
32)
33from bumble.a2dp import (
34    make_audio_sink_service_sdp_records,
35    A2DP_SBC_CODEC_TYPE,
36    SBC_MONO_CHANNEL_MODE,
37    SBC_DUAL_CHANNEL_MODE,
38    SBC_SNR_ALLOCATION_METHOD,
39    SBC_LOUDNESS_ALLOCATION_METHOD,
40    SBC_STEREO_CHANNEL_MODE,
41    SBC_JOINT_STEREO_CHANNEL_MODE,
42    SbcMediaCodecInformation
43)
44
45Context = {
46    'output': None
47}
48
49
50# -----------------------------------------------------------------------------
51def sdp_records():
52    service_record_handle = 0x00010001
53    return {
54        service_record_handle: make_audio_sink_service_sdp_records(service_record_handle)
55    }
56
57
58# -----------------------------------------------------------------------------
59def codec_capabilities():
60    # NOTE: this shouldn't be hardcoded, but passed on the command line instead
61    return MediaCodecCapabilities(
62        media_type                = AVDTP_AUDIO_MEDIA_TYPE,
63        media_codec_type          = A2DP_SBC_CODEC_TYPE,
64        media_codec_information   = SbcMediaCodecInformation.from_lists(
65            sampling_frequencies  = [48000, 44100, 32000, 16000],
66            channel_modes         = [
67                SBC_MONO_CHANNEL_MODE,
68                SBC_DUAL_CHANNEL_MODE,
69                SBC_STEREO_CHANNEL_MODE,
70                SBC_JOINT_STEREO_CHANNEL_MODE
71            ],
72            block_lengths         = [4, 8, 12, 16],
73            subbands              = [4, 8],
74            allocation_methods    = [SBC_LOUDNESS_ALLOCATION_METHOD, SBC_SNR_ALLOCATION_METHOD],
75            minimum_bitpool_value = 2,
76            maximum_bitpool_value = 53
77        )
78    )
79
80
81# -----------------------------------------------------------------------------
82def on_avdtp_connection(server):
83    # Add a sink endpoint to the server
84    sink = server.add_sink(codec_capabilities())
85    sink.on('rtp_packet', on_rtp_packet)
86
87
88# -----------------------------------------------------------------------------
89def on_rtp_packet(packet):
90    header           = packet.payload[0]
91    fragmented       = header >> 7
92    start            = (header >> 6) & 0x01
93    last             = (header >> 5) & 0x01
94    number_of_frames = header & 0x0F
95
96    if fragmented:
97        print(f'RTP: fragment {number_of_frames}')
98    else:
99        print(f'RTP: {number_of_frames} frames')
100
101    Context['output'].write(packet.payload[1:])
102
103
104# -----------------------------------------------------------------------------
105async def main():
106    if len(sys.argv) < 4:
107        print('Usage: run_a2dp_sink.py <device-config> <transport-spec> <sbc-file> [<bt-addr>]')
108        print('example: run_a2dp_sink.py classic1.json usb:0 output.sbc')
109        return
110
111    print('<<< connecting to HCI...')
112    async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
113        print('<<< connected')
114
115        with open(sys.argv[3], 'wb') as sbc_file:
116            Context['output'] = sbc_file
117
118            # Create a device
119            device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
120            device.classic_enabled = True
121
122            # Setup the SDP to expose the sink service
123            device.sdp_service_records = sdp_records()
124
125            # Start the controller
126            await device.power_on()
127
128            # Create a listener to wait for AVDTP connections
129            listener = Listener(Listener.create_registrar(device))
130            listener.on('connection', on_avdtp_connection)
131
132            if len(sys.argv) >= 5:
133                # Connect to the source
134                target_address = sys.argv[4]
135                print(f'=== Connecting to {target_address}...')
136                connection = await device.connect(target_address, transport=BT_BR_EDR_TRANSPORT)
137                print(f'=== Connected to {connection.peer_address}!')
138
139                # Request authentication
140                print('*** Authenticating...')
141                await connection.authenticate()
142                print('*** Authenticated')
143
144                # Enable encryption
145                print('*** Enabling encryption...')
146                await connection.encrypt()
147                print('*** Encryption on')
148
149                server = await Protocol.connect(connection)
150                listener.set_server(connection, server)
151                sink = server.add_sink(codec_capabilities())
152                sink.on('rtp_packet', on_rtp_packet)
153            else:
154                # Start being discoverable and connectable
155                await device.set_discoverable(True)
156                await device.set_connectable(True)
157
158            await hci_source.wait_for_termination()
159
160
161# -----------------------------------------------------------------------------
162logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
163asyncio.run(main())
164