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 22from typing import Any, Dict 23 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: Dict[Any, Any] = {'output': None} 46 47 48# ----------------------------------------------------------------------------- 49def sdp_records(): 50 service_record_handle = 0x00010001 51 return { 52 service_record_handle: make_audio_sink_service_sdp_records( 53 service_record_handle 54 ) 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=[ 75 SBC_LOUDNESS_ALLOCATION_METHOD, 76 SBC_SNR_ALLOCATION_METHOD, 77 ], 78 minimum_bitpool_value=2, 79 maximum_bitpool_value=53, 80 ), 81 ) 82 83 84# ----------------------------------------------------------------------------- 85def on_avdtp_connection(server): 86 # Add a sink endpoint to the server 87 sink = server.add_sink(codec_capabilities()) 88 sink.on('rtp_packet', on_rtp_packet) 89 90 91# ----------------------------------------------------------------------------- 92def on_rtp_packet(packet): 93 header = packet.payload[0] 94 fragmented = header >> 7 95 # start = (header >> 6) & 0x01 96 # last = (header >> 5) & 0x01 97 number_of_frames = header & 0x0F 98 99 if fragmented: 100 print(f'RTP: fragment {number_of_frames}') 101 else: 102 print(f'RTP: {number_of_frames} frames') 103 104 Context['output'].write(packet.payload[1:]) 105 106 107# ----------------------------------------------------------------------------- 108async def main() -> None: 109 if len(sys.argv) < 4: 110 print( 111 'Usage: run_a2dp_sink.py <device-config> <transport-spec> <sbc-file> ' 112 '[<bt-addr>]' 113 ) 114 print('example: run_a2dp_sink.py classic1.json usb:0 output.sbc') 115 return 116 117 print('<<< connecting to HCI...') 118 async with await open_transport_or_link(sys.argv[2]) as hci_transport: 119 print('<<< connected') 120 121 with open(sys.argv[3], 'wb') as sbc_file: 122 Context['output'] = sbc_file 123 124 # Create a device 125 device = Device.from_config_file_with_hci( 126 sys.argv[1], hci_transport.source, hci_transport.sink 127 ) 128 device.classic_enabled = True 129 130 # Setup the SDP to expose the sink service 131 device.sdp_service_records = sdp_records() 132 133 # Start the controller 134 await device.power_on() 135 136 # Create a listener to wait for AVDTP connections 137 listener = Listener.for_device(device) 138 listener.on('connection', on_avdtp_connection) 139 140 if len(sys.argv) >= 5: 141 # Connect to the source 142 target_address = sys.argv[4] 143 print(f'=== Connecting to {target_address}...') 144 connection = await device.connect( 145 target_address, transport=BT_BR_EDR_TRANSPORT 146 ) 147 print(f'=== Connected to {connection.peer_address}!') 148 149 # Request authentication 150 print('*** Authenticating...') 151 await connection.authenticate() 152 print('*** Authenticated') 153 154 # Enable encryption 155 print('*** Enabling encryption...') 156 await connection.encrypt() 157 print('*** Encryption on') 158 159 server = await Protocol.connect(connection) 160 listener.set_server(connection, server) 161 sink = server.add_sink(codec_capabilities()) 162 sink.on('rtp_packet', on_rtp_packet) 163 else: 164 # Start being discoverable and connectable 165 await device.set_discoverable(True) 166 await device.set_connectable(True) 167 168 await hci_transport.source.wait_for_termination() 169 170 171# ----------------------------------------------------------------------------- 172logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper()) 173asyncio.run(main()) 174