Skip to content

Instantly share code, notes, and snippets.

@aarmea
Last active July 20, 2023 23:46
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save aarmea/f3010fd58629acda9c458e883ee010b5 to your computer and use it in GitHub Desktop.
Save aarmea/f3010fd58629acda9c458e883ee010b5 to your computer and use it in GitHub Desktop.
Open and close an animatronic mouth on a Raspberry Pi GPIO pin
#!/usr/bin/env python
"""
animatronic_mouth.py
This script animates a motorized mouth on a Raspberry Pi GPIO pin so that it
appears to be speaking alongside the audio on the specified PulseAudio source
(which usually should be a sink's monitor).
Find PA_SOURCE with `pactl list` and look for a monitor device that corresponds
to your output device.
See here for a detailed discussion: https://albertarmea.com/post/alexa-tree/
"""
import RPi.GPIO as GPIO
import struct
import subprocess
# TODO: Move these two to a configuration file somewhere
# TODO: Make this a service
MOUTH_PIN = 24
PA_SOURCE = "alsa_output.usb-C-Media_Electronics_Inc._USB_Audio_Device-00.analog-stereo.monitor"
# We're not playing this stream back anywhere, so to avoid using too much CPU
# time, use settings that are just high enough to detect when there is speech.
PA_FORMAT = "u8" # 8 bits per sample
PA_CHANNELS = 1 # Mono
PA_RATE = 2000 # Hz
PA_BUFFER = 32 # frames for a latency of 64 ms
SAMPLE_THRESHOLD = 4
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.setup(MOUTH_PIN, GPIO.OUT, initial=GPIO.LOW)
# Capture audio using `pacat` -- PyAudio looked like a cleaner choice but
# doesn't support capturing monitor devices, so it can't be used to capture
# system output.
parec = subprocess.Popen(["/usr/bin/pacat", "--record", "--device="+PA_SOURCE,
"--rate="+str(PA_RATE), "--channels="+str(PA_CHANNELS),
"--format="+PA_FORMAT, "--latency="+str(PA_BUFFER)], stdout=subprocess.PIPE)
while not parec.stdout.closed:
# Mono audio with 1 byte per sample makes parsing trivial
sample = ord(parec.stdout.read(1)) - 128
GPIO.output(MOUTH_PIN, abs(sample) > SAMPLE_THRESHOLD)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment