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