add basic support for audio over the network

This commit is contained in:
2025-05-22 15:26:00 +02:00
parent c368fd5c85
commit c778681d4c
4 changed files with 161 additions and 7 deletions
+26 -5
View File
@@ -49,6 +49,7 @@ from bumble.audio import io as audio_io
from auracast import auracast_config
from auracast.utils.read_lc3_file import read_lc3_file
from auracast.network_audio_receiver import NetworkAudioReceiverUncoded
# modified from bumble
@@ -339,8 +340,25 @@ class Streamer():
audio_source = big_config[i].audio_source
input_format = big_config[i].input_format
# --- New: network_uncoded mode using NetworkAudioReceiver ---
if isinstance(audio_source, NetworkAudioReceiverUncoded):
# Start the UDP receiver coroutine so packets are actually received
asyncio.create_task(audio_source.receive())
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=audio_source.samplerate,
)
lc3_frame_samples = encoder.get_frame_samples()
big['pcm_bit_depth'] = 16
big['lc3_frame_samples'] = lc3_frame_samples
big['audio_input'] = audio_source
big['encoder'] = encoder
big['precoded'] = False
# precoded lc3 from ram
if isinstance(big_config[i].audio_source, bytes):
elif isinstance(big_config[i].audio_source, bytes):
big['precoded'] = True
lc3_frames = iter(big_config[i].audio_source)
@@ -536,16 +554,19 @@ if __name__ == "__main__":
#config.transport='serial:/dev/serial/by-id/usb-SEGGER_J-Link_001057705357-if02,1000000,rtscts' # transport for nrf54l15dk
#config.transport='serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_95A087EADB030B24-if00,115200,rtscts' #nrf52dongle hci_uart usb cdc
#config.transport='usb:2fe3:000b' #nrf52dongle hci_usb # TODO: iso packet over usb not supported
#config.transport= 'auto'
config.transport='serial:/dev/ttyAMA2,1000000,rtscts' # transport for raspberry pi
config.transport= 'auto'
#config.transport='serial:/dev/ttyAMA2,1000000,rtscts' # transport for raspberry pi
for big in config.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
#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
# --- Network_uncoded mode using NetworkAudioReceiver ---
big.audio_source = NetworkAudioReceiverUncoded(port=50007, samplerate=16000, channels=1, chunk_size=1024)
# 16kHz works reliably with 3 streams
# 24kHz is only working with 2 streams - probably airtime constraint
+67
View File
@@ -0,0 +1,67 @@
import asyncio
import socket
import logging
import numpy as np
from typing import AsyncGenerator
class NetworkAudioReceiverUncoded:
"""
Receives PCM audio over UDP and provides an async generator interface for uncoded PCM frames.
Combines network receiving and input logic for use with Auracast streamer.
"""
def __init__(self, port: int = 50007, samplerate: int = 16000, channels: int = 1, chunk_size: int = 1024):
self.port = port
self.samplerate = samplerate
self.channels = channels
self.chunk_size = chunk_size
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.bind(('0.0.0.0', self.port))
self.sock.setblocking(False)
self._running = False
# Reduce queue size for lower latency (less buffering)
self._queue = asyncio.Queue(maxsize=2) # Was 20
async def receive(self):
self._running = True
logging.info(f"NetworkAudioReceiver listening on UDP port {self.port}")
try:
while self._running:
try:
data, _ = await asyncio.get_event_loop().sock_recvfrom(self.sock, self.chunk_size * 2)
await self._queue.put(data)
except Exception:
await asyncio.sleep(0.01)
finally:
self.sock.close()
logging.info("NetworkAudioReceiver stopped.")
def stop(self):
self._running = False
async def open(self):
# Dummy PCM format object
class PCMFormat:
channels = self.channels
sample_type = 'int16'
sample_rate = self.samplerate
return PCMFormat()
def rewind(self):
pass # Not supported for live network input
async def frames(self, samples_per_frame: int) -> AsyncGenerator[np.ndarray, None]:
bytes_per_frame = samples_per_frame * 2 * self.channels # 2 bytes for int16
buf = bytearray()
while True:
data = await self._queue.get()
# Optional: log queue size for latency debugging
# logging.debug(f'NetworkAudioReceiver queue size: {self._queue.qsize()}')
if data is None:
break
buf.extend(data)
while len(buf) >= bytes_per_frame:
frame = np.frombuffer(buf[:bytes_per_frame], dtype=np.int16).reshape(-1, self.channels)
# Optional: log when a frame is yielded
# logging.debug(f'Yielding frame of shape {frame.shape}')
yield frame
buf = buf[bytes_per_frame:]