It's that time of year again when it's considered socially acceptable to terrify young children and give them candy. Oh joy. I'm here to make your job easier, by showing you how to make a simple motion-sensing Raspberry Pi Halloween soundbox. Here's a demo:

Here's What You'll Need

Probably the only part you don't already have is the motion sensor, a small and inexpensive part you should be able to find at your local Microcenter or Maplin.

  • Raspberry Pi (any model will do).
  • Motion sensor (~$3).
  • Hookup wires.
  • Wired speaker (most Bluetooth speakers will have the option to use line-in).
  • 3.5mm stereo cable, male-to-male.

Once you're done, you might want to add some synced lighting effects too, but in this tutorial we'll only be covering the scary sounds bit!

Setting Up

We're using Raspbian Jessie Lite and Python 2.7, but any Linux distro that runs on your Pi should be fine. I've left it on the standard hostname "raspberrypi.local", so begin by logging in remotely using SSH (open a Terminal window if you're on Mac. Here's how to do the same in Windows) -- or if you've opted use a full Raspbian with desktop GUI, feel free to skip to updating.

        ssh pi@raspberrypi.local
(enter raspberry as the password)

sudo apt-get update
sudo apt-get install python-pip
sudo pip install gpiozero

This installs a simple library for working with the GPIO pins in Python with many types of built-in sensors and buttons. Wire up your sensor with the signal pin on GPIO4, the VCC connected to 5V, and the GND connected to GND. This may vary according to your exact model, so confirm with a pinout diagram.

Raspberry Pi GPIO Diagram
Image Credit: raspberrypi.org

Helpfully, my Pi 2 case from Pimoroni has a pinout diagram laser-etched directly onto it.

Labelled Raspberry Pi Case

Now let's make our motion detection script.

        nano motion.py

Paste in:

        from gpiozero import MotionSensor

pir = MotionSensor(4)
while True:
    if pir.motion_detected:
        print("Motion detected!")
    else:
        print ("No motion")

Hit CTRL-X then Y to save and exit, then run with:

        python motion.py

You should see the "no motion" message repeated on screen until you wave your hand in front of the sensor, when it'll linger on the "Motion Detected!"

Motion detected in terminal

If the message doesn't change at all, you've wired it up wrong.

If you're interested in learning more about this simple GPIOZero library, take a look at this fantastic cheatsheet.

Playing Sound

Connect up you portable speaker and ensure it's powered if it needs to be. We'll use the pygame library to play sounds, so go ahead and install it:

        sudo apt-get install python-pygame

First, we need a sound file to play. If you're doing this from within the desktop environment, then go ahead and download a WAV or OGG file from somewhere (I found a good selection of free Halloween sounds here), and put it in your home directory. I'd suggest downsampling first and converting to a small OGG format anyway.

If you're connecting remotely and only using the command line, we have a little more difficulty with some sites, since the wget command may not grab the actual file. Instead, we can download it locally to our desktop and use the scp (secure copy) command to copy over the command line. You can learn more about scp here, but for now, open a new Terminal tab and type:

        scp thunder.ogg pi@raspberrypi.local:

Rename thunder.ogg as appropriate, but don't forget that final : (the command will complete without it, but it won't do what we want it to do). By default, this will transfer the file to Pi user's home directory.

Now let's modify the script to play a sound. Start by importing some new modules:

        import pygame.mixer
from pygame.mixer import Sound

Then just after the existing import statements, we'll loop the same sound over and over for testing purposes. Leave the rest of your motion sensing code as is for now -- it just won't run, since it'll be stuck in this sound-playing loop forever.

        pygame.init()
pygame.mixer.init()
#load a sound file, in the home directory of Pi user (no mp3s)
thunder = pygame.mixer.Sound("/home/pi/thunder.ogg")

while True:
        thunder.play()
        sleep(10)
        thunder.stop()

Note that when I originally tried this process, the sound refused to play and just clicked instead. The size of the file or bit-rate was the culprit: it was 24-bit and over 5 MB for a 15 second clip. Scaling that down to 16-bit using the converter I linked to above made everything work nicely, and the size was reduced to just 260KB!

