Building a Tell-Tale Heart with an Adafruit Prop-Maker FeatherWing

"The Tell-Tale Heart" is a short story by Edgar Allan Poe, in which an extremely sane man very calmly proceeds to explain to you, the reader, how he murdered the old man with whom he was lodging and hid his body under the floorboards. When the police come around, he can still hear the old man's heart beating under the floorboards. It is an excellent short story and if you haven't read it, I highly recommend you go away and do so now.

Illustration by Harry Clarke (1923)

This Halloween, we had friends coming over for a feast, and naturally I decided to build a Tell-Tale Heart of my very own to horrify them with. Come dinnertime, I would dramatically announce that the old man's heart was hidden somewhere in the house, and that the person who found it would get a wonderful prize (candy).

My plan for how the heart should work was as follows:

  • It would need to be battery powered, so it could beat unattended for a period of time.
  • It would have to make noise, so it needed a speaker and an amplifier.
  • It should beat softly, just above the range of normal human hearing, so I could hide it somewhere.
  • If it was picked up or disturbed - or even if someone moved close to it - it should begin beating much louder.
  • Once the disturbance ended, it should return to beating softly again.

It was pretty much guaranteed that with behaviour like this, it would upset and disturb my friends thoroughly!

The first thing I did was pick up a squishy heart from Flying Tiger for £5. I have no idea why they made this, but it's hideous and I love it.

Then I took a scalpel (craft knife) to the heart, and removed some of the innards, which were made of fluff. There was definitely enough space inside the heart to hide some electronic components, which convinced me that this project would actually work out.

Then I built a test circuit on a piece of breadboard, using a Challenger RP2040, a MAX98357A amplifier breakout board, and a small speaker from Pimoroni. You can see that in the image at the top of this post.

The next step was to find some heartbeat sounds, for which I went to the incredible Freesound. This involved a lot of boring trial and error, because some heartbeat sounds looked like they might work but came out fuzzy, staticky, or warped from the speaker, and others were mysteriously quiet whatever I did. I referred to the Adafruit docs for my I2S amplifier breakout a lot as I tested. It was particularly helpful that they suggested the WAV files should be converted to mono with 22 KHz sample rate and 16 bits per sample, which I did in Audacity. In the end I actually convered them to 16 KHz, 16 bits per sample, which worked well for the files I chose.

I ended up with two audio files: a slow heartbeat and a fast heartbeat. I popped these on the RP2040 board along with some testing code, and managed to make the speaker play one, then the other.

The next step was to add an acceleremoter into the mix, so the heart could react when it was picked up or sensed a vibration. At this point, I realised that for almost the price of an accelerometer board and the other materials I needed to create a working, battery-powered heart, I could just buy the Adafruit Prop-Maker FeatherWing. This supported a battery and had an onboard accelerometer and amplifier! It's such a versatile board, and I wouldn't hesiate to use one for a similar project again.

Once I got my board, with the full moon glowering through my window and wolves baying in the woods beyond the wild darkness of the unkempt garden, I finally set to work, crafting my monstrous code.

First, we pull in our imports and do some setup. This code will require you to download the extra CircuitPython libraries for the accelerometer (adafruit_lis3dh) and the NeoPixel (neopixel).

import audiobusio
import audiocore
import board
import array
import time
import math
import audiomixer
from audiomp3 import MP3Decoder
from digitalio import DigitalInOut, Direction, Pull
import adafruit_lis3dh
import neopixel

# enable external power pin
# provides power to the external components
external_power = DigitalInOut(board.EXTERNAL_POWER)
external_power.direction = Direction.OUTPUT
external_power.value = True

# Disable NeoPixel
pixel = neopixel.NeoPixel(board.NEOPIXEL, 1)
pixel.brightness = 0

fast_heartbeat_timer = 0
is_fast = False

Then, we set up the onboard amplifier. We fetch the files from the disk, use audiocore to read them into a format the processor recognises, then use audiobusio and audiomixer to create a mixer pointing at the I2S amplifier. Finally, on startup, we send the slow heartbeat noise to the mixer, which means it will begin playing immediately.

# i2s playback
slow_file = open("heartbeat_slow.wav", "rb")
fast_file = open("heartbeat_fast.wav", "rb")
slow_wave = audiocore.WaveFile(slow_file)
fast_wave = audiocore.WaveFile(fast_file)
audio = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DATA)
mixer = audiomixer.Mixer(voice_count=1, sample_rate=16000, channel_count=1,
                         bits_per_sample=16, samples_signed=True)
audio.play(mixer)

# Play slow heartbeat
mixer.voice[0].play(slow_wave, loop=True)
mixer.voice[0].level = 1.0

With our heartbeat playing, the final step is to configure the behaviour which uses the onboard accelerometer. This setup code is taken from the Adafruit documentation.

# onboard LIS3DH
i2c = board.I2C()
int1 = DigitalInOut(board.ACCELEROMETER_INTERRUPT)
lis3dh = adafruit_lis3dh.LIS3DH_I2C(i2c, int1=int1)
lis3dh.range = adafruit_lis3dh.RANGE_2_G

Then we enter an endless loop with while True.

while True:
    if is_fast == True and time.monotonic() >= fast_heartbeat_timer:
        is_fast = False
        mixer.voice[0].play(slow_wave, loop=True)
    if is_fast == False and lis3dh.shake(shake_threshold=10):
        print("SHAKEN!")
        mixer.voice[0].play(fast_wave, loop=True)
        is_fast = True
        fast_heartbeat_timer = time.monotonic() + 10

In the logic inside this loop, which runs every tick, we check if we're beating fast. If we are, we see if the 10 seconds of fast_heartbeat_timer have elapsed. If they have, we unset is_fast and start playing the slow heartbeat again.

In the second part of the logic, we check, instead, if we're beating slowly and we're sensing a vibration. If so, we start beating quickly, set is_fast to True, and set the timer to 10 seconds.

In the final cases (we're beating fast but we haven't waited 10 seconds, or we're beating slowly but we're not sensing a vibration), we just keep looping, waiting for something to happen.

That's all the code our heart needs to do its terrible works! The last bits of the project were to attach a tiny LiPo battery and a speaker to the board, and stuff it inside the soft, protective fluff of the heart.

When my friends found the heart in its hiding place, they were very upset. Some comments included:

  • "Why is it so squishy"?
  • "Oh god I can feel the heartbeat coming through the speaker."
  • "I don't like it, make it stop."
  • "aaaaaaa"

All in all, a huge Halloween success.