From 79d436e7cb26a48dc33e24df2dc55b473086c653 Mon Sep 17 00:00:00 2001 From: pstruebi Date: Wed, 26 Feb 2025 16:19:24 +0100 Subject: [PATCH] start implementing lc3 precoded support --- auracast/auracast_config.py | 1 + auracast/multicast.py | 157 +++++++++++++++++++++++++++++++----- 2 files changed, 139 insertions(+), 19 deletions(-) diff --git a/auracast/auracast_config.py b/auracast/auracast_config.py index facba53..f39dd99 100644 --- a/auracast/auracast_config.py +++ b/auracast/auracast_config.py @@ -55,6 +55,7 @@ class AuracastBigConfig: audio_source: str = 'file:./auracast/announcement_48_10_96000_en.wav' input_format: str = 'auto' loop_wav: bool = True + precode_wav: bool = False iso_que_len: int = 64 diff --git a/auracast/multicast.py b/auracast/multicast.py index 87e1b3d..7665e13 100644 --- a/auracast/multicast.py +++ b/auracast/multicast.py @@ -296,7 +296,14 @@ async def init_audio( for i, big in enumerate(bigs.values()): audio_source = big_config[i].audio_source input_format = big_config[i].input_format - audio_input = await audio_io.create_audio_input(audio_source, input_format) + + if big_config[i].audio_source.endswith('.lc3'): + pass + elif big_config[i].precode_wav: + pass + else: + audio_input = await audio_io.create_audio_input(audio_source, input_format) + audio_input.rewind = big_config[i].loop_wav pcm_format = await audio_input.open() @@ -328,10 +335,17 @@ async def init_audio( big['encoder'] = encoder class Streamer(): - def __init__(self, bigs): + def __init__( + self, + bigs, + global_config : auracast_config.AuracastGlobalConfig, + big_config: List[auracast_config.AuracastBigConfig] + ): self.task = None self.is_streaming = False self.bigs = bigs + self.global_config = global_config + self.big_config = big_config def start_streaming(self): if not self.is_streaming: @@ -348,21 +362,131 @@ class Streamer(): self.task = None async def stream(self): - # TODO: do some pre buffering so the stream is stable from the beginning. One half iso queue would be appropriate + + bigs = self.bigs + big_config = self.big_config + global_config = self.global_config + # init + for i, big in enumerate(bigs.values()): + audio_source = big_config[i].audio_source + input_format = big_config[i].input_format + + # precoded lc3 from ram + if isinstance(big_config[i].audio_source, bytes): + big['precoded'] = True + lc3_frames = big_config[i].audio_source, + + if big_config[i].loop_wav: + lc3_frames = itertools.cycle(lc3_frames) + big['lc3_frames'] = lc3_frames + + # precoded lc3 file + elif big_config[i].audio_source.endswith('.lc3'): + big['precoded'] = True + + with open (big_config[i].audio_source, 'r') as f: + lc3_frames = f.read() + + if big_config[i].loop_wav: + lc3_frames = itertools.cycle(lc3_frames) + big['lc3_frames'] = lc3_frames + + # use wav files and code them entirely before streaming + elif big_config[i].precode_wav and big_config[i].audio_source.endswith('.wav'): + big['precoded'] = True + + audio_input = await audio_io.create_audio_input(audio_source, input_format) + audio_input.rewind = False + pcm_format = await audio_input.open() + + if pcm_format.channels != 1: + print("Only 1 channels PCM configurations are supported") + return + if pcm_format.sample_type == audio_io.PcmFormat.SampleType.INT16: + pcm_bit_depth = 16 + elif pcm_format.sample_type == audio_io.PcmFormat.SampleType.FLOAT32: + pcm_bit_depth = None + else: + print("Only INT16 and FLOAT32 sample types are supported") + return + encoder = lc3.Encoder( + frame_duration_us=global_config.frame_duration_us, + sample_rate_hz=global_config.auracast_sampling_rate_hz, + num_channels=1, + input_sample_rate_hz=pcm_format.sample_rate, + ) + lc3_frame_samples = encoder.get_frame_samples() # number of the pcm samples per lc3 frame + + lc3_frames = b'' + async for pcm_frame in audio_input.frames(lc3_frame_samples): + lc3_frames += encoder.encode( + pcm_frame, num_bytes=global_config.octets_per_frame, bit_depth=pcm_bit_depth + ) + + # have a look at itertools.islice + if big_config[i].loop_wav: + lc3_frames = itertools.cycle(lc3_frames) + big['lc3_frames'] = lc3_frames + + # anything else, e.g. realtime stream from device + else: + audio_input = await audio_io.create_audio_input(audio_source, input_format) + audio_input.rewind = big_config[i].loop_wav + pcm_format = await audio_input.open() + + #try: + if pcm_format.channels != 1: + print("Only 1 channels PCM configurations are supported") + return + if pcm_format.sample_type == audio_io.PcmFormat.SampleType.INT16: + pcm_bit_depth = 16 + elif pcm_format.sample_type == audio_io.PcmFormat.SampleType.FLOAT32: + pcm_bit_depth = None + else: + print("Only INT16 and FLOAT32 sample types are supported") + return + encoder = lc3.Encoder( + frame_duration_us=global_config.frame_duration_us, + sample_rate_hz=global_config.auracast_sampling_rate_hz, + num_channels=1, + input_sample_rate_hz=pcm_format.sample_rate, + ) + lc3_frame_samples = encoder.get_frame_samples() # number of the pcm samples per lc3 frame + + big['pcm_bit_depth'] = pcm_bit_depth + big['lc3_frame_samples'] = lc3_frame_samples + big['audio_input'] = audio_input + big['encoder'] = encoder + big['precoded'] = False + + # Need for coded an uncoded audio + lc3_frame_size = global_config.octets_per_frame #encoder.get_frame_bytes(bitrate) + lc3_bytes_per_frame = lc3_frame_size #* 2 #multiplied by number of channels + big['lc3_bytes_per_frame'] = lc3_bytes_per_frame + + # TODO: Maybe do some pre buffering so the stream is stable from the beginning. One half iso queue would be appropriate logging.info("Streaming audio...") bigs = self.bigs self.is_streaming = True + # One streamer fits all while self.is_streaming: stream_finished = [False for _ in range(len(bigs))] for i, big in enumerate(bigs.values()): - pcm_frame = await anext(big['audio_input'].frames(big['lc3_frame_samples']), None) - if pcm_frame is None: # Not all streams may stop at the same time - stream_finished[i] = True - continue - lc3_frame = big['encoder'].encode( - pcm_frame, num_bytes=big['lc3_bytes_per_frame'], bit_depth=big['pcm_bit_depth'] - ) + if big['precoded']:# everything was already lc3 coded beforehand + lc3_frame = b"" + for _ in range(big['lc3_bytes_per_frame']): # have a look at itertools.islice + lc3_frame += next(big['lc3_frames']).to_bytes() # TODO: use async ? + else: + pcm_frame = await anext(big['audio_input'].frames(big['lc3_frame_samples']), None) + if pcm_frame is None: # Not all streams may stop at the same time + stream_finished[i] = True + continue + + lc3_frame = big['encoder'].encode( + pcm_frame, num_bytes=big['lc3_bytes_per_frame'], bit_depth=big['pcm_bit_depth'] + ) + await big['iso_queue'].write(lc3_frame) if all(stream_finished): # Take into account that multiple files have different lengths @@ -387,12 +511,7 @@ async def broadcast(global_conf: auracast_config.AuracastGlobalConfig, big_conf: global_conf, big_conf ) - await init_audio( - bigs, - global_conf, - big_conf - ) - streamer = Streamer(bigs) + streamer = Streamer(bigs, global_conf, big_conf) streamer.start_streaming() await asyncio.wait([streamer.task]) @@ -429,10 +548,10 @@ if __name__ == "__main__": #auracast_config.broadcast_es, #auracast_config.broadcast_it, ] - #for big in bigs: # TODO. investigate this further - # big.code = 'ff'*16 # returns hci/HCI_ENCRYPTION_MODE_NOT_ACCEPTABLE_ERROR + for big in bigs: # TODO. investigate this further + #big.code = 'ff'*16 # returns hci/HCI_ENCRYPTION_MODE_NOT_ACCEPTABLE_ERROR #big.code = '78 e5 dc f1 34 ab 42 bf c1 92 ef dd 3a fd 67 ae' - + big.precode_wav = True # 16kHz works reliably with 3 streams # 24kHz is only working with 2 streams - probably airtime constraint