refactor: simplify ALSA audio configuration and improve device detection

- Replaced complex multi-PCM asound.conf with streamlined dsnoop-based ch1/ch2 devices using matched 5ms periods
- Updated sounddevice utilities to recognize dsnoop and hw:X,Y patterns for better device filtering
- Adjusted audio input latency parameters to align with new ALSA period configuration
This commit is contained in:
2025-11-07 12:37:00 +01:00
parent a83ca982cf
commit 82cbe6c1b7
6 changed files with 93 additions and 58 deletions

6
.gitignore vendored
View File

@@ -45,7 +45,5 @@ src/auracast/.env
src/auracast/server/certs/ca/ca_cert.srl
src/auracast/server/credentials.json
pcm1862-i2s.dtbo
test.wav
both.wav
vin1.wav
vin2.wav
ch1.wav
ch2.wav

View File

@@ -218,6 +218,10 @@ sudo ldconfig # refresh linker cache
- echo i2c-dev | sudo tee -a /etc/modules
- read temp /src/scripts/temp
# test recording
arecord -f cd -c 1 -D record_left left.wav -r48000
arecord -f cd -c 1 -D record_right right.wav -r48000
# Known issues:
- When running on a laptop there might be issues switching between usb and browser audio input since they use the same audio device

View File

@@ -84,17 +84,26 @@ class ModSoundDeviceAudioInput(audio_io.SoundDeviceAudioInput):
logging.warning("Failed to query sounddevice backend/device info: %s", e)
# Create RawInputStream with injected low-latency parameters
# Match dsnoop period (5 ms @ 48 kHz -> 240 frames). For other rates, keep 5 ms.
_sr = int(self._pcm_format.sample_rate)
_block = max(24, _sr // 200)
_extra = None
try:
_extra = sd.AlsaSettings(period_size=_block, periods=2)
except Exception:
_extra = None
self._stream = sd.RawInputStream(
samplerate=self._pcm_format.sample_rate,
device=self._device,
channels=self._pcm_format.channels,
dtype='int16',
blocksize=240, # Match frame size
latency=0.010,
blocksize=_block,
latency=0.005,
extra_settings=_extra,
)
self._stream.start()
logging.info(f"SoundDeviceAudioInput: Opened with blocksize=240, latency=0.010 (10ms)")
logging.info(f"SoundDeviceAudioInput: Opened with blocksize={_block}, latency=0.010 (~10ms target)")
return audio_io.PcmFormat(
audio_io.PcmFormat.Endianness.LITTLE,
@@ -686,9 +695,9 @@ class Streamer():
enable_drift_compensation = getattr(global_config, 'enable_adaptive_frame_dropping', False)
# Hardcoded parameters (unit: milliseconds)
drift_threshold_ms = 2.0 if enable_drift_compensation else 0.0
static_drop_ms = 1 if enable_drift_compensation else 0.0
static_drop_ms = drift_threshold_ms if enable_drift_compensation else 0.0
# Guard interval measured in LC3 frames (10 ms each); 50 => 500 ms cooldown
discard_guard_frames = int(2*sample_rate / 1000) if enable_drift_compensation else 0
discard_guard_frames = int(sample_rate / 1000) // 2 if enable_drift_compensation else 0
# Derived sample counts
drop_threshold_samples = int(sample_rate * drift_threshold_ms / 1000.0)
static_drop_samples = int(sample_rate * static_drop_ms / 1000.0)
@@ -1126,7 +1135,6 @@ if __name__ == "__main__":
#config.debug = True
# Enable clock drift compensation to prevent latency accumulation
# With ~43 samples/sec drift (0.89ms/sec), threshold of 2ms will trigger every ~2.2 seconds
run_async(
broadcast(

View File

@@ -232,13 +232,19 @@ def get_alsa_usb_inputs():
name = dev.get('name', '').lower()
# Filter for USB devices based on common patterns:
# - Contains 'usb' in the name
# - hw:X,Y pattern (ALSA hardware devices)
# - hw:X or hw:X,Y pattern present anywhere in name (ALSA hardware devices)
# - dsnoop/ch1/ch2 convenience entries from asound.conf
# Exclude: default, dmix, pulse, pipewire, sysdefault
if any(exclude in name for exclude in ['default', 'dmix', 'pulse', 'pipewire', 'sysdefault']):
continue
# Include if it has 'usb' in name or matches hw:X pattern
if 'usb' in name or re.match(r'hw:\d+', name):
# Include if it has 'usb' or contains an hw:* token, or matches common dsnoop/mono aliases
if (
'usb' in name or
re.search(r'hw:\d+(?:,\d+)?', name) or
name.startswith('dsnoop') or
name in ('ch1', 'ch2')
):
usb_inputs.append((idx, dev))
return usb_inputs

View File

@@ -1,39 +1,27 @@
# --- raw HW by card ID (no sharing) ---
pcm.adc_hw_stereo {
type hw
card "i2s" # from `arecord -l`: card 2: i2s [pcm1862 on i2s]
device 0
pcm.ch1 {
type dsnoop
ipc_key 234884
slave {
pcm "hw:CARD=i2s,DEV=0"
channels 2
rate 48000
format S16_LE
period_size 240 # 5 ms @ 48 kHz
buffer_size 480 # 2 periods (≈10 ms total)
}
bindings.0 0
}
# --- mono splits (do the routing here) ---
pcm.adc_in1 {
type route
slave.pcm "adc_hw_stereo"
slave.channels 2
ttable.0.0 1.0 # left -> mono
ttable.1.0 0.0
}
pcm.adc_in2 {
type route
slave.pcm "adc_hw_stereo"
slave.channels 2
ttable.0.0 0.0
ttable.1.0 1.0 # right -> mono
}
# --- diagnostics: exact slot taps (bypass any sharing/conversion) ---
pcm.adc_ch0 {
type route
slave.pcm "adc_hw_stereo"
slave.channels 2
ttable.0.0 1.0
ttable.1.0 0.0
}
pcm.adc_ch1 {
type route
slave.pcm "adc_hw_stereo"
slave.channels 2
ttable.0.0 0.0
ttable.1.0 1.0
}
pcm.ch2 {
type dsnoop
ipc_key 2241234
slave {
pcm "hw:CARD=i2s,DEV=0"
channels 2
rate 48000
format S16_LE
period_size 240 # 5 ms @ 48 kHz
buffer_size 480 # 2 periods (≈10 ms total)
}
bindings.0 1
}

View File

@@ -1,16 +1,47 @@
import sounddevice as sd, pprint
from auracast.utils.sounddevice_utils import devices_by_backend
from auracast.utils.sounddevice_utils import (
devices_by_backend,
get_alsa_inputs,
get_alsa_usb_inputs,
get_network_pw_inputs,
refresh_pw_cache,
)
print("PortAudio library:", sd._libname)
print("PortAudio version:", sd.get_portaudio_version())
print("\nHost APIs:")
pprint.pprint(sd.query_hostapis())
print("\nDevices:")
pprint.pprint(sd.query_devices())
apis = sd.query_hostapis()
pprint.pprint(apis)
# Example: only PulseAudio devices on Linux
print("\nOnly PulseAudio devices:")
for i, d in devices_by_backend("PulseAudio"):
print(f"{i}: {d['name']} in={d['max_input_channels']} out={d['max_output_channels']}")
print("\nAll Devices (with host API name):")
devs = sd.query_devices()
for i, d in enumerate(devs):
ha_name = apis[d['hostapi']]['name'] if isinstance(d.get('hostapi'), int) and d['hostapi'] < len(apis) else '?'
if d.get('max_input_channels', 0) > 0:
print(f"IN {i:>3}: {d['name']} api={ha_name} in={d['max_input_channels']}")
elif d.get('max_output_channels', 0) > 0:
print(f"OUT {i:>3}: {d['name']} api={ha_name} out={d['max_output_channels']}")
else:
print(f"DEV {i:>3}: {d['name']} api={ha_name} (no I/O)")
print("\nALSA input devices (PortAudio ALSA host):")
for i, d in devices_by_backend('ALSA'):
if d.get('max_input_channels', 0) > 0:
print(f"ALSA {i:>3}: {d['name']} in={d['max_input_channels']}")
print("\nALSA USB-filtered inputs:")
for i, d in get_alsa_usb_inputs():
print(f"USB {i:>3}: {d['name']} in={d['max_input_channels']}")
print("\nRefreshing PipeWire caches...")
try:
refresh_pw_cache()
except Exception:
pass
print("PipeWire Network inputs (from cache):")
for i, d in get_network_pw_inputs():
print(f"NET {i:>3}: {d['name']} in={d.get('max_input_channels', 0)}")