From 82cbe6c1b7184f2bb0cd2a6dd27ca1fd965a53ca Mon Sep 17 00:00:00 2001 From: pstruebi Date: Fri, 7 Nov 2025 12:37:00 +0100 Subject: [PATCH] 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 --- .gitignore | 6 +-- README.md | 4 ++ src/auracast/multicast.py | 20 +++++--- src/auracast/utils/sounddevice_utils.py | 12 +++-- src/misc/asound.conf | 62 ++++++++++--------------- src/scripts/list_sd_nodes.py | 47 +++++++++++++++---- 6 files changed, 93 insertions(+), 58 deletions(-) diff --git a/.gitignore b/.gitignore index 1170d49..cde643e 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md index 24e2973..0aa2130 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/auracast/multicast.py b/src/auracast/multicast.py index 02203bb..e59433d 100644 --- a/src/auracast/multicast.py +++ b/src/auracast/multicast.py @@ -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( diff --git a/src/auracast/utils/sounddevice_utils.py b/src/auracast/utils/sounddevice_utils.py index 9d7c1f8..2c30e89 100644 --- a/src/auracast/utils/sounddevice_utils.py +++ b/src/auracast/utils/sounddevice_utils.py @@ -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 diff --git a/src/misc/asound.conf b/src/misc/asound.conf index 43e72eb..62fda10 100644 --- a/src/misc/asound.conf +++ b/src/misc/asound.conf @@ -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 +} \ No newline at end of file diff --git a/src/scripts/list_sd_nodes.py b/src/scripts/list_sd_nodes.py index 270eb6a..6c850f7 100644 --- a/src/scripts/list_sd_nodes.py +++ b/src/scripts/list_sd_nodes.py @@ -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)}")