toes-audio/setup-audio.sh

739 lines
20 KiB
Bash
Executable File

#!/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=<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 <firmware>/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('<ii', data))
left = [l for l, _ in samples]
right = [r for _, r in samples]
def rms(xs):
return math.sqrt(sum(x * x for x in xs) / len(xs)) if xs else 0.0
def peak(xs):
return max((abs(x) for x in xs), default=0)
l_rms = rms(left)
r_rms = rms(right)
l_peak = peak(left)
r_peak = peak(right)
full_scale = float(2**31 - 1)
print(f"Saved: {path}")
print(f"Rate: {rate} Hz Frames: {frames}")
print(f"Left : rms={int(l_rms):>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"