Talking skull on an Arduino Uno
A PIR wakes a skull: its eyes light and a servo jaw follows a hand-timed cue list in step with a DFPlayer track, until BUSY says the line is over. The whole build, an Arduino Uno and 9 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Intermediate Runs in your browser. Free, and no account needed.
Build it as the Halloween Prop Kit: the parts in numbered bags and a booklet that checks every step. Free.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Talking skull: a PIR sees a trick-or-treater, the skull's eyes light up and
// its jaw moves in time with a spoken line played by a DFPlayer Mini.
//
// PIR VCC -> 5 V GND -> GND OUT -> pin 2 (high while it sees movement)
// jaw servo PWM -> pin 9, VCC -> 5 V, GND -> GND
// DFPlayer VCC -> 5 V GND -> GND TX -> pin 10 RX <- 1 k <- pin 11
// BUSY -> pin 3 (low while a track plays), speaker on SPK_1 and SPK_2
// eyes two green LEDs on pins 5 and 6, each through 220 ohms to GND
//
// How the jaw keeps time. There are two honest ways to sync a jaw to a voice.
//
// 1. Follow the audio. Rectify and smooth the speaker signal into an
// envelope, read it on an analog pin, and open the jaw as far as the
// voice is loud. Ready-made "audio servo driver" boards do exactly this.
// It works with any recording, but the jaw chatters on every breath.
// 2. Script it. Listen to the track once, note when each syllable starts,
// and write those moments down as cues. It takes ten minutes per line and
// looks crisp, because you decide when the mouth shuts.
//
// This sketch does the second. SCRIPT below is the cue list for track 001:
// "Welcome... to my HAUNTED house! Heh heh heh heh." Each cue says when, in
// milliseconds from the start of the track, the jaw should be at what angle.
// The simulated DFPlayer plays silence, so there is no audio to follow here;
// a scripted jaw moves exactly as it will on the porch.
//
// The track's real end comes from BUSY, not from the script, so a longer
// recording never leaves the jaw hanging open.
//
// On the bench: a PIR needs about a minute after power-up to settle, and
// fires at random until it has. Give the servo its own 5 V supply with its
// ground joined to the Uno's.
//
// Things to try. Click the PIR. Record your own line, time its syllables, and
// replace SCRIPT. Make the eyes flare on the loud syllables: the cue's angle
// is a good stand-in for how loud it is.
#include <SoftwareSerial.h>
#include <DFRobotDFPlayerMini.h>
#include <Servo.h>
const int PIR_PIN = 2;
const int BUSY_PIN = 3;
const int JAW_PIN = 9;
const int EYE_LEFT = 5;
const int EYE_RIGHT = 6;
const int JAW_SHUT = 5; // degrees
const unsigned long REST_MS = 10000; // quiet time between visitors
struct Cue {
uint16_t at; // ms from the start of the track
uint8_t angle; // jaw angle, JAW_SHUT is closed
};
// "Wel-come... to my HAUN-TED house! Heh heh heh heh."
const Cue SCRIPT[] = {
{0, 30}, {200, 5}, {320, 35}, {560, 5}, // wel-come
{900, 20}, {1040, 5}, {1120, 25}, {1260, 5}, // to my
{1450, 45}, {1700, 10}, {1780, 40}, {1980, 5}, // HAUN-TED
{2100, 35}, {2450, 5}, // house!
{2700, 30}, {2850, 5}, {2950, 30}, {3100, 5}, // heh heh
{3200, 30}, {3350, 5}, {3450, 30}, {3600, 5}, // heh heh
};
const int CUES = sizeof(SCRIPT) / sizeof(SCRIPT[0]);
SoftwareSerial link(10, 11); // RX, TX
DFRobotDFPlayerMini player;
Servo jaw;
bool haveSound = false;
bool talking = false;
unsigned long started = 0;
unsigned long quietSince = 0;
int nextCue = 0;
unsigned int visitors = 0;
void eyes(int level) {
analogWrite(EYE_LEFT, level);
analogWrite(EYE_RIGHT, level);
}
void speak() {
talking = true;
started = millis();
nextCue = 0;
eyes(255);
if (haveSound) player.play(1);
visitors++;
Serial.print("Talking to visitor ");
Serial.println(visitors);
}
void setup() {
Serial.begin(9600);
link.begin(9600);
pinMode(PIR_PIN, INPUT);
pinMode(BUSY_PIN, INPUT);
jaw.attach(JAW_PIN);
jaw.write(JAW_SHUT);
haveSound = player.begin(link);
if (haveSound) {
player.volume(24);
} else {
Serial.println("No DFPlayer answered: the skull will mime");
}
Serial.println("Skull ready, saying hello once");
speak();
}
void loop() {
unsigned long now = millis();
if (talking) {
unsigned long t = now - started;
// Play every cue whose moment has come.
while (nextCue < CUES && t >= SCRIPT[nextCue].at) {
jaw.write(SCRIPT[nextCue].angle);
nextCue++;
}
// Done when the script is spent and the player says the track is over
// (BUSY goes high). Without a player, the script alone decides.
bool playing = haveSound && digitalRead(BUSY_PIN) == LOW;
if (nextCue >= CUES && !playing && t > 500) {
talking = false;
quietSince = now;
jaw.write(JAW_SHUT);
eyes(20);
Serial.println("Done, waiting for the next visitor");
}
return;
}
if (now - quietSince >= REST_MS && digitalRead(PIR_PIN) == HIGH) {
speak();
}
}
Parts list
11 parts, plus the jumper wires. Every one is in the editor's parts bin.
- 1 × Arduino Uno R3
- 1 × Full-size breadboard
- 1 × DFPlayer Mini MP3 module
- 1 × Resistor, 1k Ω
- 2 × Resistor, 220 Ω
- 2 × LED, green
- 1 × Motion sensor
- 1 × Hobby servo
- 1 × Speaker, 8 ohm
How it is wired
14 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.
- Arduino Uno R3 pin 11; Resistor, 1k Ω pin 1
- Arduino Uno R3 pin 10; DFPlayer Mini MP3 module pin TX
- Arduino Uno R3 pin 9; Hobby servo pin PWM
- Arduino Uno R3 pin 6; Resistor, 220 Ω (2) pin 1
- Arduino Uno R3 pin 5; Resistor, 220 Ω (1) pin 1
- Arduino Uno R3 pin 3; DFPlayer Mini MP3 module pin BUSY
- Arduino Uno R3 pin 2; Motion sensor pin OUT
- Arduino Uno R3 pin 5V; DFPlayer Mini MP3 module pin VCC; Motion sensor pin VCC; Hobby servo pin VCC
- Ground: Arduino Uno R3 pin GND; DFPlayer Mini MP3 module pin GND; LED, green (1) pin C; LED, green (2) pin C; Motion sensor pin GND; Hobby servo pin GND
- DFPlayer Mini MP3 module pin RX; Resistor, 1k Ω pin 2
- DFPlayer Mini MP3 module pin SPK_1; Speaker, 8 ohm pin 1
- DFPlayer Mini MP3 module pin SPK_2; Speaker, 8 ohm pin 2
- Resistor, 220 Ω (1) pin 2; LED, green (1) pin A
- Resistor, 220 Ω (2) pin 2; LED, green (2) pin A
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.