Install CircuitPython
The Adafruit Feather M4 ships with CircuitPython, but let's go ahead and update it to the latest version. It's super easy with the circuitpython.org website. Follow the directions on the Feather M4 Express guide.
Adafruit Circuit Python Libraries
Download the CircuitPython library bundle per the Feather M4 guide instructions here. Unzip the files into a folder on your computer. Create a new folder on the CIRCUITPY drive and name it lib. The following libraries are required to run the code properly.
- adafruit_bus_device (directory)
- adafruit_lis3dh.mpy
- neopixel.mpy
Find all these files in the library bundle and copy them into the lib file you just made on your CIRCUITPY drive.
Upload Files
Click the link below to download the project zip – This contains the code and audio files. Upload the code.py file to the CIRCUITPY drive root (main) folder.
Create a new folder at the top level of the CIRCUITPY drive and name it sounds. Upload the audio files to that folder. The code will run properly when all of the files have been uploaded.
Check out the image above to see what your CIRCUITPY drive should look like when all the files are in place.
# SPDX-FileCopyrightText: 2019 Kattni Rembor Adafruit Industries # SPDX-FileCopyrightText: 2019 Erin St Blaine for Adafruit Industries # SPDX-FileCopyrightText: 2019 Limor Fried for Adafruit Industries # # SPDX-License-Identifier: MIT """ Prop-Maker based Burning Wizard Staff Adafruit invests time and resources providing this open source code. Please support Adafruit and open source hardware by purchasing products from Adafruit! Written by Kattni Rembor, Erin St Blaine & Limor Fried for Adafruit Industries Copyright (c) 2020 Adafruit Industries Licensed under the MIT license. All text above must be included in any redistribution. """ import time import random import digitalio import audioio import audiocore import board import neopixel import adafruit_lis3dh # CHANGE TO MATCH YOUR RING AND STRIP SETUP NUM_RING = 12 #12 pixel ring NUM_STRIP = 44 # 44 pixels in my NeoPixel strip NUM_PIXELS = NUM_STRIP + NUM_RING #total number of pixels NEOPIXEL_PIN = board.D5 # PropMaker Wing uses D5 for NeoPixel plug POWER_PIN = board.D10 # CUSTOMISE COLORS HERE: COLOR = (200, 30, 0) # Default idle is orange ALT_COLOR = (0, 200, 200) # hit color is teal SWING_COLOR = (200, 200, 200) #swing animation color is white TOP_COLOR = (100, 100, 0) #top color is yellow-green YELL_COLOR = (200, 0, 200) #yell color is purple # CUSTOMISE IDLE PULSE SPEED HERE: 0 is fast, above 0 slows down IDLE_PULSE_SPEED = 0 # Default is 0 seconds SWING_BLAST_SPEED = 0.007 # CUSTOMISE BRIGHTNESS HERE: must be a number between 0 and 1 IDLE_PULSE_BRIGHTNESS_MIN = 0.2 # Default minimum idle pulse brightness IDLE_PULSE_BRIGHTNESS_MAX = 1 # Default maximum idle pulse brightness # CUSTOMISE SENSITIVITY HERE: smaller numbers = more sensitive to motion HIT_THRESHOLD = 1150 SWING_THRESHOLD = 800 YELL_THRESHOLD = 700 # Set to the length in seconds of the "on.wav" and "yell1.wav" files POWER_ON_SOUND_DURATION = 3.0 YELL_SOUND_DURATION = 1.0 enable = digitalio.DigitalInOut(POWER_PIN) enable.direction = digitalio.Direction.OUTPUT enable.value = False # Set up NeoPixels strip = neopixel.NeoPixel(NEOPIXEL_PIN, NUM_PIXELS, brightness=1, auto_write=False) strip.fill(0) # NeoPixels off ASAP on startup strip.show() audio = audioio.AudioOut(board.A0) # Speaker wave_file = None # Set up accelerometer on I2C bus, 4G range: i2c = board.I2C() # uses board.SCL and board.SDA # i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller accel = adafruit_lis3dh.LIS3DH_I2C(i2c) accel.range = adafruit_lis3dh.RANGE_4_G COLOR_IDLE = COLOR # 'idle' color is the default for the staff handle COLOR_HIT = ALT_COLOR # "hit" color is ALT_COLOR set above COLOR_SWING = SWING_COLOR # "swing" color is SWING_COLOR set above COLOR_TOP = TOP_COLOR #"top" color is idle color for the ring def play_wav(name, loop=False): """ Play a WAV file in the 'sounds' directory. :param name: partial file name string, complete name will be built around this, e.g. passing 'foo' will play file 'sounds/foo.wav'. :param loop: if True, sound will repeat indefinitely (until interrupted by another sound). """ global wave_file # pylint: disable=global-statement print("playing", name) if wave_file: wave_file.close() try: wave_file = open('sounds/' + name + '.wav', 'rb') wave = audiocore.WaveFile(wave_file) audio.play(wave, loop=loop) except OSError: pass # we'll just skip playing then def power(sound, duration, reverse): """ Animate NeoPixels with accompanying sound effect for power on. @param sound: sound name (similar format to play_wav() above) @param duration: estimated duration of sound, in seconds (>0.0) @param reverse: Reverses animation. If True, begins animation at end of strip. """ if reverse: prev = NUM_PIXELS else: prev = 0 start_time = time.monotonic() # Save audio start time play_wav(sound) while True: elapsed = time.monotonic() - start_time # Time spent playing sound if elapsed > duration: # Past sound duration? break # Stop animating total_animation_time = elapsed / duration # Animation time, 0.0 to 1.0 if reverse: total_animation_time = 1.0 - total_animation_time # 1.0 to 0.0 if reverse threshold = int(NUM_PIXELS * total_animation_time + 0.5) num = threshold - prev # Number of pixels to light on this pass if num != 0: if reverse: strip[threshold:prev] = [ALT_COLOR] * -num else: strip[prev:threshold] = [ALT_COLOR] * num strip.show() prev = threshold def mix(color_1, color_2, weight_2): """ Blend between two colors with a given ratio. :param color_1: first color, as an (r,g,b) tuple :param color_2: second color, as an (r,g,b) tuple :param weight_2: Blend weight (ratio) of second color, 0.0 to 1.0 :return (r,g,b) tuple, blended color """ if weight_2 < 0.0: weight_2 = 0.0 elif weight_2 > 1.0: weight_2 = 1.0 weight_1 = 1.0 - weight_2 return (int(color_1[0] * weight_1 + color_2[0] * weight_2), int(color_1[1] * weight_1 + color_2[1] * weight_2), int(color_1[2] * weight_1 + color_2[2] * weight_2)) # List of swing wav files without the .wav in the name for use with play_wav() swing_sounds = [ 'swing1', 'swing2', 'swing3', ] # List of hit wav files without the .wav in the name for use with play_wav() hit_sounds = [ 'hit1', 'hit2', 'hit3', 'hit4', ] # List of yell wav files without the .wav in the name for use with play_wav() yell_sounds = [ 'yell1', ] mode = 0 # Initial mode = OFF # Setup idle pulse idle_brightness = IDLE_PULSE_BRIGHTNESS_MIN # current brightness of idle pulse idle_increment = 0.01 # Initial idle pulse direction # Main loop while True: if mode == 0: # If currently off... enable.value = True power('on', POWER_ON_SOUND_DURATION, True) # Power up! play_wav('idle', loop=True) # Play idle sound now mode = 1 # Idle mode time.sleep(1.0) #pause before moving on # Setup for idle pulse idle_brightness = IDLE_PULSE_BRIGHTNESS_MIN idle_increment = 0.01 # lights the ring in COLOR_TOP color: strip[0:NUM_RING] = [([int(c*idle_brightness) for c in COLOR_TOP])] * NUM_RING # lights the strip in COLOR_IDLE color: strip[NUM_RING:NUM_PIXELS] = [([int(c*idle_brightness) for c in COLOR_IDLE])] * NUM_STRIP strip.show() elif mode >= 1: # If not OFF mode... x, y, z = accel.acceleration # Read accelerometer accel_total = x * x + z * z #x axis used for hit and for swing accel_yell = y * y + z * z #y axis used for yell # Square root isn't needed, since we're # comparing thresholds...use squared values instead.) if accel_total > HIT_THRESHOLD: # Large acceleration on x axis = HIT TRIGGER_TIME = time.monotonic() # Save initial time of hit play_wav(random.choice(hit_sounds)) # Start playing 'hit' sound COLOR_ACTIVE = COLOR_HIT # Set color to fade from mode = 3 # HIT mode elif mode == 1 and accel_total > SWING_THRESHOLD: # Mild acceleration on x axis = SWING TRIGGER_TIME = time.monotonic() # Save initial time of swing play_wav(random.choice(swing_sounds)) # Randomly choose from available swing sounds # make a larson scanner strip_backup = strip[0:-1] for p in range(-1, len(strip)): for i in range(p-1, p+2): # shoot a 'ray' of 3 pixels if 0 <= i < len(strip): strip[i] = COLOR_SWING strip.show() time.sleep(SWING_BLAST_SPEED) if 0 <= (p-1) < len(strip): strip[p-1] = strip_backup[p-1] # restore previous color at the tail strip.show() while audio.playing: pass # wait till we're done mode = 2 # we'll go back to idle mode elif mode == 1 and accel_yell > YELL_THRESHOLD: # Motion on Y axis = YELL TRIGGER_TIME = time.monotonic() # Save initial time of swing # run a color down the staff, opposite of power-up previous = 0 audio_start_time = time.monotonic() # Save audio start time play_wav(random.choice(yell_sounds)) # Randomly choose from available yell sounds sound_duration = YELL_SOUND_DURATION while True: time_elapsed = time.monotonic() - audio_start_time # Time spent playing sound if time_elapsed > sound_duration: # Past sound duration? break # Stop animating animation_time = time_elapsed / sound_duration # Animation time, 0.0 to 1.0 pixel_threshold = int(NUM_PIXELS * animation_time + 0.5) num_pixels = pixel_threshold - previous # Number of pixels to light on this pass if num_pixels != 0: # light pixels in YELL_COLOR: strip[previous:pixel_threshold] = [YELL_COLOR] * num_pixels strip.show() previous = pixel_threshold while audio.playing: pass # wait till we're done mode = 4 # we'll go back to idle mode elif mode == 1: # Idle pulse idle_brightness += idle_increment # Pulse up if idle_brightness > IDLE_PULSE_BRIGHTNESS_MAX or \ idle_brightness < IDLE_PULSE_BRIGHTNESS_MIN: # Then... idle_increment *= -1 # Pulse direction flip # light the ring: strip[0:NUM_RING] = [([int(c*idle_brightness) for c in COLOR_TOP])] * NUM_RING # light the strip: strip[NUM_RING:NUM_PIXELS] = [([int(c*idle_brightness) for c in COLOR_IDLE])] * NUM_STRIP strip.show() time.sleep(IDLE_PULSE_SPEED) # Idle pulse speed set above elif mode > 1: # If in SWING or HIT or YELL mode... if audio.playing: # And sound currently playing... blend = time.monotonic() - TRIGGER_TIME # Time since triggered if mode == 2: # If SWING, blend = abs(0.5 - blend) * 3.0 # ramp up, down strip.fill(mix(COLOR_ACTIVE, COLOR, blend)) # Fade from hit/swing to base color strip.show() else: # No sound now, but still SWING or HIT modes play_wav('idle', loop=True) # Resume idle sound mode = 1 # Return to idle mode
Customizing Your Code
The best way to edit and upload your code is with the Mu Editor, a simple Python editor that works with Adafruit CircuitPython hardware. It's written in Python and works on Windows, MacOS, Linux and Raspberry Pi. The serial console is built right in so you get immediate feedback from your board's serial output. Instructions for installing Mu is here.
Audio Files
Adafruit CircuitPython supports 16-bit, Mono, 22.050kHz .wav audio format. See this guide to help format any audio files you might want to use in this project besides the files provided.
In the main loop, the swing and hit modes randomly choose from a list of sounds. For example, swing1.wav, swing2.wav, swing3, etc. This makes the motion effects feel much more varied and less repetitive.
The yell mode plays yell1.wav. If you want more yelling sounds, you can add more files -- just call them yell2.wav, yell3.wav etc and you can integrate them into the code.
- Power on – on.wav
- Idle loop – idle.wav
- Swing 1 – swing1.wav
- Swing 2 – swing2.wav
- Swing 3 – swing3.wav
- Hit 1 – hit1.wav
- Hit 2 – hit2.wav
- Hit 3 – hit3.wav
- Hit 4 – hit4.wav
- Yell – yell1.wav
Open the code in the Mu editor (or another text editor) and look near the top. You'll see a lot of variables that you can change to customize the color palettes and sensitivity of the motion of your staff.
First, change the value of NUM_STRIP
to match the actual number of pixels in your NeoPixel strip. If you're using a different sized ring, you can change that number here as well.
# CHANGE TO MATCH YOUR RING AND STRIP SETUP NUM_RING = 12 #12 pixel ring NUM_STRIP = 44 # 44 pixels in my NeoPixel strip NUM_PIXELS = NUM_STRIP + NUM_RING #total number of pixels
Colors
The default color for the handle of the staff at idle is orange. The format here is R, G, B -- so we've made orange by mixing 200 Red, 50 Green, and 0 Blue values. You can play with the numbers here to mix your own colors, but be sure to keep the values under around 200.
More about mixing colors in CircuitPython can be found here.
# CUSTOMISE COLORS HERE: COLOR = (200, 50, 0) # Default idle is orange ALT_COLOR = (0, 200, 200) # hit color is teal SWING_COLOR = (200, 200, 200) #swing animation color is white TOP_COLOR = (100, 100, 0) #top color is yellow-green YELL_COLOR = (200, 0, 200) #yell color is purple
Idle Pulse Animation Speed
Next, you can customize the speed and brightness of the idle pulse animation and the swing blast speed.
# CUSTOMISE IDLE PULSE SPEED HERE: 0 is fast, above 0 slows down IDLE_PULSE_SPEED = 0 # Default is 0 seconds SWING_BLAST_SPEED = 0.007 # CUSTOMISE BRIGHTNESS HERE: must be a number between 0 and 1 IDLE_PULSE_BRIGHTNESS_MIN = 0.2 # Default minimum idle pulse brightness IDLE_PULSE_BRIGHTNESS_MAX = 1 # Default maximum idle pulse brightness
Sensitivity
This section allows you to adjust the motion sensitivity of the staff. I have it set up so that the swing and yell thresholds trigger pretty easily, and I have to really move the staff hard to get the hit mode to trigger.
The swing mode triggers when the staff is moved along the X axis, and the yell mode triggers when the staff is moved along the Y axis. The hit mode is set up on the X axis also, but requires a firmer movement.
This means that to activate the swing animation, I move the staff left to right, and to activate the yell animation I move it front to back.
These numbers will really affect the feel and control of your staff, so don't be afraid to play around with them until it all feels just right.
# CUSTOMISE SENSITIVITY HERE: smaller numbers = more sensitive to motion HIT_THRESHOLD = 1150 SWING_THRESHOLD = 800 YELL_THRESHOLD = 700
Wav File Updates
If you change the .wav files for power-up or for the yell mode, you can adjust the animation times here.
The code will automatically adjust the animation lengths for the hit and swing modes to match the length of your .wav files.
# Set to the length in seconds of the "on.wav" and "yell1.wav" files POWER_ON_SOUND_DURATION = 3.0 YELL_SOUND_DURATION = 1.0
If you add or remove .wav files, you'll also need to update the list further down in the code (starting at line 146):
# List of swing wav files without the .wav in the name for use with play_wav() swing_sounds = [ 'swing1', 'swing2', 'swing3', ] # List of hit wav files without the .wav in the name for use with play_wav() hit_sounds = [ 'hit1', 'hit2', 'hit3', 'hit4', ] # List of yell wav files without the .wav in the name for use with play_wav() yell_sounds = [ 'yell1', ]
Adding your Own Wav Files
I found that even following the very specific guidelines for .wav file setup, some files would crash the Feather when triggered. If you're having this problem, head to this guide and re-crunch your files using Audacity.
This is a teeny tiny speaker, so sound files with more high-end frequencies will sound a lot better than deep, booming sounds. Sound files that have a lot of (or even any) low frequencies do not sound good when played back over small speakers. Too much low end will cause a small speaker to distort. To optimize your sound files for small speaker playback use a High Pass Filter (HPF) in your preferred audio editing software. Rolling off low frequencies below 250 Hz is a good starting point.
Audacity is great and simple audio editing app, and best of all, it’s free!
If your character doesn't resonate with the sounds included with this project, but you don't want to create your own, try using the files from the Zelda Master Sword project as an alternative. It's amazing how the personality of the staff changes with different sound effects.