#!/bin/bash # setup-audio.sh — Set up barepi audio (TLV320DAC3100 + TDK T5848 mics) on a fresh Pi # Usage: sudo ./setup-audio.sh set -euo pipefail if [ "$(id -u)" -ne 0 ]; then echo "Run as root: sudo ./setup-audio.sh" exit 1 fi TARGET_USER="${TARGET_USER:-${SUDO_USER:-}}" if [ -z "$TARGET_USER" ] || [ "$TARGET_USER" = "root" ]; then echo "Could not determine target user." >&2 echo "Run with sudo from the target account or pass TARGET_USER=." >&2 exit 1 fi TARGET_HOME="$(getent passwd "$TARGET_USER" | cut -d: -f6 || true)" if [ -z "$TARGET_HOME" ] || [ ! -d "$TARGET_HOME" ]; then echo "Could not determine home directory for user: $TARGET_USER" >&2 exit 1 fi CONFIG="/boot/firmware/config.txt" if [ ! -f "$CONFIG" ]; then echo "Could not find $CONFIG" >&2 exit 1 fi FIRMWARE_DIR="$(dirname "$CONFIG")" # Ubuntu 25.10+ piboot-try layout sets "os_prefix=current/" (or similar) in # config.txt, and the firmware reads overlays from that prefix. Older layouts # (e.g. Ubuntu 24.04, Raspberry Pi OS) read directly from /overlays. OS_PREFIX="$(awk -F= '/^[[:space:]]*os_prefix[[:space:]]*=/ { gsub(/[[:space:]]/,"",$2); print $2; exit }' "$CONFIG")" if [ -n "$OS_PREFIX" ]; then OVERLAYS="${FIRMWARE_DIR}/${OS_PREFIX%/}/overlays" else OVERLAYS="${FIRMWARE_DIR}/overlays" fi mkdir -p "$OVERLAYS" RESTORE_HELPER="/usr/local/sbin/barepi-audio-restore" RESTORE_SERVICE="/etc/systemd/system/barepi-audio-restore.service" MODULES_LOAD_FILE="/etc/modules-load.d/barepi-audio.conf" have_audio_modules() { modinfo snd-soc-simple-card >/dev/null 2>&1 \ && modinfo snd-soc-tlv320aic31xx >/dev/null 2>&1 \ && modinfo snd-soc-ics43432 >/dev/null 2>&1 } echo "=== barepi audio setup ===" echo "User: $TARGET_USER Home: $TARGET_HOME" echo "Config: $CONFIG" echo "Overlays: $OVERLAYS${OS_PREFIX:+ (os_prefix=$OS_PREFIX)}" echo "" # 1. Install packages echo "[1/7] Installing packages..." apt-get update apt-get install -y alsa-utils libasound2-plugins swh-plugins i2c-tools device-tree-compiler if ! have_audio_modules; then KERNEL_MODULES_PKG="linux-modules-$(uname -r)" if apt-cache show "$KERNEL_MODULES_PKG" >/dev/null 2>&1; then apt-get install -y "$KERNEL_MODULES_PKG" echo " Installed $KERNEL_MODULES_PKG" fi fi if ! have_audio_modules; then echo "Missing required audio kernel modules: snd-soc-simple-card, snd-soc-tlv320aic31xx, snd-soc-ics43432" >&2 echo "Install a kernel package that provides them (e.g. linux-modules-\$(uname -r)) and rerun." >&2 exit 1 fi { echo "i2c-dev" echo "snd-soc-simple-card" echo "snd-soc-tlv320aic31xx" echo "snd-soc-ics43432" } > "$MODULES_LOAD_FILE" modprobe i2c-dev || true modprobe snd-soc-simple-card || true modprobe snd-soc-tlv320aic31xx || true modprobe snd-soc-ics43432 || true echo " Done." # 2. Compile and install device tree overlay echo "[2/7] Installing device tree overlay..." DTS=$(mktemp /tmp/i2s-audio-XXXX.dts) trap 'rm -f "$DTS"' EXIT cat > "$DTS" << 'DTS_EOF' /dts-v1/; /plugin/; / { compatible = "brcm,bcm2712"; fragment@0 { target = <&i2s_clk_producer>; __overlay__ { status = "okay"; }; }; fragment@1 { target-path = "/"; __overlay__ { tlv320_dvdd: regulator-tlv320-dvdd { compatible = "regulator-fixed"; regulator-name = "tlv320-dvdd-1v8"; regulator-min-microvolt = <1800000>; regulator-max-microvolt = <1800000>; regulator-always-on; }; tlv320_iovdd: regulator-tlv320-iovdd { compatible = "regulator-fixed"; regulator-name = "tlv320-iovdd-3v3"; regulator-min-microvolt = <3300000>; regulator-max-microvolt = <3300000>; regulator-always-on; }; tlv320_avdd: regulator-tlv320-avdd { compatible = "regulator-fixed"; regulator-name = "tlv320-avdd-3v3"; regulator-min-microvolt = <3300000>; regulator-max-microvolt = <3300000>; regulator-always-on; }; tlv320_hpvdd: regulator-tlv320-hpvdd { compatible = "regulator-fixed"; regulator-name = "tlv320-hpvdd-3v3"; regulator-min-microvolt = <3300000>; regulator-max-microvolt = <3300000>; regulator-always-on; }; tlv320_spkvdd: regulator-tlv320-spkvdd { compatible = "regulator-fixed"; regulator-name = "tlv320-spkvdd-5v0"; regulator-min-microvolt = <5000000>; regulator-max-microvolt = <5000000>; regulator-always-on; }; /* * The T5848 is a control-less I2S microphone from Linux's point * of view, so use the generic ICS43432 codec DAI as a simple * capture endpoint and keep the speaker path unchanged. */ mic_codec: t5848-codec { compatible = "invensense,ics43432"; #sound-dai-cells = <0>; }; }; }; fragment@2 { target = <&i2c_arm>; __overlay__ { #address-cells = <1>; #size-cells = <0>; status = "okay"; dac_codec: tlv320dac3100@18 { compatible = "ti,tlv320dac3100"; reg = <0x18>; #sound-dai-cells = <0>; AVDD-supply = <&tlv320_avdd>; IOVDD-supply = <&tlv320_iovdd>; DVDD-supply = <&tlv320_dvdd>; HPVDD-supply = <&tlv320_hpvdd>; SPLVDD-supply = <&tlv320_spkvdd>; SPRVDD-supply = <&tlv320_spkvdd>; reset-gpios = <&gpio 12 1>; /* GPIO_ACTIVE_LOW */ ti,pll-clkin-src = <1>; status = "okay"; }; }; }; fragment@3 { target-path = "/"; __overlay__ { sound { compatible = "simple-audio-card"; #address-cells = <1>; #size-cells = <0>; simple-audio-card,name = "barepi-audio"; simple-audio-card,format = "i2s"; status = "okay"; simple-audio-card,dai-link@0 { reg = <0>; format = "i2s"; bitclock-master = <&cpu0>; frame-master = <&cpu0>; cpu0: cpu { sound-dai = <&i2s_clk_producer>; }; codec0: codec { sound-dai = <&dac_codec>; system-clock-frequency = <1536000>; }; }; simple-audio-card,dai-link@1 { reg = <1>; format = "i2s"; bitclock-master = <&cpu1>; frame-master = <&cpu1>; cpu1: cpu { sound-dai = <&i2s_clk_producer>; }; codec1: codec { sound-dai = <&mic_codec>; }; }; }; }; }; }; DTS_EOF dtc -@ -I dts -O dtb -o "$OVERLAYS/i2s-audio.dtbo" "$DTS" rm -f "$DTS" trap - EXIT echo " Installed $OVERLAYS/i2s-audio.dtbo" # 3. Patch config.txt echo "[3/7] Configuring config.txt..." CHANGED=0 if grep -q '^dtparam=i2c_arm=on' "$CONFIG"; then echo " dtparam=i2c_arm=on already set." elif grep -q '^#.*dtparam=i2c_arm=on' "$CONFIG"; then sed -i 's/^#.*dtparam=i2c_arm=on/dtparam=i2c_arm=on/' "$CONFIG" CHANGED=1 echo " Uncommented dtparam=i2c_arm=on." else if grep -q '^dtparam=audio=on' "$CONFIG"; then sed -i '/^dtparam=audio=on/a dtparam=i2c_arm=on' "$CONFIG" else echo "dtparam=i2c_arm=on" >> "$CONFIG" fi CHANGED=1 echo " Added dtparam=i2c_arm=on." fi if grep -q '^dtparam=spi=on' "$CONFIG"; then echo " dtparam=spi=on already set." elif grep -q '^#.*dtparam=spi=on' "$CONFIG"; then sed -i 's/^#.*dtparam=spi=on/dtparam=spi=on/' "$CONFIG" CHANGED=1 echo " Uncommented dtparam=spi=on." else if grep -q '^dtparam=audio=on' "$CONFIG"; then sed -i '/^dtparam=audio=on/a dtparam=spi=on' "$CONFIG" else echo "dtparam=spi=on" >> "$CONFIG" fi CHANGED=1 echo " Added dtparam=spi=on." fi if grep -q '^dtparam=i2s=on' "$CONFIG"; then echo " dtparam=i2s=on already set." elif grep -q '^#.*dtparam=i2s=on' "$CONFIG"; then sed -i 's/^#.*dtparam=i2s=on/dtparam=i2s=on/' "$CONFIG" CHANGED=1 echo " Uncommented dtparam=i2s=on." else if grep -q '^dtparam=audio=on' "$CONFIG"; then sed -i '/^dtparam=audio=on/a dtparam=i2s=on' "$CONFIG" else echo "dtparam=i2s=on" >> "$CONFIG" fi CHANGED=1 echo " Added dtparam=i2s=on." fi if grep -q '^dtoverlay=i2s-audio' "$CONFIG"; then echo " dtoverlay=i2s-audio already set." elif grep -q '^#.*dtoverlay=i2s-audio' "$CONFIG"; then sed -i 's/^#.*dtoverlay=i2s-audio/dtoverlay=i2s-audio/' "$CONFIG" CHANGED=1 echo " Uncommented dtoverlay=i2s-audio." else if grep -q '^dtparam=i2s=on' "$CONFIG"; then sed -i '/^dtparam=i2s=on/a dtoverlay=i2s-audio' "$CONFIG" else echo "dtoverlay=i2s-audio" >> "$CONFIG" fi CHANGED=1 echo " Added dtoverlay=i2s-audio." fi if grep -q '^gpio=12=op,dh' "$CONFIG"; then echo " gpio=12=op,dh already set." elif grep -q '^#.*gpio=12=op,dh' "$CONFIG"; then sed -i 's/^#.*gpio=12=op,dh/gpio=12=op,dh/' "$CONFIG" CHANGED=1 echo " Uncommented gpio=12=op,dh." elif grep -q '^gpio=12=' "$CONFIG"; then echo " Existing gpio=12=... line found; left unchanged." >&2 echo " Make sure it keeps the DAC out of reset (gpio=12=op,dh)." >&2 else if grep -q '^dtoverlay=i2s-audio' "$CONFIG"; then sed -i '/^dtoverlay=i2s-audio/a gpio=12=op,dh' "$CONFIG" elif grep -q '^dtparam=i2s=on' "$CONFIG"; then sed -i '/^dtparam=i2s=on/a gpio=12=op,dh' "$CONFIG" else echo "gpio=12=op,dh" >> "$CONFIG" fi CHANGED=1 echo " Added gpio=12=op,dh." fi # 4. Install .asoundrc echo "[4/7] Installing .asoundrc..." cat > "$TARGET_HOME/.asoundrc" << 'ASOUNDRC' defaults.pcm.rate_converter "samplerate_best" pcm.!default { type plug slave { pcm "filtered" rate 48000 } } pcm.filtered { type ladspa slave.pcm "out" path "/usr/lib/ladspa" plugins [ { label highpass_iir input { controls [ 100 1 ] } } { label hardLimiter input { controls [ 0 1 0 ] } } ] } pcm.out { type plug slave { pcm "hw:CARD=barepiaudio,DEV=1" format S16_LE } } pcm.barepi_playback { type plug slave { pcm "out" rate 48000 } } pcm.barepi_capture { type plug slave { pcm "hw:CARD=barepiaudio,DEV=0" format S32_LE rate 48000 channels 2 } } ctl.!default { type hw card barepiaudio } ASOUNDRC chown "$TARGET_USER:$TARGET_USER" "$TARGET_HOME/.asoundrc" echo " Installed $TARGET_HOME/.asoundrc" # 5. Install volume.sh echo "[5/7] Installing volume.sh..." cat > "$TARGET_HOME/volume.sh" << 'VOLSCRIPT' #!/bin/bash # DAC3100 speaker-path helper # Usage: ./volume.sh [dac] [driver] [analog] # dac: 0-175 (digital volume, 127=0dB) # driver: 0-3 (class-D gain: 6/12/18/24 dB) # analog: 0-127 (analog volume, 127=0dB) set -euo pipefail CARD_ID="barepiaudio" CARD_NAME="barepi-audio" DEFAULT_DAC=127 DEFAULT_DRIVER=0 DEFAULT_ANALOG=50 find_card() { awk -v id="$CARD_ID" -v name="$CARD_NAME" ' index($0, "[" id "]") || index($0, name) { print $1; exit } ' /proc/asound/cards } show_status() { echo "Using ALSA card: $CARD ($CARD_NAME)" amixer -c "$CARD" sget 'DAC' || true amixer -c "$CARD" sget 'DAC Left Input' || true amixer -c "$CARD" sget 'DAC Right Input' || true amixer -c "$CARD" sget 'Output Left From Left DAC' || true amixer -c "$CARD" sget 'Output Right From Right DAC' || true amixer -c "$CARD" sget 'Speaker' || true amixer -c "$CARD" sget 'Speaker Driver' || true amixer -c "$CARD" sget 'Speaker Analog' || true } enable_speaker_path() { echo "Enabling TLV320 speaker path..." amixer -c "$CARD" sset 'DAC Left Input' 'Left Data' >/dev/null || true amixer -c "$CARD" sset 'DAC Right Input' 'Right Data' >/dev/null || true amixer -c "$CARD" sset 'Output Left From Left DAC' on >/dev/null || true amixer -c "$CARD" sset 'Output Right From Right DAC' on >/dev/null || true amixer -c "$CARD" sset 'Speaker' on >/dev/null || true amixer -c "$CARD" sset 'HP Left' off >/dev/null || true amixer -c "$CARD" sset 'HP Right' off >/dev/null || true amixer -c "$CARD" sset 'HP Driver' 0 >/dev/null || true amixer -c "$CARD" sset 'HP Driver' off >/dev/null || true } CARD="$(find_card || true)" if [ -z "$CARD" ]; then echo "Could not find ALSA card for $CARD_NAME." >&2 echo "Reboot after setup, then check /proc/asound/cards." >&2 exit 1 fi if [ $# -eq 0 ]; then show_status exit 0 fi DAC=${1:-$DEFAULT_DAC} DRIVER=${2:-$DEFAULT_DRIVER} ANALOG=${3:-$DEFAULT_ANALOG} enable_speaker_path echo "Setting levels..." amixer -c "$CARD" sset 'DAC' "${DAC},${DAC}" >/dev/null amixer -c "$CARD" sset 'Speaker Driver' "$DRIVER" >/dev/null || true amixer -c "$CARD" sset 'Speaker Driver' on >/dev/null || true amixer -c "$CARD" sset 'Speaker Analog' "$ANALOG" >/dev/null || true alsactl store "$CARD" >/dev/null 2>&1 || alsactl store >/dev/null 2>&1 || true echo "" echo "Using ALSA card: $CARD ($CARD_NAME)" echo "Set: dac=$DAC driver=$DRIVER analog=$ANALOG" echo "Example: ./volume.sh 127 0 50" VOLSCRIPT chown "$TARGET_USER:$TARGET_USER" "$TARGET_HOME/volume.sh" chmod +x "$TARGET_HOME/volume.sh" echo " Installed $TARGET_HOME/volume.sh" # 6. Install a mic test helper echo "[6/7] Installing test-mics.sh..." cat > "$TARGET_HOME/test-mics.sh" << 'MICTEST' #!/bin/bash # Capture a short stereo sample from the barepi mic path and print per-channel stats. # Usage: # ./test-mics.sh [seconds] [out.wav] # ./test-mics.sh --thsel-high [seconds] [out.wav] # ./test-mics.sh --thsel-low [seconds] [out.wav] set -euo pipefail RATE=48000 FORMAT=S32_LE CHANNELS=2 DURATION=3 OUT="" THSEL="" CAPTURE_PCM="${CAPTURE_PCM:-barepi_capture}" usage() { cat <<'EOF' Usage: ./test-mics.sh [seconds] [out.wav] ./test-mics.sh --thsel-high [seconds] [out.wav] ./test-mics.sh --thsel-low [seconds] [out.wav] EOF } set_thsel() { local level="$1" command -v pinctrl >/dev/null 2>&1 || return 0 if [ "$level" = "high" ]; then sudo pinctrl set 8 op pn dh >/dev/null else sudo pinctrl set 8 op pn dl >/dev/null fi } prepare_wake_pin() { command -v pinctrl >/dev/null 2>&1 || return 0 sudo pinctrl set 13 ip pn >/dev/null 2>&1 || true } show_pins() { command -v pinctrl >/dev/null 2>&1 || return 0 sudo pinctrl get 8,13 || true } positional=() while [ $# -gt 0 ]; do case "$1" in --thsel-high) THSEL="high" shift ;; --thsel-low) THSEL="low" shift ;; -h|--help) usage exit 0 ;; *) positional+=("$1") shift ;; esac done if [ ${#positional[@]} -ge 1 ]; then DURATION="${positional[0]}" fi if [ ${#positional[@]} -ge 2 ]; then OUT="${positional[1]}" fi if [ -z "$OUT" ]; then OUT="/tmp/barepi-mics-$(date +%Y%m%d-%H%M%S).wav" fi if ! arecord -L 2>/dev/null | grep -qx 'barepi_capture'; then CAPTURE_PCM="plughw:CARD=barepiaudio,DEV=0" fi if [ -n "$THSEL" ]; then set_thsel "$THSEL" fi prepare_wake_pin show_pins echo "Recording ${DURATION}s from ${CAPTURE_PCM} -> ${OUT}" arecord -q -D "$CAPTURE_PCM" -f "$FORMAT" -c "$CHANNELS" -r "$RATE" -d "$DURATION" "$OUT" python3 - <<'PY' "$OUT" import math import struct import sys import wave path = sys.argv[1] with wave.open(path, 'rb') as w: channels = w.getnchannels() width = w.getsampwidth() rate = w.getframerate() frames = w.getnframes() data = w.readframes(frames) if channels != 2 or width != 4: print(f"Recorded {channels}ch / {width * 8}bit at {rate} Hz") sys.exit(0) samples = list(struct.iter_unpack('10} peak={l_peak:>10} peak_dBFS={20*math.log10(max(l_peak,1)/full_scale):6.1f}") print(f"Right: rms={int(r_rms):>10} peak={r_peak:>10} peak_dBFS={20*math.log10(max(r_peak,1)/full_scale):6.1f}") PY MICTEST chown "$TARGET_USER:$TARGET_USER" "$TARGET_HOME/test-mics.sh" chmod +x "$TARGET_HOME/test-mics.sh" echo " Installed $TARGET_HOME/test-mics.sh" # 7. Install a boot-time restore helper so the speaker path comes up after reboot echo "[7/7] Installing boot-time mixer restore..." cat > "$RESTORE_HELPER" << 'RESTORESCRIPT' #!/bin/bash set -euo pipefail CARD_ID="barepiaudio" CARD_NAME="barepi-audio" I2C_BUS="${I2C_BUS:-1}" CODEC_ADDR="${CODEC_ADDR:-0x18}" MIC_THSEL_LEVEL="${MIC_THSEL_LEVEL:-high}" DAC=${1:-127} DRIVER=${2:-0} ANALOG=${3:-50} find_card() { awk -v id="$CARD_ID" -v name="$CARD_NAME" ' index($0, "[" id "]") || index($0, name) { print $1; exit } ' /proc/asound/cards } codec_ready() { i2cget -y -f "$I2C_BUS" "$CODEC_ADDR" 0 >/dev/null 2>&1 } set_mixer() { amixer -q -c "$CARD" "$@" >/dev/null 2>&1 || true } set_mic_pins() { if ! command -v pinctrl >/dev/null 2>&1; then return 0 fi case "$MIC_THSEL_LEVEL" in low) pinctrl set 8 op pn dl >/dev/null 2>&1 || true ;; *) pinctrl set 8 op pn dh >/dev/null 2>&1 || true ;; esac pinctrl set 13 ip pn >/dev/null 2>&1 || true } CARD="" for _ in $(seq 1 20); do CARD="$(find_card || true)" if [ -n "$CARD" ]; then break fi sleep 1 done if [ -z "$CARD" ]; then echo "barepi-audio card not present; skipping restore." exit 0 fi for _ in $(seq 1 20); do if codec_ready; then break fi sleep 1 done if ! codec_ready; then echo "TLV320 codec not responding on i2c-${I2C_BUS} addr ${CODEC_ADDR}; skipping restore." exit 0 fi set_mic_pins set_mixer sset 'DAC Left Input' 'Left Data' set_mixer sset 'DAC Right Input' 'Right Data' set_mixer sset 'Output Left From Left DAC' on set_mixer sset 'Output Right From Right DAC' on set_mixer sset 'Speaker' on set_mixer sset 'Speaker Driver' "$DRIVER" set_mixer sset 'Speaker Driver' on set_mixer sset 'Speaker Analog' "$ANALOG" set_mixer sset 'DAC' "${DAC},${DAC}" set_mixer sset 'HP Left' off set_mixer sset 'HP Right' off set_mixer sset 'HP Driver' '0,0' set_mixer sset 'HP Driver' off RESTORESCRIPT chmod 755 "$RESTORE_HELPER" cat > "$RESTORE_SERVICE" << EOF [Unit] Description=Restore barepi TLV320 speaker routing After=sound.target Wants=sound.target [Service] Type=oneshot ExecStart=$RESTORE_HELPER 127 0 50 [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable barepi-audio-restore.service >/dev/null if grep -q '\[barepiaudio\]' /proc/asound/cards 2>/dev/null || grep -q 'barepi-audio' /proc/asound/cards 2>/dev/null; then "$RESTORE_HELPER" 127 0 50 || true echo " Applied mixer defaults to the live barepi-audio card." else echo " Live barepi-audio card not present yet; defaults will be applied at boot." fi echo "" echo "=== Done ===" if [ "$CHANGED" -eq 1 ]; then echo "config.txt was modified." fi echo "Reboot to load the updated overlay and audio module state." echo "Speaker routing defaults and mic GPIO defaults will be restored automatically after boot." echo "Manual speaker check/adjust: $TARGET_HOME/volume.sh" echo "Manual mic test: $TARGET_HOME/test-mics.sh"