refractoring

This commit is contained in:
2025-02-20 12:51:16 +01:00
parent b0980fceb5
commit f2335df4a7
6 changed files with 247 additions and 201 deletions

6
.gitignore vendored
View File

@@ -4,7 +4,6 @@ coverage/ # Coverage results after running tests with coverage tools
.dist-info/ # Wheel metadata (use poetry build to handle this) .dist-info/ # Wheel metadata (use poetry build to handle this)
*.egg-info/ # Egg info directory (automatically created by pip) *.egg-info/ # Egg info directory (automatically created by pip)
auracast.egg-info/ auracast.egg-info/
.vscode/ # IDE configuration (edit in VS Code)
# Ignore these file types and extensions # Ignore these file types and extensions
*.pyc # Compiled Python files (.pyc, .pyo are automatically ignored by git) *.pyc # Compiled Python files (.pyc, .pyo are automatically ignored by git)
@@ -16,11 +15,11 @@ venv/
env/ env/
# Ignore any IDE configurations or project-specific metadata # Ignore any IDE configurations or project-specific metadata
.vscode/**
.pycharm/** .pycharm/**
*.iml *.iml
.project .project
.settings .settings
.vscode/settings.json
# Ignore test results and logs (adjust to your specific testing framework) # Ignore test results and logs (adjust to your specific testing framework)
/testresults/** /testresults/**
@@ -35,4 +34,5 @@ env/
__pycache__/ __pycache__/
# Exclude .env file from all platforms # Exclude .env file from all platforms
*/.env */.env

19
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,19 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: current file",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"justMyCode": true,
"args": [
]
}
]
}

12
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,12 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "pip install -e bumble",
"type": "shell",
"command": "pip install -e ../bumble --config-settings editable_mode=compat"
}
]
}

View File

@@ -53,6 +53,7 @@ class AuracastBigConfig:
name: str = 'Broadcast0' name: str = 'Broadcast0'
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'
iso_que_len: int = 64
loop_wav: bool = True loop_wav: bool = True

View File

