diff --git a/.vscode/tasks.json b/.vscode/tasks.json index fb1d702..e2c55d4 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -3,10 +3,15 @@ // for the documentation about the tasks.json format "version": "2.0.0", "tasks": [ + { + "label": "Setup project for development", + "type": "shell", + "command": "./venv/bin/python -m pip install -e ." + }, { "label": "pip install -e bumble", "type": "shell", "command": "./venv/bin/python -m pip install -e ../bumble --config-settings editable_mode=compat" - } + }, ] } \ No newline at end of file diff --git a/auracast/auracast_config.py b/auracast/auracast_config.py index f39dd99..ef036e3 100644 --- a/auracast/auracast_config.py +++ b/auracast/auracast_config.py @@ -54,7 +54,7 @@ class AuracastBigConfig: program_info: str = 'Some Announcements' audio_source: str = 'file:./auracast/announcement_48_10_96000_en.wav' input_format: str = 'auto' - loop_wav: bool = True + loop: bool = True precode_wav: bool = False iso_que_len: int = 64 @@ -66,7 +66,7 @@ broadcast_de = AuracastBigConfig( name = 'Broadcast0', language='deu', program_info = 'Announcements German', - audio_source = 'file:./auracast/announcement_48_10_96000_de.wav', + audio_source = 'file:./auracast/testdata/announcement_de.wav', ) broadcast_en = AuracastBigConfig( @@ -75,7 +75,7 @@ broadcast_en = AuracastBigConfig( name = 'Broadcast1', language='eng', program_info = 'Announcements English', - audio_source = 'file:./auracast/announcement_48_10_96000_en.wav', + audio_source = 'file:./auracast/testdata/announcement_en.wav', ) broadcast_fr = AuracastBigConfig( @@ -84,7 +84,7 @@ broadcast_fr = AuracastBigConfig( name = 'Broadcast2', language='fra', program_info = 'Announcements French', - audio_source = 'file:./auracast/announcement_48_10_96000_fr.wav', + audio_source = 'file:./auracast/testdata/announcement_fr.wav', ) broadcast_es = AuracastBigConfig( @@ -93,7 +93,7 @@ broadcast_es = AuracastBigConfig( name = 'Broadcast3', language='spa', program_info = 'Announcements Spanish', - audio_source = 'file:./auracast/announcement_48_10_96_es.wav', + audio_source = 'file:./auracast/testdata/announcement_es.wav', ) broadcast_it = AuracastBigConfig( @@ -102,5 +102,5 @@ broadcast_it = AuracastBigConfig( name = 'Broadcast4', language='ita', program_info = 'Announcements Italian', - audio_source = 'file:./auracast/announcement_48_10_96_it.wav', + audio_source = 'file:./auracast/testdata/announcement_it.wav', ) \ No newline at end of file diff --git a/auracast/multicast.py b/auracast/multicast.py index 7665e13..af62b91 100644 --- a/auracast/multicast.py +++ b/auracast/multicast.py @@ -94,6 +94,36 @@ class ModWaveAudioInput(audio_io.ThreadedAudioInput): audio_io.WaveAudioInput = ModWaveAudioInput + +def read_lc3_file(filepath): + filepath = filepath.replace('file:', '') + with open(filepath, 'rb') as f_lc3: + header = struct.unpack('=HHHHHHHI', f_lc3.read(18)) + if header[0] != 0xcc1c: + raise ValueError('Invalid bitstream file') + + # found in liblc3 - decoder.py + samplerate = header[2] * 100 + nchannels = header[4] + frame_duration = header[5] * 10 + stream_length = header[7] + #lc3_frame_size = struct.unpack('=H', f_lc3.read(2))[0] + logging.info('Loaded lc3 file: %s', filepath) + logging.info('samplerate: %s', samplerate) + logging.info('nchannels %s', nchannels) + logging.info('frame_duration %s', frame_duration) + logging.info('stream_length %s', stream_length) + + lc3_bytes= b'' + while True: + b = f_lc3.read(2) + if b == b'': + break + lc3_frame_size = struct.unpack('=H', b)[0] + lc3_bytes += f_lc3.read(lc3_frame_size) + + return lc3_bytes + # ----------------------------------------------------------------------------- # Logging # ----------------------------------------------------------------------------- @@ -303,8 +333,8 @@ async def init_audio( pass else: audio_input = await audio_io.create_audio_input(audio_source, input_format) - - audio_input.rewind = big_config[i].loop_wav + + audio_input.rewind = big_config[i].loop pcm_format = await audio_input.open() #try: @@ -335,8 +365,18 @@ async def init_audio( big['encoder'] = encoder class Streamer(): + """ + Streamer class that supports multiple input formats. See bumble for streaming from wav or device + Added functionallity on top of bumble: + - precode wav files, + - lc3 coded files + - just use a .lc3 file as audio_source + - lc3 coded from ram + - use a bytestring b'' as audio_source + """ + def __init__( - self, + self, bigs, global_config : auracast_config.AuracastGlobalConfig, big_config: List[auracast_config.AuracastBigConfig] @@ -362,7 +402,6 @@ class Streamer(): self.task = None async def stream(self): - bigs = self.bigs big_config = self.big_config global_config = self.global_config @@ -374,26 +413,28 @@ class Streamer(): # 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 = iter(big_config[i].audio_source) + + if big_config[i].loop: 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 + filename = big_config[i].audio_source.replace('file:', '') - with open (big_config[i].audio_source, 'r') as f: - lc3_frames = f.read() + lc3_bytes = read_lc3_file(filename) + lc3_frames = iter(lc3_bytes) - if big_config[i].loop_wav: + if big_config[i].loop: 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 + big['precoded'] = True audio_input = await audio_io.create_audio_input(audio_source, input_format) audio_input.rewind = False @@ -416,22 +457,23 @@ class Streamer(): 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'' + + lc3_bytes = b'' async for pcm_frame in audio_input.frames(lc3_frame_samples): - lc3_frames += encoder.encode( + lc3_bytes += encoder.encode( pcm_frame, num_bytes=global_config.octets_per_frame, bit_depth=pcm_bit_depth ) + lc3_frames = iter(lc3_bytes) # have a look at itertools.islice - if big_config[i].loop_wav: + if big_config[i].loop: lc3_frames = itertools.cycle(lc3_frames) - big['lc3_frames'] = lc3_frames + big['lc3_frames'] = lc3_frames - # anything else, e.g. realtime stream from device - else: + # anything else, e.g. realtime stream from device (bumble) + else: audio_input = await audio_io.create_audio_input(audio_source, input_format) - audio_input.rewind = big_config[i].loop_wav + audio_input.rewind = big_config[i].loop pcm_format = await audio_input.open() #try: @@ -457,8 +499,8 @@ class Streamer(): big['lc3_frame_samples'] = lc3_frame_samples big['audio_input'] = audio_input big['encoder'] = encoder - big['precoded'] = False - + 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 @@ -475,6 +517,7 @@ class Streamer(): if big['precoded']:# everything was already lc3 coded beforehand lc3_frame = b"" + # list(itertools.islice(iterator, 3)) 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: @@ -548,11 +591,13 @@ if __name__ == "__main__": #auracast_config.broadcast_es, #auracast_config.broadcast_it, ] - for big in bigs: # TODO. investigate this further + for big in bigs: # TODO: encrypted streams are not working #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 - + big.audio_source = big.audio_source.replace('.wav', '_10_16_32.lc3') #lc3 precoded files + big.audio_source = read_lc3_file(big.audio_source) # load files in advance + # 16kHz works reliably with 3 streams # 24kHz is only working with 2 streams - probably airtime constraint # TODO: with more than three broadcasters (16kHz) no advertising (no primary channels is present anymore) diff --git a/auracast/multicast_control.py b/auracast/multicast_control.py index 5226fcf..6416266 100644 --- a/auracast/multicast_control.py +++ b/auracast/multicast_control.py @@ -131,7 +131,7 @@ async def main(): #auracast_config.broadcast_it, ] for conf in big_conf: - conf.loop_wav = False + conf.loop = False # look into: #async with MyAPI() as api: diff --git a/auracast/announcement_48_10_96000_de.wav b/auracast/testdata/announcement_de.wav similarity index 100% rename from auracast/announcement_48_10_96000_de.wav rename to auracast/testdata/announcement_de.wav diff --git a/auracast/testdata/announcement_de_10_16_32.lc3 b/auracast/testdata/announcement_de_10_16_32.lc3 new file mode 100644 index 0000000..f438354 Binary files /dev/null and b/auracast/testdata/announcement_de_10_16_32.lc3 differ diff --git a/auracast/announcement_48_10_96000_en.wav b/auracast/testdata/announcement_en.wav similarity index 100% rename from auracast/announcement_48_10_96000_en.wav rename to auracast/testdata/announcement_en.wav diff --git a/auracast/testdata/announcement_en_10_16_32.lc3 b/auracast/testdata/announcement_en_10_16_32.lc3 new file mode 100644 index 0000000..cc224da Binary files /dev/null and b/auracast/testdata/announcement_en_10_16_32.lc3 differ diff --git a/auracast/announcement_48_10_96000_en_stereo.wav b/auracast/testdata/announcement_en_stereo.wav similarity index 100% rename from auracast/announcement_48_10_96000_en_stereo.wav rename to auracast/testdata/announcement_en_stereo.wav diff --git a/auracast/testdata/announcement_en_stereo_10_16_32.lc3 b/auracast/testdata/announcement_en_stereo_10_16_32.lc3 new file mode 100644 index 0000000..7dc7f02 Binary files /dev/null and b/auracast/testdata/announcement_en_stereo_10_16_32.lc3 differ diff --git a/auracast/announcement_48_10_96_es.wav b/auracast/testdata/announcement_es.wav similarity index 100% rename from auracast/announcement_48_10_96_es.wav rename to auracast/testdata/announcement_es.wav diff --git a/auracast/testdata/announcement_es_10_16_32.lc3 b/auracast/testdata/announcement_es_10_16_32.lc3 new file mode 100644 index 0000000..bc0856c Binary files /dev/null and b/auracast/testdata/announcement_es_10_16_32.lc3 differ diff --git a/auracast/announcement_48_10_96000_fr.wav b/auracast/testdata/announcement_fr.wav similarity index 100% rename from auracast/announcement_48_10_96000_fr.wav rename to auracast/testdata/announcement_fr.wav diff --git a/auracast/testdata/announcement_fr_10_16_32.lc3 b/auracast/testdata/announcement_fr_10_16_32.lc3 new file mode 100644 index 0000000..e07bad0 Binary files /dev/null and b/auracast/testdata/announcement_fr_10_16_32.lc3 differ diff --git a/auracast/announcement_48_10_96_it.wav b/auracast/testdata/announcement_it.wav similarity index 100% rename from auracast/announcement_48_10_96_it.wav rename to auracast/testdata/announcement_it.wav diff --git a/auracast/testdata/announcement_it_10_16_32.lc3 b/auracast/testdata/announcement_it_10_16_32.lc3 new file mode 100644 index 0000000..4034f5d Binary files /dev/null and b/auracast/testdata/announcement_it_10_16_32.lc3 differ diff --git a/auracast/testdata/encode_lc3.py b/auracast/testdata/encode_lc3.py new file mode 100644 index 0000000..a67cd8e --- /dev/null +++ b/auracast/testdata/encode_lc3.py @@ -0,0 +1,25 @@ +# use liblc3 +import os +import subprocess + +frame_dur_ms=10 +srate=16000 +bps=32000 + +if __name__ == '__main__': + workdir = os.path.dirname(__file__) + os.chdir(workdir) + files = os.listdir(workdir) + filtered = [file for file in files if file.endswith('.wav')] + + for file in filtered: + cmd = [ + 'elc3', + '-b', f'{bps}', + '-m', f'{frame_dur_ms}' , + '-r', f'{srate}', + f'{file}', f'{file.replace('.wav', '')}_{frame_dur_ms}_{srate//1000}_{bps//1000}.lc3' + ] + print("Executing: ", " ".join(cmd)) + ret = subprocess.run(cmd, check=True) + print(ret.returncode, ret.stdout, ret.stderr)