Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ myenv
# installation flags and installed libraries
installers/i2s_mic_installed.flag
installers/i2s_speech_recognition_installed.flag
installers/audio/backups/*

speech.wav
24 changes: 24 additions & 0 deletions environments/voice.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Laptop β€” local development and testing environment with no hardware modules.
# Useful for developing, testing, and running the Discord bot locally.
# Matches modules configured with environment: 'laptop' or without 'robot' restriction.

# ── Core ──────────────────────────────────────────────────────────────────────

messaging_service:
enabled: true

logwrapper:
enabled: true

speechinput:
enabled: true
config:
wake_word: ['cody','kodi'] # if set, will only process speech that contains this word. Can be used to avoid processing random speech in the environment.
start_on_boot: true

chatgpt:
enabled: true

ttsmodule:
enabled: false # need to add credit

22 changes: 22 additions & 0 deletions install_audio.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

sudo python3 -m pip install --upgrade click --break-system-packages

sudo python3 -m pip install --upgrade setuptools --break-system-packages
sudo python3 -m pip install --upgrade adafruit-python-shell --break-system-packages

sudo python3 installers/i2samp.py

# Example aplay -l output on CM5 before googlevoicehat overlay takes effect (first boot after install):
# card 0: vc4hdmi0 [vc4-hdmi-0], device 0: MAI PCM i2s-hifi-0 [MAI PCM i2s-hifi-0]
# card 1: vc4hdmi1 [vc4-hdmi-1], device 0: MAI PCM i2s-hifi-0 [MAI PCM i2s-hifi-0]
# card 2: I2S [Dual I2S], device 0: 1f000a0000.i2s-dit-hifi dit-hifi-0 [...]
#
# After reboot with googlevoicehat overlay active - card numbers can vary between boots!
# card 0: vc4hdmi0 [vc4-hdmi-0], ...
# card 1: vc4hdmi1 [vc4-hdmi-1], ...
# card 2: sndrpigooglevoi [snd_rpi_googlevoicehat_soundcar], device 0: Google voiceHAT SoundCard HiFi ...
# OR (on a subsequent restart):
# card 1: sndrpigooglevoi [snd_rpi_googlevoicehat_soundcar], device 0: Google voiceHAT SoundCard HiFi ...
# card 2: vc4hdmi1 [vc4-hdmi-1], ...
#
# This is why asound.conf must reference the card by NAME (sndrpigooglevoi) not by number.
163 changes: 163 additions & 0 deletions installers/audio/Audio.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# I2S Audio β€” CM5 Carrier Board

## Hardware

| Signal | GPIO | Connected to |
|--------|------|--------------|
| BCLK (I2S clock) | 18 | MAX98357A + ICS-43434 |
| LRCLK (word select) | 19 | MAX98357A + ICS-43434 |
| SDO (TX, data out) | 21 | MAX98357A DIN |
| SDI (RX, data in) | 20 | ICS-43434 DATA (both mics) |
| SD_MODE (amp enable) | 16 | MAX98357A SD |

Two ICS-43434 I2S microphones share the same DATA line (GPIO 20). One has its L/R SELECT pin tied to GND (left channel), the other to 3.3 V (right channel), so they form a stereo pair on a single wire.

The CM5 I2S controller is the Synopsys DesignWare (`snps,designware-i2s`) on the RP1 chip. It supports **`S32_LE` at `48000 Hz` only**.

---

## Device Tree Overlay

The kernel learns about hardware connections through a **Device Tree overlay** β€” a small binary patch applied at boot that tells the kernel which drivers to load and how to wire them together.

### Files

| File | Purpose |
|------|---------|
| `installers/audio/cm5-i2s-audio.dts` | Human-readable source describing the hardware |
| `cm5-i2s-audio.dtbo` (compiled from above) | Binary patch applied by the bootloader β€” installed to `/boot/firmware/overlays/` |

The DTS source creates a `simple-audio-card` with **two DAI (Digital Audio Interface) links** on the same I2S controller:

- **DAI link 0** β†’ `invensense,ics43432` codec driver β†’ ALSA PCM **device 0** (capture)
- **DAI link 1** β†’ `maxim,max98357a` codec driver β†’ ALSA PCM **device 1** (playback)

### Why not `googlevoicehat-soundcard`?

The `voicehat-codec` driver only defines a speaker output in its DAPM (Dynamic Audio Power Management) routing. DAPM is how the kernel knows which hardware paths to power up for a given operation. Because the voicehat codec had no capture DAPM route, the RP1's I2S RX engine was never enabled β€” microphone recordings returned silence regardless of wiring.

The new overlay uses dedicated drivers (`max98357a`, `ics43432`) that have correct DAPM routes for both playback and capture.

### Installing the overlay

`install-audio.sh` (run from the `installers/` directory on the device) compiles the DTS and installs it:

```bash
cd ~/modular-biped/installers
bash install-audio.sh
sudo reboot
```

Requires `dtc` (`sudo apt install device-tree-compiler` if missing).

### `config.txt` entry

```
dtoverlay=cm5-i2s-audio
hdmi_ignore_edid_audio=1
#dtparam=audio=on ← keep disabled
```

---

## ALSA Configuration (`/etc/asound.conf`)

After reboot the sound card appears as:

```
card 0: cm5i2saudio [cm5-i2s-audio]
device 0: ics43432-hifi (capture)
device 1: MAX98357A HiFi (playback)
```

The ALSA card number can change between boots when HDMI devices are present, so always reference the card by **name** (`cm5i2saudio`).

`asound.conf` defines a software pipeline on top of the hardware:

- **`pcm.speakerbonnet`** β€” raw hardware playback (`hw:cm5i2saudio,1`)
- **`pcm.dmixer`** β€” software mixer so multiple apps can play simultaneously (`type dmix`)
- **`pcm.softvol`** β€” software volume control on top of dmixer
- **`pcm.mic_plug`** β€” capture with format conversion (`type plug` over `hw:cm5i2saudio,0`), so apps that request `S16_LE` are transparently converted to the `S32_LE` the hardware requires
- **`pcm.!default`** β€” `type asym` routes playback and capture independently (dmix/softvol are playback-only; without `asym`, capture would be blocked)

On Raspberry Pi OS Bookworm, PipeWire is the default audio server and overrides `pcm.!default` for most applications. The `asound.conf` chain is used by CLI tools (`aplay`, `arecord`) that access ALSA directly.

---

## Testing

**List devices:**
```bash
aplay -l # shows device 1 (MAX98357A, playback)
arecord -l # shows device 0 (ICS-43434, capture)
```

**Speaker test (direct hardware, both channels):**
```bash
speaker-test -D hw:cm5i2saudio,1 -c 2 -r 48000 -t sine -l 1
```

**Microphone test (record 3 s, play back):**
```bash
arecord -D hw:cm5i2saudio,0 -f S32_LE -c 2 -r 48000 -d 3 test.wav
aplay -D hw:cm5i2saudio,1 test.wav
```

**Via default device (through PipeWire or asound.conf chain):**
```bash
arecord -D default -f S16_LE -c 2 -r 48000 -d 3 /tmp/test.wav
aplay -D default /tmp/test.wav
```

---

## Speech Recognition (`SpeechInput` module)

The `SpeechInput` module uses the `speech_recognition` library, which requests audio at **16000 Hz**. The hardware only supports 48000 Hz, so a device that handles resampling is required.

The module is configured to use the `pulse` device, which is PipeWire's PulseAudio compatibility interface. This is the correct choice because:

- The direct hardware device (`hw:cm5i2saudio,0`) rejects any sample rate other than 48000 Hz β€” `speech_recognition`'s 16000 Hz request will fail immediately.
- The `pulse` device routes through PipeWire, which resamples transparently between whatever the application requests and what the hardware needs.

---

## `aplay.service`

This service was originally used to hold the hardware open to prevent a pop on first playback. **Do not enable it on PipeWire systems** β€” it holds an exclusive ALSA dmix lock and causes PipeWire to time out. `install-audio.sh` explicitly disables it. The service file is kept in the repo only as a reference for pure-ALSA setups.

---

## Troubleshooting

| Symptom | Likely cause | Fix |
|---------|-------------|-----|
| `aplay -l` shows no `cm5i2saudio` card | Overlay not applied | Check `dmesg \| grep -i "i2s\|max98\|ics43"` for probe errors; confirm `/boot/firmware/overlays/cm5-i2s-audio.dtbo` exists |
| Speaker silent, no errors | SD_MODE GPIO wrong | Verify GPIO 16 is correct for the carrier board; check `pinctrl get 16` |
| Microphone records silence | DAPM not routing capture | Confirm you are using `cm5-i2s-audio` overlay (not `googlevoicehat-soundcard`); check `sudo find /sys/kernel/debug/asoc -name "Capture"` exists |
| `arecord` error: unsupported format | Wrong format for hw device | Use `-f S32_LE` with `hw:cm5i2saudio,0`; use `-f S16_LE` with `default` (plug converts) |
| PipeWire stream timeout | `aplay.service` is running | `sudo systemctl disable --now aplay.service` |

---

## Terms

**Device Tree** β€” A data structure the bootloader passes to the Linux kernel describing hardware that cannot be auto-detected (I2S buses, GPIO assignments, codec chip connections, etc.).

**DTS (Device Tree Source)** β€” Human-readable text source file (like `cm5-i2s-audio.dts`).

**DTB (Device Tree Blob)** β€” The compiled binary form of a DTS, loaded at boot.

**DTBO (Device Tree Blob Overlay)** β€” A partial DTB patched on top of the base device tree at boot. Pi overlays are `.dtbo` files in `/boot/firmware/overlays/`. `dtoverlay=name` in `config.txt` loads `name.dtbo`.

**DAPM (Dynamic Audio Power Management)** β€” The ASoC (ALSA System on Chip) subsystem that manages which hardware audio paths are powered at any time. A codec driver must define DAPM widgets and routes for both playback and capture; if a capture route is missing, the I2S RX engine is never enabled even if the hardware supports it.

**DAI (Digital Audio Interface)** β€” The connection between the CPU-side I2S controller and a codec chip. A sound card can have multiple DAI links (one per codec), each appearing as a separate PCM device in ALSA.

**dmix** β€” ALSA plugin that software-mixes multiple playback streams so more than one app can play audio simultaneously.

**softvol** β€” ALSA plugin providing software volume control for hardware that has no built-in mixer.

**asym** β€” ALSA plugin that routes playback and capture to independent devices, needed when the playback chain (dmix/softvol) cannot handle capture.

**plug** β€” ALSA plugin that transparently converts sample format and rate between what an application requests and what the hardware supports (e.g. S16_LE β†’ S32_LE).
9 changes: 9 additions & 0 deletions installers/audio/aplay.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

[Unit]
Description=Invoke aplay from /dev/zero at system start.

[Service]
ExecStart=/usr/bin/aplay -D default -t raw -r 48000 -c 2 -f S32_LE /dev/zero

[Install]
WantedBy=multi-user.target
148 changes: 148 additions & 0 deletions installers/audio/archive/i2samp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import os

try:
from adafruit_shell import Shell
from clint.textui import colored
except ImportError:
raise RuntimeError("The library 'adafruit_shell' was not found. To install, try typing: sudo pip3 install adafruit-python-shell")

shell = Shell()

BLACKLIST = "/etc/modprobe.d/raspi-blacklist.conf"
PRODUCT_NAME = "I2S Amplifier"
OVERLAY = "googlevoicehat-soundcard"

def driver_loaded(driver_name):
return shell.run_command(f"lsmod | grep -q '{driver_name}'", suppress_message=True)

def main():
reboot = False
shell.clear()
if not shell.is_raspberry_pi():
shell.bail("Non-Raspberry Pi board detected.")
print("\nThis script will install everything needed to use\n"
f"{PRODUCT_NAME}.\n")
print(colored.red("--- Warning ---"))
print("\nAlways be careful when running scripts and commands\n"
"copied from the internet. Ensure they are from a\n"
"trusted source.\n")
if not shell.prompt("Do you wish to continue?"):
print("\nAborting...")
shell.exit()

print("\nChecking hardware requirements...")

# Enable I2S overlay
config = shell.get_boot_config()
if config is None:
shell.bail("No Device Tree Detected, not supported")

print(f"\nAdding Device Tree Entry to {config}")

if shell.pattern_search(config, f"^dtoverlay={OVERLAY}$"):
print("dtoverlay already active")
else:
shell.write_text_file(config, f"dtoverlay={OVERLAY}")
reboot = True

if os.path.exists(BLACKLIST):
print("\nCommenting out Blacklist entry in", BLACKLIST)
shell.pattern_replace(BLACKLIST, "^blacklist[[:space:]]*snd_soc_max98357a.*", "#blacklist snd_soc_max98357a")
shell.pattern_replace(BLACKLIST, "^blacklist[[:space:]]*snd_soc_max98357a_i2c.*", "#blacklist snd_soc_max98357a_i2c")
shell.pattern_replace(BLACKLIST, "^blacklist[[:space:]]*snd_soc_max98357a.*", "#blacklist snd_soc_max98357a")

print("Configuring sound output")
if os.path.exists("/etc/asound.conf"):
if os.path.exists("/etc/asound.conf.old"):
shell.remove("/etc/asound.conf.old")
shell.move("/etc/asound.conf", "/etc/asound.conf.old")
shell.write_text_file("~/asound.conf",
"""
# Use the card name (sndrpigooglevoi) instead of a hardcoded card number.
# The ALSA card number for the I2S device can change between boots when HDMI
# devices are present, so referencing by name ensures the correct device is
# always targeted.

pcm.speakerbonnet {
type hw
card sndrpigooglevoi
}

pcm.dmixer {
type dmix
ipc_key 1024
ipc_perm 0666
slave {
pcm "speakerbonnet"
period_time 0
period_size 1024
buffer_size 8192
rate 44100
channels 2
}
}

ctl.dmixer {
type hw
card sndrpigooglevoi
}

pcm.softvol {
type softvol
slave.pcm "dmixer"
control.name "PCM"
control.card sndrpigooglevoi
}

ctl.softvol {
type hw
card sndrpigooglevoi
}

# Separate playback and capture defaults using asym so that:
# - aplay / speaker output uses the softvol/dmix chain
# - arecord / microphone input uses the I2S capture device directly
pcm.!default {
type asym
playback.pcm "softvol"
capture.pcm "hw:sndrpigooglevoi"
}
""")
shell.move("~/asound.conf", "/etc/asound.conf")


print("Installing aplay systemd unit")
shell.write_text_file("/etc/systemd/system/aplay.service", """
[Unit]
Description=Invoke aplay from /dev/zero at system start.

[Service]
ExecStart=/usr/bin/aplay -D default -t raw -r 44100 -c 2 -f S16_LE /dev/zero

[Install]
WantedBy=multi-user.target""", append=False)

shell.run_command("sudo systemctl daemon-reload")
shell.run_command("sudo systemctl disable aplay")
print("\nYou can optionally activate '/dev/zero' playback in\n"
"the background at boot. This will remove all\n"
"popping/clicking but does use some processor time.\n\n")
if shell.prompt("Activate '/dev/zero' playback in background? [RECOMMENDED]\n", default="y"):
shell.run_command("sudo systemctl enable aplay")
reboot = True

if driver_loaded("max98357a"):
print(f"\nWe can now test your {PRODUCT_NAME}")
shell.warn("Set your speakers at a low volume if possible!")
if shell.prompt("Do you wish to test your system now?"):
print("Testing...")
shell.run_command("speaker-test -l5 -c2 -t wav")
print("\n" + colored.green("All done!"))
print(f"\nEnjoy your new {PRODUCT_NAME}!")
if reboot:
shell.prompt_reboot()

# Main function
if __name__ == "__main__":
shell.require_root()
main()
Loading
Loading