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