The key to the painting's interactivity will be sound sensing. When you scream, the microphone built into the Circuit Playground Express will detect it and then instruct a wave file we load on the board to play over the amp and speaker, as well as run the servo back and forth.
CircuitPython Setup
To get started, you'll want to set up the CPX for use with CircuitPython by following this guide. When you're ready, and can upload code to the board return here.
To use the Crickit with the CPX, follow the steps listed here to install the special build of CircuitPython, as well as the latest library bundle.
Adafruit really likes using the Mu editor to edit the CircuitPython code. See this guide on loading and using Mu.
Playing Audio
The Circuit Playground Express plays back .wav files. If you would like to prepare your own files, follow the instructions in this guide. Download the following files and then uncompress the .zip file. Copy the .wav files to your CPX, which shows up as CPLAYBOOT on your computer.
Code
You can copy the code here and then paste it into Mu. Save it to your Circuit Playground Express as code.py
# SPDX-FileCopyrightText: 2018 John Edgar Park for Adafruit Industries # # SPDX-License-Identifier: MIT import time import math import array import audiobusio import audioio import audiocore import board from adafruit_crickit import crickit # Number of samples to read at once. NUM_SAMPLES = 160 # Remove DC bias before computing RMS. def normalized_rms(values): minbuf = int(mean(values)) samples_sum = sum( float(sample - minbuf) * (sample - minbuf) for sample in values ) return math.sqrt(samples_sum / len(values)) def mean(values): return sum(values) / len(values) mic = audiobusio.PDMIn(board.MICROPHONE_CLOCK, board.MICROPHONE_DATA, sample_rate=16000, bit_depth=16) # Record an initial sample to calibrate. Assume it's quiet when we start. samples = array.array('H', [0] * NUM_SAMPLES) mic.record(samples, len(samples)) head_servo = crickit.servo_1 head_servo.set_pulse_width_range(min_pulse=500, max_pulse=2500) head_servo.angle = 90 # center the head. # Set audio out on speaker. a = audioio.AudioOut(board.A0) # Start playing the file (in the background). def play_file(wavfile): print("Playing scream!") with open(wavfile, "rb") as f: wav = audiocore.WaveFile(f) a.play(wav) while a.playing: head_servo.angle = 60 time.sleep(.01) head_servo.angle = 120 time.sleep(.01) while True: mic.record(samples, len(samples)) magnitude = normalized_rms(samples) print(((magnitude),)) # formatting is for the Mu plotter. if magnitude < 1000: # it's quiet, do nothing. pass else: print("LOUD") head_servo.angle = 60 time.sleep(.05) head_servo.angle = 120 time.sleep(.05) head_servo.angle = 90 time.sleep(.02) play_file("scream_low.wav") head_servo.angle = 90 time.sleep(2)
Here's how the code works. First, we'll import some libraries that add capabilities to our CircuitPython code.
Then, we'll create a variable NUM_SAMPLES
to represent the number of samples we'll use when testing the microphone for loud sounds.
import time import math import array import audiobusio import audioio import audiocore import board from adafruit_crickit import crickit # Number of samples to read at once. NUM_SAMPLES = 160
Helper Proceedures
Next, we define a procedure named normalized_rms
that will be used for removing DC bias from our mic readings before computing the root mean square (a type of average).
def normalized_rms(values): minbuf = int(mean(values)) samples_sum = sum( float(sample - minbuf) * (sample - minbuf) for sample in values ) return math.sqrt(samples_sum / len(values))
The mean procedure is also defined -- this is used to calculate the mean of our sampled values.
def mean(values): return sum(values) / len(values)
Microphone
Now we'll set up the microphone object to use the Circuit Playground Express's microphone at a sample rate of 16,000 and a bit depth of 16.
And, we'll immediately take a sample of the room's ambient sound to provide a baseline.
mic = audiobusio.PDMIn(board.MICROPHONE_CLOCK, board.MICROPHONE_DATA, sample_rate=16000, bit_depth=16) # Record an initial sample to calibrate. Assume it's quiet when we start. samples = array.array('H', [0] * NUM_SAMPLES) mic.record(samples, len(samples))
Servo Setup
Here we'll instantiate a servo object named head_servo
on the Crickit's Servo 1 port with pulse width ranging from 500 to 2500 microseconds. We'll then set the servo to its center position of 90° (the servo rotational range is from 0° to 180°).
head_servo = crickit.servo_1 head_servo.set_pulse_width_range(min_pulse=500, max_pulse=2500) head_servo.angle = 90 # center the head.
Audio Wave File Playback
We'll prepare for our audio wave file playback by setting the audio output on the speaker connected to the Crickit's sound port -- this is driven by the Circuit Playground Express board's A0 pad.
Then, we define a procedure named play_file that will have two key jobs -- it will play back wave files stored on the Circuit Playground Express and, while playing, it will turn the head servo back and forth 30° in either direction with a very, very short pause in between.
# Set audio out on speaker. a = audioio.AudioOut(board.A0) # Start playing the file (in the background). def play_file(wavfile): print("Playing scream!") with open(wavfile, "rb") as f: wav = audiocore.WaveFile(f) a.play(wav) while a.playing: head_servo.angle = 60 time.sleep(.01) head_servo.angle = 120 time.sleep(.01)
Main Loop
Now we are at the main loop of the program, the part that runs over and over and over again endlessly.
Here, the microphone is sampled, and the normalized root mean square is calculated and assigned to a variable named magnitude.
Then, the magnitude value is printed to the serial port (it is formatted funny with lots of parenthesis so that it is a tuple and can be graphed in the Mu plotter). This is useful to watch when you first are setting it up.
Try yelling at the painting and watch the values change. You want a number that is below the yelling magnitude that you can use in the next section.
while True: mic.record(samples, len(samples)) magnitude = normalized_rms(samples) print(((magnitude),)) # formatting is for the Mu plotter.
If the yelling volume caused a magnitude reading of 2000, say, and normal room noise is below 400, then 1000 is a good magnitude to use as your scream threshold.
This if
statement checks to see if the current magnitude is below 1000. If it is, nothing happens. Else
, if the reading is higher, the serial monitor will print "LOUD" and then the head will wiggle a couple of times and then play back the wave file!
After this is done, the head returns to resting position, and the system waits a couple of seconds before the main loop runs again -- this helps prevent any self screaming feedback loops, and people who think they can just get The Scream to scream constantly!
if magnitude < 1000: # it's quiet, do nothing. pass else: print("LOUD") head_servo.angle = 60 time.sleep(.05) head_servo.angle = 120 time.sleep(.05) head_servo.angle = 90 time.sleep(.02) play_file("scream_low.wav") head_servo.angle = 90 time.sleep(2)
Page last edited January 21, 2025
Text editor powered by tinymce.