• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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