@@ -97,228 +97,243 @@ def run_async(async_command: Coroutine) -> None:
) )
async def setup_broadcast( async def setup_broadcast(
device,
global_config : auracast_config.AuracastGlobalConfig, global_config : auracast_config.AuracastGlobalConfig,
big_config: List[auracast_config.AuracastBigConfig] big_config: List[auracast_config.AuracastBigConfig]
) -> dict:
) -> None: bap_sampling_freq = getattr(bap.SamplingFrequency, f"FREQ_{global_config.auracast_sampling_rate_hz}")
async with create_device(global_config) as device: bigs = {}
if not device.supports_le_periodic_advertising: for i, conf in enumerate(big_config):
logger.error(color('Periodic advertising not supported', 'red')) bigs[f'big{i}'] = {}
return # Config advertising set
bigs[f'big{i}']['basic_audio_announcement'] = bap.BasicAudioAnnouncement(
bap_sampling_freq = getattr(bap.SamplingFrequency, f"FREQ_{global_config.auracast_sampling_rate_hz}") presentation_delay=global_config.presentation_delay_us,
bigs = {} subgroups=[
for i, conf in enumerate(big_config): bap.BasicAudioAnnouncement.Subgroup(
bigs[f'big{i}'] = {} codec_id=hci.CodingFormat(codec_id=hci.CodecID.LC3),
# Config advertising set codec_specific_configuration=bap.CodecSpecificConfiguration(
bigs[f'big{i}']['basic_audio_announcement'] = bap.BasicAudioAnnouncement( sampling_frequency=bap_sampling_freq,
presentation_delay=global_config.presentation_delay_us, frame_duration=bap.FrameDuration.DURATION_10000_US,
subgroups=[ octets_per_codec_frame=global_config.octets_per_frame,
bap.BasicAudioAnnouncement.Subgroup( ),
codec_id=hci.CodingFormat(codec_id=hci.CodecID.LC3), metadata=le_audio.Metadata(
codec_specific_configuration=bap.CodecSpecificConfiguration( [
sampling_frequency=bap_sampling_freq, le_audio.Metadata.Entry(
frame_duration=bap.FrameDuration.DURATION_10000_US, tag=le_audio.Metadata.Tag.LANGUAGE, data=conf.language.encode()
octets_per_codec_frame=global_config.octets_per_frame,
),
metadata=le_audio.Metadata(
[
le_audio.Metadata.Entry(
tag=le_audio.Metadata.Tag.LANGUAGE, data=conf.language.encode()
),
le_audio.Metadata.Entry(
tag=le_audio.Metadata.Tag.PROGRAM_INFO, data=conf.program_info.encode()
),
le_audio.Metadata.Entry(
tag=le_audio.Metadata.Tag.BROADCAST_NAME, data=conf.name.encode()
),
]
),
bis=[
bap.BasicAudioAnnouncement.BIS(
index=1,
codec_specific_configuration=bap.CodecSpecificConfiguration(
audio_channel_allocation=bap.AudioLocation.FRONT_LEFT
),
), ),
], le_audio.Metadata.Entry(
) tag=le_audio.Metadata.Tag.PROGRAM_INFO, data=conf.program_info.encode()
], ),
) le_audio.Metadata.Entry(
logger.info('Setup Advertising') tag=le_audio.Metadata.Tag.BROADCAST_NAME, data=conf.name.encode()
advertising_manufacturer_data = ( ),
b'' ]
if global_config.manufacturer_data is None ),
else bytes( bis=[
core.AdvertisingData( bap.BasicAudioAnnouncement.BIS(
[ index=1,
( codec_specific_configuration=bap.CodecSpecificConfiguration(
core.AdvertisingData.MANUFACTURER_SPECIFIC_DATA, audio_channel_allocation=bap.AudioLocation.FRONT_LEFT
struct.pack('<H', global_config.manufacturer_data[0]) ),
+ global_config.manufacturer_data[1], ),
) ],
]
) )
],
)
logger.info('Setup Advertising')
advertising_manufacturer_data = (
b''
if global_config.manufacturer_data is None
else bytes(
core.AdvertisingData(
[
(
core.AdvertisingData.MANUFACTURER_SPECIFIC_DATA,
struct.pack('<H', global_config.manufacturer_data[0])
+ global_config.manufacturer_data[1],
)
]
) )
) )
bigs[f'big{i}']['broadcast_audio_announcement'] = bap.BroadcastAudioAnnouncement(conf.id) )
advertising_set = await device.create_advertising_set( bigs[f'big{i}']['broadcast_audio_announcement'] = bap.BroadcastAudioAnnouncement(conf.id)
random_address=conf.random_address, advertising_set = await device.create_advertising_set(
advertising_parameters=bumble.device.AdvertisingParameters( random_address=conf.random_address,
advertising_event_properties=bumble.device.AdvertisingEventProperties( advertising_parameters=bumble.device.AdvertisingParameters(
is_connectable=False advertising_event_properties=bumble.device.AdvertisingEventProperties(
), is_connectable=False
primary_advertising_interval_min=round(100),
primary_advertising_interval_max=round(200),
advertising_sid=i,
primary_advertising_phy=hci.Phy.LE_1M, # 2m phy config throws error - because for primary advertising channels, 1mbit is only supported
secondary_advertising_phy=hci.Phy.LE_2M, # this is the secondary advertising beeing send on non advertising channels (extendend advertising)
#advertising_tx_power= # tx power in dbm (max 20)
#secondary_advertising_max_skip=10,
), ),
advertising_data=( primary_advertising_interval_min=round(100),
bigs[f'big{i}']['broadcast_audio_announcement'].get_advertising_data() primary_advertising_interval_max=round(200),
+ bytes( advertising_sid=i,
core.AdvertisingData( primary_advertising_phy=hci.Phy.LE_1M, # 2m phy config throws error - because for primary advertising channels, 1mbit is only supported
[(core.AdvertisingData.BROADCAST_NAME, conf.name.encode())] secondary_advertising_phy=hci.Phy.LE_2M, # this is the secondary advertising beeing send on non advertising channels (extendend advertising)
) #advertising_tx_power= # tx power in dbm (max 20)
#secondary_advertising_max_skip=10,
),
advertising_data=(
bigs[f'big{i}']['broadcast_audio_announcement'].get_advertising_data()
+ bytes(
core.AdvertisingData(
[(core.AdvertisingData.BROADCAST_NAME, conf.name.encode())]
) )
+ advertising_manufacturer_data
),
periodic_advertising_parameters=bumble.device.PeriodicAdvertisingParameters(
periodic_advertising_interval_min=round(80),
periodic_advertising_interval_max=round(160),
),
periodic_advertising_data=bigs[f'big{i}']['basic_audio_announcement'].get_advertising_data(),
auto_restart=True,
auto_start=True,
)
bigs[f'big{i}']['advertising_set'] = advertising_set
logging.info('Start Periodic Advertising')
await advertising_set.start_periodic()
logging.info('Setup BIG')
if global_config.qos_config.iso_int_multiple_10ms == 1:
frame_enable = 0
else:
frame_enable = 1
big = await device.create_big(
bigs[f'big{i}']['advertising_set'],
parameters=bumble.device.BigParameters(
num_bis=1,
sdu_interval=global_config.qos_config.iso_int_multiple_10ms*10000, # Is the same as iso interval
max_sdu=global_config.octets_per_frame,
max_transport_latency=global_config.qos_config.max_transport_latency_ms,
rtn=global_config.qos_config.number_of_retransmissions,
broadcast_code=(
bytes.fromhex(conf.code) if conf.code else None
),
framing=frame_enable # needed if iso interval is not frame interval of codedc
),
)
bigs[f'big{i}']['big'] = big
for bis_link in big.bis_links:
await bis_link.setup_data_path(
direction=bis_link.Direction.HOST_TO_CONTROLLER
) )
+ advertising_manufacturer_data
),
periodic_advertising_parameters=bumble.device.PeriodicAdvertisingParameters(
periodic_advertising_interval_min=round(80),
periodic_advertising_interval_max=round(160),
),
periodic_advertising_data=bigs[f'big{i}']['basic_audio_announcement'].get_advertising_data(),
auto_restart=True,
auto_start=True,
)
bigs[f'big{i}']['advertising_set'] = advertising_set
iso_queue = bumble.device.IsoPacketStream(big.bis_links[0], 64) logging.info('Start Periodic Advertising')
await advertising_set.start_periodic()
logging.info('Setup ISO Data Path') logging.info('Setup BIG')
if global_config.qos_config.iso_int_multiple_10ms == 1:
frame_enable = 0
else:
frame_enable = 1
bigs[f'big{i}']['iso_queue'] = iso_queue big = await device.create_big(
bigs[f'big{i}']['advertising_set'],
parameters=bumble.device.BigParameters(
num_bis=1,
sdu_interval=global_config.qos_config.iso_int_multiple_10ms*10000, # Is the same as iso interval
max_sdu=global_config.octets_per_frame,
max_transport_latency=global_config.qos_config.max_transport_latency_ms,
rtn=global_config.qos_config.number_of_retransmissions,
broadcast_code=(
bytes.fromhex(conf.code) if conf.code else None
),
framing=frame_enable # needed if iso interval is not frame interval of codedc
),
)
bigs[f'big{i}']['big'] = big
logging.debug(f'big{i} parameters are:') for bis_link in big.bis_links:
logging.debug('%s', pprint.pformat(vars(big))) await bis_link.setup_data_path(
logging.debug(f'Finished setup of big{i}.') direction=bis_link.Direction.HOST_TO_CONTROLLER
await asyncio.sleep(i+1) # Wait for advertising to set up
logging.info("Broadcasting...")
def on_flow():
data_packet_queue = iso_queue.data_packet_queue
print(
f'\rPACKETS: pending={data_packet_queue.pending}, '
f'queued={data_packet_queue.queued}, '
f'completed={data_packet_queue.completed}',
end='',
) )
if global_conf.debug: iso_queue = bumble.device.IsoPacketStream(big.bis_links[0], conf.iso_que_len)
bigs[f'big{0}']['iso_queue'][0].data_packet_queue.on('flow', on_flow)
logging.info('Setup ISO Data Path')
bigs[f'big{i}']['iso_queue'] = iso_queue
logging.debug(f'big{i} parameters are:')
logging.debug('%s', pprint.pformat(vars(big)))
logging.debug(f'Finished setup of big{i}.')
await asyncio.sleep(i+1) # Wait for advertising to set up
def on_flow():
data_packet_queue = iso_queue.data_packet_queue
print(
f'\rPACKETS: pending={data_packet_queue.pending}, '
f'queued={data_packet_queue.queued}, '
f'completed={data_packet_queue.completed}',
end='',
)
if global_conf.debug:
bigs[f'big{0}']['iso_queue'].data_packet_queue.on('flow', on_flow)
return bigs
async def setup_audio(
bigs,
global_config : auracast_config.AuracastGlobalConfig,
big_config: List[auracast_config.AuracastBigConfig]
):
for i, big in enumerate(bigs.values()):
audio_source = big_config[i].audio_source
input_format = 'auto'
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
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['pcm_bit_depth'] = pcm_bit_depth
big['lc3_bytes_per_frame'] = lc3_bytes_per_frame
big['lc3_frame_samples'] = lc3_frame_samples
big['audio_input'] = audio_input
big['encoder'] = encoder
async def streamer(bigs):
# TODO: do some pre buffering so the stream is stable from the beginning. One half iso queue would be appropriate
logging.info("Streaming audio...")
while True:
stream_finished = [False for _ in range(len(bigs))]
for i, big in enumerate(bigs.values()): for i, big in enumerate(bigs.values()):
audio_source = big_config[i].audio_source pcm_frame = await anext(big['audio_input'].frames(big['lc3_frame_samples']), None)
input_format = 'auto' if pcm_frame is None: # Not all streams may stop at the same time
audio_input = await audio_io.create_audio_input(audio_source, input_format) stream_finished[i] = True
audio_input.rewind = big_config[i].loop_wav continue
pcm_format = await audio_input.open()
#try: lc3_frame = big['encoder'].encode(
if pcm_format.channels != 1: pcm_frame, num_bytes=big['lc3_bytes_per_frame'], bit_depth=big['pcm_bit_depth']
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 await big['iso_queue'].write(lc3_frame)
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
bigs[f'big{i}']['lc3_frame_samples'] = lc3_frame_samples if all(stream_finished): # Take into account that multiple files have different lengths
bigs[f'big{i}']['audio_input'] = audio_input print('All streams finished, stopping streamer')
bigs[f'big{i}']['encoder'] = encoder break
async def streamer(bigs):
# TODO: do some pre buffering so the stream is stable from the beginning. One half iso queue would be appropriate
while True:
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( #return streamer(bigs)
pcm_frame, num_bytes=lc3_bytes_per_frame, bit_depth=pcm_bit_depth #await stream # running until stream ends
)
await big['iso_queue'].write(lc3_frame) # iso_queue.write(lc3_frame)
if all(stream_finished): # TODO: Take into account that multiple files have different lengths
print('All streams finished, stopping streamer')
break
stream = streamer(bigs)
await stream # running until stream ends
return bigs
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Main # Main
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
async def broadcast(global_conf: auracast_config.AuracastGlobalConfig, big_conf: List[auracast_config.AuracastBigConfig]): async def broadcast(global_conf: auracast_config.AuracastGlobalConfig, big_conf: List[auracast_config.AuracastBigConfig]):
"""Start a broadcast as a source.""" """Start a broadcast."""
ret = await setup_broadcast( async with create_device(global_conf) as device:
global_conf, if not device.supports_le_periodic_advertising:
big_conf logger.error(color('Periodic advertising not supported', 'red'))
) return
bigs = await setup_broadcast( # the bigs dictionary contains all the global configurations
device,
global_conf,
big_conf
)
await setup_audio(
bigs,
global_conf,
big_conf
)
await streamer(bigs)
# make a second coroutine to run the streaming - maybe even use the streamer coroutine # make a second coroutine to run the streaming - maybe even use the streamer coroutine
# start it without await and go into a infinite loop were further instrucations via a ui can be given ? # start it without await and go into a infinite loop were further instrucations via a ui can be given ?
@@ -342,7 +357,7 @@ if __name__ == "__main__":
# global_conf.transport='usb:2fe3:000b' #nrf52dongle hci_usb # TODO: iso packet over usb not supported # global_conf.transport='usb:2fe3:000b' #nrf52dongle hci_usb # TODO: iso packet over usb not supported
# TODO: How can we use other iso interval than 10ms ?(medium or low rel) ? - nrf53audio receiver repports I2S tx underrun # TODO: How can we use other iso interval than 10ms ?(medium or low rel) ? - nrf53audio receiver repports I2S tx underrun
#global_conf.qos_config = auracast_config.qos_config_mono_medium_rel #global_conf.qos_config = auracast_config.qos_config_mono_medium_rel
global_conf.qos_config = auracast_config.qos_config_mono_high_rel global_conf.qos_config = auracast_config.qos_config_mono_high_rel
@@ -366,7 +381,7 @@ if __name__ == "__main__":
global_conf.auracast_sampling_rate_hz = 16000 global_conf.auracast_sampling_rate_hz = 16000
global_conf.octets_per_frame = 40 # 32kbps@16kHz global_conf.octets_per_frame = 40 # 32kbps@16kHz
#global_conf.debug = True #global_conf.debug = True
run_async( run_async(
broadcast( broadcast(
global_conf, global_conf,
@@ -382,4 +397,3 @@ if __name__ == "__main__":
# (realtime audio network uncoded) # (realtime audio network uncoded)
# TODO: add support for playing new files will keeping the advertising running # TODO: add support for playing new files will keeping the advertising running

View File

@@ -4,7 +4,7 @@ version = "0.0.1"
requires-python = ">=3.8" requires-python = ">=3.8"
dependencies = [ dependencies = [
"bumble @ git+ssh://git@ssh.pstruebi.xyz:222/auracaster/bumble.git@e027bcb57a0f29c82e3c02c8bb8691dcb91eac62", "bumble @ git+ssh://git@ssh.pstruebi.xyz:222/auracaster/bumble_mirror.git@e027bcb57a0f29c82e3c02c8bb8691dcb91eac62",
"lc3 @ git+ssh://git@ssh.pstruebi.xyz:222/auracaster/liblc3.git@7558637303106c7ea971e7bb8cedf379d3e08bcc", "lc3 @ git+ssh://git@ssh.pstruebi.xyz:222/auracaster/liblc3.git@7558637303106c7ea971e7bb8cedf379d3e08bcc",
"sounddevice", "sounddevice",
] ]