1# Copyright 2021-2023 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 datetime 20import functools 21import logging 22import sys 23import os 24import io 25import struct 26import secrets 27 28from typing import Dict 29 30from bumble.core import AdvertisingData 31from bumble.device import Device 32from bumble.hci import ( 33 CodecID, 34 CodingFormat, 35 HCI_IsoDataPacket, 36) 37from bumble.profiles.bap import ( 38 AseStateMachine, 39 UnicastServerAdvertisingData, 40 CodecSpecificConfiguration, 41 CodecSpecificCapabilities, 42 ContextType, 43 AudioLocation, 44 SupportedSamplingFrequency, 45 SupportedFrameDuration, 46 PacRecord, 47 PublishedAudioCapabilitiesService, 48 AudioStreamControlService, 49) 50from bumble.profiles.cap import CommonAudioServiceService 51from bumble.profiles.csip import CoordinatedSetIdentificationService, SirkType 52 53from bumble.transport import open_transport_or_link 54 55 56def _sink_pac_record() -> PacRecord: 57 return PacRecord( 58 coding_format=CodingFormat(CodecID.LC3), 59 codec_specific_capabilities=CodecSpecificCapabilities( 60 supported_sampling_frequencies=( 61 SupportedSamplingFrequency.FREQ_8000 62 | SupportedSamplingFrequency.FREQ_16000 63 | SupportedSamplingFrequency.FREQ_24000 64 | SupportedSamplingFrequency.FREQ_32000 65 | SupportedSamplingFrequency.FREQ_48000 66 ), 67 supported_frame_durations=( 68 SupportedFrameDuration.DURATION_7500_US_SUPPORTED 69 | SupportedFrameDuration.DURATION_10000_US_SUPPORTED 70 ), 71 supported_audio_channel_count=[1, 2], 72 min_octets_per_codec_frame=26, 73 max_octets_per_codec_frame=240, 74 supported_max_codec_frames_per_sdu=2, 75 ), 76 ) 77 78 79file_outputs: Dict[AseStateMachine, io.BufferedWriter] = {} 80 81 82# ----------------------------------------------------------------------------- 83async def main() -> None: 84 if len(sys.argv) < 3: 85 print('Usage: run_cig_setup.py <config-file>' '<transport-spec-for-device>') 86 return 87 88 print('<<< connecting to HCI...') 89 async with await open_transport_or_link(sys.argv[2]) as hci_transport: 90 print('<<< connected') 91 92 device = Device.from_config_file_with_hci( 93 sys.argv[1], hci_transport.source, hci_transport.sink 94 ) 95 device.cis_enabled = True 96 97 await device.power_on() 98 99 csis = CoordinatedSetIdentificationService( 100 set_identity_resolving_key=secrets.token_bytes(16), 101 set_identity_resolving_key_type=SirkType.PLAINTEXT, 102 ) 103 device.add_service(CommonAudioServiceService(csis)) 104 device.add_service( 105 PublishedAudioCapabilitiesService( 106 supported_source_context=ContextType.PROHIBITED, 107 available_source_context=ContextType.PROHIBITED, 108 supported_sink_context=ContextType(0xFF), # All context types 109 available_sink_context=ContextType(0xFF), # All context types 110 sink_audio_locations=( 111 AudioLocation.FRONT_LEFT | AudioLocation.FRONT_RIGHT 112 ), 113 sink_pac=[_sink_pac_record()], 114 ) 115 ) 116 117 ascs = AudioStreamControlService(device, sink_ase_id=[1], source_ase_id=[2]) 118 device.add_service(ascs) 119 120 advertising_data = ( 121 bytes( 122 AdvertisingData( 123 [ 124 ( 125 AdvertisingData.COMPLETE_LOCAL_NAME, 126 bytes('Bumble LE Audio', 'utf-8'), 127 ), 128 ( 129 AdvertisingData.FLAGS, 130 bytes( 131 [ 132 AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG 133 | AdvertisingData.BR_EDR_HOST_FLAG 134 | AdvertisingData.BR_EDR_CONTROLLER_FLAG 135 ] 136 ), 137 ), 138 ( 139 AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, 140 bytes(PublishedAudioCapabilitiesService.UUID), 141 ), 142 ] 143 ) 144 ) 145 + csis.get_advertising_data() 146 + bytes(UnicastServerAdvertisingData()) 147 ) 148 149 def on_pdu(ase: AseStateMachine, pdu: HCI_IsoDataPacket): 150 # LC3 format: |frame_length(2)| + |frame(length)|. 151 sdu = b'' 152 if pdu.iso_sdu_length: 153 sdu = struct.pack('<H', pdu.iso_sdu_length) 154 sdu += pdu.iso_sdu_fragment 155 file_outputs[ase].write(sdu) 156 157 def on_ase_state_change( 158 state: AseStateMachine.State, 159 ase: AseStateMachine, 160 ) -> None: 161 if state != AseStateMachine.State.STREAMING: 162 if file_output := file_outputs.pop(ase): 163 file_output.close() 164 else: 165 file_output = open(f'{datetime.datetime.now().isoformat()}.lc3', 'wb') 166 codec_configuration = ase.codec_specific_configuration 167 assert isinstance(codec_configuration, CodecSpecificConfiguration) 168 # Write a LC3 header. 169 file_output.write( 170 bytes([0x1C, 0xCC]) # Header. 171 + struct.pack( 172 '<HHHHHHI', 173 18, # Header length. 174 codec_configuration.sampling_frequency.hz 175 // 100, # Sampling Rate(/100Hz). 176 0, # Bitrate(unused). 177 bin(codec_configuration.audio_channel_allocation).count( 178 '1' 179 ), # Channels. 180 codec_configuration.frame_duration.us 181 // 10, # Frame duration(/10us). 182 0, # RFU. 183 0x0FFFFFFF, # Frame counts. 184 ) 185 ) 186 file_outputs[ase] = file_output 187 assert ase.cis_link 188 ase.cis_link.sink = functools.partial(on_pdu, ase) 189 190 for ase in ascs.ase_state_machines.values(): 191 ase.on( 192 'state_change', 193 functools.partial(on_ase_state_change, ase=ase), 194 ) 195 196 await device.create_advertising_set( 197 advertising_data=advertising_data, 198 auto_restart=True, 199 ) 200 201 await hci_transport.source.terminated 202 203 204# ----------------------------------------------------------------------------- 205logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper()) 206asyncio.run(main()) 207