add support for lc3 files and raw bytes
This commit is contained in:
7
.vscode/tasks.json
vendored
7
.vscode/tasks.json
vendored
@@ -3,10 +3,15 @@
|
|||||||
// for the documentation about the tasks.json format
|
// for the documentation about the tasks.json format
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "Setup project for development",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "./venv/bin/python -m pip install -e ."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "pip install -e bumble",
|
"label": "pip install -e bumble",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "./venv/bin/python -m pip install -e ../bumble --config-settings editable_mode=compat"
|
"command": "./venv/bin/python -m pip install -e ../bumble --config-settings editable_mode=compat"
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@ class AuracastBigConfig:
|
|||||||
program_info: str = 'Some Announcements'
|
program_info: str = 'Some Announcements'
|
||||||
audio_source: str = 'file:./auracast/announcement_48_10_96000_en.wav'
|
audio_source: str = 'file:./auracast/announcement_48_10_96000_en.wav'
|
||||||
input_format: str = 'auto'
|
input_format: str = 'auto'
|
||||||
loop_wav: bool = True
|
loop: bool = True
|
||||||
precode_wav: bool = False
|
precode_wav: bool = False
|
||||||
iso_que_len: int = 64
|
iso_que_len: int = 64
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ broadcast_de = AuracastBigConfig(
|
|||||||
name = 'Broadcast0',
|
name = 'Broadcast0',
|
||||||
language='deu',
|
language='deu',
|
||||||
program_info = 'Announcements German',
|
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(
|
broadcast_en = AuracastBigConfig(
|
||||||
@@ -75,7 +75,7 @@ broadcast_en = AuracastBigConfig(
|
|||||||
name = 'Broadcast1',
|
name = 'Broadcast1',
|
||||||
language='eng',
|
language='eng',
|
||||||
program_info = 'Announcements English',
|
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(
|
broadcast_fr = AuracastBigConfig(
|
||||||
@@ -84,7 +84,7 @@ broadcast_fr = AuracastBigConfig(
|
|||||||
name = 'Broadcast2',
|
name = 'Broadcast2',
|
||||||
language='fra',
|
language='fra',
|
||||||
program_info = 'Announcements French',
|
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(
|
broadcast_es = AuracastBigConfig(
|
||||||
@@ -93,7 +93,7 @@ broadcast_es = AuracastBigConfig(
|
|||||||
name = 'Broadcast3',
|
name = 'Broadcast3',
|
||||||
language='spa',
|
language='spa',
|
||||||
program_info = 'Announcements Spanish',
|
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(
|
broadcast_it = AuracastBigConfig(
|
||||||
@@ -102,5 +102,5 @@ broadcast_it = AuracastBigConfig(
|
|||||||
name = 'Broadcast4',
|
name = 'Broadcast4',
|
||||||
language='ita',
|
language='ita',
|
||||||
program_info = 'Announcements Italian',
|
program_info = 'Announcements Italian',
|
||||||
audio_source = 'file:./auracast/announcement_48_10_96_it.wav',
|
audio_source = 'file:./auracast/testdata/announcement_it.wav',
|
||||||
)
|
)
|
||||||
@@ -94,6 +94,36 @@ class ModWaveAudioInput(audio_io.ThreadedAudioInput):
|
|||||||
|
|
||||||
audio_io.WaveAudioInput = ModWaveAudioInput
|
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
|
# Logging
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -303,8 +333,8 @@ async def init_audio(
|
|||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
audio_input = await audio_io.create_audio_input(audio_source, input_format)
|
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()
|
pcm_format = await audio_input.open()
|
||||||
|
|
||||||
#try:
|
#try:
|
||||||
@@ -335,8 +365,18 @@ async def init_audio(
|
|||||||
big['encoder'] = encoder
|
big['encoder'] = encoder
|
||||||
|
|
||||||
class Streamer():
|
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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
bigs,
|
bigs,
|
||||||
global_config : auracast_config.AuracastGlobalConfig,
|
global_config : auracast_config.AuracastGlobalConfig,
|
||||||
big_config: List[auracast_config.AuracastBigConfig]
|
big_config: List[auracast_config.AuracastBigConfig]
|
||||||
@@ -362,7 +402,6 @@ class Streamer():
|
|||||||
self.task = None
|
self.task = None
|
||||||
|
|
||||||
async def stream(self):
|
async def stream(self):
|
||||||
|
|
||||||
bigs = self.bigs
|
bigs = self.bigs
|
||||||
big_config = self.big_config
|
big_config = self.big_config
|
||||||
global_config = self.global_config
|
global_config = self.global_config
|
||||||
@@ -374,26 +413,28 @@ class Streamer():
|
|||||||
# precoded lc3 from ram
|
# precoded lc3 from ram
|
||||||
if isinstance(big_config[i].audio_source, bytes):
|
if isinstance(big_config[i].audio_source, bytes):
|
||||||
big['precoded'] = True
|
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)
|
lc3_frames = itertools.cycle(lc3_frames)
|
||||||
big['lc3_frames'] = lc3_frames
|
big['lc3_frames'] = lc3_frames
|
||||||
|
|
||||||
# precoded lc3 file
|
# precoded lc3 file
|
||||||
elif big_config[i].audio_source.endswith('.lc3'):
|
elif big_config[i].audio_source.endswith('.lc3'):
|
||||||
big['precoded'] = True
|
big['precoded'] = True
|
||||||
|
filename = big_config[i].audio_source.replace('file:', '')
|
||||||
|
|
||||||
with open (big_config[i].audio_source, 'r') as f:
|
lc3_bytes = read_lc3_file(filename)
|
||||||
lc3_frames = f.read()
|
lc3_frames = iter(lc3_bytes)
|
||||||
|
|
||||||
if big_config[i].loop_wav:
|
if big_config[i].loop:
|
||||||
lc3_frames = itertools.cycle(lc3_frames)
|
lc3_frames = itertools.cycle(lc3_frames)
|
||||||
big['lc3_frames'] = lc3_frames
|
big['lc3_frames'] = lc3_frames
|
||||||
|
|
||||||
# use wav files and code them entirely before streaming
|
# use wav files and code them entirely before streaming
|
||||||
elif big_config[i].precode_wav and big_config[i].audio_source.endswith('.wav'):
|
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 = await audio_io.create_audio_input(audio_source, input_format)
|
||||||
audio_input.rewind = False
|
audio_input.rewind = False
|
||||||
@@ -416,22 +457,23 @@ class Streamer():
|
|||||||
input_sample_rate_hz=pcm_format.sample_rate,
|
input_sample_rate_hz=pcm_format.sample_rate,
|
||||||
)
|
)
|
||||||
lc3_frame_samples = encoder.get_frame_samples() # number of the pcm samples per lc3 frame
|
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):
|
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
|
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
|
# have a look at itertools.islice
|
||||||
if big_config[i].loop_wav:
|
if big_config[i].loop:
|
||||||
lc3_frames = itertools.cycle(lc3_frames)
|
lc3_frames = itertools.cycle(lc3_frames)
|
||||||
big['lc3_frames'] = lc3_frames
|
big['lc3_frames'] = lc3_frames
|
||||||
|
|
||||||
# anything else, e.g. realtime stream from device
|
# anything else, e.g. realtime stream from device (bumble)
|
||||||
else:
|
else:
|
||||||
audio_input = await audio_io.create_audio_input(audio_source, input_format)
|
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()
|
pcm_format = await audio_input.open()
|
||||||
|
|
||||||
#try:
|
#try:
|
||||||
@@ -457,8 +499,8 @@ class Streamer():
|
|||||||
big['lc3_frame_samples'] = lc3_frame_samples
|
big['lc3_frame_samples'] = lc3_frame_samples
|
||||||
big['audio_input'] = audio_input
|
big['audio_input'] = audio_input
|
||||||
big['encoder'] = encoder
|
big['encoder'] = encoder
|
||||||
big['precoded'] = False
|
big['precoded'] = False
|
||||||
|
|
||||||
# Need for coded an uncoded audio
|
# Need for coded an uncoded audio
|
||||||
lc3_frame_size = global_config.octets_per_frame #encoder.get_frame_bytes(bitrate)
|
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
|
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
|
if big['precoded']:# everything was already lc3 coded beforehand
|
||||||
lc3_frame = b""
|
lc3_frame = b""
|
||||||
|
# list(itertools.islice(iterator, 3))
|
||||||
for _ in range(big['lc3_bytes_per_frame']): # have a look at itertools.islice
|
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 ?
|
lc3_frame += next(big['lc3_frames']).to_bytes() # TODO: use async ?
|
||||||
else:
|
else:
|
||||||
@@ -548,11 +591,13 @@ if __name__ == "__main__":
|
|||||||
#auracast_config.broadcast_es,
|
#auracast_config.broadcast_es,
|
||||||
#auracast_config.broadcast_it,
|
#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 = '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.code = '78 e5 dc f1 34 ab 42 bf c1 92 ef dd 3a fd 67 ae'
|
||||||
big.precode_wav = True
|
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
|
# 16kHz works reliably with 3 streams
|
||||||
# 24kHz is only working with 2 streams - probably airtime constraint
|
# 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)
|
# TODO: with more than three broadcasters (16kHz) no advertising (no primary channels is present anymore)
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ async def main():
|
|||||||
#auracast_config.broadcast_it,
|
#auracast_config.broadcast_it,
|
||||||
]
|
]
|
||||||
for conf in big_conf:
|
for conf in big_conf:
|
||||||
conf.loop_wav = False
|
conf.loop = False
|
||||||
|
|
||||||
# look into:
|
# look into:
|
||||||
#async with MyAPI() as api:
|
#async with MyAPI() as api:
|
||||||
|
|||||||
BIN
auracast/testdata/announcement_de_10_16_32.lc3
vendored
Normal file
BIN
auracast/testdata/announcement_de_10_16_32.lc3
vendored
Normal file
Binary file not shown.
BIN
auracast/testdata/announcement_en_10_16_32.lc3
vendored
Normal file
BIN
auracast/testdata/announcement_en_10_16_32.lc3
vendored
Normal file
Binary file not shown.
BIN
auracast/testdata/announcement_en_stereo_10_16_32.lc3
vendored
Normal file
BIN
auracast/testdata/announcement_en_stereo_10_16_32.lc3
vendored
Normal file
Binary file not shown.
BIN
auracast/testdata/announcement_es_10_16_32.lc3
vendored
Normal file
BIN
auracast/testdata/announcement_es_10_16_32.lc3
vendored
Normal file
Binary file not shown.
BIN
auracast/testdata/announcement_fr_10_16_32.lc3
vendored
Normal file
BIN
auracast/testdata/announcement_fr_10_16_32.lc3
vendored
Normal file
Binary file not shown.
BIN
auracast/testdata/announcement_it_10_16_32.lc3
vendored
Normal file
BIN
auracast/testdata/announcement_it_10_16_32.lc3
vendored
Normal file
Binary file not shown.
25
auracast/testdata/encode_lc3.py
vendored
Normal file
25
auracast/testdata/encode_lc3.py
vendored
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user