diff --git a/pi/asound.debian-laptop.conf b/pi/asound.debian-laptop.conf index 9144dde1..cbdb7700 100644 --- a/pi/asound.debian-laptop.conf +++ b/pi/asound.debian-laptop.conf @@ -1,13 +1,17 @@ -# Dedicated ALSA routing for the Debian laptop rover profile. +# Reference ALSA routing for the Debian laptop rover profile. # # This intentionally mirrors pi/asound.conf as closely as a normal PC can: # one fixed hardware card, one dmix playback engine, separate softvol controls # for TTS/horn/forwarded audio, and a raw capture alias for the rover mic. # +# The Debian laptop installer no longer copies this file directly. It renders +# /etc/asound.conf from /etc/roverd-installer.env so laptops with HDMI as card 0 +# can point these same logical mixer devices at their real speaker card. +# # This is NOT meant to preserve normal desktop audio behavior. The laptop rover # installer disables PipeWire/PulseAudio so roverd owns the audio hardware like # the Raspberry Pi rover does. If the laptop's real speaker/mic card is not ALSA -# card 0, change the hw:0,0/card 0 references below to the card shown by: +# card 0, rerun the Debian laptop installer and answer the ALSA prompts using: # aplay -l # arecord -l diff --git a/pi/install/debian_laptop_profile.sh b/pi/install/debian_laptop_profile.sh index 1ab64c8f..e95c6456 100644 --- a/pi/install/debian_laptop_profile.sh +++ b/pi/install/debian_laptop_profile.sh @@ -31,6 +31,8 @@ install_debian_laptop_deps() { ffmpeg alsa-utils v4l-utils ca-certificates flite espeak python3 curl xz-utils unzip libasound2-plugins libgcc-s1 libstdc++6 libc++1-14 libc++abi1-14 } +DEBIAN_LAPTOP_INSTALLER_CONFIG="/etc/roverd-installer.env" + disable_debian_laptop_desktop_audio_stack() { # This profile is for a dedicated rover laptop. PipeWire/PulseAudio are good # desktop defaults, but they can grab the hardware device and make the rover's @@ -60,14 +62,280 @@ disable_debian_laptop_desktop_audio_stack() { pkill -x pulseaudio >/dev/null 2>&1 || true } -install_debian_laptop_audio_support() { - if [[ ! -f pi/asound.debian-laptop.conf ]]; then - log "WARNING: pi/asound.debian-laptop.conf missing; skipping Debian laptop ALSA config install" +derive_debian_laptop_alsa_card_from_device() { + local device="$1" + + # The common ALSA hardware device shape is hw:CARD,DEVICE. Pulling the card + # number from that string gives the installer a useful default while still + # allowing the prompt to handle named cards or uncommon PCM strings. + if [[ "$device" =~ ^hw:([0-9]+),[0-9]+$ ]]; then + printf '%s\n' "${BASH_REMATCH[1]}" return fi - install -m 0644 pi/asound.debian-laptop.conf /etc/asound.conf - log "Installed dedicated Debian laptop ALSA config to /etc/asound.conf" + printf '0\n' +} + +read_debian_laptop_installer_value() { + local prompt="$1" + local default_value="$2" + local value="" + + # Prompting through /dev/tty keeps this usable even when the installer is + # launched through sudo with stdin redirected. The caller already checks for + # an interactive terminal before reaching this function, so failure here is + # genuinely unexpected and should stop the install instead of guessing. + read -r -p "${prompt} [${default_value}]: " value &2 + exit 1 + fi + if [[ ! "$ROVERD_ALSA_CAPTURE_DEVICE" =~ ^[A-Za-z0-9_.,:+/-]+$ ]]; then + echo "Invalid ROVERD_ALSA_CAPTURE_DEVICE: $ROVERD_ALSA_CAPTURE_DEVICE" >&2 + exit 1 + fi + + # Softvol controls and ctl.!default need the playback card, because + # TTSMaster, HornMaster, and ForwardMaster are all playback mixer controls. + # Keep this numeric to match the prompt and avoid needing quoted ALSA card + # ids in the generated config. + if [[ ! "$ROVERD_ALSA_PLAYBACK_CARD" =~ ^[0-9]+$ ]]; then + echo "Invalid ROVERD_ALSA_PLAYBACK_CARD: $ROVERD_ALSA_PLAYBACK_CARD" >&2 + exit 1 + fi +} + +load_debian_laptop_installer_config_file() { + local config_path="$1" + local line key val + + # Read only the small allowlist this installer owns. Avoid sourcing the file + # because it lives in /etc and is meant to be installer data, not shell code. + while IFS= read -r line || [[ -n "$line" ]]; do + [[ "$line" =~ ^[[:space:]]*$ ]] && continue + [[ "$line" =~ ^[[:space:]]*# ]] && continue + + if [[ "$line" =~ ^[[:space:]]*([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then + key="${BASH_REMATCH[1]}" + val="${BASH_REMATCH[2]}" + else + continue + fi + + val="${val#${val%%[![:space:]]*}}" + val="${val%${val##*[![:space:]]}}" + if [[ "$val" =~ ^\".*\"$ ]]; then + val="${val:1:${#val}-2}" + elif [[ "$val" =~ ^\'.*\'$ ]]; then + val="${val:1:${#val}-2}" + fi + + case "$key" in + ROVERD_ALSA_PLAYBACK_DEVICE|ROVERD_ALSA_PLAYBACK_CARD|ROVERD_ALSA_CAPTURE_DEVICE) + printf -v "$key" '%s' "$val" + export "$key" + ;; + esac + done < "$config_path" +} + +write_debian_laptop_installer_config_file() { + local config_path="$1" + local tmp_path + + tmp_path="$(mktemp)" + # This file is intentionally plain KEY=VALUE shell-style data so future + # installs can reuse the same laptop-specific card choices without asking + # again. It is still parsed by an allowlist reader instead of sourced. + cat > "$tmp_path" </dev/tty; } 2>/dev/null; then + echo "Missing $DEBIAN_LAPTOP_INSTALLER_CONFIG and no interactive terminal is available for ALSA setup." >&2 + echo "Run sudo ./pi/install_roverd.sh --debian-laptop once from a terminal, then reuse the generated config for future installs." >&2 + exit 1 + fi + + log "No $DEBIAN_LAPTOP_INSTALLER_CONFIG found; creating Debian laptop ALSA installer config" + if command -v aplay >/dev/null 2>&1; then + echo "Playback devices from aplay -l:" >/dev/tty + aplay -l >/dev/tty 2>/dev/tty || true + fi + if command -v arecord >/dev/null 2>&1; then + echo "Capture devices from arecord -l:" >/dev/tty + arecord -l >/dev/tty 2>/dev/tty || true + fi + + ROVERD_ALSA_PLAYBACK_DEVICE="$(read_debian_laptop_installer_value "ALSA playback device for rover speaker output" "${ROVERD_ALSA_PLAYBACK_DEVICE:-hw:0,0}")" + ROVERD_ALSA_PLAYBACK_CARD="$(read_debian_laptop_installer_value "ALSA playback card number for mixer controls" "${ROVERD_ALSA_PLAYBACK_CARD:-$(derive_debian_laptop_alsa_card_from_device "$ROVERD_ALSA_PLAYBACK_DEVICE")}")" + ROVERD_ALSA_CAPTURE_DEVICE="$(read_debian_laptop_installer_value "ALSA capture device for rover microphone input" "${ROVERD_ALSA_CAPTURE_DEVICE:-$ROVERD_ALSA_PLAYBACK_DEVICE}")" + + validate_debian_laptop_alsa_config + write_debian_laptop_installer_config_file "$DEBIAN_LAPTOP_INSTALLER_CONFIG" + log "Wrote Debian laptop ALSA installer config to $DEBIAN_LAPTOP_INSTALLER_CONFIG" + fi + + ROVERD_ALSA_PLAYBACK_DEVICE="${ROVERD_ALSA_PLAYBACK_DEVICE:-hw:0,0}" + ROVERD_ALSA_PLAYBACK_CARD="${ROVERD_ALSA_PLAYBACK_CARD:-$(derive_debian_laptop_alsa_card_from_device "$ROVERD_ALSA_PLAYBACK_DEVICE")}" + ROVERD_ALSA_CAPTURE_DEVICE="${ROVERD_ALSA_CAPTURE_DEVICE:-$ROVERD_ALSA_PLAYBACK_DEVICE}" + validate_debian_laptop_alsa_config +} + +render_debian_laptop_asound_config() { + local tmp_path + + tmp_path="$(mktemp)" + # The rover-facing ALSA names stay stable even when the laptop's physical + # sound card changes. dmixer owns the one real playback PCM, while tts, + # horn, and forward each wrap that mixer with a separate softvol control. + cat > "$tmp_path" <