From 808f634c4f5f3510619e4c8093e3bd0f8c990d70 Mon Sep 17 00:00:00 2001 From: pstruebi Date: Tue, 4 Feb 2025 09:00:04 +0100 Subject: [PATCH] add single auracast again --- auracast/auracast.py | 276 ++++++++++++++++++++++++++++++++++++++++++ auracast/multicast.py | 5 - 2 files changed, 276 insertions(+), 5 deletions(-) create mode 100644 auracast/auracast.py diff --git a/auracast/auracast.py b/auracast/auracast.py new file mode 100644 index 0000000..2dbcc04 --- /dev/null +++ b/auracast/auracast.py @@ -0,0 +1,276 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +from __future__ import annotations + +import asyncio +import contextlib +import logging +import wave +import itertools +from typing import cast, Any, AsyncGenerator, Coroutine, Dict, Optional, Tuple +from typing import List + + +try: + import lc3 # type: ignore # pylint: disable=E0401 +except ImportError as e: + raise ImportError("Try `python -m pip install \".[lc3]\"`.") from e + +from bumble.colors import color +from bumble import company_ids +from bumble import core +from bumble import gatt +from bumble import hci +from bumble.profiles import bap +from bumble.profiles import le_audio +from bumble.profiles import pbp +from bumble.profiles import bass +import bumble.device +import bumble.transport +import bumble.utils +from bumble.device import Host + +import auracast_config + + +def modified_on_hci_number_of_completed_packets_event(self, event): + for connection_handle, num_completed_packets in zip( + event.connection_handles, event.num_completed_packets + ): + if connection := self.connections.get(connection_handle): + connection.acl_packet_queue.on_packets_completed(num_completed_packets) + elif connection_handle not in itertools.chain( + self.cis_links.keys(), + self.sco_links.keys(), + itertools.chain.from_iterable(self.bigs.values()), + ): + logger.warning( + 'received packet completion event for unknown handle ' + f'0x{connection_handle:04X}' + ) + self.emit('hci_number_of_completed_packets_event', event) + +Host.on_hci_number_of_completed_packets_event = modified_on_hci_number_of_completed_packets_event + + + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + +@contextlib.asynccontextmanager +async def create_device(config: auracast_config.AuracastGlobalConfig) -> AsyncGenerator[bumble.device.Device, Any]: + async with await bumble.transport.open_transport(config.transport) as ( + hci_source, + hci_sink, + ): + device_config = bumble.device.DeviceConfiguration( + name=config.device_name, + address=config.auracast_device_address, + keystore='JsonKeyStore', + ) + + device = bumble.device.Device.from_config_with_hci( + device_config, + hci_source, + hci_sink, + ) + await device.power_on() + + yield device + + +def run_async(async_command: Coroutine) -> None: + try: + asyncio.run(async_command) + except core.ProtocolError as error: + if error.error_namespace == 'att' and error.error_code in list( + bass.ApplicationError + ): + message = bass.ApplicationError(error.error_code).name + else: + message = str(error) + + print( + color('!!! An error occurred while executing the command:', 'red'), message + ) + +async def run_broadcast( + global_config : auracast_config.AuracastGlobalConfig, + big_config: List[auracast_config.AuracastBigConfig] + +) -> None: + async with create_device(global_config) as device: + if not device.supports_le_periodic_advertising: + logger.error(color('Periodic advertising not supported', 'red')) + return + + with wave.open(big_config[0].broacast_wav_file_path, 'rb') as wav: + logger.info('Encoding wav file into lc3...') + logger.info('Frame rate of .wav file is: %s', wav.getframerate()) + encoder = lc3.Encoder( + frame_duration_us=global_config.frame_duration_us, + sample_rate_hz=global_config.auracast_sampling_rate_khz, + num_channels=1, + input_sample_rate_hz=wav.getframerate(), + ) + frames = list[bytes]() + while pcm := wav.readframes(encoder.get_frame_samples()): + frames.append( + encoder.encode(pcm, num_bytes=global_config.octets_per_frame, bit_depth=wav.getsampwidth() * 8) + ) + del encoder + print('Encoding complete.') + + # Config advertising set + bap_sampling_freq = getattr(bap.SamplingFrequency, f"FREQ_{global_config.auracast_sampling_rate_khz}") + basic_audio_announcement = bap.BasicAudioAnnouncement( + presentation_delay=global_config.presentation_delay_us, + subgroups=[ + bap.BasicAudioAnnouncement.Subgroup( + codec_id=hci.CodingFormat(codec_id=hci.CodecID.LC3), + codec_specific_configuration=bap.CodecSpecificConfiguration( + sampling_frequency=bap_sampling_freq, + frame_duration=bap.FrameDuration.DURATION_10000_US, + octets_per_codec_frame=global_config.octets_per_frame, + ), + metadata=le_audio.Metadata( + [ + le_audio.Metadata.Entry( + tag=le_audio.Metadata.Tag.LANGUAGE, data=big_config[0].broadcast_language.encode() + ), + le_audio.Metadata.Entry( + tag=le_audio.Metadata.Tag.PROGRAM_INFO, data=big_config[0].broadcast_program_info.encode() + ), + ] + ), + bis=[ + bap.BasicAudioAnnouncement.BIS( + index=1, + codec_specific_configuration=bap.CodecSpecificConfiguration( + audio_channel_allocation=bap.AudioLocation.FRONT_LEFT + ), + ), + ], + ) + ], + ) + logging.info('Setup Advertising') + broadcast_audio_announcement = bap.BroadcastAudioAnnouncement(big_config[0].broadcast_id) + advertising_set0 = await device.create_advertising_set( + advertising_parameters=bumble.device.AdvertisingParameters( + advertising_event_properties=bumble.device.AdvertisingEventProperties( + is_connectable=False + ), + primary_advertising_interval_min=100, + primary_advertising_interval_max=200, + advertising_sid=0 + # TODO: use 2mbit phy + ), + advertising_data=( + broadcast_audio_announcement.get_advertising_data() + + bytes( + core.AdvertisingData( + [(core.AdvertisingData.BROADCAST_NAME, big_config[0].broadcast_name.encode())] + ) + ) + ), + periodic_advertising_parameters=bumble.device.PeriodicAdvertisingParameters( + periodic_advertising_interval_min=80, + periodic_advertising_interval_max=160, + ), + periodic_advertising_data=basic_audio_announcement.get_advertising_data(), + auto_restart=True, + auto_start=True, + ) + + logging.info('Start Periodic Advertising') + await advertising_set0.start_periodic() + + logging.info('Setup BIG') + big0 = await device.create_big( + advertising_set0, + parameters=bumble.device.BigParameters( + num_bis=1, + sdu_interval=global_config.frame_duration_us, + max_sdu=global_config.octets_per_frame, # is this octets per frame ? + max_transport_latency=65, + rtn=4, + broadcast_code=( + bytes.fromhex(big_config[0].broadcast_code) if big_config[0].broadcast_code else None + ), + ), + ) + + logging.info('Setup ISO Data Path') + for bis_link in big0.bis_links: + await bis_link.setup_data_path( + direction=bis_link.Direction.HOST_TO_CONTROLLER + ) + + frames_iterator = itertools.cycle(frames) + logging.info("Broadcasting...") + + def on_packet_complete(event): + frame = next(frames_iterator) + big0.bis_links[0].write(frame) + + device.host.on('hci_number_of_completed_packets_event', on_packet_complete) + + on_packet_complete('') # Send the first packet, to get the event loop running + + while True: + await asyncio.sleep(1) + +# ----------------------------------------------------------------------------- +# Main +# ----------------------------------------------------------------------------- + +def broadcast(global_conf: auracast_config.AuracastGlobalConfig, big_conf: List[auracast_config.AuracastBigConfig]): + """Start a broadcast as a source.""" + run_async( + run_broadcast( + global_conf, + big_conf + ) + ) + + + +# ----------------------------------------------------------------------------- +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + + global_conf = auracast_config.global_base_config + + #global_conf.transport='serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_81BD14B8D71B5662-if00,1000000,rtscts' # transport for nrf52 dongle + + #global_conf.transport='serial:/dev/serial/by-id/usb-SEGGER_J-Link_001050076061-if02,1000000,rtscts' # transport for nrf53dk + + global_conf.transport='serial:/dev/serial/by-id/usb-SEGGER_J-Link_001057705357-if02,1000000,rtscts' # transport for nrf54l15dk + + bigs = [ + auracast_config.broadcast_de + ] + global_conf.octets_per_frame=60# 48kbps@24kHz + + broadcast( + global_conf, + bigs + ) diff --git a/auracast/multicast.py b/auracast/multicast.py index cf9b7e4..241cd76 100644 --- a/auracast/multicast.py +++ b/auracast/multicast.py @@ -288,11 +288,6 @@ if __name__ == "__main__": global_conf.transport='serial:/dev/serial/by-id/usb-SEGGER_J-Link_001057705357-if02,1000000,rtscts' # transport for nrf54l15dk - # TODO: why are nrf54l15 and nrf52 not streaming - no sound can be heard. Advertising starts, streaming too but no sound - # nrf5340 audio dk log says: audio_datapath: Pres comp state never gets to locked - # Look at le audio transmitted packages with wireshark next ?? - - bigs = [ auracast_config.broadcast_de,