Inspiration

Inspired by Disneyland’s Pirates of the Caribbean ride, this talking skull was a focal point for this Halloween decorations several years ago. 3D printed and Trinket powered, this was a pretty successful first attempt at animatronics. Pirates of the Caribbean is our favorite ride at Disneyland (the sights… the smell… you know what I mean). I’ve been fascinated by the talking skull just after the bayou and prior to the first drop since I was a kid. This isn’t a faithful reproduction but is inspired by the ride.

The technique used to synchronize the jaw movement to the sound is crude at best. The amount of servo movement is somewhat proportional to the volume of the sound. The sound signal is pretty noisy… no pun intended. A fair bit of smoothing is applied and then the code maps the volume to one of five different positions from closed to full open. There really didn’t seem to need to be any subtle variation in the amount the jaw opens on the skull since… well… it’s a skull.

UPDATE – 22.09.19

The Trinket Pro used in this project is generally deprecated by the friendly folks at Adafruit. This project would work equally well with something like an Arduino Nano or one of the replacement boards recommended by Adafruit. When swapping boards, make sure you’re using an analog pin to connect to the left audio line out and digital pins for the rest of the connections. Update pin numbers in the code as needed.

NOTE: The Audio FX board will only play .wav and .ogg audio files—.mp3 files are not supported.

Basic method of operation

The way this thing is supposed to work is as follows… “supposed to” being the operative term:

  1. The passive infrared sensor detects motion
  2. A random track number is selected and the track duration is noted
  3. The audio is triggered
  4. Using the left line out speaker pin, a series of quick readings are taken and averaged
  5. The sensor reading is mapped to one of five servo/jaw positions
  6. The servo moves the jaw to that position
  7. There is a slight pause to prevent excessive chattering
  8. Readings and movement are repeated for the duration of the track

 

When configuring the code, I used the serial monitor to determine what the minimum and maximum values were on the analog pin connected to the left audio line out. Before uploading the audio files to the board, I tried to normalize each track to similar levels for more consistent results track to track.

Pinout

Code

				
					/*

Nothing happens in a vacuum. This project owes thanks to the following sources for demonstrating it could be done and the general approach to get it done:

https://www.instructables.com/id/Moving-a-Servo-To-Sound-Signals/
https://rimstar.org/science_electronics_projects/arduino_controlled_skull_scareduino.htm

 */
// includes
#include <SoftwareSerial.h>
#include "Adafruit_Soundboard.h"
#include <Servo.h>

//soundboard pin definitions
#define SFX_TX 6
#define SFX_RX 5
#define SFX_RST 8

// software serial
SoftwareSerial ss = SoftwareSerial(SFX_TX, SFX_RX);
Adafruit_Soundboard sfx = Adafruit_Soundboard(&ss, NULL, SFX_RST);

// objects
Servo myservo;

// variables
// general
// audio analog input
int inputPin = A0;

// calibration
int sensorValue = 0;
int sensorMin = 1023;
int sensorMax = 0;

// smoothing
int readings = 0;
int total = 0;
int average = 0;
// higher numbers remove the noise but take time to average
const int numReadings = 185;

// sound playback
uint8_t randomTrack = 0; // initial calibration track
unsigned long currentMillis = millis();
unsigned long trackOnTime;
unsigned long trackDuration;

// servo
int pos = 0;
int servoPin = 4;

// mouth postions
int mouthPos00 = 20; //closed
int mouthPos01 = 15;
int mouthPos02 = 10;
int mouthPos03 = 5;
int mouthPos04 = 0; // full open
// short delay and low samples will result in jitters
// allow enough time for servo movement
int moveDelay = 25;

// pir
int pirInput = 3;
int pirState = LOW;
int pirStatus = 0;
int triggerDelay = 1000;
int triggerReset = 1000;

void setup() {
  // soundboard
  ss.begin(9600);
  
  // initialize serial communication with computer
  Serial.begin(9600);

// randomize
  randomSeed(A1);
  
  // calibrate the audio input
  // calibrate during the first ten seconds
  
  sfx.playTrack(randomTrack);
  while (millis() < 10000) {
    sensorValue = analogRead(inputPin);
    // record the maximum sensor value
    if (sensorValue > sensorMax) {
      sensorMax = sensorValue;
    }
    // record the minimum sensor value
    if (sensorValue < sensorMin) {
      sensorMin = sensorValue;
    }
  }
  sfx.stop();
  
Serial.println(sensorMin);
Serial.println(sensorMax);

  // attach servo to pin
  myservo.attach(servoPin);
  myservo.write(mouthPos00);

  // pir
  pinMode(pirInput, INPUT);
}
void loop() {
  // check for motion
  pirState = digitalRead(pirInput);
  if (pirState == HIGH) {

    // wait some duration after detecting motion
    delay(triggerDelay);
    // random seed
    //pick random track 0-4
    randomTrack = random(6);

    if (randomTrack == 0) {
      trackDuration = 20650;
    } else if (randomTrack == 1) {
      trackDuration = 19650;
    } else if (randomTrack == 2) {
      trackDuration = 16600;
    } else if (randomTrack == 3) {
      trackDuration = 22950;
    } else if (randomTrack == 4) {
      trackDuration = 24800;
    } else if (randomTrack == 5) {
      trackDuration = 21500;
    }

    //Serial.println(randomTrack);
    sfx.playTrack(randomTrack);
    trackOnTime = millis();
    currentMillis = millis();

    while (currentMillis - trackOnTime <= trackDuration) {
      // get a numReadings number of sensor readings
      for (int i = 0; i <= numReadings; i++) {
        readings = analogRead(inputPin);
        total = total + readings;
      }
      // calculate the average
      sensorValue = total / numReadings;
      // zero total
      total = 0;

      // apply the calibration to the sensor reading
      sensorValue = map(sensorValue, sensorMin, sensorMax, 0, 1000);
      // in case the sensor value is outside the range seen during calibration
      sensorValue = constrain(sensorValue, 0, 1000);

      //Serial.println(sensorValue);

      // move the mouth based on averaged and smoothed sensor reading
      if (sensorValue <= 5) {
        myservo.write(mouthPos00);
        delay(moveDelay);
      } else if (sensorValue > 5 && sensorValue <= 15) {
        myservo.write(mouthPos01);
        delay(moveDelay);
      } else if (sensorValue > 15 && sensorValue <= 40) {
        myservo.write(mouthPos02);
        delay(moveDelay);
      } else if (sensorValue > 40 && sensorValue <= 90) {
        myservo.write(mouthPos03);
        delay(moveDelay);
      } else if (sensorValue > 90) {
        myservo.write(mouthPos04);
        delay(moveDelay);
      }
      currentMillis = millis();

    }
    myservo.write(mouthPos00);
    delay(moveDelay);
    // wait some duration before checking for movement again
    delay(triggerReset);

  }
}
				
			

Links

Here are some useful links to get you a bit further along if you would like to replicate this project.

Adafruit Audio FX Soundboard tutorial

3D model of human skull