feature/network_audio (#6)

- make the device work standalone with webui
- add support for aes67
- may more things

Co-authored-by: pstruebi <struebin.patrick.com>
Reviewed-on: https://gitea.pstruebi.xyz/auracaster/bumble-auracast/pulls/6
This commit was merged in pull request #6.
This commit is contained in:
2025-08-27 12:43:35 +02:00
parent c368fd5c85
commit 0d72804997
69 changed files with 3827 additions and 189 deletions

View File

@@ -0,0 +1,155 @@
"""
multicast_script
=================
Loads environment variables from a .env file located next to this script
and configures the multicast broadcast. Only UPPERCASE keys are read.
Environment variables
---------------------
- LOG_LEVEL: Logging level for the script.
Default: INFO. Examples: DEBUG, INFO, WARNING, ERROR.
- INPUT: Select audio capture source.
Values:
- "usb" (default): first available USB input device.
- "aes67": select AES67 inputs. Two forms:
* INPUT=aes67 -> first available AES67 input.
* INPUT=aes67,<substr> -> case-insensitive substring match against
the device name, e.g. INPUT=aes67,8f6326.
- BROADCAST_NAME: Name of the broadcast (Auracast BIG name).
Default: "Broadcast0".
- PROGRAM_INFO: Free-text program/broadcast info.
Default: "Some Announcements".
- LANGUATE: ISO 639-3 language code used by config (intentional key name).
Default: "deu".
- PULSE_LATENCY_MSEC: Pulse/PipeWire latency hint in milliseconds.
Default: 3.
Examples (.env)
---------------
LOG_LEVEL=DEBUG
INPUT=aes67,8f6326
BROADCAST_NAME=MyBroadcast
PROGRAM_INFO="Live announcements"
LANGUATE=deu
"""
import logging
import os
import time
from dotenv import load_dotenv
from auracast import multicast
from auracast import auracast_config
from auracast.utils.sounddevice_utils import list_usb_pw_inputs, list_network_pw_inputs
if __name__ == "__main__":
logging.basicConfig( #export LOG_LEVEL=DEBUG
level=os.environ.get('LOG_LEVEL', logging.INFO),
format='%(module)s.py:%(lineno)d %(levelname)s: %(message)s'
)
os.chdir(os.path.dirname(__file__))
# Load .env located next to this script (only uppercase keys will be referenced)
load_dotenv(dotenv_path='.env')
os.environ.setdefault("PULSE_LATENCY_MSEC", "3")
usb_inputs = list_usb_pw_inputs()
logging.info("USB pw inputs:")
for i, d in usb_inputs:
logging.info(f"{i}: {d['name']} in={d['max_input_channels']}")
aes67_inputs = list_network_pw_inputs()
logging.info("AES67 pw inputs:")
for i, d in aes67_inputs:
logging.info(f"{i}: {d['name']} in={d['max_input_channels']}")
# Input selection (usb | aes67). Default to usb.
# Allows specifying an AES67 device by substring: INPUT=aes67,<substring>
# Example: INPUT=aes67,8f6326 will match a device name containing "8f6326".
input_env = os.environ.get('INPUT', 'usb') or 'usb'
parts = [p.strip() for p in input_env.split(',', 1)]
input_mode = (parts[0] or 'usb').lower()
iface_substr = (parts[1].lower() if len(parts) > 1 and parts[1] else None)
selected_dev = None
if input_mode == 'aes67':
if not aes67_inputs and not iface_substr:
# No AES67 inputs and no specific target -> fail fast
raise RuntimeError("No AES67 audio inputs found.")
if iface_substr:
# Loop until a matching AES67 input becomes available
while True:
current = list_network_pw_inputs()
sel = next(((i, d) for i, d in current if iface_substr in (d.get('name','').lower())), None)
if sel:
input_sel = sel[0]
selected_dev = sel[1]
logging.info(f"Selected AES67 input by match '{iface_substr}': index={input_sel}")
break
logging.info(f"Waiting for AES67 input matching '{iface_substr}'... retrying in 2s")
time.sleep(2)
else:
input_sel, selected_dev = aes67_inputs[0]
logging.info(f"Selected first AES67 input: index={input_sel}, device={selected_dev['name']}")
else:
if usb_inputs:
input_sel, selected_dev = usb_inputs[0]
logging.info(f"Selected first USB input: index={input_sel}, device={selected_dev['name']}")
else:
raise RuntimeError("No USB audio inputs found.")
TRANSPORT1 = 'serial:/dev/ttyAMA3,1000000,rtscts' # transport for raspberry pi gpio header
TRANSPORT2 = 'serial:/dev/ttyAMA4,1000000,rtscts' # transport for raspberry pi gpio header
# Capture at 48 kHz to avoid PipeWire resampler latency; encode LC3 at 24 kHz
CAPTURE_SRATE = 48000
LC3_SRATE = 24000
OCTETS_PER_FRAME=60
# Read uppercase-only settings from environment/.env
broadcast_name = os.environ.get('BROADCAST_NAME', 'Broadcast0')
program_info = os.environ.get('PROGRAM_INFO', 'Some Announcements')
# Note: 'LANGUATE' (typo) is intentionally used as requested, maps to config.language
language = os.environ.get('LANGUATE', 'deu')
# Determine capture channel count based on selected device (prefer up to 2)
try:
max_in = int((selected_dev or {}).get('max_input_channels', 1))
except Exception:
max_in = 1
channels = max(1, min(2, max_in))
config = auracast_config.AuracastConfigGroup(
bigs = [
auracast_config.AuracastBigConfig(
name=broadcast_name,
program_info=program_info,
language=language,
iso_que_len=1,
audio_source=f'device:{input_sel}',
input_format=f"int16le,{CAPTURE_SRATE},{channels}",
sampling_frequency=LC3_SRATE,
octets_per_frame=OCTETS_PER_FRAME,
),
#auracast_config.AuracastBigConfigEng(),
],
immediate_rendering=True,
presentation_delay_us=40000,
qos_config=auracast_config.AuracastQosHigh(),
auracast_sampling_rate_hz = LC3_SRATE,
octets_per_frame = OCTETS_PER_FRAME, # 32kbps@16kHz
transport=TRANSPORT1
)
#config.debug = True
multicast.run_async(
multicast.broadcast(
config,
config.bigs
)
)