add basic support for audio over the network
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:]
|
||||
Reference in New Issue
Block a user