If you notice a nasty hiss from your speakers when your Python app is running, but not otherwise, type:

        sudo nano /boot/config.txt

And add this line at the end:

        disable_audio_dither=1

Restart for the changes to take effect. Or don't bother, since it sort of sounded like rain to me anyway.

Finally, let's modify the main motion-checking loop to play the sound when motion is detected. We'll use a 15-second delay so that the whole loop can be played, and to act as a spam buffer for when there's lot of non-stop motion.

        while True:
    if pir.motion_detected:
        print("Motion detected!")
        thunder.play()
        # ensure playback has been fully completed before resuming motion detection, prevents "spamming" of sound
        sleep(15)
        thunder.stop()
    else:
        print ("No motion")

Start Automatically

We probably want to set this up somewhere with a battery and no internet connection, so the script needs to run on restart without having to open up a command line. To do this, we'll use the simplest method possible: the crontab. Type:

        sudo crontab -e

If this is the first time running this command, it'll begin by asking you what editor to use. I chose option 2, for nano. It'll boot into your chosen editor, so add the following line:

        @reboot python /home/pi/motion.py &

This means your motion.py script will run on every startup, and do so silently (so any output from the script will be ignored). Reboot to try it out.

If nothing plays despite there being motion, or you hear just a little click, you may not have used the full file path, or your file may need converting to a lower bitrate and smaller file size.

Add More Sounds

Playing the same effect over and over is a little boring, so let's add some randomness to it. Download some more Halloween sounds, remembering to scale them down to a sensible size and bitrate, then send them over to your Pi using scp as before. I added three different types of scream.

Modify the code so that instead of defining a single pygame.mixer.Sound variable, we're actually creating an array of sounds. This is simple with Python, just surround a comma separated list of them with square brackets, like so:

        sounds = [
        pygame.mixer.Sound("/home/pi/thunder.ogg"),
        pygame.mixer.Sound("/home/pi/scary_scream.ogg"),
        pygame.mixer.Sound("/home/pi/girl_scream.ogg"),
        pygame.mixer.Sound("/home/pi/psycho_scream.ogg")
        ]

Next, import the random library into your file, with:

        import random

Now modify the main motion-sensing loop as follows:

        while True:
    if pir.motion_detected:
        print("Motion detected!")
        playSound = random.choice(sounds)
        playSound.play()
        # ensure playback has been fully completed before resuming motion detection, prevents "spamming" of sound
        sleep(15)
        playSound.stop()
    else:
        print ("No motion")

Note the minor change: instead of playing the single Sound variable, we're using the random.choice function to pick a random sound from our sounds array, then playing that.

Here's the full code in case you're having problems:

        import pygame
from pygame.mixer import Sound
from gpiozero import MotionSensor
from time import sleep
import random

pygame.init()
pygame.mixer.init()
#load a sound file, same directory as script (no mp3s)

sounds = [
        pygame.mixer.Sound("/home/pi/thunder.ogg"),
        pygame.mixer.Sound("/home/pi/scary_scream.ogg"),
        pygame.mixer.Sound("/home/pi/girl_scream.ogg"),
        pygame.mixer.Sound("/home/pi/psycho_scream.ogg")
        ]

pir = MotionSensor(4)
while True:
    if pir.motion_detected:
        print("Motion detected!")
        playSound = random.choice(sounds)
        playSound.play()
        # ensure playback has been fully completed before resuming motion detection, prevents "spamming" of sound
        sleep(15)
        playSound.stop()
    else:
        print ("No motion")

With only four samples, there's a high probability of repetition each time, but you can add more samples if that's annoying.

That's it! Hide it in the bushes with some scary LED monster eyes, and you should be able to save yourself some candy as all the kids run away screaming before they even reach the door. Or go hide in the closet because an angry mom is out for blood after you made little Johnny cry.

Disclaimer: MakeUseOf is not responsible for any personal injury that may result from your use of this project!

Will you be making this motion-activated soundbox in order to scare the local trick-or-treaters? Have you set up any scary effects with a Raspberry Pi this Halloween? Please let us know about it in the comments below!