diff --git a/auracast/auracast_config.py b/auracast/auracast_config.py index 80978c0..eae5202 100644 --- a/auracast/auracast_config.py +++ b/auracast/auracast_config.py @@ -28,8 +28,8 @@ class AuracastGlobalConfig: device_name: str = 'Auracaster' transport: str = '' auracast_device_address: hci.Address = hci.Address('F0:F1:F2:F3:F4:F5') - auracast_sampling_rate_hz: int = 24000 - octets_per_frame: int = 60 #48kbps@24kHz # bitrate = octets_per_frame * 8 / frame len + auracast_sampling_rate_hz: int = 16000 + octets_per_frame: int = 40 #48kbps@24kHz # bitrate = octets_per_frame * 8 / frame len frame_duration_us: int = 10000 presentation_delay_us: int = 40000 manufacturer_data: tuple[int, bytes] = None diff --git a/auracast/multicast.py b/auracast/multicast.py index 88f7e98..3ab3eb7 100644 --- a/auracast/multicast.py +++ b/auracast/multicast.py @@ -49,8 +49,7 @@ from bumble.audio import io as audio_io import auracast_config -def chunker(b, size): - return (b[i:i+size] for i in range(0, len(b), size)) + # ----------------------------------------------------------------------------- # Logging @@ -95,7 +94,7 @@ def run_async(async_command: Coroutine) -> None: color('!!! An error occurred while executing the command:', 'red'), message ) -async def setup_broadcast( +async def init_broadcast( device, global_config : auracast_config.AuracastGlobalConfig, big_config: List[auracast_config.AuracastBigConfig] @@ -246,7 +245,7 @@ async def setup_broadcast( return bigs -async def setup_audio( +async def init_audio( bigs, global_config : auracast_config.AuracastGlobalConfig, big_config: List[auracast_config.AuracastBigConfig] @@ -291,8 +290,7 @@ class Streamer(): self.is_streaming = False self.bigs = bigs - def start_streaming_task(self): - + def start_streaming(self): if not self.is_streaming: self.task = asyncio.create_task(self.stream()) else: @@ -325,7 +323,8 @@ class Streamer(): await big['iso_queue'].write(lc3_frame) if all(stream_finished): # Take into account that multiple files have different lengths - print('All streams finished, stopping streamer') + logging.info('All streams finished, stopping streamer') + self.is_streaming = True break @@ -340,18 +339,18 @@ async def broadcast(global_conf: auracast_config.AuracastGlobalConfig, big_conf: logger.error(color('Periodic advertising not supported', 'red')) return - bigs = await setup_broadcast( # the bigs dictionary contains all the global configurations + bigs = await init_broadcast( # the bigs dictionary contains all the global configurations device, global_conf, big_conf ) - await setup_audio( + await init_audio( bigs, global_conf, big_conf ) streamer = Streamer(bigs) - streamer.start_streaming_task() + streamer.start_streaming() await asyncio.wait([streamer.task]) @@ -413,5 +412,3 @@ if __name__ == "__main__": # realtime audio locally # realtime audio network lc3 coded # (realtime audio network uncoded) - - # TODO: add support for playing new files will keeping the advertising running diff --git a/auracast/multicast_commander.py b/auracast/multicast_commander.py deleted file mode 100644 index 2eb7c16..0000000 --- a/auracast/multicast_commander.py +++ /dev/null @@ -1,124 +0,0 @@ -import logging -from typing import cast, Any, AsyncGenerator, Coroutine, Dict, Optional, Tuple, List -import bumble -import bumble.device -import bumble.transport -import bumble.utils - -import asyncio -import aioconsole -import multicast - -import auracast_config - -class Multicaster: - def __init__(self, global_conf: auracast_config.AuracastGlobalConfig, big_conf: List[auracast_config.AuracastBigConfig]): - self.running = False - self.task = None # Holds the background print task - self.global_conf = global_conf - self.big_conf = big_conf - self.device = None - self.bigs = None - - - async def setup_broadcast(self): - self.device_acm = multicast.create_device(self.global_conf) - - agen = self.device_acm.__aenter__() # Manually triggering setup - device = await agen - - self.bigs = await multicast.setup_broadcast( # the bigs dictionary contains all the global configurations - device, - self.global_conf, - self.big_conf - ) - self.device = device - - async def setup_audio(self): - await multicast.setup_audio( - self.bigs, - self.global_conf, - self.big_conf - ) - - async def stream_audio(self): - await multicast.streamer( - self.bigs - ) - - def start(self): - """Starts the background task if it's not already running.""" - if not self.running: - self.running = True - self.task = asyncio.create_task(self._printer_coroutine()) - - def stop(self): - """Stops the background task if running.""" - if self.running: - self.running = False - if self.task: - self.task.cancel() # Cancel the task safely - self.task = None - - def set_message(self, new_message): - """Updates the message being printed.""" - self.message = new_message - - async def __del__(self): - await self.device_acm.__aexit__(None, None, None) # Manually triggering teardown - - -async def command_line_ui(printer): - while True: - command = await aioconsole.ainput("\nCommands: [start|stop|set|quit] > ") - - if command.strip().lower() == "start": - printer.start() - print("💡 Printer started!") - - elif command.strip().lower() == "stop": - printer.stop() - print("🛑 Printer stopped!") - - elif command.strip().lower().startswith("set "): - _, new_message = command.split(" ", 1) - printer.set_message(new_message) - print(f"✏️ Message updated to: {new_message}") - - elif command.strip().lower() == "quit": - printer.stop() - print("👋 Exiting...") - break # Exit loop - - else: - print("❌ Invalid command. Use [start|stop|set |quit]") - -async def main(): - logging.basicConfig( - level=logging.DEBUG, - format='%(module)s.py:%(lineno)d %(levelname)s: %(message)s' - ) - - global_conf = auracast_config.global_base_config - #global_conf.transport='serial:/dev/serial/by-id/usb-SEGGER_J-Link_001057705357-if02,1000000,rtscts' # transport for nrf54l15dk - global_conf.transport='serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_81BD14B8D71B5662-if00,115200,rtscts' #nrf52dongle hci_uart usb cdc - - big_conf = [ - auracast_config.broadcast_de, - auracast_config.broadcast_en, - auracast_config.broadcast_fr, - #auracast_config.broadcast_es, - #auracast_config.broadcast_it, - ] - - # look into: - #async with MyAPI() as api: - #pass - - caster = Multicaster(global_conf, big_conf) - await caster.setup_broadcast() - - #await command_line_ui(caster) - -# Run the application -asyncio.run(main()) \ No newline at end of file diff --git a/auracast/multicast_control.py b/auracast/multicast_control.py new file mode 100644 index 0000000..5d93f13 --- /dev/null +++ b/auracast/multicast_control.py @@ -0,0 +1,142 @@ +import logging +from typing import cast, Any, AsyncGenerator, Coroutine, Dict, Optional, Tuple, List +import bumble +import bumble.device +import bumble.transport +import bumble.utils + +import asyncio +import aioconsole +import multicast + +import auracast_config + +class Multicaster: + def __init__( + self, + global_conf: auracast_config.AuracastGlobalConfig, + big_conf: List[auracast_config.AuracastBigConfig] + ): + self.is_auracast_init = False + self.is_audio_init = False + self.streaming = False + self.task = None # Holds the background print task + self.global_conf = global_conf + self.big_conf = big_conf + self.device = None + self.bigs = None + self.streamer=None + + async def init_broadcast(self): + self.device_acm = multicast.create_device(self.global_conf) + + agen = self.device_acm.__aenter__() # Manually triggering setup + device = await agen + + self.bigs = await multicast.init_broadcast( # the bigs dictionary contains all the global configurations + device, + self.global_conf, + self.big_conf + ) + self.device = device + self.is_auracast_init = True + + async def init_audio(self): + await multicast.init_audio( + self.bigs, + self.global_conf, + self.big_conf + ) + self.is_audio_init = True + self.streamer = multicast.Streamer(self.bigs) + + def start_streaming(self): + self.streamer.start_streaming() + + def stop_streaming(self): + self.streamer.stop_streaming() + + async def reset(self): + await self.shutdown() # Manually triggering teardown + self.__init__(self.global_conf, self.big_conf) + + async def shutdown(self): + await self.device.stop_advertising() + for big in self.bigs.values(): + await big['advertising_set'].stop() + + await self.device_acm.__aexit__(None, None, None) # Manually triggering teardown + + +# example commandline ui +async def command_line_ui(caster: Multicaster): + while True: + command = await aioconsole.ainput("\nCommands: [start|stop|init|quit] > ") + + if command.strip().lower() == "start_audio": + caster.start_streaming() + print("Audio started!") + + elif command.strip().lower() == "stop_audio": + caster.stop_streaming() + print("Audio stopped!") + + elif command.strip().lower() == "stop": + print("👋 Stopping...") + caster.stop_streaming() + await caster.reset() + + elif command.strip().lower() == "init": + await caster.reset() + await caster.init_broadcast() + await caster.init_audio() + + elif command.strip().lower() == "init_audio": + await caster.init_audio() + + #elif command.strip().lower().startswith("set "): + #_, new_message = command.split(" ", 1) + #multicaster.set_message(new_message) + #print(f"✏️ Message updated to: {new_message}") + + elif command.strip().lower() == "quit": + print("👋 Exiting...") + if caster.device: + caster.stop_streaming() + await caster.shutdown() + break # Exit loop + else: + print("Invalid command.") + +async def main(): + logging.basicConfig( + level=logging.DEBUG, + format='%(module)s.py:%(lineno)d %(levelname)s: %(message)s' + ) + + global_conf = auracast_config.global_base_config + #global_conf.transport='serial:/dev/serial/by-id/usb-SEGGER_J-Link_001057705357-if02,1000000,rtscts' # transport for nrf54l15dk + global_conf.transport='serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_81BD14B8D71B5662-if00,115200,rtscts' #nrf52dongle hci_uart usb cdc + + big_conf = [ + auracast_config.broadcast_de, + auracast_config.broadcast_en, + auracast_config.broadcast_fr, + #auracast_config.broadcast_es, + #auracast_config.broadcast_it, + ] + for conf in big_conf: + conf.loop_wav = False + + # look into: + #async with MyAPI() as api: + #pass + + caster = Multicaster(global_conf, big_conf) + await caster.init_broadcast() + await caster.init_audio() + + await command_line_ui(caster) + +# Run the application +asyncio.run(main()) \ No newline at end of file