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