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:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)}")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user